sorcery  enchanter.lua at [93f944b581]

File enchanter.lua artifact cca22cad27 part of check-in 93f944b581


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[current_player;main]
		listring[context;item]
	]] .. 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()
		meta:set_string(key, sorcery.lib.str.meta_armor(pack(data),true))
		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 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) end
		end
		return p
	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, oddly_breakable_by_hand = 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;
		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

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

	-- 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 = 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,math.floor(props.cost * strength)) end

		if ch > sp.reliability then goto skip end
		sparks[#sparks + 1] = {
			color = sorcery.lib.color(data.tone):brighten(1.1);
			count = strength * 7;
		}
	::skip::end end
	if totalcost > 0 then
		local conservation = sorcery.enchant.strength(tool,'conserve') * 0.5
		totalcost = totalcost - (totalcost * conservation)
		ench.energy = math.max(0,ench.energy - (totalcost - (material.data.energysource or 0)))
	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.5,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)

	-- perform leyline checks and call notify if necessary
	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
		for _,p in pairs(sorcery.ley.txofs) do
			local sum = vector.add(pos,p)
			if minetest.get_item_group(minetest.get_node(sum).name, 'sorcery_ley_device') ~= 0 then
				sorcery.ley.notify(sum)
			end
		end
	end
end)

minetest.register_chatcommand('enchants', {
	description = 'Log information about the currently held object\'s enchantment';
	privs = { server = true };
	func = function(caller,params)
		local tool = minetest.get_player_by_name(caller):get_wielded_item()
		minetest.chat_send_player(caller, dump(sorcery.enchant.get(tool)))
		local binary = tool:get_meta():get_string('sorcery_enchantment_recs')
		local dumpout = ''
		for i=1,string.len(binary) do
			dumpout = dumpout .. string.format('%x%s',string.byte(binary,i),(i%16==0 and '\n') or ' ') 
		end
		print(dumpout)
	end;
})