sorcery  leylines.lua at [f4a14cad78]

File leylines.lua artifact a10175fdaa part of check-in f4a14cad78


-- contains functions for determining information about
-- the nearest leyline and its currents

sorcery.ley = {}

-- leylines are invisible force-currents that rise up from the core of the earth, carrying magical energy upwards. they weaken as they get closer to the surface. each leyline also has between one and three 'affinities', which control how easily they can be wielded to perform particular sorts of magic. for instance, praxic-affine leylines will charge wands enchanted with praxic spells more quickly than leylines lacking this affinity.
-- leylines are one of two mystic energy forms; the other is aetheric energy, which is beamed down from Heaven by the sun during the day and is the power wielded by the gods. mortals can make limited use of aetheric force by collecting it and beaming it from place to place -- see aether.lua (aether is stored and manipulated by use of diamond)
-- leylines are always available, unlike aetheric force, which can only be collected during the day. but aetheric force is accessible wherever one can see the sky, and the higher up you are, the more you can collect, whereas leylines vary randomly in strength and affinity by position.

sorcery.ley.estimate = function(pos)
	local affs = {
		'praxic'; 'counterpraxic'; 'cognic';
		'mandatic'; 'occlutic'; 'imperic';
		'syncretic'; 'entropic';
	};
	local forcemap = minetest.get_perlin(0xe9a01d, 3, 2, 150)
	local aff1map  = minetest.get_perlin(0x10eb03, 3, 2, 300)
	local aff2map  = minetest.get_perlin(0x491e12, 3, 2, 240)
	local txpos = { 
		x = pos.x;
		y = pos.z; --- :( :( :( :(
		z = pos.y;
	}

	local normalize = function(map)
		local v = map:get_2d(txpos)
		return (v + 6) / 12 -- seriously??
	end

	local zfac = (txpos.z / -1024) + 1
	local force = math.min(1, normalize(forcemap) * zfac)
	local aff1 = affs[math.ceil(#affs * normalize(aff1map))] or 'fail'
	local aff2v, aff2 = math.ceil(normalize(aff2map) * (#affs * 2))
	if aff2v <= #affs then aff2 = affs[aff2v] end

	return {
		force = force;
		aff = { aff1, aff2 };
	}
end

sorcery.ley.chargetype = function(stack)
	if minetest.get_item_group(stack:get_name(),'sorcery_wand') ~= 0 then
		return 'wear'
	else
		local e = sorcery.enchant.get(stack)
		if e and #e.spells > 0 then
			return 'enchant'
		end
	end
	return false
end

sorcery.ley.getcharge = function(stack)
	local chargetype = sorcery.ley.chargetype(stack)
	if not chargetype then return false end
	if chargetype == 'wear' then
		return (65535 - stack:get_wear()) / 15, 65535 / 15
	elseif chargetype == 'enchant' then
		local e = sorcery.enchant.get(stack)
		local mat = sorcery.itemclass.get(stack:get_name(), 'material')
		return e.energy, mat.data.maxenergy
	end
end

sorcery.ley.setcharge = function(stack, charge, overcharge)
	local max = select(2, sorcery.ley.getcharge(stack))
	if not max then return false end
	if charge > max and not overcharge then charge = max end

	local chargetype = sorcery.ley.chargetype(stack)
	if chargetype == 'wear' then
		stack:set_wear(65535 - charge * 15)
	elseif chargetype == 'enchant' then
		local e = sorcery.enchant.get(stack)
		e.energy = charge
		sorcery.enchant.set(stack,e)
	end
	return stack
end

-- leyline energy can be transmitted via a conduit from a leysink. however, it cannot be stored like aetheric energy can be; leyline energy must be drawn when needed unless it is bound up in an enchantment (which simply delays its expression). leysinks provide a constant source of ley-force.
-- there are two nodes for transmitting leyline energy, wires and conduits. wires transmit a limited amount of energy, but are cheap and small. conduits transmit much more, but are expensive and take up full blocks. both are composed of a carrier metal and copper, which prevents the ley-force from leaking out as dangerous radiance.

minetest.register_node('sorcery:conduit', {
	description = 'Conduit';
	tiles = {
		'sorcery_conduit_copper_top.png';
		'sorcery_conduit_copper_top.png';
		'sorcery_conduit_copper_side.png';
	};
	groups = {
		sorcery_ley_device = 1;
		cracky = 3;
	};
	_sorcery = {
		ley = { mode = 'signal'; power = 100 };
		recipe = { note = 'Conducts up to <b>100 thaum</b>' };
	};
})
minetest.register_craft {
	output = 'sorcery:conduit 4';
	recipe = {
		{'default:copper_ingot', 'default:copper_ingot',  'default:copper_ingot'};
		{'default:copper_ingot', 'sorcery:electrumblock', 'default:copper_ingot'};
		{'default:copper_ingot', 'default:copper_ingot',  'default:copper_ingot'};
	};
};

local makeswitch = function(switch, desc, tex, tiles, power)
	for _,active in pairs{true,false} do
		local turn = function(pos)
			local n = minetest.get_node(pos)
			minetest.sound_play('doors_steel_door_open', {
				gain = 0.7;
				pos = pos;
			}, true)
			local leymap = active and sorcery.ley.mapnet(pos) or nil
			minetest.swap_node(pos, {
				name = active and (switch .. '_off')
							   or  switch;
				param1 = n.param1;
				param2 = n.param2;
			})
			if active then
				-- if we're turning it off, use the old map,
				-- because post-swap the network will be
				-- broken and notify won't reach everyone
				leymap.map[leymap.startpos] = nil
				sorcery.ley.notifymap(leymap.map)
			else sorcery.ley.notify(pos) end
		end
		local tl = table.copy(tiles)
		tl[6] = tex .. '^sorcery_ley_switch_panel.png^sorcery_ley_switch_' .. (active and 'down' or 'up') .. '.png';
		minetest.register_node(switch .. (active and '' or '_off'), {
			description = desc;
			drop = switch;
			tiles = tl;
			paramtype2 = 'facedir';
			groups = {
				cracky = 2; choppy = 1;
				punch_operable = 1;
				sorcery_ley_device = active and 1 or 0;
			};
			_sorcery = {
				ley = active and {
					mode = 'signal'; power = power;
				} or nil;
			};
			on_punch = function(pos,node,puncher,point)
				if puncher ~= nil then
					if puncher:get_wielded_item():is_empty() then
						turn(pos)
					end
				end
				return minetest.node_punch(pos,node,puncher,point)
			end;
			on_rightclick = turn;
		})
	end
end

for _,b in pairs {
	{'Applewood', 'wood', 'default_wood.png'};
	{'Junglewood', 'junglewood', 'default_junglewood.png'};
	{'Pine', 'pine_wood', 'default_pine_wood.png'};
	{'Acacia', 'acacia_wood', 'default_pine_wood.png'};
	{'Aspen', 'aspen_wood', 'default_aspen_wood.png'};
	{'Stone', 'stone', 'default_stone.png'};
	{'Cobblestone', 'cobble', 'default_cobble.png'};
	{'Stone Brick', 'stonebrick', 'default_stone_brick.png'};
	{'Brick', 'brick', 'default_brick.png'};
} do
	local id = 'sorcery:conduit_half_' .. b[2]
	local switch = 'sorcery:conduit_switch_' .. b[2]
	local item = (b[4] or 'default') .. ':' .. b[2]
	local tex = b[3]
	local mod = '^[lowpart:50:'
	local sidemod = '^[transformR270^[lowpart:50:'
	local unflip = '^[transformR90'
	local tiles = {
		'sorcery_conduit_copper_top.png'..mod..tex; -- top
		tex..mod..'sorcery_conduit_copper_top.png';
		tex .. sidemod .. 'sorcery_conduit_copper_side.png' .. unflip; -- side
		'sorcery_conduit_copper_side.png' .. sidemod .. tex .. unflip; -- side
		'sorcery_conduit_copper_side.png'; -- back
		tex; -- front
	}
	minetest.register_node(id, {
		description = 'Half-' .. b[1] .. ' Conduit';
		paramtype2 = 'facedir';
		groups = {
			cracky = 2;
			choppy = 1;
			sorcery_ley_device = 1;
		};
		_sorcery = {
			ley = { mode = 'signal'; power = 50 };
			recipe = { note = 'Conducts up to <b>50 thaum</b>' };
		};
		tiles = tiles;
	})
	minetest.register_craft {
		output = id .. ' 4';
		recipe = {
			{item, 'sorcery:conduit'};
			{item, 'sorcery:conduit'};
		};
	};
	makeswitch(switch, b[1] .. " Conduit Switch", tex, tiles, 5)
	minetest.register_craft {
		output = switch;
		recipe = {
			{'xdecor:lever_off',id};
		};
	}
end
makeswitch('sorcery:conduit_switch', "Conduit Switch", 'sorcery_conduit_copper_side.png', {
	'sorcery_conduit_copper_top.png';
	'sorcery_conduit_copper_top.png';
	'sorcery_conduit_copper_side.png';
	'sorcery_conduit_copper_side.png';
	'sorcery_conduit_copper_side.png';
	'sorcery_conduit_copper_side.png';
}, 10)
minetest.register_craft {
	output = 'sorcery:conduit_switch';
	recipe = {
		{'xdecor:lever_off','sorcery:conduit'};
	};
}

for name,metal in pairs(sorcery.data.metals) do
	if metal.conduct then
		local cable = 'sorcery:cable_' .. name
		minetest.register_node(cable, {
			description = sorcery.lib.str.capitalize(name) .. " Cable";
			drawtype = 'nodebox';
			groups = {
				sorcery_ley_device = 1; snappy = 3; attached = 1;
				sorcery_ley_cable = 1;
			};
			_sorcery = {
				ley = { mode = 'signal', power = metal.conduct };
				recipe = { note = 'Conducts up to <b>' .. metal.conduct .. ' thaum</b>'; };
			};
			sunlight_propagates = true;
			node_box = {
				type = 'connected';
				disconnected   = { -0.05, -0.35, -0.40; 0.05, -0.25, 0.40 };
				connect_front  = { -0.05, -0.35, -0.50; 0.05, -0.25, 0.05 };
				connect_back   = { -0.05, -0.35, -0.05; 0.05, -0.25, 0.50 };
				connect_right  = { -0.05, -0.35, -0.05; 0.50, -0.25, 0.05 };
				connect_left   = { -0.50, -0.35, -0.05; 0.05, -0.25, 0.05 };
				connect_top    = { -0.05, -0.35, -0.05; 0.05,  0.50, 0.05 };
				connect_bottom = { -0.05, -0.50, -0.05; 0.05, -0.35, 0.05 };
			};
			connects_to = { 'group:sorcery_ley_device', 'default:mese' };
			-- harcoding mese is kind of cheating -- figure out a
			-- better way to do this for the longterm
			paramtype = 'light';
			-- paramtype2 = 'facedir';
			after_place_node = function(pos, placer, stack, point)
				local vec = vector.subtract(point.under, pos)
				local n = minetest.get_node(pos)
				n.param2 = minetest.dir_to_facedir(vec)
				minetest.swap_node(pos,n)
			end;
			tiles = { 'sorcery_ley_plug.png' };
		})

		minetest.register_craft {
			output = cable .. ' 8';
			recipe = {
				{'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'};
				{ metal.parts.fragment, metal.parts.fragment, metal.parts.fragment };
				{'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'};
			};
			replacements = {
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
			};
		};
	end
end

-- ley.notify will normally be called automatically, but if a
-- ley-producer or consume has fluctuating levels of energy
-- consumption, it should call this function when levels change
sorcery.ley.notifymap = function(map)
	for pos,name in pairs(map) do
		local props = minetest.registered_nodes[name]._sorcery
		if props and props.on_leychange then
			props.on_leychange(pos)
		end
	end
end
sorcery.ley.notify = function(pos)
	local n = sorcery.ley.mapnet(pos)
	if n then sorcery.ley.notifymap(n.map) end
end

sorcery.ley.field_to_current = function(strength,time)
	local ley_factor = 0.25
	-- a ley harvester will produce this much current with
	-- access to a full-strength leyline
	
	return (strength * ley_factor) * time;
end

do -- register condenser
	local gem = sorcery.lib.image('default_diamond_block.png')
	local amethyst = gem:multiply(sorcery.lib.color(sorcery.data.gems.amethyst.tone))
	local emerald = gem:multiply(sorcery.lib.color(sorcery.data.gems.emerald.tone))
	local box = {
		type = 'fixed';
		fixed = {
			-0.5, -0.5, -0.5;
			 0.5,  1.2,  0.5;
		};
	};
	minetest.register_node('sorcery:condenser', {
		description = 'Condenser';
		drawtype = 'mesh';
		paramtype2 = 'facedir';
		mesh = 'sorcery-condenser.obj';
		selection_box = box;
		collision_box = box;
		tiles = {
			amethyst:render();
			'sorcery_condenser.png';
			'default_tin_block.png';
			'default_stone.png';
			'default_copper_block.png';
			emerald:render();
		};
		groups = {
			cracky = 2;
			sorcery_ley_device = 1;
			sorcery_magitech = 1;
		};
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			meta:set_string('infotext','Condenser')
		end;
		_sorcery = {
			ley = { mode = 'produce';
				power = function(pos,time)
					return sorcery.ley.field_to_current(sorcery.ley.estimate(pos).force, time);
				end;
				affinity = function(pos)
					return sorcery.ley.estimate(pos).aff
				end;
			};
			recipe = {
				note = 'Captures radiant force and suffuses it through distribution net. Energy production varies with local leyline strength.';
			};
		};
	})
	
	minetest.register_abm {
		name = 'Condenser sound effects';
		nodenames = {'sorcery:condenser'};
		neighbors = {'group:sorcery_ley_device'};
		interval = 5.6, chance = 1, catch_up = false;
		action = function(pos)
			local force = sorcery.ley.estimate(pos).force
			minetest.sound_play('sorcery_condenser_bg', {
				pos = pos, max_hear_distance = 5 + 4*force, gain = force*0.3;
			})
		end;
	}
end

minetest.register_craft {
	output = 'sorcery:condenser';
	recipe = {
		{'sorcery:accumulator'};
		{'sorcery:conduit'};
	};
}
sorcery.ley.txofs = {
	{x =  0, z =  0, y =  0};
	{x = -1, z =  0, y =  0};
	{x =  1, z =  0, y =  0};
	{x =  0, z = -1, y =  0};
	{x =  0, z =  1, y =  0};
	{x =  0, z =  0, y = -1};
	{x =  0, z =  0, y =  1};
}
sorcery.ley.mapnet = function(startpos,power)
	-- this function returns a list of all the nodes accessible from
	-- a ley network and their associated positions
	local net,checked = {},{}
	power = power or 0
	
	local devices = {
		consume = {};
		produce = {};
		signal = {};
	}
	local numfound = 0
	local maxconduct = 0
	local minconduct
	local startkey
	local foundp = function(p)
		for _,k in pairs(checked) do
			if vector.equals(p,k) then return true end
		end
		return false
	end
	-- we're implementing this with a recursive function to start with
	-- but this could rapidly lead to stack overflows so we should
	-- replace it with a linear one at some point
	local function find(positions)
		local searchnext = {}
		for _,pos in pairs(positions) do
			for _,p in pairs(sorcery.ley.txofs) do
				local sum = vector.add(pos,p)
				if not foundp(sum) then
					checked[#checked + 1] = sum
					local nodename = sorcery.lib.node.force(sum).name
					if minetest.get_item_group(nodename,'sorcery_ley_device') ~= 0
					   or sorcery.data.compat.ley[nodename] then
						local d = sorcery.ley.sample(pos,1,nodename,{query={mode=true}})
						assert(d.mode == 'signal'
						    or d.mode == 'consume'
						    or d.mode == 'produce')
						devices[d.mode][#(devices[d.mode]) + 1] = {
							id = nodename; pos = sum;
						}
						if d.mode == 'signal' then
							d.power = sorcery.ley.sample(pos,1,nodename,{query={power=true}}).power
							if d.power > power then
								if minconduct then
									if d.power < minconduct then
										minconduct = d.power
									end
								else minconduct = d.power end
								if d.power > maxconduct then
									maxconduct = d.power
								end
							end
						end
						numfound = numfound + 1;
						net[sum] = nodename;
						if not startkey then
							if vector.equals(startpos,sum) then
								startkey = sum
							end
						end
						searchnext[#searchnext + 1] = sum;
					end
				end
			end
		end
		if #searchnext > 0 then find(searchnext) end
	end

	find{startpos}

	if numfound > 0 then
		return {
			count = numfound;
			map = net;
			devices = devices;
			startpos = startkey;
			conduct = {
				min = minconduct;
				max = maxconduct;
			};
		}
	else return nil end
end

do local afftbl = {
		[1] = 'praxic';   [2] = 'counterpraxic';
		[3] = 'cognic';   [4] = 'syncretic';
		[5] = 'mandatic'; [6] = 'occlutic';
		[7] = 'imperic';  [8] = 'entropic';
	}
	local modetbl = {
		[0] = 'none';
		[1] = 'consume';
		[2] = 'produce';
		[3] = 'signal';
	}
	for i=1,#afftbl  do afftbl [afftbl [i]] = i end
	for i=1,#modetbl do modetbl[modetbl[i]] = i end
	local m = sorcery.lib.marshal
	local enc, dec = m.transcoder {
		mode = m.t.u8;
		minpower = m.t.u32; -- min power generated/consumed * 10,000
		maxpower = m.t.u32; -- max power generated/consumed * 10,000
		affinity = m.g.array(m.t.u8); -- indexes into afftbl
	}
	sorcery.ley.encode = function(l)
		local idxs = {}
		for _,k in pairs(l.affinity) do
			idxs[#idxs+1] = afftbl[k]
		end
		return meta_armor(enc {
			mode = modetbl[l.mode];
			minpower = l.minpower * 10000;
			maxpower = l.maxpower * 10000;
			affinity = idxs;
		}, true)
	end
	sorcery.ley.decode = function(str)
		local obj = dec(meta_dearmor(str,true))
		local affs = {}
		for _,k in pairs(obj.affinity) do
			affs[#affs+1] = afftbl[k]
		end
		return {
			mode = modetbl[obj.mode];
			minpower = obj.minpower / 10000.0;
			maxpower = obj.maxpower / 10000.0;
			power = (obj.minpower == obj.maxpower) and obj.minpower or nil;
			affinity = affs;
		}
	end
end
sorcery.ley.setnode = function(pos,l)
	local meta = minetest.get_meta(pos)
	meta:set_string('sorcery:ley',sorcery.ley.encode(l))
end

sorcery.ley.sample = function(pos,timespan,name,flags)
	-- returns how much ley-force can be transmitted by a
	-- device over timespan
	local ret = {}
	minetest.load_area(pos)
	name = name or minetest.get_node(pos).name
	flags = flags or {}
	flags.query = flags.query or {
		mode = true; power = true; affinity = true;
		minpower = true; maxpower = true;
	}
	local props = minetest.registered_nodes[name]._sorcery

	local evaluate = function(v)
		if type(v) == 'function' then
			return v(pos)
		else return v end
	end

	local leymeta do
		local nm = minetest.get_meta(pos)
		if nm:contains('sorcery:ley') then
			leymeta = sorcery.ley.decode(nm:get_string('sorcery:ley'))
		end
	end

	local compat = sorcery.data.compat.ley[name] 

	local lookup = function(k,default)
		if leymeta and leymeta[k] then return leymeta[k]
		elseif props and props.ley and props.ley[k] then return props.ley[k]
		elseif compat and compat[k] then return compat[k]
		else return default end 
	end
	if flags.query.mode     then ret.mode     = evaluate(lookup('mode','none')) end
	if flags.query.affinity then ret.affinity = evaluate(lookup('affinity',{})) end
	if flags.query.minpower or flags.query.maxpower or flags.query.power then
		local condset = function(name,var)
			if flags.query[name] then ret[name] = var end
		end
		local p = lookup('power')
		if p then
			if type(p) == 'function' then
			-- we have a single function to calculate power usage; we need to
			-- check whether it returns min,max or a constant
				local min, max = p(pos,timespan)
				if (not max) or min == max then
					ret.power = min
					condset('power',min)
					condset('minpower',min)
					condset('maxpower',min)
				else
					condset('minpower',min)
					condset('maxpower',max)
				end
			else -- power usage is simply a constant
				condset('power',p * timespan)
				condset('minpower',p * timespan)
				condset('maxpower',p * timespan)
			end
		else
			local feval = function(v)
				if type(v) == 'function' then
					return v(pos,timespan)
				else return v * timespan end
			end
			local min = feval(lookup('minpower'))
			local max = feval(lookup('maxpower'))
			condset('minpower',min)
			condset('maxpower',max)
			if min == max then condset('power',min) end
		end
	end

	if ret.power then
		if flags.query.minpower and not ret.minpower then ret.minpower = power end
		if flags.query.maxpower and not ret.maxpower then ret.maxpower = power end
	end
	return ret
end

sorcery.ley.netcaps = function(pos,timespan,exclude,minconduct)
	local net = sorcery.ley.mapnet(pos,minconduct)
	local maxpower = 0
	local freepower = 0
	local affs,usedaffs = {},{}
	local flexpowerdevs = {}
	local devself
	for _,n in pairs(net.devices.produce) do
		if vector.equals(pos,n.pos) then devself = n end
		if not exclude or not vector.equals(n.pos,exclude) then
			local ln = sorcery.ley.sample(n.pos,timespan,n.id)
			n.powersupply = ln.power
			n.affinity = ln.affinity
			maxpower = maxpower + ln.power
			-- production power does not vary, tho at some point it
			-- might be useful to enable some kind of power scaling
			for _,a in pairs(ln.affinity) do
				affs[a] = (affs[a] or 0) + 1
			end
		end
	end
	freepower = maxpower;
	for _,n in pairs(net.devices.consume) do
		if vector.equals(pos,n.pos) then devself = n end
		if not exclude or not vector.equals(n.pos,exclude) then
			local ln = sorcery.ley.sample(n.pos,timespan,n.id, {
				query = { power = true; minpower = true; maxpower = true; affinity = true; };
			})
			n.powerdraw = (ln.minpower <= freepower) and ln.minpower or 0
			freepower = freepower - n.powerdraw
			-- merge in sample data and return it along with the map
			n.minpower = ln.minpower
			n.maxpower = ln.maxpower
			n.affinity = ln.affinity
			if ln.maxpower > ln.minpower then
				flexpowerdevs[#flexpowerdevs+1] = n
			end
			for _,a in pairs(ln.affinity) do
				usedaffs[a] = (usedaffs[a] or 0) + 1
			end
		end
	end

	-- now we know the following: all devices; if possible, have been
	-- given the minimum amount of power they need to run. if freepower
	-- < 0 then the network is overloaded and inoperable. if freepower>0,
	-- we now need to distribute the remaining power to devices that
	-- have a variable power consumption. there's no clean way of doing
	-- this, so we use the following algorithm:
	--   1. take a list of devices that want more power
	--   2. divide the amount of free power by the number of such devices
	--      to derive the maximum power that can be allocated to any device
	--   3. iterate through the devices. increase their power consumption by
	--      the maximum term. any device that is satiated can be removed from
	--      the list.
	--   4. if there is still power remaining, repeat until there is not.

	while freepower > 0 and #flexpowerdevs > 0 do
		local nextiter = {}
		local maxgive = freepower / #flexpowerdevs
		for _,d in pairs(flexpowerdevs) do
			local give = math.min(maxgive,d.maxpower - d.powerdraw)
			freepower = freepower - give
			d.powerdraw = d.powerdraw + give
			if d.powerdraw < d.maxpower then
				nextiter[#nextiter+1] = d
			end
		end
		flexpowerdevs = nextiter
	end
	
	return {
		net = net;
		freepower = freepower;
		maxpower = maxpower;
		affinity = affs;
		affinity_balance = usedaffs;
		self = devself;
	}
end

minetest.register_on_placenode(function(pos, node)
	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
		sorcery.ley.notify(pos)
	end
end)

local constants = {
	generator_max_energy_output = 5;
	-- how much energy a generator makes after

	generator_time_to_max_energy = 150;
	-- seconds of activity
	
	generator_power_drain_speed = 0.1;
	-- points of energy output drained per second of no fuel
}
local update_generator = function(pos)
	minetest.get_node_timer(pos):start(1)
end
local generator_update_formspec = function(pos)
	local meta = minetest.get_meta(pos)
	local burnprog = math.min(1,meta:get_float('burnleft') / meta:get_float('burntime'))
	local power = meta:get_float('power')
	local inv = meta:get_inventory()
	local lamps = ''
	for i=0,4 do 
		local color
		if power - i >= 1 then
			color = 'red'
		elseif power - i > 0 then
			color = 'yellow'
		else
			color = 'off'
		end
		lamps = lamps .. string.format([[
			image[%f,0.5;1,1;sorcery_statlamp_%s.png]
		]], 2.5 + i, color)
	end
	meta:set_string('formspec', string.format([[
		size[8,5.8]
		list[context;fuel;0.5,0.5;1,1]
		list[current_player;main;0,2;8,4]
		image[1.5,0.5;1,1;default_furnace_fire_bg.png^[lowpart:%u%%:default_furnace_fire_fg.png]
	]], math.floor(burnprog * 100)) .. lamps)
end
for _,active in pairs{true,false} do
	local id = 'sorcery:generator' .. (active and '_active' or '')
	minetest.register_node(id, {
		description = 'Generator';
		paramtype2 = 'facedir';
		groups = {
			cracky = 2;
			sorcery_ley_device = 1;
			sorcery_device_generator = active and 1 or 2;
			not_in_creative_inventory = active and nil or 1;
		};
		drop = 'sorcery:generator';
		tiles = {
			'sorcery_ley_generator_top.png';
			'sorcery_ley_generator_bottom.png';
			'sorcery_ley_generator_side.png';
			'sorcery_ley_generator_side.png';
			'sorcery_ley_generator_back.png';
			'sorcery_ley_generator_front_' .. (active and 'on' or 'off') .. '.png';
		};
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			meta:set_string('infotext','Generator')
			meta:set_float('burntime',0)
			meta:set_float('burnleft',0)
			meta:set_float('power',0)
			generator_update_formspec(pos)
			inv:set_size('fuel',1)
		end;
		after_dig_node = sorcery.lib.node.purge_container;
		on_metadata_inventory_put = update_generator;
		on_metadata_inventory_take = update_generator;
		on_timer = function(pos,delta)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			local self = sorcery.lib.node.force(pos)
			local timeleft = meta:get_float('burnleft') - delta
			local again = false
			local power = meta:get_float('power')
			local burning = active
			if timeleft < 0 then timeleft = 0 end
			if not active or timeleft == 0 then
				if inv:is_empty('fuel') then
					-- no fuel, can't start/keep going. drain power if
					-- necessary, otherwise bail
					burning = false
					if power > 0 then
						power = math.max(0, power - constants.generator_power_drain_speed)
						again = true
					end
				else
					-- fuel is present, let's burn it
					local res,decin = minetest.get_craft_result {
						method = 'fuel';
						items = {inv:get_stack('fuel',1)};
					}
					meta:set_float('burntime',res.time)
					timeleft = res.time
					inv:set_stack('fuel',1,decin.items[1])
					again = true
					burning = true
				end
			else
				local eps = constants.generator_max_energy_output / constants.generator_time_to_max_energy
				power = math.min(constants.generator_max_energy_output, power + eps*delta)
				again = true
			end
			::stop:: meta:set_float('power',power)
			         meta:set_float('burnleft',timeleft)
					 generator_update_formspec(pos)
					 if burning and not active then
						 minetest.swap_node(pos, {
							 name = 'sorcery:generator_active';
							 param1 = self.param1, param2 = self.param2;
						 })
					 elseif active and not burning then
						 minetest.swap_node(pos, {
							 name = 'sorcery:generator';
							 param1 = self.param1, param2 = self.param2;
						 })
					 end
					 return again
		end;
		allow_metadata_inventory_put = function(pos,listname,index,stack,user)
			local res = minetest.get_craft_result {
				method = 'fuel';
				items = {stack};
			}
			if res.time ~= 0 then return stack:get_count()
				else return 0 end
		end;
		_sorcery = {
			ley = {
				mode = 'produce', affinity = {'praxic'};
				power = function(pos,delta)
					local meta = minetest.get_meta(pos)
					return meta:get_float('power') * delta;
				end;
			};
			recipe = {
				note = 'Temporarily provide up to <b>' ..tostring(constants.generator_max_energy_output) .. ' thaum</b> of ley-force from heat by burning fuel';
			};
		};
	})
end