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
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
-- print("not enough power")
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
for _,u in pairs(dsp.users) do
if u.object:get_player_name() == name then
found = true
end
end
if not found then
portal_context.users[name] = nil
end
end
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
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))
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
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
user.dest = nil
user.time = 0
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;
}