sorcery  Artifact [12b64fac56]

Artifact 12b64fac56446a0f1e03475fc152abd5e5e9548c466950e6e8d0d2ccb2d31de7:

  • File portal.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: 14548) [annotate] [blame] [check-ins using]

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_spark.png'):multiply(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;
}