548 lines
15 KiB
Lua
Executable File
548 lines
15 KiB
Lua
Executable File
-- Code by UjEdwin and mrob27
|
|
-- License, revision history and instructions are in README.txt
|
|
--
|
|
-- Design documentation is also in README.txt
|
|
|
|
local op_prtl = {}
|
|
local id_p0rtal = {}
|
|
local nxt_id = 0 -- this counts how many portalpairs have been created; TODO: this will go away
|
|
local portalgun_step_interval = 0.1
|
|
local portalgun_time=0
|
|
local portalgun_lifetime = 5000 -- We delete portals that unused for this long
|
|
local portalgun_running = false
|
|
local portalgun_max_range = 13
|
|
|
|
local function portalgun_getLength(a)-- get length of an array / table
|
|
local count = 0
|
|
for _ in pairs(a) do count = count + 1 end
|
|
return count
|
|
end
|
|
|
|
-- return a node definition for the node at pos, with a fallback in case
|
|
-- that pos is not presently loaded
|
|
local function node_ok(pos)
|
|
local fallback = "default:dirt"
|
|
local nd = minetest.get_node_or_nil(pos)
|
|
if not nd then
|
|
-- pos is outside the area currently loaded
|
|
return minetest.registered_nodes[fallback]
|
|
end
|
|
-- use the node's name to find the node definition
|
|
local nodef = minetest.registered_nodes[nd.name]
|
|
if nodef then
|
|
return nodef
|
|
end
|
|
return minetest.registered_nodes[fallback]
|
|
end
|
|
|
|
-- try to figure out the object's physical height. There are
|
|
-- several cases that need to be handled. Not all objects
|
|
-- have a collisionbox
|
|
local function object_height(ob)
|
|
if ob:is_player() then
|
|
-- We assume players have a height of 1.8 metres
|
|
print "object_height: is player, returning 1.8"
|
|
return 1.8
|
|
end
|
|
|
|
local ent = ob:get_luaentity()
|
|
if ent then
|
|
local cb = ent.collisionbox;
|
|
if cb then
|
|
-- We got lucky, an object whose entity actually defines a
|
|
-- collisionbox! The height is in cb[5]
|
|
print("object_height: cb ("..cb[1]..", "..cb[2]..", "..cb[3]
|
|
..", "..cb[4]..", "..cb[5]..", "..cb[6]..")")
|
|
return cb[5]
|
|
elseif ent.name=="__builtin:item" then
|
|
local iname = ItemStack(ent.itemstring):get_name()
|
|
print("object_height: dropped obj '"..ent.itemstring
|
|
.."', iname '"..iname.."'")
|
|
cb = minetest.registered_entities[ent.name].collisionbox
|
|
if cb == nil then
|
|
cb = minetest.registered_items[iname].collisionbox
|
|
end
|
|
if cb then
|
|
-- This seems to never happen
|
|
print(" found cb ("..cb[1]..", "..cb[2]..", "..cb[3]
|
|
..", "..cb[4]..", "..cb[5]..", "..cb[6]..")")
|
|
return cb[5]
|
|
else
|
|
-- Try to get the visual attribute, but it is never available
|
|
local vs = minetest.registered_entities[ent.name].visual
|
|
print(" no cb, visual '"..minetest.serialize(vs).."'")
|
|
-- TODO: Can we test what version of the Minetest engine
|
|
-- we're in? The size of __builtin:item objects changed,
|
|
-- look in game/item_entity.lua for a call to register_entity
|
|
-- it used to be about 0.33 and is presently 0.6
|
|
return 0.6
|
|
end
|
|
else
|
|
print("object_height: entity '"..ent.name.."'")
|
|
cb = minetest.registered_entities[ent.name].collisionbox
|
|
if cb then
|
|
-- This seems to never happen
|
|
print(" found cb ("..cb[1]..", "..cb[2]..", "..cb[3]
|
|
..", "..cb[4]..", "..cb[5]..", "..cb[6]..")")
|
|
return cb[5]
|
|
else
|
|
print(" no cb, assume small")
|
|
end
|
|
end
|
|
else
|
|
-- we couldn't get a laentity
|
|
end
|
|
|
|
-- if we get here we couldn't figure it out at all.
|
|
return 0.1
|
|
end
|
|
|
|
minetest.register_on_leaveplayer(
|
|
-- when a player leaves the game, make their portals expire
|
|
function(user)
|
|
local uname = user:get_player_name()
|
|
for i=1, portalgun_getLength(id_p0rtal),1 do
|
|
if id_p0rtal[i]~=0 and id_p0rtal[i].user==uname then
|
|
id_p0rtal[i].lifetime=-1
|
|
return 0
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
-- step function or an individual portal: checks if there are things to
|
|
-- teleport
|
|
local function portalgun_step_proc(portal, id)
|
|
if id_p0rtal[id] == 0 then
|
|
return 0
|
|
end
|
|
-- print ("[portalgun] sproc(" .. portal .. ", " .. id .. ")")
|
|
|
|
-- check if portals have run out of lifetime. (If they are used, by
|
|
-- an object getting teleported, the timer is reset, see below)
|
|
id_p0rtal[id].lifetime = id_p0rtal[id].lifetime-1
|
|
if id_p0rtal[id].lifetime < 0 then
|
|
if id_p0rtal[id].portal1 ~= 0 then
|
|
id_p0rtal[id].portal1:remove()
|
|
end
|
|
if id_p0rtal[id].portal2 ~= 0 then
|
|
id_p0rtal[id].portal2:remove()
|
|
end
|
|
id_p0rtal[id] = 0
|
|
return 0
|
|
end
|
|
|
|
-- get position and direction of entry portal (pos1, d1) and
|
|
-- of exit portal (pos2, d2)
|
|
local pos1 = 0
|
|
local pos2 = 0
|
|
local d1 = 0
|
|
local d2 = 0
|
|
if portal == 1 then
|
|
pos1 = id_p0rtal[id].portal1_pos
|
|
pos2 = id_p0rtal[id].portal2_pos
|
|
d1 = id_p0rtal[id].portal1_dir
|
|
d2 = id_p0rtal[id].portal2_dir
|
|
else
|
|
pos1 = id_p0rtal[id].portal2_pos
|
|
pos2 = id_p0rtal[id].portal1_pos
|
|
d1 = id_p0rtal[id].portal2_dir
|
|
d2 = id_p0rtal[id].portal1_dir
|
|
end
|
|
|
|
|
|
if (pos2 ~= 0) and (pos1 ~= 0) then
|
|
-- we have two portals, so teleporting is possible
|
|
-- check all objects within a radius of 1.5 (it has to be this big
|
|
-- to catch players, whose "position" is a point near the feet)
|
|
for ii, ob in pairs(minetest.get_objects_inside_radius(pos1, 1.5)) do
|
|
local ent = ob:get_luaentity()
|
|
|
|
-- TODO: use object height to get a more refined sense of the
|
|
-- object's true distance from the portal, and ignore if object
|
|
-- is not within a closer radius
|
|
-- local height = object_height(ob)
|
|
|
|
if ent and ent.name == "portalgun:portal" then
|
|
-- this object is the portal itself; ignore
|
|
else
|
|
-- ======= set velocity then teleport
|
|
local p = pos2
|
|
local x = 0
|
|
local y = 0
|
|
local z = 0
|
|
|
|
if ob:is_player() then
|
|
if d2 == "x+" then ob:set_look_yaw(math.pi/-2)
|
|
elseif d2 == "x-" then ob:set_look_yaw(math.pi/2)
|
|
elseif d2 == "z+" then ob:set_look_yaw(0)
|
|
elseif d2 == "z-" then ob:set_look_yaw(math.pi)
|
|
end
|
|
else
|
|
-- get object's current velocity.
|
|
local v = ob:getvelocity()
|
|
|
|
-- compute the magnitude of the velocity
|
|
local vmag = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
|
|
|
|
-- compute exit velocity. Objects always exit in a
|
|
-- direction perpendicular to the exit portal, with
|
|
-- speed equal to the speed they had when entering.
|
|
v.x = 0
|
|
v.y = 0
|
|
v.z = 0
|
|
if d2 == "x+" then
|
|
v.x = vmag
|
|
ob:setyaw(math.pi/-2)
|
|
elseif d2 == "x-" then
|
|
v.x = vmag*-1
|
|
ob:setyaw(math.pi/2)
|
|
elseif d2 == "y+" then
|
|
v.y = vmag
|
|
elseif d2 == "y-" then
|
|
v.y = vmag*-1
|
|
elseif d2 == "z+" then
|
|
v.z = vmag
|
|
ob:setyaw(0)
|
|
elseif d2 == "z-" then
|
|
v.z = vmag*-1
|
|
ob:setyaw(math.pi)
|
|
end
|
|
|
|
ob:setvelocity({x = v.x, y = v.y, z = v.z})
|
|
end
|
|
|
|
-- Calculate exit point, 2 nodes away from the exit portal
|
|
-- in whatever direction the exit portal is facing. It has
|
|
-- to be 2 nodes away so we don't immediately get
|
|
-- teleported again.
|
|
-- TODO: Once we manage to decrease the capture radius of 1.5,
|
|
-- we may also diminish this distance.
|
|
if d2 == "x+" then x = 2
|
|
elseif d2 == "x-" then x = -2
|
|
elseif d2 == "y+" then y = 2
|
|
elseif d2 == "y-" then y = -2
|
|
elseif d2 == "z+" then z = 2
|
|
elseif d2 == "z-" then z = -2
|
|
end
|
|
|
|
ob:moveto({x = p.x+x, y = p.y+y, z = p.z+z}, false)
|
|
|
|
-- this portal has been used, so we should reset the
|
|
-- portal's expiration timer
|
|
id_p0rtal[id].lifetime = portalgun_lifetime
|
|
|
|
-- ======= end of set velocity part then teleport
|
|
end
|
|
end
|
|
end
|
|
return 1
|
|
end
|
|
|
|
minetest.register_globalstep(
|
|
-- periodically check all portals to see if objects have gotten within
|
|
-- range
|
|
function(dtime)
|
|
-- if we have no portals, just exit right away
|
|
if not portalgun_running then
|
|
return 0
|
|
end
|
|
-- print "[portalgun] gstep"
|
|
|
|
portalgun_time = portalgun_time + dtime
|
|
if portalgun_time < portalgun_step_interval then
|
|
return
|
|
end
|
|
|
|
portalgun_time=0
|
|
local use=0
|
|
|
|
for i=1, portalgun_getLength(id_p0rtal),1 do
|
|
use = use + portalgun_step_proc(1, i)
|
|
use = use + portalgun_step_proc(2, i)
|
|
end
|
|
if (use == 0) and (portalgun_getLength(id_p0rtal) > 0) then
|
|
id_p0rtal = {}
|
|
portalgun_running = false
|
|
end
|
|
end
|
|
)
|
|
|
|
minetest.register_entity("portalgun:portal", { -- the portals
|
|
visual = "mesh",
|
|
mesh = "portalgun_portal_xp.obj",
|
|
id=0,
|
|
physical = false,
|
|
textures ={"portalgun_blue.png"},
|
|
visual_size = {x=1, y=1},
|
|
-- automatic_rotate = math.pi * 2.9,
|
|
spritediv = {x=7, y=0},
|
|
collisionbox = {0,0,0,0,0,0},
|
|
on_activate = function(self, staticdata)
|
|
self.owner = ""
|
|
self.pnum = 0
|
|
self.id = 0
|
|
if staticdata then
|
|
local tmp = minetest.deserialize(staticdata)
|
|
if tmp then
|
|
if tmp.owner then
|
|
self.owner = tmp.owner
|
|
end
|
|
if tmp.pnum then
|
|
self.pnum = tmp.pnum
|
|
end
|
|
if tmp.id then
|
|
self.id = tmp.id
|
|
end
|
|
end
|
|
end
|
|
if self.pnum > 0 then
|
|
-- print ("[portalgun] activate #" .. self.id .. ", p"..self.pnum.." for " .. self.owner)
|
|
else
|
|
-- print ("[portalgun] portal just created")
|
|
end
|
|
|
|
-- a portal entity is being loaded into the environment because
|
|
-- a player is nearby
|
|
self.id = nxt_id
|
|
if not id_p0rtal[self.id] then
|
|
-- print "[portalgun] not in pg_p table, removing."
|
|
self.object:remove()
|
|
return
|
|
end
|
|
local d=""
|
|
local prj = 0
|
|
if id_p0rtal[self.id].project then
|
|
prj = id_p0rtal[self.id].project
|
|
end
|
|
-- print ("[portalgun] id now "..self.id..", project="..prj)
|
|
if prj==1 then
|
|
d=id_p0rtal[self.id].portal1_dir
|
|
self.object:set_properties({textures = {"portalgun_blue.png"},})
|
|
else
|
|
d=id_p0rtal[self.id].portal2_dir
|
|
self.object:set_properties({textures = {"portalgun_orange.png"},})
|
|
end
|
|
|
|
if d=="x+" then self.object:setyaw(math.pi * 0)
|
|
elseif d=="x-" then self.object:setyaw(math.pi * 1)
|
|
elseif d=="y+" then self.object:set_properties({mesh = "portalgun_portal_yp.obj",}) -- becaouse there is no "setpitch"
|
|
elseif d=="y-" then self.object:set_properties({mesh = "portalgun_portal_ym.obj",}) -- becaouse there is no "setpitch"
|
|
elseif d=="z+" then self.object:setyaw(math.pi * 0.5)
|
|
elseif d=="z-" then self.object:setyaw(math.pi * 1.5)
|
|
self.object:set_hp(1000)
|
|
end
|
|
end,
|
|
get_staticdata = function(self)
|
|
local tmp = {
|
|
owner = self.owner,
|
|
pnum = self.pnum,
|
|
id = self.id,
|
|
}
|
|
-- print ("get_sd id="..self.id.." pnum="..self.pnum.." owner="..self.owner)
|
|
return minetest.serialize(tmp)
|
|
end,
|
|
})
|
|
|
|
local function portal_useproc(itemstack, user, pointed_thing, RMB, remove)
|
|
-- print "[portalgun] useproc"
|
|
local pnum = 1
|
|
if RMB then
|
|
pnum = 2
|
|
end
|
|
if pointed_thing.type ~= "node" then
|
|
-- print "[portals] useproc: pt.type is not a node"
|
|
return itemstack
|
|
end
|
|
local pos = pointed_thing.under
|
|
local node = node_ok(pos)
|
|
local nn = node.name
|
|
|
|
-- portals can only be placed on walkable nodes: not torches, papyrus, etc.
|
|
-- NOTE: We might also want to exclude certain node types (e.g. steps,
|
|
-- fence, trapdoors, etc)
|
|
if (not node.walkable) then
|
|
return itemstack
|
|
end
|
|
|
|
pos = user:getpos()
|
|
local dir = user:get_look_dir() -- unit vector
|
|
local uname = user:get_player_name()
|
|
local found = false
|
|
local len = portalgun_getLength(id_p0rtal)
|
|
|
|
-- in my mods is as default I set 0 or false in a array when not
|
|
-- using anymore, then clear the array when not used, that saves much.
|
|
|
|
-- this check if you can hold shift+leftclick to clear your user-portals,
|
|
-- lifetime=0 means the portals will die to next run.
|
|
for i=1, len,1 do
|
|
if id_p0rtal[i]~=0
|
|
and id_p0rtal[i]~=nil
|
|
and id_p0rtal[i].user==uname
|
|
then
|
|
if not RMB then
|
|
-- left mouse button
|
|
if remove then
|
|
id_p0rtal[i].lifetime=0
|
|
return itemstack
|
|
end
|
|
end
|
|
found = true
|
|
nxt_id = i
|
|
break
|
|
end
|
|
end
|
|
|
|
if not found then
|
|
-- this user hasn't made any portals yet
|
|
|
|
-- create a new set of portals, to add to the global list in the event
|
|
-- this is the first time this player has used the gun
|
|
local ob={}
|
|
ob.project=1
|
|
ob.lifetime=portalgun_lifetime
|
|
ob.portal1=0
|
|
ob.portal2=0
|
|
ob.portal1_dir=0
|
|
ob.portal2_dir=0
|
|
ob.portal2_pos=0
|
|
ob.portal1_pos=0
|
|
ob.user = uname
|
|
|
|
table.insert(id_p0rtal, ob)
|
|
nxt_id = len+1
|
|
end
|
|
|
|
portalgun_running = true
|
|
nxt_id = portalgun_getLength(id_p0rtal)
|
|
|
|
-- we scan away from the player to find out what they hit. we need to
|
|
-- start 1.5 blocks above the player's location because the gun is
|
|
-- being held about that far above the player's feet.
|
|
pos.y = pos.y+1.5
|
|
|
|
-- the project
|
|
for i = 1, (portalgun_max_range+1), 1 do
|
|
if minetest.get_node({x=pos.x+(dir.x*i), y=pos.y+(dir.y*i), z=pos.z+(dir.z*i)}).name~="air" then
|
|
local id = nxt_id
|
|
if id_p0rtal[id]==0 then
|
|
return itemstack
|
|
end
|
|
id_p0rtal[id].lifelim=portalgun_lifetime
|
|
local lpos={x=pos.x+(dir.x*(i-1)), y=pos.y+(dir.y*(i-1)), z=pos.z+(dir.z*(i-1))}
|
|
local cpos={x=pos.x+(dir.x*i), y=pos.y+(dir.y*i), z=pos.z+(dir.z*i)}
|
|
local x=math.floor((lpos.x-cpos.x)+ 0.5)
|
|
local y=math.floor((lpos.y-cpos.y)+ 0.5)
|
|
local z=math.floor((lpos.z-cpos.z)+ 0.5)
|
|
local portal_dir=0
|
|
|
|
-- the rotation & poss of the portals
|
|
|
|
if x>0 then
|
|
portal_dir="x+"
|
|
cpos.x=(math.floor(cpos.x+ 0.5))+0.504
|
|
elseif x<0 then
|
|
portal_dir="x-"
|
|
cpos.x=(math.floor(cpos.x+ 0.5))-0.504
|
|
elseif y>0 then
|
|
portal_dir="y+"
|
|
cpos.y=(math.floor(cpos.y+ 0.5))+0.504
|
|
elseif y<0 then
|
|
portal_dir="y-"
|
|
cpos.y=(math.floor(cpos.y+ 0.5))-0.504
|
|
elseif z>0 then
|
|
portal_dir="z+"
|
|
cpos.z=(math.floor(cpos.z+ 0.5))+0.504
|
|
elseif z<0 then
|
|
portal_dir="z-"
|
|
cpos.z=(math.floor(cpos.z+ 0.5))-0.504
|
|
end
|
|
|
|
local obj = 0
|
|
if RMB then
|
|
id_p0rtal[id].project=2
|
|
id_p0rtal[id].portal2_dir=portal_dir
|
|
id_p0rtal[id].portal2_pos=cpos
|
|
if id_p0rtal[id].portal2~=0 then
|
|
id_p0rtal[id].portal2:remove()
|
|
end
|
|
obj = minetest.env:add_entity(cpos, "portalgun:portal")
|
|
id_p0rtal[id].portal2 = obj
|
|
else
|
|
id_p0rtal[id].project=1
|
|
id_p0rtal[id].portal1_dir=portal_dir
|
|
id_p0rtal[id].portal1_pos=cpos
|
|
if id_p0rtal[id].portal1~=0 then
|
|
id_p0rtal[id].portal1:remove()
|
|
end
|
|
obj = minetest.env:add_entity(cpos, "portalgun:portal")
|
|
id_p0rtal[id].portal1 = obj
|
|
end
|
|
if obj then
|
|
-- fill in its staticdata
|
|
local ent = obj:get_luaentity()
|
|
if ent then
|
|
ent.owner = uname
|
|
ent.pnum = pnum
|
|
ent.id = id
|
|
end
|
|
end
|
|
|
|
local op = uname
|
|
if RMB then
|
|
op = op .. "2"
|
|
else
|
|
op = op .. "1"
|
|
end
|
|
if (not op_prtl[op]) or (op_prtl[op] == 0) then
|
|
op_prtl[op] = {}
|
|
end
|
|
op_prtl[op].pnum = pnum
|
|
op_prtl[op].portal = obj
|
|
op_prtl[op].owner = uname
|
|
op_prtl[op].pos = cpos
|
|
op_prtl[op].dir = portal_dir
|
|
|
|
|
|
-- print ("[portalgun] created #"..id.." p"..pnum.." for "..uname)
|
|
if not remove then
|
|
minetest.sound_play("portalgun_shoot", {pos=pos})
|
|
end
|
|
return itemstack
|
|
end
|
|
end
|
|
return itemstack
|
|
end
|
|
|
|
minetest.register_tool("portalgun:gun", {
|
|
description = "Portal Gun",
|
|
inventory_image = "portalgun_gun_blue.png",
|
|
range = portalgun_max_range,
|
|
wield_image = "portalgun_gun_blue.png",
|
|
-- groups = { not_in_creative_inventory = 0 },
|
|
on_use = function(itemstack, user, pointed_thing)
|
|
-- print "[portalgun] on_use"
|
|
local key = user:get_player_control()
|
|
if (key.sneak) then
|
|
-- remove both portals
|
|
return portal_useproc(itemstack, user, pointed_thing, false, true)
|
|
else
|
|
-- place blue portal
|
|
return portal_useproc(itemstack, user, pointed_thing, false, false)
|
|
end
|
|
end,
|
|
|
|
on_place = function(itemstack, user, pointed_thing)
|
|
-- print "[portalgun] on_place"
|
|
local key = user:get_player_control()
|
|
if (key.sneak) then
|
|
-- remove both portals
|
|
return portal_useproc(itemstack, user, pointed_thing, false, true)
|
|
else
|
|
-- place orange portal
|
|
return portal_useproc(itemstack, user, pointed_thing, true, false)
|
|
end
|
|
end,
|
|
})
|