sorcery  Artifact [f2cf52d41a]

Artifact f2cf52d41acdd72f0144f8deae666614b35829797bb9dd68bffa7265bc0898bf:

  • File runeforge.lua — part of check-in [147592b8e9] at 2020-10-26 03:58:08 on branch trunk — add over-time spellcasting abstraction to enable metamagic and in particular disjunction, add more animations and sound effects, add excavation spell, possibly some others, forget when the last commit was, edit a bunch of magitech to make it subject to the disjunction mechanism (throw up a disjunction aura and waltz right through those force fields bby, wheee), also illumination spells, tweak runeforge and rune frequence to better the balance and also limit player frustration, move some math functions into their own library category, various tweaks and bugfixes, probably other shit i don't remember (user: lexi, size: 16843) [annotate] [blame] [check-ins using]

-- 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

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

	rune_cache_max = 4;
	-- 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'};
		simple   = {grade = 2; name = 'Simple';   infusion = 'sorcery:powder_silver'};
		great    = {grade = 3; name = 'Great';    infusion = 'sorcery:powder_gold'};
		splendid = {grade = 4; name = 'Splendid'; infusion = 'sorcery:powder_electrum'};
		exalted  = {grade = 5; name = 'Exalted';  infusion = 'sorcery:powder_levitanium'};
		supreme  = {grade = 6; name = 'Supreme';  infusion = 'sorcery:essence_force'};
	};
}
local calc_phial_props = function(phial) --> mine interval: float, time factor: float
	local g = phial:get_definition()._proto.data.grade
	local i = constants.rune_mine_interval 
	local fac = (g-1) / 5
	return 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
	minetest.register_craftitem(id, {
		description = sorcery.lib.color(rune.tone):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)

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);
	sorcery.register_potion_tbl {
		name = id;
		label = f('%s Phial',p.name);
		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.";
		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, data = p };
		};
	}
	sorcery.register.infusions.link {
		infuse = p.infusion;
		into = 'sorcery:potion_subtle';
		output = 'sorcery:'..id;
	}
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.minrune 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_string('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 = spell.name, spell.desc, spell.cast, spell.apply, spell.remove, spell.mingrade -- 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]
		title = sp.name or title
		desc = sp.desc or desc
		cast = sp.desc or cast
		apply = sp.apply or apply
		remove = sp.remove or remove
		mingrade = sp.mingrade or remove
		base_spell = false
	end
	
	return {
		rune = rune, grade = rg;
		spell = spell, mingrade = mingrade;
		name = title, desc = desc;
		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 int, powerfac = calc_phial_props(i:get_stack('phial',1))
		local rolls = math.floor(time/int)
		local newrunes = {}
		for _=1,rolls do
			local choices = {}
			for name,rune in pairs(sorcery.data.runes) do
				-- print('considering',name)
				-- print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower)
				if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then
					choices[#choices + 1] = rune
				end
			end
			if #choices > 0 then
				-- if multiple runes were rolled up, be nice to the player
				-- and pick the rarest one to give them
				local rare, choice = 0
				for i,c in pairs(choices) do
					if c.rarity > rare then
						rare = c.rarity
						choice = c
					end
				end
				newrunes[#newrunes + 1] = ItemStack(choice.item)
			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 = math.random(#constants.rune_grades)
				rune_set(r,{grade = qual})
				i:add_item('cache',r)
				-- consume a phial
				local ph = i:get_stack('phial',1)
				local n = ph:get_name()
				ph:take_item(1) i:set_stack('phial',1,ph)
				minetest.add_item(pos,i:add_item('refuse',ItemStack(sorcery.register.residue.db[n])))
			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;cache;%f,0.25;%u,1;]
		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;]

		image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
	]], (10.5 - 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;
		oddly_breakable_by_hand = 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'};
	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
				print(dump(wrench))
				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