sorcery  infuser.lua at [17322fc1b2]

File infuser.lua artifact 5216402488 part of check-in 17322fc1b2


local log = sorcery.logger('infuser')

local infuser_formspec = function(percent,active)
	return string.format([[
		size[8,7]
		list[context;infusion;3.5,0;1,1;]
		list[context;potions;2.5,1.7;1,1;0]
			list[context;potions;3.5,2;1,1;1]
			list[context;potions;4.5,1.7;1,1;2]
			image[2.5,1.7;1,1;vessels_shelf_slot.png]
			image[3.5,2;1,1;vessels_shelf_slot.png]
			image[4.5,1.7;1,1;vessels_shelf_slot.png]
		image[3.5,1;1,1;gui_furnace_arrow_bg.png^[lowpart:%d:gui_furnace_arrow_fg.png^[transformR180]
		list[current_player;main;0,3.3;8,4;]
		animated_image[2.5,0;1,2;tube_l;sorcery_ui_infuse_tube.png;28;%s;28]
		animated_image[4.5,0;1,2;tube_l;sorcery_ui_infuse_tube.png^[transformFX;28;%s;28]
		listring[context;infusion]
		listring[current_player;main]
		listring[context;potions]
		listring[current_player;main]
		listring[context;infusion]
	]], percent,
		active and '71' or '0',
		active and '71' or '0')
end

local infuser_stop = function(pos)
	local meta = minetest.get_meta(pos)
	meta:set_float('runtime', 0)
	meta:set_string('formspec', infuser_formspec(0))
	meta:set_string('infotext', 'Infuser')
	minetest.get_node_timer(pos):stop()
end
sorcery.alchemy = {}

sorcery.alchemy.is_module = function(name)
	local def = minetest.registered_nodes[name]
	if def and def._sorcery and def._sorcery.alchemy and def._sorcery.alchemy.module then
		return def._sorcery.alchemy.module
	else return nil end
end

local infuser_mods = function(pos)
	local getmod = function(pos)
		local st = sorcery.lib.node.force(pos)
		return sorcery.alchemy.is_module(st.name)
	end

	local st = sorcery.lib.node.force(pos)
	if st.name ~= 'sorcery:infuser' then return false end
	local devs = {}

	repeat
		pos = vector.offset(pos, 0, -1, 0)
		local mod = getmod(pos)
		if mod then
			if mod.attach == 'stack' then
				devs[#devs+1] = { pos = pos, mod = mod }
				if mod.stmod_connect then
					for _,ofs in pairs(mod.stmod_connect) do
						local wh = vector.add(pos,ofs)
						local stm = getmod(wh)
						if stm and stm.attach == 'stand' then
							-- TODO check facing correct direction
							devs[#devs+1] = {pos = wh, mod = stm}
						end
					end
				end
				if mod.endstack then break end
			else break end -- invalid attachment
		else break end
	until false

	local props = {}

	local comp = function(a,b) return function(...) return a(b(...)) end end
	local mult = function(a,b) return a * b end;
	local sum  = function(a,b) return a + b end;
	local combine = {
		brewtime = mult;
		onbrew = function(a, b)
			return function(ctx)
				ctx.stack = b(ctx)
				return a(ctx)
			end
		end;
	}

	for _, d in pairs(devs) do
		for k,v in pairs(d.mod) do
			if combine[k] then
				if props[k]
					then props[k] = (combine[k])(v, props[k])
					else props[k] = v
				end
			end
		end
	end
 
	return props, devs
end

local elixir_can_apply = function(elixir, potion)
	-- accepts an elixir def and potion def
	if elixir        == nil or
	   potion        == nil then return false end

	if elixir.apply and potion._proto and potion._proto.quals then
		-- the ingredient is an elixir and at least one potion has a
		-- quality that can be enhanced
		if elixir.qual and potion._proto and not potion._proto.quals[elixir.qual] then
			-- does the elixir have a property used to denote
			-- compatibility? if so, check the potion to see if it's
			-- marked as incompatible
			return false
		else
			return true
		end
	end
	
	return false
end

local effects_table = function(potion)
	local meta = potion:get_meta()
	local tbl = {}
	for k,v in pairs(sorcery.data.elixirs) do
		if not v.qual then goto skip end
		local val = meta:get_int(v.qual)
		if val > 0 then
			local aff, title, desc = v.describe(potion)
			if val > 3 then title = title .. ' x' .. val
			elseif val == 3 then title = 'thrice-' .. title
			elseif val == 2 then title = 'twice-' .. title
			end
			tbl[#tbl + 1] = {
				title = sorcery.lib.str.capitalize(title);
				desc = desc;
				affinity = aff;
			}
		end
	::skip::end
	return tbl
end

sorcery.alchemy.elixir_apply = function(elixir, potion)
	if not potion then return end
	local pdef = potion:get_definition()
	if elixir_can_apply(elixir, pdef) then
		elixir.apply(potion, pdef._proto)
		potion:get_meta():set_string('description', sorcery.lib.ui.tooltip {
			title = string.format('%s %s', pdef._proto.name, pdef._proto.kind.label);
			desc = pdef._proto.desc;
			color = sorcery.lib.color(pdef._proto.color):readable();
			props = effects_table(potion);
		});
	end
	return potion
end

sorcery.alchemy.infuse = function(infusion, potions)
	local ingredient = infusion:get_name()
	local creations = {}
	local brewed = false
	for i = 1,#potions do
		if potions[i]:is_empty() then goto skip end
		local base = potions[i]:get_name()
		local potion = potions[i]:get_definition()
		local elixir = infusion:get_definition()
		brewed = true
		if elixir_can_apply(elixir._proto, potion) then
			local newstack = sorcery.alchemy.elixir_apply(elixir._proto, potions[i])
			if newstack ~= nil then
				creations[i] = {
					output = newstack;
					elixir = elixir;
					enhance = true;
				}
			else return true end
		else
			for _,v in pairs(sorcery.register.infusions.db) do
				if v.infuse == ingredient and v.into == base then
					-- transform the base into the infusion
					local out = ItemStack(v.output)
					if out ~= nil then
						creations[i] = {
							recipe = v;
							output = out;
							proto = minetest.registered_items[v.output]._proto;
							brew = true;
						}
					else return true end
				end
			end
		end
	::skip:: end

	local residue = ItemStack(sorcery.register.residue.db[ingredient])
	return creations, residue
end

local infuser_timer = function(pos, elapsed)
	local meta = minetest.get_meta(pos)

	local inv = meta:get_inventory()
	local infusion = inv:get_list('infusion')
	local potions = inv:get_list('potions')
	local elixir = infusion[1]:get_definition()
	local probe = sorcery.spell.probe(pos)
	local fx = infuser_mods(pos)
	local sparkle_color = {sorcery.lib.color(255, 0, 145)};
	if probe.disjunction then return true end

	local potionct = 0

	local cancel = true
	do
		local ingredient -- *eyeroll*
		if infusion[1]:is_empty() then goto cancel end
		ingredient = infusion[1]:get_name()
		for i = 1,#potions do
			if potions[i]:is_empty() then goto skip end
			potionct = potionct + 1
			local base = potions[i]:get_name()
			local potion = potions[i]:get_definition()
			if elixir_can_apply(elixir._proto,potion) then
				-- at least one combination makes a valid potion;
				-- we can start the infuser
				if elixir._proto.color then
					sparkle_color[#sparkle_color+1] = sorcery.lib.color(elixir._proto.color)
				end
				cancel = false
			end
			for _,v in pairs(sorcery.register.infusions.db) do
				if v.infuse == ingredient and v.into == base then
					-- at least one combination makes a valid
					-- potion; we can start the infuser
					if v.output.data and v.output.data.color then
						sparkle_color[#sparkle_color+1] = sorcery.lib.color(v.output.data.color)
					end
					cancel = false
				end
			end
		::skip:: end

		::cancel:: if cancel then
			infuser_stop(pos)
			return false
		end

		::start::
	end

	local time = meta:get_float("runtime") or 0
	local newtime = time + elapsed
	local infusion_time = potionct * (15 * 60) * (fx.brewtime or 1)-- 15 minutes per potion
		-- FIXME make dependent on recipe
	local percent = math.min(100, math.floor(100 * (newtime / infusion_time)))
	local spawn = function(particle, scale, amt)
		minetest.add_particlespawner {
			amount = amt / 6;
			time = 2;
			minpos = pos;
			maxpos = pos;
			minvel = {x = -0.1, y = 0.05, z = -0.1};
			maxvel = {x = 0.1, y = 0.1, z = 0.1};
			minacc = {x = 0, y = 0.1, z = 0};
			maxacc = {x = 0, y = 0.4, z = 0};
			minexptime = 2;
			maxexptime = 4;
			minsize = 0.5 * scale;
			maxsize = 3 * scale;
			glow = 14;
			texture = particle;
			animation = {
				type = "vertical_frames";
				aspect_h = 16;
				aspect_w = 16;
				length = 4.1;
			};
		}
	end
	local spark = sorcery.lib.image('sorcery_spark.png')
	for i = 1,4 do
		local fac = 1 / i
		local _, spc = sorcery.lib.tbl.pick(sparkle_color)
		local sp = spark:glow(spc)

		spawn(sp:render(), fac, (32/fac) * 4)
	end

	local discharge = sorcery.lib.node.discharger(pos)
	
	if newtime >= infusion_time then
		-- finished
		local ingredient = infusion[1]:get_name()
		local result, residue = sorcery.alchemy.infuse(infusion[1], potions)
		for i, r in pairs(result) do
			local out = r.output
			if r.brew then
				if fx.onbrew then out = fx.onbrew {
					pos = pos;
					stack = out;
					potion = r.proto;
					infusion = infusion[1];
					recipe = r.recipe;
				} end
				log.act(string.format('an infuser at %s has brewed a %s potion',
					minetest.pos_to_string(pos), out:get_name()))
			elseif r.enhance then
				if fx.onenhance then out = fx.onenhance {
					pos = pos;
					stack = out;
					potion = r.proto;
					elixir = r.elixir;
				} end
				log.act(string.format('an infuser at %s has enhanced a %s potion with a %s elixir',
					minetest.pos_to_string(pos), out:get_name(), infusion[1]:get_name()))
			end
			inv:set_stack('potions',i,discharge(out))
		end

		inv:set_stack('infusion',1,residue)

		infuser_stop(pos)
		return false
	else
		meta:set_float('runtime', newtime)
		meta:set_string('formspec', infuser_formspec(percent, true))
		meta:set_string('infotext', 'Infuser (active)')
		return true
	end
end

local infuser_start = function(pos)
	local meta = minetest.get_meta(pos)
	infuser_stop(pos)
	infuser_timer(pos,0)
	minetest.get_node_timer(pos):start(2)
end

local infrad = 0.42;
local infbox = {
	type = 'fixed';
	fixed = {
		-infrad, -0.5, -infrad;
		 infrad,  0.5,  infrad;
	};
};

minetest.register_node("sorcery:infuser", {
	description = "Infuser";
	drawtype = "mesh";
	mesh = "sorcery-infuser.obj";
	sunlight_propagates = true;
	paramtype = "light";
	after_dig_node = sorcery.lib.node.purge_container;
	tiles = { -- FIXME
		"default_stone.png",
		"default_copper_block.png",
		"default_steel_block.png",
		"default_bronze_block.png",
		"default_tin_block.png",
	};
	paramtype2 = 'facedir';
	groups = {
		cracky = 2, dig_immediate = 2, heavy = 1;
		sorcery_alchemy = 1, sorcery_magitech = 1;
	};
	_sorcery = {
		recipe = {
			note = 'Infuse special ingredients into liquids to create and alter powerful potions';
		};
	};
	selection_box = infbox, collision_box = infbox;

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		inv:set_size('infusion', 1)
		inv:set_size('potions', 3)
		meta:set_string('infotext','Infuser')
		infuser_timer(pos,0)
	end;

	on_timer = infuser_timer;

	on_metadata_inventory_move = infuser_start;
	on_metadata_inventory_put = infuser_start;
	allow_metadata_inventory_put = function(pos, listname, index, stack, player)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		if not inv:get_stack(listname,index):is_empty() then
			return 0
		end

		if listname == 'infusion' then
			return 1
		elseif listname == 'potions' then
			if minetest.get_item_group(stack:get_name(), "vessel") >= 1 then
				return 1
			end
		end
		return 0
	end;
	allow_metadata_inventory_move = function(pos, from_list, from_idx, to_list, to_idx, count, player)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		if inv:get_stack(to_list, to_idx):is_empty() then
			if to_list == 'potions' and to_list ~= from_list then
				if minetest.get_item_group(
					--[[name]] inv:get_stack(from_list, from_idx):get_name(),
					--[[group]] "vessel") >= 1 then
					return 1
				else
					return 0
				end
			else
				return 1
			end
		else
			return 0
		end
	end;
})

minetest.register_node("sorcery:infuser_accelerator", {
	description = "Infusion Accelerator";
	drawtype = "mesh";
	mesh = "sorcery-infuser-accelerator.obj";
	sunlight_propagates = true;
	paramtype = "light";
	paramtype2 = "facedir";
	tiles = { -- FIXME
		"basic_materials_brass_block.png",
		"default_stone.png",
		"default_copper_block.png",
	};
	paramtype2 = 'facedir';
	groups = {
		cracky = 2, heavy = 1;
		sorcery_alchemy = 1, sorcery_magitech = 1;
	};
	_sorcery = {
		recipe = {
			note = 'Connect to an infuser to speed up its operation; these can be stacked';
		};
		alchemy = {
			module = {
				attach = 'stack';
				brewtime = 0.6;
			};
		};
	};
	selection_box = infbox, collision_box = infbox;
})

sorcery.alchemy.purge_stand = function(pos, node, meta, who)
	sorcery.lib.node.purge_container(pos,node,meta,who);
	local mod = sorcery.alchemy.is_module(node.name)
	if not mod then return end
	if mod.stmod_connect then
		for _,ofs in pairs(mod.stmod_connect) do
			local p = vector.add(pos,ofs)
			local nn = sorcery.lib.node.force(p).name
			local pm = sorcery.alchemy.is_module(nn)
			if pm then -- drop any attached modules
				minetest.dig_node(p)
			end
		end
	end
end

minetest.register_node("sorcery:infuser_stand", {
	description = "Infuser Stand";
	drawtype = "mesh";
	mesh = "sorcery-infuser-stand.obj";
	paramtype2 = "facedir";
	after_dig_node = sorcery.alchemy.purge_stand;
	tiles = { -- FIXME
		"default_silver_sand.png",
		"default_stone.png",
		"default_copper_block.png",
		"default_wood.png",
		"default_aspen_wood.png",
		"default_steel_block.png",
	};
	paramtype2 = 'facedir';
	groups = {
		cracky = 2, choppy = 2, heavy = 1;
		sorcery_alchemy = 1, sorcery_magitech = 1;
	};
	_sorcery = {
		recipe = {
			note = 'Improve the effectiveness of an infuser by using a stand to attach modules';
		};
		alchemy = {
			module = {
				attach = 'stack';
				endstack = true;
				stmod_connect = {
					{x =  1, z =  0, y = 0};
					{x = -1, z =  0, y = 0};
					{x =  0, z =  1, y = 0};
					{x =  0, z = -1, y = 0};
				};
			};
		};
	};
	selection_box = {
		type = 'fixed', fixed = {
			-0.5, -0.5, -0.5,
			 0.5,  0.5,  0.5
		};
	};
	collision_box = {
		type = 'fixed', fixed = {
			-0.5, -0.5, -0.5,
			 0.5,  0.5,  0.5
		};
	};

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		inv:set_size('shelves',6 * 2)
		meta:set_string('infotext','Infuser Stand')
		meta:set_string('formspec', [[
			size[8,6.25]
			list[context;shelves;0,0;3,2;]
			list[context;shelves;5,0;3,2;6]
			list[current_player;main;0,2.5;8,4;]
		]])
	end;
})

minetest.register_craft {
	output = 'sorcery:infuser_stand';
	recipe = {
		{'sorcery:screw_copper','sorcery:conduction_plate','sorcery:screw_copper'};
		{'group:wood','basic_materials:wet_cement','group:wood'};
		{'default:bronze_ingot','sorcery:inversion_matrix','default:bronze_ingot'}
	};
}

minetest.register_craft {
	output = 'sorcery:infuser_accelerator';
	recipe = {
		{'sorcery:brass_ingot','sorcery:conduction_plate','sorcery:brass_ingot'};
		{'basic_materials:motor','sorcery:catalytic_convector','basic_materials:motor'};
		{'default:stone','sorcery:leyline_stabilizer','default:stone'};
	};
}

sorcery.alchemy.register_mod = function(prop, m)
	minetest.register_node(prop.name, sorcery.lib.tbl.merge({
		sunlight_propagates = true;
		paramtype = 'light', paramtype2 = 'facedir';
		drawtype = 'mesh';

		groups = sorcery.lib.tbl.merge({
			cracky = 2;
			sorcery_magitech = 1;
			sorcery_alchemy = 1;
		}, prop.groups or {});

		_sorcery = sorcery.lib.tbl.merge({
			alchemy = {
				module = sorcery.lib.tbl.merge({attach = 'stand'}, prop.module);
			};
			recipe = {
				note = prop.note;
			}
		}, prop.func or {});

		node_placement_prediction = '';

		on_place = function(stack, user, where)
			if not where or where.type ~= 'node' then return nil end
			local nd = minetest.get_node(where.under)
			if not nd then return nil end
			local def = minetest.registered_nodes[nd.name]
			if def and def._sorcery and def._sorcery.alchemy and def._sorcery.alchemy.module then
				local mod = def._sorcery.alchemy.module
				if not mod.stmod_connect then return nil end
				for _, ofs in pairs(mod.stmod_connect) do
					ofs = vector.rotate(ofs, vector.dir_to_rotation(minetest.facedir_to_dir(nd.param2)))
					if vector.equals(vector.add(where.under, ofs), where.above)
						and sorcery.lib.node.is_air(where.above) then goto valid end
				end
				do return nil end

				::valid::
				minetest.set_node(where.above, {
					name = prop.name;
					param2 = minetest.dir_to_facedir(vector.subtract(where.under, where.above));
				})
				stack:take_item(1)
				return stack
			end
		end;
	}, m))
end

sorcery.alchemy.register_mod({
	name = 'sorcery:infuser_mod_prolongator';
	note = 'Attached to an infuser using a stand, a Prolongator will cause it to produce longer-lasting potions';
	module = {
		onbrew = function(ctx)
			return sorcery.alchemy.elixir_apply(sorcery.data.elixirs.Longevity, ctx.stack)
		end;
	}
}, {
	description = 'Infusion Prolongator';
	mesh = 'sorcery-infuser-mod-prolongator.obj';
	inventory_image = 'sorcery_infuser_mod_prolongator.png';
	tiles = {
		sorcery.lib.image('default_diamond_block.png'):multiply(sorcery.lib.color(255,50,150)):render();
		'default_silver_sand.png';
		'default_copper_block.png';
		'default_steel_block.png';
	};
	selection_box = { type = 'fixed', fixed = { -0.23, -0.5, -0.2;  0.5, 0, 0.5; }; };
	collision_box = { type = 'fixed', fixed = { -0.23, -0.5, -0.2;  0.5, 0, 0.5; }; };
})

sorcery.alchemy.register_mod({
	name = 'sorcery:infuser_mod_amplifier';
	note = 'Attached to an infuser using a stand, an Amplifier will cause it to produce more powerful potions';
	module = {
		onbrew = function(ctx)
			return sorcery.alchemy.elixir_apply(sorcery.data.elixirs.Force, ctx.stack)
		end;
	}
}, {
	description = 'Infusion Amplifier';
	mesh = 'sorcery-infuser-mod-amplifier.obj';
	inventory_image = 'sorcery_infuser_mod_amplifier.png';
	tiles = {
		sorcery.lib.image('default_diamond_block.png'):multiply(sorcery.lib.color(167,210,255)):render();
		'default_silver_sand.png';
		'default_copper_block.png';
		'default_steel_block.png';
		'default_wood.png';
		'default_aspen_wood.png';
	};
	selection_box = { type = 'fixed', fixed = { -0.41, -0.5, -0.1;  0.25, 0.22, 0.5; }; };
	collision_box = { type = 'fixed', fixed = { -0.41, -0.5, -0.1;  0.25, 0.22, 0.5; }; };
})

-- sorcery.alchemy.register_mod({
-- 	name = 'sorcery:infuser_mod_auxiliator';
-- 	note = 'Attached to an infuser using a stand, provides an extra potion slot';
-- 	module = {
-- 		onbrew = function(ctx)
-- 		end;
-- 	}
-- }, {
-- 	description = 'Infusion Auxiliator';
-- 	mesh = 'sorcery-infuser-mod-auxiliator.obj';
-- 	inventory_image = 'sorcery_infuser_mod_auxiliator.png';
-- 	tiles = {
-- 		-- sorcery.lib.image('default_diamond_block.png'):multiply(sorcery.lib.color(167,210,255)):render();
-- 	};
-- 	selection_box = { type = 'fixed', fixed = { -0.41, -0.5, -0.1;  0.25, 0.22, 0.5; }; };
-- 	collision_box = { type = 'fixed', fixed = { -0.41, -0.5, -0.1;  0.25, 0.22, 0.5; }; };
-- })

minetest.register_craft {
	output = 'sorcery:infuser_mod_prolongator';
	recipe = {
		-- this recipe is crap + way too cheap, come up with
		-- some fancy new ingredients and use them instead FIXME
		{'basic_materials:silver_wire','stairs:slab_copperblock',''};
		{'default:steel_ingot','sorcery:core_syncretic','default:steel_ingot'};
		{'sorcery:gem_ruby','default:copperblock','sorcery:gem_ruby'};
	};
}

minetest.register_craft {
	output = 'sorcery:infuser_mod_amplifier';
	recipe = {
		-- this recipe is crap + way too cheap, come up with
		-- some fancy new ingredients and use them instead FIXME
		{'basic_materials:silver_wire','stairs:slab_copperblock',''};
		{'group:wood','sorcery:core_syncretic','group:wood'};
		{'sorcery:gem_sapphire','default:copperblock','sorcery:gem_sapphire'};
	};
}


-- other mods:
-- * speed amplifier
-- * randomizer
-- * device that enables some kind of higher-tier potionmaking
-- * device that allows attaching 1 or 2 stand mods without terminating the stack
-- * radia collector - captures spare radia, occasionally can produce an extra potion, sometimes random