sorcery  enchanter.lua at [5fbd1ed57c]

File enchanter.lua artifact 569b7fff67 part of check-in 5fbd1ed57c


local hitbox = {
	type = 'fixed';
	fixed = {
		-0.5, -0.5, -0.5;
		0.5, 0.1, 0.5;
	};
}

local enchanter_update = function(pos)
	local meta = minetest.get_meta(pos)
	local inv = meta:get_inventory()
	local item = inv:get_stack('item',1)
	local slots = ''
	local imat = sorcery.enchant.getsubj(item)
	if imat and imat.data.slots then
		local n = #imat.data.slots
		local sw, sh = 2.1, 2.1;
		local w = sw * n;
		local item_enchantment = sorcery.enchant.get(item)
		local spells = item_enchantment.spells
		for i=1,n do
			local slot=imat.data.slots[i]
			local x = (4 - (w/2) + (sw * (i-1))) + 0.2
			local offtbl = {
				[1] = {0};
				[2] = {0.3, 0.3};
				[3] = {0.3,   0, 0.3};
				[4] = {0.3,   0,   0, 0.3};
				[5] = {0.3,   0, 0.1,   0, 0.3};
				[6] = {0.3, 0.1,   0, 0.1, 0.3};
			};
			local y = 3.1 - offtbl[n][i]
			local affs = #slot.affinity
			local iconf = math.min(math.floor(slot.confluence * 10), 20)
			local pwr = ''
			local ap = {}
			for i,aff in pairs(slot.affinity) do
				pwr = pwr .. string.format([[
					image[%f,%f;%f,%f;sorcery_pentacle_power_%s.png^[verticalframe:20:%u]
				]], x,y, sw,sh, aff, math.max(1,iconf - i))
				ap[#ap+1] = {
					title = sorcery.lib.str.capitalize(aff) .. ' affinity';
					color = sorcery.lib.color(sorcery.data.affinities[aff].color);
					desc = sorcery.data.affinities[aff].desc;
				}
			end
			local hovertitle = 'Empty spell slot';
			local conf = tostring(math.floor(slot.confluence*100)) .. '%'
			local hoverdesc = 'An enchantment of one the following affinities can be anchored into this slot at ' .. conf .. ' confluence';
			local hovercolor = nil
			for _,sp in pairs(spells) do
				local spdata = sorcery.data.enchants[sp.id]
				if sp.slot == i then
					hovercolor = sorcery.lib.color(spdata.tone):readable()
					hovertitle = sorcery.lib.str.capitalize(sp.id)
					hoverdesc = sorcery.lib.str.capitalize(spdata.desc) .. '. Anchored in this slot at ' .. conf .. ' confluence'
					if sp.boost > 10 then
						hoverdesc = hoverdesc .. ' and boosted by ' .. tostring((sp.boost - 10) * 10) .. '%'
					elseif sp.boost < 10 then
						hoverdesc = hoverdesc .. ' but weakened by ' .. tostring((10 - sp.boost) * 10) .. '%'
					end
					hoverdesc = hoverdesc .. '.'
					local addrune = function(tex)
						pwr = pwr .. string.format([[
							image[%f,%f;%f,%f;%s]
						]], x+0.43,y+0.6,sw/2.7,sh/2.7, tex)
					end
					local rune = 'sorcery_enchant_' .. sp.id .. '.png'
					local energy = item_enchantment.energy
					local fullenergy = imat.data.maxenergy
					if energy <= fullenergy then
						addrune(rune .. '^[opacity:110^[lowpart:' .. tostring(math.floor((energy/fullenergy) * 100)) .. '%:' .. rune)
					elseif energy == fullenergy then addrune(rune)
					elseif energy >= fullenergy then
						addrune(rune .. '^[colorize:#ffffff:' ..
							tostring(255 * math.min(1,(energy / fullenergy * 2))))
					end
					break
				end
			end
			slots = slots .. string.format([[
				image[%f,%f;%f,%f;sorcery_pentacle.png]
				tooltip[%f,%f;%f,%f;%s;%s;%s]
			]],
				x,y, sw,sh,
				x+0.20,y+0.16, sw-0.84,sh-0.76,
				minetest.formspec_escape(sorcery.lib.ui.tooltip {
					title = hovertitle;
					desc = hoverdesc;
					color = hovercolor;
					props = ap;
				}),
				'#37002C','#FFC8F5'
			) .. pwr
		end
	end

	meta:set_string('formspec', [[
		size[8,8.5]
		background[-0.25,-0.25;8.5,9;sorcery_enchanter_bg.png;true]
		image[2.13,0;4.35,4;sorcery_enchanter_glyphs.png]
		list[context;foci;3.5,0;1,1;0]
		list[context;item;3.5,1.2;1,1;]
		list[context;foci;2.5,2;1,1;1]
		list[context;foci;4.5,2;1,1;2]
		list[current_player;main;0,4.7;8,4;]
		listring[context;foci]
		listring[current_player;main]
		listring[context;item]
		listring[current_player;main]
	]] .. slots)
end

sorcery.enchant = {} do
	sorcery.enchant.update_enchanter = enchanter_update
	local m = sorcery.lib.marshal
	local ench_t = m.g.struct {
		id = m.t.str;
		slot = m.t.u8;
		boost = m.t.u8; -- every enchantment has an intrinsic force
		-- separate from the confluence of the slot, which is
		-- determined by the composition of the wand used to generate
		-- it (for instance, a gold-wired wand at low wear, or a wand
		-- with specific gemstones, may have a boost level above 10)
		-- boost is divided by 10 to yield a float
		reliability = m.t.u8;
		-- reliability is a value between 0 and 100, where 0 means the
		-- spell fails every time and 100 means it never fails. not
		-- relevant for every spell
	}
	local pack, unpack = m.transcoder {
		spells = m.g.array(8, ench_t);
		energy = m.t.u16;
	}
	sorcery.enchant.getsubj = function(item)
		if not item:is_empty() then
			local eligible = {}
			for name, spell in pairs(sorcery.data.enchants) do
				for g,v in pairs(item:get_definition().groups) do
					if v~= 0 and sorcery.lib.tbl.has(spell.groups,g) then
						eligible[#eligible+1] = name
						goto skip
					end
				end
			::skip::end
			return sorcery.matreg.lookup[item:get_name()], eligible
		else return nil end
	end
	local key = 'sorcery_enchantment_recs'
	sorcery.enchant.set = function(stack, data, noup)
		local meta = stack:get_meta()
		if data then
			meta:set_string(key, sorcery.lib.str.meta_armor(pack(data),true))
		else
			meta:set_string(key, '')
		end
		if not noup then stack=sorcery.enchant.stackup(stack) end
	end
	sorcery.enchant.get = function(stack)
		local meta = stack:get_meta()
		if meta:contains(key) then
			local data = sorcery.lib.str.meta_dearmor(meta:get_string(key),true)
			return unpack(data)
		else
			return {
				spells = {};
				energy = 0;
			}
		end
	end
	sorcery.enchant.strength = function(stack,id)
		-- this functions should be used whenever you need to
		-- determine the power of a particular enchantment on
		-- an enchanted item.
		local e = sorcery.enchant.get(stack)
		local p = 0.0
		local ct = 0
		local slots = sorcery.matreg.lookup[stack:get_name()].data.slots
		-- TODO handle strength-boosting spells!
		for _,s in pairs(e.spells) do
			if s.id == id then
				p = p + ((s.boost * slots[s.slot].confluence)/10)
				ct = ct + 1
			end
		end
		return p, ct
	end
	sorcery.enchant.stackup = function(stack)
		-- stack update function. this should be called whenever
		-- the enchantment status of a stack changes; it will
		-- alter/reset tool capabilities and tooltip as necessary
		local e = sorcery.enchant.get(stack)
		local meta = stack:get_meta()
		local def = stack:get_definition()
		local mat = sorcery.enchant.getsubj(stack)
		local done = {}
		local props = {}
		local interference = {}
		-- meta:set_string('tool_capabilities','')
		meta:set_tool_capabilities(nil); -- TODO this probably only works
		-- in >5.3; maybe bring in the old JSON mechanism so it works in
		-- older versions as well?
		local basecaps = def.tool_capabilities
		for _,s in pairs(e.spells) do
			if done[s.id] then goto skip end
			done[s.id] = true
			local pwr = sorcery.enchant.strength(stack,s.id)
				-- somewhat wasteful…
			local e = sorcery.data.enchants[s.id]
			if e.apply then stack = e.apply(stack,pwr,basecaps) end
			props[#props+1] = {
				title = e.name;
				desc = e.desc;
				color = sorcery.lib.color(e.tone);
			}
			local inf = mat.data.slots[s.slot].interference
			if inf then for k,v in pairs(inf) do
				interference[k] = (interference[k] or 0) + v
			end end
		::skip::end
		if #interference > 0 then
			if interference.speed then stack = sorcery.data.enchants.pierce.apply(stack,-interference.speed,basecaps) end
			if interference.durability then stack = sorcery.data.enchants.endure.apply(stack,-interference.durability,basecaps) end
		end
		meta = stack:get_meta() -- necessary? unclear
		if #e.spells > 0 then
			meta:set_string('description', sorcery.lib.ui.tooltip {
				title = 'Enchanted ' .. def.description;
				props = props;
			})
		else
			meta:set_string('description',def.description)
		end
		return stack
	end
end

minetest.register_node('sorcery:enchanter', {
	description = 'Enchanter';
	drawtype = 'mesh';
	mesh = 'sorcery-enchanter.obj';
	paramtype = 'light';
	paramtype2 = 'facedir';
	groups = { cracky = 2, dig_immediate = 2, sorcery_magitech = 1 };
	sunlight_propagates = true;
	selection_box = hitbox;
	collision_box = hitbox;
	after_dig_node = sorcery.lib.node.purge_container;
	tiles = {
		"default_obsidian.png";
		"default_steel_block.png";
		"default_bronze_block.png";
		"default_junglewood.png";
		"default_gold_block.png";
	};
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		meta:set_string('infotext','Enchanter')
		inv:set_size('item', 1)
		inv:set_size('foci', 3)
		enchanter_update(pos)
	end;
	on_metadata_inventory_put  = enchanter_update;
	on_metadata_inventory_move = enchanter_update;
	on_metadata_inventory_take = enchanter_update;
})

minetest.register_craftitem('sorcery:enchanter_channeler',{
	inventory_image = 'sorcery_enchanter_channeler.png';
	description = 'Channeler';
})
minetest.register_craftitem('sorcery:enchanter_pedestal',{
	inventory_image = 'sorcery_enchanter_pedestal.png';
	description = 'Pedestal';
})
minetest.register_craft {
	output = 'sorcery:enchanter_channeler';
	recipe = {
		{'sorcery:grease_enchanting','sorcery:platinum_ingot','sorcery:grease_enchanting'};
		{'basic_materials:gold_wire','basic_materials:steel_strip','basic_materials:gold_wire'};
		{'','sorcery:electrum_ingot',''};
	};
	replacements = {
		{'sorcery:grease_enchanting','xdecor:bowl'};
		{'sorcery:grease_enchanting','xdecor:bowl'};
		{'basic_materials:gold_wire','basic_materials:empty_spool'};
		{'basic_materials:gold_wire','basic_materials:empty_spool'};
	};
}
minetest.register_craft {
	output = 'sorcery:enchanter_pedestal';
	recipe = {
		{'basic_materials:copper_strip','group:wood','basic_materials:copper_strip'};
		{'','sorcery:iridium_ingot',''};
		{'','group:wood',''};
	};
}
minetest.register_craft {
	output = 'sorcery:enchanter';
	recipe = {
		{'sorcery:grease_sealant','sorcery:enchanter_channeler','sorcery:grease_sealant'};
		{'sorcery:enchanter_channeler','sorcery:enchanter_pedestal','sorcery:enchanter_channeler'};
		{'group:wood','group:wood','group:wood'};
	};
	replacements = {
		{'sorcery:grease_sealant','xdecor:bowl'};
		{'sorcery:grease_sealant','xdecor:bowl'};
	};
}
for i=1,10 do
	minetest.register_node('sorcery:air_flash_' .. i, {
		drawtype = 'airlike';
		pointable = false; walkable = false;
		buildable_to = true;
		sunlight_propagates = true;
		light_source = i + 4;
		groups = {
			air = 1, sorcery_air = 1;
			not_in_creative_inventory = 1;
		};
		drop = {max_items = 0, items = {}};
		on_blast = function() end; -- not affected by explosions
		on_construct = function(pos)
			minetest.get_node_timer(pos):start(0.05)
		end;
		on_timer = function(pos)
			if i <= 2 then minetest.remove_node(pos) else
				minetest.set_node(pos, {name='sorcery:air_flash_1'})
				return true
			end
		end
	});
end

local function enchpwrhud(user, flash, fac)
	-- this function displays or changes the HUD element
	-- that shows how much energy is left in an enchanted
	-- item. it is called by the dig handler and sequences
	-- callbacks to remove the HUD from the screen once
	-- its timeleft property has been used up. timeleft is
	-- reset if the 'flash' argument, which indicates
	-- whether the enchantment has just been used, is set
	-- to true; otherwise, it is left alone.
	--
	-- this whole thing is really unfriendly and without
	-- FP tricks it would have been intolerably painful
	-- to implement. minetest needs better primitives.
	local frame = math.ceil(16 * (1-math.min(1,fac)))
	if tostring(frame) == '-0' then frame = '0' -- ??????
		else frame = tostring(frame) end  
	local tex = 'sorcery_ui_manaring_' .. (flash and 'flash_' or '') .. frame .. '.png';
	local c = sorcery.ctx.get(user)
	if not c.hud_ench then
		local hid = user:hud_add {
			name = 'sorcery:manaring';
			hud_elem_type = 'image';
			text = tex;
			position = { x = 0.5, y = 0.5 };
			offset   = { x = 0,   y = 0  };
			scale    = { x = 2.0, y = 2.0 };
			z_index  = 0;
		}
		c.hud_ench = {
			id = hid;
			timeleft = 2.0;
			fn = false;
			fac = fac;
		}
		c = c.hud_ench
	else
		c = c.hud_ench
		c.fac = fac
		user:hud_change(c.id,'text',tex)
		if flash then c.timeleft = 2.0 end
	end
	if c.fn == false then
		c.fn = true
		local delta = 0.10
		-- tried making Δ conditional on 'flash' but it
		-- turns out that causes the flash not to always
		-- disappear in a timely manner. solving this
		-- efficiently would be a major, complex headache
		-- so i'm just compromising and setting delta to a
		-- constant :/
		minetest.after(delta, function()
			if not sorcery.ctx.stat(user) then return end
			local u = sorcery.ctx.get(user)
			local h = u.hud_ench
			if not h then return end
			if h.timeleft - delta <= 0 then
				user:hud_remove(h.id)
				u.hud_ench = nil
			else
				h.timeleft = h.timeleft - delta
				h.fn = false
				enchpwrhud(user, false, h.fac)
			end
		end)
	end
end

minetest.register_on_dignode(function(pos, node, puncher)
	if puncher == nil then return end -- i don't know why
	-- this is necessary but you get rare crashes without it

	-- perform leyline checks and call notify if necessary
	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
		sorcery.lib.node.notifyneighbors(pos)
	end

	-- is there an active disjunction in effect here?
	-- if so, return immediately and perform no magic
	local probe = sorcery.spell.probe(pos)
	if probe.disjunction then return end

	-- we're goint to do something VERY evil here and
	-- replace the air with a "glow-air" that removes
	-- itself after a short period of time, to create
	-- a flash of light when an enchanted tool's used
	-- to dig out a node
	local tool = puncher:get_wielded_item()
	local ench = sorcery.enchant.get(tool)
	if #ench.spells == 0 then return end
	local sparks = {}
	-- local spark = function(name,color)
	local material = sorcery.enchant.getsubj(tool)
	local totalcost = 0
	do local done = {} for _,sp in pairs(ench.spells) do
		if done[sp.id] then goto skip end
		done[sp.id] = true

		local data = sorcery.data.enchants[sp.id]
		local strength, nsp = sorcery.enchant.strength(tool,sp.id)
		local ch = math.random(1,100)
		local props = {
			fail = ch > sp.reliability;
			user = puncher;
			pos = pos;
			node = node;
			tool = tool;
			material = material.data;
			enchantment = ench;
			power = strength;
			spell = sp;
			sparks = sparks;
			cost = data.cost;
		} 
		if data.on_dig then data.on_dig(props) end
		if props.cost ~= 0 then totalcost = totalcost + math.max(1,props.cost * nsp) --[[math.max(1,math.floor(props.cost * strength))]] end
		-- strength does not increase cost but number of spell slots occupied does -- incentive for the player to be more clever, less brute-force-y

		if ch > sp.reliability then goto skip end
		sparks[#sparks + 1] = {
			color = sorcery.lib.color(data.tone):brighten(1.1);
			count = strength * 8;
		}
	::skip::end end
	if totalcost > 0 then
		local conservation = math.floor(sorcery.enchant.strength(tool,'conserve') * 6)
		-- totalcost = totalcost - (totalcost * conservation)
		if conservation == 0 or math.random(conservation) == 1 then
			ench.energy = math.max(0,ench.energy - (totalcost - (material.data.energysource or 0)))
		end
	end
	if #sparks == 0 then return end
	if math.random(5) == 1 then
		minetest.set_node(pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
	end
	local range = function(min, max)
		local span = max - min
		local val = math.random() * span
		return val + min
	end
	for _,s in pairs(sparks) do
		for i=1,math.floor(s.count * range(1,3))  do
			local life = range(0.3,1);
			minetest.add_particle {
				pos = {
					x = pos.x + range(-0.5,0.5);
					z = pos.z + range(-0.5,0.5);
					y = pos.y + range(-0.5,0.5);
				};
				acceleration = {
					x = range(-0.5,0.5);
					z = range(-0.5,0.5);
					y = -0.1;
				};
				velocity = {
					x = range(-1.3,1.3);
					z = range(-1.3,1.3);
					y = range( 0.3,0.9);
				};
				expirationtime = life;
				size = range(0.2,1.5);
				texture = sorcery.lib.image('sorcery_spark.png'):multiply(s.color):render();
				glow = 14;
				animation = {
					type = "vertical_frames";
					aspect_w = 16;
					aspect_h = 16;
					length = life + 0.1;
				};
			}
		end
	end

	-- destroy spells that can no longer be sustained
	if ench.energy == 0 then
		ench.spells = {}
		sorcery.enchant.set(tool,ench)
	else
		sorcery.enchant.set(tool,ench,true)
	end
	puncher:set_wielded_item(tool)

	local epct = ench.energy / material.data.maxenergy
	enchpwrhud(puncher, true, epct)
end)