sorcery  altar.lua at tip

File altar.lua from the latest check-in


local altar_item_offset = {
	x = 0, y = -0.3, z = 0.13
}
local log = sorcery.logger('altar')
local L = sorcery.lib

local range = function(min, max)
	local span = max - min
	local val = math.random() * span
	return val + min
end

local get_altar_ent = function(pos)
	for _, obj in pairs(minetest.get_objects_inside_radius(pos,0.9)) do
		if not obj then goto nextloop end
		local ent = obj:get_luaentity()
		if not ent then goto nextloop end

		if ent.name == "sorcery:altar_item" then
			return obj
		end

		::nextloop::
	end
	return nil
end

local update_altar = function(pos, object)
	local meta = minetest.get_meta(pos)
	local contents = meta:get_inventory()

	if object == nil then
		object = get_altar_ent(pos)
		if object == nil then
			object = minetest.add_entity({
				x = pos.x + altar_item_offset.x,
				y = pos.y + altar_item_offset.y,
				z = pos.z + altar_item_offset.z
			}, "sorcery:altar_item")
		end
	end

	if contents:is_empty('item') then
		if object ~= nil then object:remove() end
	else
		local itemstring = contents:get_stack('item',1):to_string()
		local props = object:get_properties()
		local node = minetest.get_node(pos)
		props.wield_item = itemstring
		object:set_properties(props)
		object:set_yaw(math.pi*2 - node.param2*(math.pi / 2))
	end
end

-- remove unknown gifts
minetest.register_on_mods_loaded(function()
	for name, god in pairs(sorcery.data.gods) do
		local bad = {}
		for g in pairs(god.gifts) do
			-- can't mutate table while we're iterating it
			if not minetest.registered_nodes[g] then bad[#bad+1] = g end
		end
		for _, g in ipairs(bad) do god.gifts[g] = nil end
	end
end)

local std_god_interval = 40

for name, god in pairs(sorcery.data.gods) do
	local hitbox = {
		0-(god.idol.width / 2.0), 0-(god.idol.height / 2.0), -0.15,
		   god.idol.width / 2.0,     god.idol.height / 2.0,   0.15
	} -- {xmin, ymin, zmin,
	  -- xmax, ymax, zmax} in nodes from node center.
	paramtype = "light";
	if god.idol.craft then
		minetest.register_craft {
			output = 'sorcery:idol_' .. name;
			recipe = god.idol.craft;
		};
	end
	local god_interval = std_god_interval * (god.freq or 1)
	local add_sparkles = function(pos,divine_favor)
		divine_favor = divine_favor or minetest.get_meta(pos):get_int('favor')
		if divine_favor > 5 then
			local ct = divine_favor / 5
			minetest.add_particlespawner {
				texture = L.image('sorcery_glitter.png'):glow(L.color(god.color)):render();
				glow = 14;
				amount = ct / (1 / god_interval), time = god_interval;
				minpos = pos:offset(-0.4, -0.5, -0.4);
				maxpos = pos:offset( 0.4,god.idol.height-0.5,0.4);
				minvel = vector.new(0, 0.05, 0);
				minvel = vector.new(0, 0.1, 0);
				minacc = vector.new(0, 0.1, 0);
				maxacc = vector.new(0, 0.2, 0);
				minexptime = 4, maxexptime = 4;
				minsize = 0.3, maxsize = 1;
				animation = {
					type = 'vertical_frames';
					length = 0.1;
					aspect_w = 16, aspect_h = 16;
				}
			}
		end
	end
	sorcery.lib.node.reg_autopreserve('sorcery:idol_' .. name, {
		description = god.idol.desc;
		drawtype = "mesh";
		mesh = 'sorcery-idol-' .. name .. '.obj';
		paramtype = 'light';
		paramtype2 = 'facedir';
		sunlight_propagates = true;
		stack_max = 1;
		tiles = god.idol.tex;
		selection_box = { type = "fixed"; fixed = {hitbox}; };
		collision_box = { type = "fixed"; fixed = {hitbox}; };
		groups = { cracky = 2, sorcery_idol = 1, heavy = 1, sorcery_worship = 1, sorcery_instantiate = 1};
		_sorcery = {
			idol_god = name;
			on_load = function(pos) add_sparkles(pos) end;
		};

		on_construct = function(pos)
			minetest.get_node_timer(pos):start(god_interval)
			add_sparkles(pos)
		end;

		on_timer = function(pos, elapsed)
			local altar = minetest.find_node_near(pos, 3, "sorcery:altar")
			-- TODO even without an altar, an idol with high favor could still be the source of miracles
			-- refills nearby partly empty troughs at cost to favor?
			if not altar then return true end

			local altarmeta = minetest.get_meta(altar)
			local inv = altarmeta:get_inventory()
			local idolmeta = minetest.get_meta(pos)
			local divine_favor = idolmeta:get_int('favor')
			add_sparkles(pos,divine_favor)
			local bestow = function(item,color)
				if type(item) == 'string' then
					item = ItemStack(item)
				end
				if color == nil then
					color = sorcery.lib.color(god.color)
				end
				for i=0,32 do
					minetest.add_particle{
						pos = {
							x = altar.x + range(-0.4,0.4);
							z = altar.z + range(-0.4,0.4);
							y = altar.y + range(-0.2,0.3);
						};
						expirationtime = range(3,8);
						size = range(1,3);
						velocity = {
							x = range(-0.6, 0.6);
							z = range(-0.6, 0.6);
							y = range(-0.6, 0.6);
						};
						acceleration = {
							x = 0; y = range(0,1); z = 0;
						};
						texture = sorcery.lib.image('sorcery_sparkle.png'):
							transform(math.random(8) - 1):
							multiply(color:brighten(1.7)):
								render();
						glow = 14;
					}
				end
				for i=0,48 do
					minetest.add_particle{
						pos = {
							x = altar.x + range(-0.3,0.3);
							z = altar.z + range(-0.3,0.3);
							y = altar.y + range(-0.3,0.3);
						};
						expirationtime = range(3,8);
						size = range(2,9);
						velocity = {
							x = range(-0.5, 0.5);
							z = range(-0.5, 0.5);
							y = range(-0.3, 0.8);
						};
						acceleration = {
							x = 0; y = range(0,1); z = 0;
						};
						texture = 'sorcery_divine_radiance_'..math.random(9)..'.png' ..
							'^[multiply:' .. color:hex() ..
							'^[opacity:' .. (math.random(127) + 127) ..
							'^[transform' .. (math.random(8) - 1);
						glow = math.random(14 - 9) + 9;
					}
				end
				inv:set_stack('item',1,item)
			end
			if inv:is_empty('item') then 
				-- nothing on the altar. decide if we're going to do a
				-- gift cycle by rolling against the god's stinginess
				if god.gifts and next(god.gifts) and math.random(god.stinginess) == 1 then
					-- we've beat the odds and started a gift cycle. now
					-- we pick a random gift and roll against its rarity
					-- to determine if the god is feeling generous
					local gift = sorcery.lib.tbl.pick(god.gifts)
					local data = god.gifts[gift]
					local value, rarity = data[1], data[2]
					if value <= divine_favor and math.random(rarity) == 1 then
						bestow(gift)
						log.act(god.name,'has produced',gift,'upon an altar as a gift')
						if math.random(god.generosity) == 1 then
							-- unappreciated gifts may incur divine
							-- irritation
							divine_favor = divine_favor - 1
						end
					end
				end
				
				goto refresh
			end

			-- gods are taciturn and unpredictable creatures, but if you
			-- have won their favor for your idol through sacrifice and
			-- obeisance, they are more likely to pay attention to you
			-- and lay blessings more rapidly upon you
			do
				local chance_to_act = math.max(2, god.laziness - divine_favor)
				if math.random(chance_to_act) ~= 1 then goto refresh end

				local stack = inv:get_stack('item',1)
				local itemname = stack:get_name()

				-- loop through the sacrifices accepted by this god and check
				-- whether the item on the altar is any of them
				for s, value in pairs(god.sacrifice) do
					if itemname == s then
						if value < 0 then
							bestow("sorcery:ash", sorcery.lib.color(254,117,103))
						else
							if idolmeta:get_string('last_sacrifice') == s then
								-- the gods are getting bored
								value = math.floor(value / 2)
							end
							bestow(nil)
						end
						divine_favor = divine_favor + value
						
						log.actf("%s has accepted a sacrifice of %s, raising divine favor by %u points to %u at altar %s", god.name, s, value, divine_favor, minetest.pos_to_string(pos))

						idolmeta:set_string('last_sacrifice', s)

						goto refresh
					end
				end

				-- loop through the list of things this god will consecrate and
				-- check whether the item on the altar is any of them
				for s, cons in pairs(god.consecrate) do
					if itemname == s then
						local cost, tx
						if type(cons) == "table" then
							cost, tx = cons[1], cons[2]
							tx = tx[math.random(#tx)]
						elseif type(cons) == 'function' then
							cost, tx = cons {
								favor = divine_favor;
								pos = pos;
								altar = altarmeta;
								idol = idolmeta;
								god = god;
							}
						end
						-- preserve wear
						local gift
						if type(tx) == 'string' then
							gift = ItemStack(tx)
						else gift = tx end
						if not gift:is_known() then goto skip end
						local wear = stack:get_wear()
						if wear > 0 then
							gift:set_wear(wear)
						end
						-- preserve meta
						local gm = gift:get_meta()
						gm:from_table(
							sorcery.lib.tbl.merge(
								stack:get_meta():to_table(),
								gm:to_table()
							)
						) -- oof
						-- reflash enchantments to ensure label is accurate
						local ench = sorcery.enchant.get(gift)
						if #ench.spells > 0 then
							-- add a bit of energy as a gift?
							if math.random(math.ceil(god.stinginess * 0.5)) == 1 then
								local max = 0.05 * god.generosity
								ench.energy = ench.energy * range(0.7*max,max)
							end
							sorcery.enchant.set(gift,ench)
						end

						if divine_favor >= cost then
							bestow(gift)
							divine_favor = divine_favor - cost
							log.act(god.name, 'has consecrated', s, 'into', tx, 'for the cost of', cost, 'points of divine favor')
							goto refresh
						end
					end
				::skip::end
			end

			::refresh::
			idolmeta:set_int('favor', divine_favor)
			update_altar(altar,nil)
			return true
		end;
	})

end

minetest.register_entity("sorcery:altar_item", {
	initial_properties = {
		visual = "wielditem";
		visual_size = { x = 0.2, y = 0.2 };
		wield_item = "air";
		glow = 11; -- why the fuck isn't light working normally?
		collisionbox = {0};
		physical = false;
		pointable = false;
	};

	on_activate = function(self,data,dtime)
		local pos = self.object:get_pos()
		local nodepos = {
			x = pos.x - altar_item_offset.x,
			y = pos.y - altar_item_offset.y,
			z = pos.z - altar_item_offset.z
		}
		if minetest.get_node(nodepos).name ~= "sorcery:altar" then
			self.object:remove()
		else
			update_altar(nodepos,self.object)
		end
	end
})
minetest.register_node("sorcery:altar", {
	description = "Altar";
	drawtype = "mesh";
	mesh = "sorcery-altar.obj";
	paramtype = "light";
	paramtype2 = "facedir";
	sunlight_propagates = true;
	tiles = { 
		"default_sandstone.png",
		"default_silver_sandstone.png",
		"default_copper_block.png",
		"default_steel_block.png",
		"default_gold_block.png",
		"default_coal_block.png"
	};
	selection_box = {
		type = "fixed",
		fixed = { {-0.5, -0.5, -0.5, 0.5, -0.09, 0.5} }
	};
	collision_box = {
		type = "fixed",
		fixed = { {-0.5, -0.5, -0.5, 0.5, -0.09, 0.5} }
	};

	groups = {cracky = 2, dig_immediate = 2, sorcery_worship = 1};

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		inv:set_size('item', 1)
	end;

	on_destruct = function(pos)
		local ent = get_altar_ent(pos)
		if not ent then return nil end

		ent:remove()
	end;

	on_rightclick = function(pos,node,user,stack)
		local meta = minetest.get_meta(pos)
		local contents = meta:get_inventory()
		if contents:is_empty('item') then
			if stack:is_empty() then return stack end
			local new_item = stack:take_item(1)
			contents:set_stack('item',1,new_item)
		else
			local pinv = user:get_inventory()
			if pinv == nil
				then return stack end

			local give_item = contents:get_stack('item',1)

			-- this is more complex than it should really need to
			-- be. because minetest implements a modify-current-
			-- stack-through-return-value feature, which is very
			-- poorly integrated with features for controlling the
			-- player's inventory, it's not enough to just say
			-- "give them the item" and let minetest work out where
			-- to place it. the value returned by this function
			-- always overrides any changes to the inventory made
			-- by this function, so if the current stack matches
			-- the object being removed, it will seem to just
			-- disappear into the ether, as the original stack
			-- argument overwrites it. (yes, returning nil has the
			-- same effect, i tried. bug imo.)

			if stack:item_fits(give_item) then
				-- first check if the item we're taking fits onto 
				-- the selected stack, and just increment it if so.
				stack:add_item(give_item)
			elseif not pinv:room_for_item('main',give_item) then
				-- it doesn't fit onto the current stack, but does
				-- it fit into the inventory somewhere else? if not,
				-- we need to bail without changing anything
				return stack
			else -- it fits in the inventory
				pinv:add_item('main',give_item)
			end

			-- clear the contents of the altar
			contents:set_stack('item',1,ItemStack(nil))

		end

		update_altar(pos,nil)
		return stack
	end
})