sorcery  runeforge.lua at [58edda50fc]

File runeforge.lua artifact bae6ceb605 part of check-in 58edda50fc


-- TODO make some kind of disposable "filter" tool that runeforges require 
-- to generate runes and that wears down over time, to make amulets more
-- expensive than they currently are? the existing system is neat but
-- i think amulets are a little overpowered for something that just
-- passively consumes ley-current
--  -- are phials & rune-wrenches enough for this now?

local constants = {
	rune_mine_interval = 180;
	-- how often a powered forge rolls for new runes

	rune_cache_max = 6;
	-- how many runes a runeforge can hold at a time
	
	rune_grades = {'Fragile', 'Weak', 'Ordinary', 'Pristine', 'Sublime'};
	-- how many grades of rune quality/power there are
	
	amulet_grades = {'Slight', 'Minor', 'Major', 'Grand', 'Ultimate' };
	-- what kind of amulet each rune grade translates to
	
	phial_kinds = {
		lesser   = {grade = 1, name = 'Lesser';   infusion = 'sorcery:powder_brass';
			dist = { Fragile = 1, Weak = 0.7,  Ordinary = 0.1, Pristine = 0.05, Sublime = 0.01 };
		};
		simple   = {grade = 2, name = 'Simple';   infusion = 'sorcery:powder_silver';
			dist = { Fragile = 1, Weak = 0.8,  Ordinary = 0.2, Pristine = 0.07, Sublime = 0.015 };
		};
		great    = {grade = 3, name = 'Great';    infusion = 'sorcery:powder_gold';
			dist = { Fragile = 1, Weak = 0.9,  Ordinary = 0.5, Pristine = 0.1,  Sublime = 0.05 };
		};
		splendid = {grade = 4, name = 'Splendid'; infusion = 'sorcery:powder_electrum';
			dist = { Fragile = 1, Weak = 0.95, Ordinary = 0.7, Pristine = 0.3,  Sublime = 0.1 };
		};
		exalted  = {grade = 5, name = 'Exalted';  infusion = 'sorcery:powder_iridium';
			dist = { Fragile = 0, Weak = 1,    Ordinary = 0.9, Pristine = 0.5,  Sublime = 0.25 };
		};
		supreme  = {grade = 6, name = 'Supreme';  infusion = 'sorcery:powder_levitanium';
			dist = { Fragile = 0, Weak = 0,    Ordinary = 1,   Pristine = 0.7,  Sublime = 0.4 };
		};
	};
}
local calc_phial_props = function(phial) --> mine interval: float, power factor: float
	local m = phial:get_meta()
	local g = phial:get_definition()._proto.data.grade
	local i = constants.rune_mine_interval 
	local fac = (g-1) / 5
	fac = fac + 0.1 * m:get_int('speed')
	return math.max(3,i - ((i*0.5) * fac)), 0.5 * fac
end
sorcery.register.runes.foreach('sorcery:generate',{},function(name,rune)
	local id = 'sorcery:rune_' .. name
	rune.image = rune.image or string.format('sorcery_rune_%s.png',name)
	rune.item = id
	local c = sorcery.lib.color(rune.tone)
	minetest.register_craftitem(id, {
		description = c:darken(0.7):bg(c:readable():fmt(rune.name .. ' Rune'));
		short_description = rune.name .. ' Rune';
		inventory_image = rune.image;
		stack_max = 1;
		groups = {
			sorcery_rune = 1;
			not_in_creative_inventory = 1;
		};
		_proto = { id = name, data = rune };
	})
end)

local phkind = {
	label = 'Phial';
	desc = 'An alchemical substance which rune forges consume while coalescing new runes';
}

for name,p in pairs(constants.phial_kinds) do
	local f = string.format
	local color = sorcery.lib.color(142,232,0)
	local fac = p.grade / 6
	local id = f('phial_%s', name);
	local fname = f('%s Phial',p.name);
	local desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed and how much energy is required by the process, and affects your odds of getting a high-quality rune."
	sorcery.register_potion_tbl {
		name = id;
		label = fname;
		desc = desc;
		color = color:brighten(1 + fac*0.5);
		imgvariant = (fac >= 5) and 'sparkle' or 'dull';
		glow = 5+p.grade;
		extra = {
			groups = { sorcery_phial = p.grade };
			_proto = { id = name, desc = desc, name = p.name, kind = phkind, data = p, quals = {force = true, speed = true}, color = color };
		};
	}
	sorcery.register.infusions.link {
		infuse = p.infusion;
		into = 'sorcery:potion_subtle';
		output = 'sorcery:'..id;
		_proto = {
			data = { color = color };
		};
	}
end

local register_rune_wrench = function(w)
	local mp = sorcery.data.metals[w.metal].parts
	minetest.register_tool(w.name, {
		description = w.desc;
		inventory_image = w.img;
		groups = {
			sorcery_magitech = 1;
			sorcery_rune_wrench = 1;
			crafttool = 50;
		};
		_proto = w;
		_sorcery = {
			recipe = { note = w.note };
		};
	})
	minetest.register_craft {
		output = w.name;
		recipe = {
			{'',                        mp.fragment,''};
			{'',                        mp.ingot,   mp.fragment};
			{'sorcery:fragment_vidrium','',         ''};
		};
	}
end

register_rune_wrench {
	name = 'sorcery:rune_wrench', desc = 'Rune Wrench';
	img = 'sorcery_rune_wrench.png', metal = 'brass';
	powers = { imbue = 30 };
	note = 'A runeworking tool used to imbue amulets with enchantments';
}

register_rune_wrench {
	name = 'sorcery:rune_wrench_iridium', desc = 'Iridium Rune Wrench';
	img = 'sorcery_rune_wrench_iridium.png', metal = 'iridium';
	powers = { imbue = 80, extract = 40 };
	note = 'A rare and powerful runeworking tool used to imbue amulets with enchantments, or extract runes intact from enchanted amulets';
}

local rune_set = function(stack,r)
	local m = stack:get_meta()
	local def = stack:get_definition()._proto.data
	local grade
	if r.grade then grade = r.grade
	elseif m:contains 'rune_grade' then grade = m:get_int 'rune_grade' end

	local qpfx = constants.rune_grades[grade]
	local title = sorcery.lib.color(def.tone):readable():fmt(string.format('%s %s Rune',qpfx,def.name))

	m:set_int('rune_grade',grade)
	m:set_string('description',title)
end

sorcery.amulet = {}
sorcery.amulet.setrune = function(stack,rune,user)
	local m = stack:get_meta()
	if rune then
		local rp = rune:get_definition()._proto
		local rg = rune:get_meta():get_int 'rune_grade'
		m:set_string('amulet_rune', rp.id)
		m:set_int('amulet_rune_grade', rg)
		local spell = sorcery.amulet.getspell(stack)
		if not spell then return nil end
		local name
		if spell.mingrade and spell.mingrade > 0 then -- indicating quality makes less sense if it's restricted
			name = string.format('Amulet of %s', spell.name)
		else
			name = string.format('Amulet of %s %s', constants.amulet_grades[rg], spell.name)
		end
		m:set_string('description', sorcery.lib.ui.tooltip {
			title = name;
			color = spell.tone;
			desc = spell.desc;
		})

		if spell.apply then spell.apply {
			stack = stack;
			meta = m;
			user = user;
			self = spell;
		} end
	else
		local spell = sorcery.amulet.getspell(stack)
		m:set_string('description','')
		m:set_string('amulet_rune','')
		m:set_string('amulet_rune_grade','')
		if spell and spell.remove then spell.remove {
			stack = stack;
			meta = m;
			user = user;
			self = spell;
		} end
	end
	return stack
end

sorcery.amulet.stats = function(stack)
	local spell = sorcery.amulet.getspell(stack)
	if not spell then return nil end
	local power = spell.grade
	
	if spell.base_spell then
		-- only consider the default effect of the frame metal
		-- if the frame doesn't totally override the spell
		power = power * (spell.framestats and spell.framestats.power or 1)
	end

	return {
		power = power;
	}
end

sorcery.amulet.getrune = function(stack)
	local m = stack:get_meta()
	if not m:contains('amulet_rune') then return nil end
	local rune = m:get_string('amulet_rune')
	local grade = m:get_int('amulet_rune_grade')
	local rs = ItemStack(sorcery.data.runes[rune].item)
	rune_set(rs, {grade = grade})
	return rs
end

sorcery.amulet.getspell = function(stack)
	local m = stack:get_meta()
	local proto = stack:get_definition()._sorcery.amulet
	if not m:contains('amulet_rune') then return nil end
	local rune = m:get_string('amulet_rune')
	local rg = m:get_int('amulet_rune_grade')
	local rd = sorcery.data.runes[rune]
	local spell = rd.amulets[proto.base]
	if not spell then return nil end
	local title,desc,cast,apply,remove,mingrade,sound = spell.name, spell.desc, spell.cast, spell.apply, spell.remove, spell.mingrade,spell.sound -- FIXME in serious need of refactoring
	local base_spell = true

	if proto.frame and spell.frame and spell.frame[proto.frame] then
		local sp = spell.frame[proto.frame]
		if not sp.mingrade or rg >= sp.mingrade then
			title = sp.name or title
			desc = sp.desc or desc
			cast = sp.cast or cast
			apply = sp.apply or apply
			remove = sp.remove or remove
			mingrade = sp.mingrade or mingrade
			sound = sp.sound or sound
			base_spell = false
		end
	end
	
	-- PLEASE, GOD, REFACTOR ME
	return {
		rune = rune, grade = rg;
		spell = spell, mingrade = mingrade;
		name = title, desc = desc, sound = sound;
		cast = cast, apply = apply, remove = remove;
		frame = proto.frame;
		framestats = proto.frame and sorcery.data.metals[proto.frame].amulet;
		tone = sorcery.lib.color(rd.tone);
		base_spell = base_spell;
	}
end

local runeforge_update = function(pos,time)
	local m = minetest.get_meta(pos)
	local i = m:get_inventory()
	local l = sorcery.ley.netcaps(pos,time or 1)
	local probe = sorcery.spell.probe(pos)

	local pow_min = l.self.powerdraw >= l.self.minpower
	local pow_max = l.self.powerdraw >= l.self.maxpower
	local has_phial = function() return not i:is_empty('phial') end

	if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes
		local phial = i:get_stack('phial',1)
		local int, powerfac = calc_phial_props(phial)
		local pf = phial:get_meta():get_int('force')
		local rolls = math.floor(time/int)
		local newrunes = {}
		for _=1,rolls do
			local choices = {}
			for name,rune in pairs(sorcery.data.runes) do
				local powreq = (rune.minpower*powerfac)*time 
				if powreq <= l.self.powerdraw then
					choices[#choices + 1] = rune
				end
			end
			for k,v in pairs(choices) do print(' * choice',k,v.item) end
			if #choices > 0 then
				for try = 1,#choices do
					local _, choice = sorcery.lib.tbl.pick(choices)
					local adjrare = math.max(2, choice.rarity - pf)
					if math.random(adjrare) == 1 then
						newrunes[#newrunes + 1] = ItemStack(choice.item)
						break
					end
				end
			end
			-- print('rune choices:',dump(choices))
			-- print('me',dump(l.self))
		end

		for _,r in pairs(newrunes) do
			if i:room_for_item('cache',r) and has_phial() then
				local qual
				-- iterate through qualities from highest to lowest, rolling against the phial's
				-- distribution for each, and stopping when we find one
				local qdist = phial:get_definition()._proto.data.dist
				for i=#constants.rune_grades,1,-1 do
					local chance = qdist[constants.rune_grades[i]]
					if chance == 1 or math.random() <= chance then
						qual = i
						break
					end
				end
				rune_set(r,{grade = qual})
				i:add_item('cache',r)
				-- consume a phial
				local ph = i:get_stack('phial',1)
				ph:take_item(1) i:set_stack('phial',1,ph)
				minetest.add_item(pos,i:add_item('refuse',ItemStack(sorcery.register.residue.db[ph:get_name()])))
			else break end
		end
	end

	has_phial = has_phial()
	local spec = string.format([[
		formspec_version[3] size[10.25,8] real_coordinates[true]
		list[context;amulet;3.40,1.50;1,1;]
		list[context;active;5.90,1.50;1,1;]

		list[context;wrench;1.25,1.75;1,1;]
		list[context;phial;7.25,1.75;1,1;]
		list[context;refuse;8.50,1.75;1,1;]

		list[current_player;main;0.25,3;8,4;]

		style_type[list;size=0.8]
		list[context;cache;%f,0.25;%u,1;]

		image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
	]], (10.5 - 0.8*(constants.rune_cache_max*1.25))/2, constants.rune_cache_max,
		((not (has_phial and pow_min)) and 'off'  ) or
		( probe.disjunction            and 'blue' ) or
	    ((has_phial and pow_max)       and 'green') or 'yellow')

	local ghost = function(slot,x,y,img)
		if i:is_empty(slot) then spec = spec .. string.format([[
			image[%f,%f;1,1;%s.png]
		]], x,y,img) end
	end

	ghost('active',5.90,1.50,'sorcery_ui_ghost_rune')
	ghost('amulet',3.40,1.50,'sorcery_ui_ghost_amulet')
	ghost('wrench',1.25,1.75,'sorcery_ui_ghost_rune_wrench')
	ghost('phial',7.25,1.75,'vessels_shelf_slot')
	
	m:set_string('formspec',spec)

	if i:is_empty('phial') then return false end
	return true
end

local rfbox = {
	type = 'fixed';
	fixed = {
		-0.5, -0.5, -0.5;
		 0.5,  0.1,  0.5;
	};
};
minetest.register_node('sorcery:runeforge', {
	description = 'Rune Forge';
	drawtype = 'mesh';
	mesh = 'sorcery-runeforge.obj';
	sunlight_propagates = true;
	paramtype = 'light';
	paramtype2 = 'facedir';
	selection_box = rfbox;
	collision_box = rfbox;
	groups = {
		choppy = 2;
		dig_immediate = 2;
		sorcery_magitech = 1;
		sorcery_tech = 1;
		sorcery_ley_device = 1;
	};
	tiles = {
		'default_diamond_block.png';
		'default_tin_block.png';
		'sorcery_metal_iridium_shiny.png';
		'sorcery_metal_vidrium_shiny.png';
		'default_copper_block.png';
	};
	_sorcery = {
		ley = {
			mode = 'consume';
			affinity = {'praxic'};
			power = function(pos,time)
				local i = minetest.get_meta(pos):get_inventory()
				if i:is_empty('phial') then return 0 end
				local phial = i:get_stack('phial',1)

				local max,min = 0
				for _,r in pairs(sorcery.data.runes) do
					if r.minpower > max then max = r.minpower end
					if min == nil or r.minpower < min then min = r.minpower end
				end
				-- high-quality phials reduce power usage
				local fac = select(2, calc_phial_props(phial))
				min = min / fac  max = max / fac
				return min*time,max*time
			end;
		};
		on_leychange = runeforge_update;
		recipe = {
			note = 'Periodically creates runes when sufficiently powered and can be used to imbue them into an amulet, giving it a powerful magical effect';
		};
	};
	on_construct = function(pos)
		local m = minetest.get_meta(pos)
		local i = m:get_inventory()
		i:set_size('cache',constants.rune_cache_max)
		i:set_size('wrench',1) i:set_size('phial',1) i:set_size('refuse',1)
		i:set_size('amulet',1) i:set_size('active',1)
		m:set_string('infotext','Rune Forge')
		runeforge_update(pos)
	end;
	after_dig_node = sorcery.lib.node.purge_only {'amulet','wrench','refuse','phial'};
	on_timer = runeforge_update;
	on_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
		local inv = minetest.get_meta(pos):get_inventory()
		local wrench if not inv:is_empty('wrench') then
			wrench = inv:get_stack('wrench',1):get_definition()._proto
		end
		local wwear = function(cap)
			local s = inv:get_stack('wrench',1)
			local wear = 65535 / wrench.powers[cap]
			s:add_wear(wear)
			inv:set_stack('wrench',1,s)
		end
		if fl == 'active' then
			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1),nil,user))
			-- only special wrenches can extract runes intact
			if wrench.powers.extract then wwear 'extract'
				minetest.sound_play('sorcery_chime', { pos = pos, gain = 0.5 })
			elseif wrench.powers.purge then wwear 'purge'
				inv:set_stack(tl,ti,ItemStack(nil))
				minetest.sound_play('sorcery_disjoin', { pos = pos, gain = 0.5 })
			end
		elseif tl == 'active' and wrench.powers.imbue then
			local amulet = sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti), user)
			local spell = sorcery.amulet.getspell(amulet)
			sorcery.vfx.enchantment_sparkle({
				under = pos;
				above = vector.add(pos,{x=0,y=1,z=0});
			}, spell.tone:brighten(1.2):hex())
			minetest.sound_play('xdecor_enchanting', { pos = pos, gain = 0.5 })
			inv:set_stack('amulet',1,amulet)
			wwear 'imbue'
		end
		-- trigger the update early to clean up the ghost image :/
		-- minetest needs a cleaner way to handle these
		runeforge_update(pos)
	end;
	on_metadata_inventory_put = function(pos, list, idx, stack, user)
		local inv = minetest.get_meta(pos):get_inventory()
		if list == 'amulet' then
			inv:set_stack('active',1,ItemStack(sorcery.amulet.getrune(stack)))
		end
		runeforge_update(pos)
		if not inv:is_empty('phial') then
			minetest.get_node_timer(pos):start(calc_phial_props(inv:get_stack('phial',1)))
		end
	end;
	on_metadata_inventory_take = function(pos, list, idx, stack, user)
		if list == 'amulet' then
			minetest.get_meta(pos):get_inventory():set_stack('active',1,ItemStack())
		end
		runeforge_update(pos)
	end;
	allow_metadata_inventory_put = function(pos,list,idx,stack,user)
		if list == 'amulet' then
			if minetest.get_item_group(stack:get_name(), 'sorcery_amulet') ~= 0 then
				return 1
			end
		end
		if list == 'phial' then
			if minetest.get_item_group(stack:get_name(), 'sorcery_phial') ~= 0 then
				return stack:get_count()
			end
		end
		if list == 'wrench' then
			if minetest.get_item_group(stack:get_name(), 'sorcery_rune_wrench') ~= 0 then
				return 1
			end
		end
		return 0
	end;
	allow_metadata_inventory_take = function(pos,list,idx,stack,user)
		if list == 'amulet' or list == 'wrench' then return 1 end
		if list == 'phial' or list == 'refuse' then return stack:get_count() end
		return 0
	end;
	allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
		local inv = minetest.get_meta(pos):get_inventory()
		local probe = sorcery.spell.probe(pos)
		local wrench if not inv:is_empty('wrench') then
			wrench = inv:get_stack('wrench',1):get_definition()._proto
		end
		if fl == 'cache' then
			if probe.disjunction then return 0 end
			if tl == 'cache' then return 1 end
			if tl == 'active' and inv:is_empty('active') then
				if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
					local amulet = inv:get_stack('amulet',1)
					local rune = inv:get_stack(fl,fi)
					local runeid = rune:get_definition()._proto.id
					local runegrade = rune:get_meta():get_int('rune_grade')
					if sorcery.data.runes[runeid].amulets[amulet:get_definition()._sorcery.amulet.base] then
						local spell do -- haaaack
							local i=ItemStack(amulet:get_name())
							local im = i:get_meta()
							im:set_string('amulet_rune',runeid)
							im:set_int('amulet_rune_grade',runegrade)
							spell = sorcery.amulet.getspell(i)
						end
						if not spell.mingrade or runegrade >= spell.mingrade then
							return 1
						end
					end
				end
			end
		end
		if fl == 'active' then
			if probe.disjunction then return 0 end
			if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end
		end
		return 0
	end;
})

do local m = sorcery.data.metals
	-- temporary recipe until a fancier multi-part crafting path can be come up with
	-- TODO: better than this
	minetest.register_craft {
		output = 'sorcery:runeforge';
		recipe = {
			{'default:copper_ingot',m.vidrium.parts.block,'default:copper_ingot'};
			{'default:diamond',m.iridium.parts.ingot,'default:diamond'};
			{'default:tin_ingot','sorcery:core_syncretic','default:tin_ingot'};
		};
	}
end