sorcery  portal.lua at tip

File portal.lua from the latest check-in


local constants = {
	portal_max_height = 5;
	-- maximum y-distance between a portal pad and its
	-- corresponding reflector
	portal_min_height = 1;
	-- minimum y-distance. should probably be 2 but maybe
	-- half-size portals will be useful for transporting
	-- items or something
	portal_jump_time = 8;
	-- number of seconds until teleportation occurs
	portal_jump_cost_local = 1;
	-- energy required to make a jump from one portal
	-- to another on the local circuit
	portal_jump_cost_per_farcaster = 2;
	-- energy cost required to power each farcaster along
	-- the route to the final destination (remote only)
	portal_node_power = 0.1;
	-- amount of power needed for each portal node to operate
	-- and be reachable
}
minetest.register_node('sorcery:portal_pad', {
	description = 'Portal Pad';
	groups = {
		cracky = 2; sorcery_portal_node = 1;
		sorcery_magitech = 1;
	};
	tiles = {
		'sorcery_portal_pad_top.png';
		'sorcery_portal_pad_bottom.png';
		'sorcery_portal_pad_side.png';
		'sorcery_portal_pad_side.png';
		'sorcery_portal_pad_front.png^[transformFX';
		'sorcery_portal_pad_front.png';
	};
})

minetest.register_node('sorcery:portal_reflector', {
	description = 'Portal Reflector';
	groups = {
		cracky = 2; sorcery_portal_node = 1;
		sorcery_magitech = 1;
	};
	tiles = {
		'sorcery_portal_pad_bottom.png';
		'sorcery_portal_pad_top.png';
		'sorcery_portal_pad_side.png^[transformFY';
		'sorcery_portal_pad_side.png^[transformFY';
		'sorcery_portal_pad_front.png^[transformFY^[transformFX';
		'sorcery_portal_pad_front.png^[transformFY';
	};
})



local portal_composition = function(pos)
	-- make sure a reasonable amount of the block is loaded
	-- so we have a shot at finding a working portal
	minetest.load_area(
		vector.add(pos, {x =  5, y =  1 + constants.portal_max_height, z =  5}),
		vector.add(pos, {x = -5, y = -1 - constants.portal_max_height, z = -5})
	)
	-- starting at a portal node, search connected blocks
	-- recursively to locate portal pads and their paired
	-- partners. return a table characterizing the portal
	-- or return false if no portal found
	
	-- first search immediate neighbors. the portal node
	-- can be connected to either reflectors or pads
	local startpoint, startwithpads
	for _, d in pairs(sorcery.lib.node.offsets.neighbors) do
		local sum = vector.add(pos,d)
		local name = minetest.get_node(sum).name
		if name == 'sorcery:portal_pad' then
			startwithpads = true
		elseif name == 'sorcery:portal_reflector' then
			startwithpads = false
		else goto skip end
		startpoint = sum
		do break end
	::skip::end

	-- if startpoint is not set, a valid portal was not found
	if not startpoint then return nil end
	local search, partner
	if startwithpads then search = 'sorcery:portal_pad'
	                     partner = 'sorcery:portal_reflector'
	                 else search = 'sorcery:portal_reflector'
	                     partner = 'sorcery:portal_pad' end

	local firsthalf = sorcery.lib.node.amass(startpoint,{search},sorcery.lib.node.offsets.nextto)
	local min,max,maxheight = nil, nil, 0
	local telepairs = {}
	for pos in pairs(firsthalf) do
		local found = false
		for i=constants.portal_min_height,constants.portal_max_height do
			local p = vector.add(pos, {y=(startwithpads and i or 0-i),x=0,z=0})
			local n = minetest.get_node(p).name
			if n == partner then
				found = true
				maxheight = math.max(maxheight, i)
				telepairs[#telepairs + 1] = {
					pad = startwithpads and pos or p;
					reflector = startwithpads and p or pos;
					height = i;
				}
			elseif n ~= 'air' and minetest.get_item_group(n,'air') == 0 then
				goto skippad
			end
		end
		if found then
			if min then min = {
				x = math.min(min.x, pos.x);
				y = math.min(min.y, pos.y);
				z = math.min(min.z, pos.z);
			} else min = pos end
			if max then max = {
				x = math.max(max.x, pos.x);
				y = math.max(max.y, pos.y);
				z = math.max(max.z, pos.z);
			} else max = pos end
		end
	::skippad::end

	if #telepairs == 0 then return nil end

	return {
		nodes = telepairs;
		dimensions = {
			padmin = min;
			padmax = max;
			height = maxheight;
		};
	}
end

local caster_farnet = function(pos)
	local tune = sorcery.attunement.verify(pos)
	if not tune then return nil end 

	local partner = minetest.get_node(tune.partner)
	if partner.name ~= 'sorcery:farcaster' then return nil end

	local ptune = sorcery.attunement.verify(tune.partner)
	if ptune.code == tune.code and vector.equals(ptune.partner, pos) then
		return tune.partner
	end
	return nil
end

local portal_context = {
	users = {}
}

local portal_circuit = function(start)
	local circuit = {}
	local network = sorcery.farcaster.junction(start,constants.portal_jump_cost_local) -- get the ley of the land. GEDDIT)
	for _, n in pairs(network) do
		for _, d in pairs(n.caps.net.devices.consume) do
			if d.id == 'sorcery:portal_node' and portal_composition(d.pos) then
				circuit[#circuit+1] = {
					pos = d.pos;
					hops = n.hops;
					route = n.route;
				}
			end
		end
	end
	return circuit
end

local portal_disposition = function(dev)
	local dim = vector.subtract(dev.dimensions.padmax, dev.dimensions.padmin)
	local radius = math.max(dim.x, dim.z, dev.dimensions.height)
	local center = vector.add(vector.divide(dim,2), dev.dimensions.padmin)
	local objs = minetest.get_objects_inside_radius(center, radius)
	local users = {}
	local occupads = {}
	for _,obj in pairs(objs) do
		if minetest.is_player(obj) then
			local pos = obj:get_pos()
			for _,n in pairs(dev.nodes) do
				-- is the player between a pad and reflector pair?
				if math.abs(pos.x - n.pad.x) < 0.5 and
				   math.abs(pos.z - n.pad.z) < 0.5 and
				   pos.y >= n.pad.y and pos.y <= n.reflector.y then
				   users[#users+1] = {
					   object = obj;
					   slot = n;
				   }
				   occupads[n] = true
				   goto skip
			   end
			end
		end
	::skip::end

	local freepads = {}
	for _,n in pairs(dev.nodes) do
		if not occupads[n] then
			freepads[#freepads+1] = n
		end
	end

	return {
		users = users;
		freepads = freepads;
		radius = radius;
		center = center;
		bounds = dim;
	}
end

local portal_destination_evaluate = function(circuit,pos)
	-- evaluation of the local network occurs before this function
	-- is ever even called, so we only need to worry about the
	-- farcaster-related transmission costs
	for i,c in pairs(circuit) do
		if vector.equals(c.pos,pos) then
			-- the destination is listed in the circuit table
			for j,r in pairs(c.route) do
				local nc = sorcery.ley.netcaps(pos,1)
				-- print('checking route for sufficient energy to power farcasters', j, nc.freepower)
				if nc.freepower < constants.portal_jump_cost_per_farcaster then
					return false -- only one route to any given portal node
					-- will be listed in the circuit table, so bail early
					-- maybe in the future farcasters should charge up,
					-- and power should be deducted when they are used?
				end
			end
			-- we've tested every leg of the route, sufficient power is
			-- available
			return true
		end
	end
	return false
end

local portal_pick_destination = function(dev,circuit,partner)
	if partner then
		if portal_destination_evaluate(circuit,partner)
			then return partner end
	end

	local scrambled = sorcery.lib.tbl.scramble(circuit)
	for i=1,#scrambled do
		if portal_destination_evaluate(circuit,scrambled[i].pos)
			then return scrambled[i].pos end
	end
end

-- minetest.register_lbm {
-- 	name = 'sorcery:activate_portals';
-- 	label = 'activate portals';
-- 	run_at_every_load = true;
-- 	nodenames = { 'sorcery:portal_node' };
-- 	action = function(pos,node)
-- 		minetest.get_node_timer(pos).start(2)
-- 	end;
-- }

minetest.register_node('sorcery:portal_node', {
	description = 'Portal Node';
	groups = {
		cracky = 2; sorcery_portal_node = 2;
		sorcery_ley_device = 1;
		sorcery_magitech = 1;
	};
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string('infotext','Portal Node')
		minetest.get_node_timer(pos):start(1)
	end;
	on_timer = function(pos,delta)
		local dev = portal_composition(pos)
		if not dev then return false end
		local dsp = portal_disposition(dev)
		local crc = portal_circuit(pos)
		local cap = sorcery.ley.netcaps(pos,delta)
		local tune = sorcery.attunement.verify(pos)
		local partner -- raw position of partner node, if any
		local probe = sorcery.spell.probe(pos)
		if tune and tune.partner then
			minetest.load_area(tune.partner)
			-- we are attuned to a partner, but is it in the circuit?
			for _,v in pairs(crc) do
				if vector.equals(v.pos,tune.partner) then
					partner = tune.partner
					break
				end
			end
		end

		if cap.self.minpower ~= cap.self.powerdraw then return true end

		-- clean out user table
		for name,user in pairs(portal_context.users) do
			if user and vector.equals(user.portal, pos) then
				local found = false
				if not probe.disjunction then
					for _,u in pairs(dsp.users) do
						if u.object:get_player_name() == name then
							found = true
						end
					end
				end
				if not found then
					if user.sound then minetest.sound_fade(user.sound,1,0) end
					portal_context.users[name] = nil
				end
			end
		end
		if probe.disjunction then return true end

		-- one user per pad only!
		for _,n in pairs(dev.nodes) do
			for _,u in pairs(dsp.users) do
				if u.slot == n then
					local pname = u.object:get_player_name()
					if not portal_context.users[pname] then
						portal_context.users[pname] = { time = 0, portal = pos } end
					local user = portal_context.users[pname]
					if not vector.equals(pos,user.portal) then
						if user.sound then
							minetest.sound_fade(user.sound,1,0)
							user.sound = nil
						end
						user.time = 0
						user.portal = pos
					end
					local cap = sorcery.ley.netcaps(pos,delta)
					local jc = (constants.portal_jump_cost_local*delta)
					if not user.dest and cap.freepower >= jc  then
						user.dest = portal_pick_destination(dev,crc,partner)
						sorcery.lib.node.preload(user.dest, u.object)
					end
					if not user.dest then goto skippad end
					local fac = math.min(1,(user.time / constants.portal_jump_time))
					if user.time == 0 then
						user.sound = minetest.sound_play('sorcery_windup', {pos=pos})
					end
					minetest.add_particlespawner {
						time = 1, amount = 100 + (fac * 200);
						minsize = 0.2 + fac*0.7, maxsize = 0.4 + fac*0.9;
						minvel = {y = 0.2, x=0,z=0}, maxvel = {y = 0.5, x=0,z=0};
						minacc = {y = 0.0, x=0,z=0}, maxacc = {y = 0.3, x=0,z=0};
						minpos = vector.add(n.pad,{x = -0.5, y = 0.5, z = -0.5});
						maxpos = vector.add(n.pad,{x =  0.5, y = 0.5, z =  0.5});
						texture = sorcery.lib.image('sorcery_spark.png'):multiply(sorcery.lib.color(255,119,255)):render();
						glow = 14;
						minexptime = 1, maxexptime = 1.3;
						animation = {
							type = 'vertical_frames';
							aspect_w = 16, aspect_h = 16;
						};
					}
					if user.time >= (constants.portal_jump_time * 0.5) then
						minetest.add_particlespawner {
							time = 2;
							amount = 500 * fac;
							minpos = { x = -0.3, y =    0, z = -0.3 };
							maxpos = { x =  0.3, y =  1.5, z =  0.3 };
							minvel = { x = -0.3, y =  0.4, z = -0.3 };
							maxvel = { x =  0.3, y =  0.6, z =  0.3 };
							maxacc = { x =    0, y =  0.5, z =    0 };
							texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color(255,144,226)):render();
							minexptime = 1.5;
							maxexptime = 2;
							minsize = 0.4;
							maxsize = 1.6 * fac;
							glow = 14;
							attached = u.object;
							animation = {
								type = 'vertical_frames', length = 2.1;
								aspect_w = 16, aspect_h = 16;
							};
						}
					end
					-- hack to try and swat an unkillable fucking impossibug
					if user.time > constants.portal_jump_time * 2 then
						user.time = 0
						if user.sound then
							minetest.sound_stop(user.sound)
							user.sound = nil
						end
					elseif user.time >= constants.portal_jump_time then
						local dd = portal_disposition(portal_composition(user.dest))
						if #dd.freepads > 0 then
							local destpad = dd.freepads[math.random(#dd.freepads)].pad
							local rng = function(min,max)
								return (math.random() * (max - min)) + min
							end
							local oldpos = u.object:get_pos()
							local colors = {
								{255,95,201}; {199,95,255};
								{142,95,255}; {255,95,154};
							}
							for i = 1,128 do
								local vel = {
									x = rng(-0.6,1.2), y = rng(-0.6,1.2), z = rng(-0.6,1.2)
								}
								local life = rng(0.5,5)
								minetest.add_particle {
									pos = vector.add(oldpos, {x = rng(-0.3,0.3), y = rng(0,1.5), z = rng(-0.3,0.3)});
									velocity = vel;
									expirationtime = life;
									acceleration = vector.multiply(vel, -0.1);
									texture = sorcery.lib.image('sorcery_spark.png'):multiply(sorcery.lib.color(colors[math.random(#colors)]):brighten(rng(1.0,1.4))):render();
									glow = 14;
									animation = {
										type = 'vertical_frames', length = life + 0.1;
										aspect_w = 16, aspect_h = 16;
									};
								}
							end
							if user.sound then
								minetest.sound_fade(user.sound,1,0)
								user.sound = nil
							end
							user.dest = nil
							user.time = 0
							user.sound = nil
							minetest.sound_play('sorcery_zap',{pos=pos},true)
							minetest.sound_play('sorcery_zap',{pos=destpad},true)
							portal_context.users[pname] = nil
							u.object:set_pos(vector.add(destpad, {y=0.5,z=0,x=0}))
						end
					else
						user.time = user.time + delta
					end

					break
				end
			end
		::skippad::end
		return true
	end;
	tiles = {
		'sorcery_portal_top.png';
		'sorcery_portal_top.png';
		'sorcery_portal_side.png';
		'sorcery_portal_side.png';
		'sorcery_portal_front.png';
		'sorcery_portal_front.png';
	};
	_sorcery = {
		attune = {
			class = 'sorcery:portal';
			accepts = 'sorcery:portal';
			source = true, target = true;
			reciprocal = false;
		};
		ley = {
			mode = 'consume', affinity = {'mandatic'};
			power = function(pos,delta)
				-- return power use if device is currently in process
				-- of teleporting a player; otherwise, return 0
				local cost = constants.portal_node_power
				for _,u in pairs(portal_context.users) do
					if u.dest and vector.equals(u.portal, pos) then
						cost = cost + constants.portal_jump_cost_local
					end
				end
				return cost * delta
			end;
		};
		
		on_leychange = function(pos) minetest.get_node_timer(pos):start(1) end;
	};
})

sorcery.portal = {
	composition = portal_composition;
	disposition = portal_disposition;
	circuit = portal_circuit;
}