people/actions/go.lua

468 lines
19 KiB
Lua

local dbg
if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end
-- "Area Node To Pos". Footpath area nodes are 1x1 areas ABOVE the path. People
-- positions (to match player positions) are 0.5 above the node they are
-- standing on. Add this to the Y coordinate to convert from a path node
-- position to a people position.
local ANTP = -0.5
-- "Path Node To Pos". But the nodes that form the actual path are below.
-- This is all very messy and needs rationalising!!
local PNTP = 0.5
-- Maximum distance from a destination at which we will attempt to use
-- pathfinding (e.g. after a collision)
local MAX_PATH_DIST = 48
people.actions.go = function(state)
if state.action.rel then
state.action.pos = vector.add(vector.round(state.pos), state.action.rel)
dbg.v2(state.ent.name.." converted relative go to destination "..minetest.pos_to_string(state.action.pos))
state.action.rel = nil
end
if (not state.action.pos) and (not state.action.footpathdest) and state.action.name then
-- The action says to go to a named place. We're going to convert this
-- into a position (if we have a way of doing so, e.g. via the footpath
-- or areas mods), ready for the next call...
if type(state.action.name) ~= "string" then
dbg.v1(state.ent.name.." can't go to a named destination that isn't a string")
return true, false
end
-- Look for a footpath node with the right name
if footpath then
local pathdestpos = footpath.get_nodepos(state.action.name)
if pathdestpos then
dbg.v2(state.ent.name.." selected path destination "..state.action.name)
-- Find the nearest footpath junction to us. We'll head to that
-- first (as a pos) and then follow the footpath network from
-- there. (Note, the nearest junction could be the destination,
-- and that's ok!)
local pathstartnode = footpath.nearest_junction(state.pos)
if pathstartnode then
dbg.v2(state.ent.name.." selected path start "..pathstartnode.name.." at "..minetest.pos_to_string(pathstartnode.pos))
state.action.pos = vector.new(pathstartnode.pos)
state.action.pos.y = state.action.pos.y + ANTP
state.action.footpathdest = state.action.name
return false
end
end
end
-- No path node, so look for any area with the actual name and
-- just go there.
if areas then
local nearest = areas:findNearestArea(state.pos, "^"..people.escape_pattern(state.action.name).."$")
if nearest then
dbg.v2(state.ent.name.." set destination for centre of area "..nearest.name)
state.action.pos = vector.interpolate(nearest.pos1, nearest.pos2, 0.5)
state.action.pos.y = state.action.pos.y - 0.5
return false
end
end
dbg.v2(state.ent.name.." couldn't find location "..state.action.name)
return true, false
end
if not state.action.pos and state.action.footpathdest then
-- path handling - only comes here when we've arrived at a new path
-- square (note, the range passed to nearest_junction determines our
-- lookahead distance
dbg.v3(state.ent.name.." at new footpath node at "..minetest.pos_to_string(state.pos))
local curpathnode, distance = footpath.nearest_junction(state.pos, nil, 10)
if curpathnode and curpathnode.pos.x == state.pos.x and curpathnode.pos.z == state.pos.z then
if curpathnode.name == state.action.footpathdest then
dbg.v2(state.ent.name.." arrived at footpath destination")
if state.action.endpos then
state.action.footpathdest = nil
state.action.pos = state.action.endpos
return false
end
if state.action.faceexit then
local jct = footpath.get_junction(curpathnode.name)
if not jct then
dbg.v1(state.ent.name.." couldn't do faceexit at "..curpathnode.name.." - no such junction")
return true, false
end
local fdir = nil
for dir, exit in pairs(jct.exits) do
if exit == state.action.faceexit then
fdir = dir
break
end
end
if not fdir then
dbg.v1(state.ent.name.." couldn't do faceexit at "..curpathnode.name.." - no such exit")
return truen, false
end
local fpos
if fdir == "n" then
fpos = vector.add(state.pos, {x=0, y=0, z=1})
elseif fdir == "s" then
fpos = vector.add(state.pos, {x=0, y=0, z=-1})
elseif fdir == "e" then
fpos = vector.add(state.pos, {x=1, y=0, z=0})
else
fpos = vector.add(state.pos, {x=-1, y=0, z=0})
end
state.yaw = vector.get_yaw(state.pos, fpos)
dbg.v2(state.ent.name.." set faceexit yaw of "..state.yaw)
end
return true, true
end
if not state.action.name then
dbg.v1(state.ent.name.." unexpectedly missing destination name - aborting go action - fix this!")
return true, false
end
-- We're at a junction
dbg.v2(state.ent.name.." is at footpath junction "..curpathnode.name.." - looking for route to "..state.action.name)
state.action.lastpathpos = vector.new(state.pos.x, state.pos.y - PNTP, state.pos.z)
local nextpathdest = footpath.get_route(curpathnode.name, state.action.name)
if not nextpathdest then
dbg.v1(state.ent.name.." can't find route")
return true, false
end
local exitoff = nil
for dir, exitto in pairs(curpathnode.neighbours) do
if exitto.name == nextpathdest[2] then
if dir == "n" then
exitoff = {x=0, z=1}
elseif dir == "s" then
exitoff = {x=0, z=-1}
elseif dir == "e" then
exitoff = {x=1, z=0}
elseif dir == "w" then
exitoff = {x=-1, z=0}
else
dbg.v1(state.ent.name.." can't go in direction "..dir)
return true, false
end
break
end
end
if not exitoff then
dbg.v1(state.ent.name.." can't get exit offset from "..curpathnode.name.." to "..nextpathdest[2])
return true, false
end
dbg.v2(state.ent.name.." leaving junction towards "..nextpathdest[2])
state.action.pos = vector.add(curpathnode.pos, {x=exitoff.x, y=ANTP, z=exitoff.z})
return false
end
if not state.action.lastpathpos then
dbg.v1(state.ent.name.." should have lastpathpos")
return true, false
end
-- extra 0.5 allowing for dodgy step-ups, for now...
local thispathpos = vector.round({x=state.pos.x, y=state.pos.y + 0.5, z=state.pos.z})
thispathpos.y = thispathpos.y - 1
local nextpathpos, digit = footpath.findnext(thispathpos, state.action.lastpathpos, false)
if nextpathpos == "unloaded" then
state.wait = 1
dbg.v3(state.ent.name.." waiting for block load")
return false
end
if not nextpathpos then
dbg.v1(state.ent.name.." can't find next footpath node at "..
minetest.pos_to_string(thispathpos).." after "..
minetest.pos_to_string(state.action.lastpathpos))
return true, false
end
if not distance or distance > 10 then distance = 10 end
while distance > 2 and not digit do
-- Look ahead a bit while going in a straight line, to increase speed
local aheadpathpos, digit = footpath.findnext(nextpathpos, thispathpos, true)
if aheadpathpos == "unloaded" then
state.wait = 1
dbg.v3(state.ent.name.." waiting for block load for lookahead")
return false
end
if aheadpathpos and not digit then
thispathpos = nextpathpos
nextpathpos = aheadpathpos
distance = distance - 1
else
break
end
end
state.action.lastpathpos = thispathpos
state.action.pos = vector.add(nextpathpos, {x=0, y=0.5, z=0})
if digit then
dbg.v3(state.ent.name.." clearing snow from path at "..minetest.pos_to_string(state.action.pos))
state.action={"go", pos=state.action.pos, prevaction={"dig", pos=vector.add(nextpathpos, {x=0, y=1, z=0}), prevaction=state.action}}
end
dbg.v3(state.ent.name.." set next footpath dest "..minetest.pos_to_string(state.action.pos))
return false
end
if not state.action.pos or not state.action.pos.x or not state.action.pos.y or not state.action.pos.z then
dbg.v1(state.ent.name.." has invalid go action "..dump(state.action))
return true, false
end
-- Get our current destination, which might be either our
-- actual final one, or an intermediate point
local curdest
if state.action.intermediate then
curdest = state.action.intermediate[1]
else
curdest = state.action.pos
end
if not (type(curdest) == "table" and curdest.x and curdest.y and curdest.z) then
dbg.v1(state.ent.name.." has invalid destination "..dump(curdest))
curdest = pos
end
local distance = vector.distance({x=state.pos.x, y=0, z=state.pos.z}, {x=curdest.x, y=0, z=curdest.z})
-- If it's a long way to the destination position, see if we can find
-- a route using a footpath instead.
if footpath and distance > 30 and not state.action.footpathdest then
local pathstartnode = footpath.nearest_junction(state.pos, nil, distance)
if pathstartnode then
local pathdestnode = footpath.nearest_junction(state.action.pos, nil, distance)
if pathdestarea then
state.action.endpos = state.action.pos
state.action.pos = vector.new(pathstartnode.pos)
state.action.pos.y = state.action.pos.y + ANTP
state.action.footpathdest = pathdestnode.name
dbg.v2(state.ent.name.." using footpath from "..pathstartnode.name.." to "..pathdestnode.name.." for intermediate route")
return false
end
end
end
if distance < 0.2 then
if state.action.intermediate then
dbg.v3(state.ent.name.." arrived intermediate ")
if #state.action.intermediate == 1 then
state.action.intermediate = nil
else
table.remove(state.action.intermediate, 1)
end
end
dbg.v2(state.ent.name.." arrived near "..minetest.pos_to_string(curdest))
state.setpos = vector.round(curdest)
state.setpos.y = state.pos.y
-- If we arrived at the pos, but we still have a
-- pathdest or intermediate dest, we carry on to do that.
if state.action.intermediate then
state.action.pos = state.action.intermediate[1]
return false
elseif state.action.footpathdest then
state.action.pos = nil
return false
end
-- We've finished!
dbg.v1(state.ent.name.." completed go action at "..minetest.pos_to_string(curdest))
return true, true
end
-- Ok, we're actually walking towards a point!
-- See if we've hit something last step, or if we're about to venture
-- somewhere unwholesome...
local unwholesome = false
local colres = state.ent.object:get_last_collision_result()
if colres.collides_xz then
dbg.v3(state.ent.name.." collided at "..minetest.pos_to_string(state.pos).." : "..dump(colres))
-- Maybe we hit a door and can open it?
-- TODO - we ought to do something here to prevent repeated opening
-- attempts if this goes wrong!
if state.ent.can_use_doors then
for _, col in pairs(colres.collisions) do
local colpos = col.node_p
local colnode = minetest.get_node(colpos)
local door = minetest.registered_nodes[colnode.name].groups.door
local gate = minetest.registered_nodes[colnode.name].groups.gate
if (door and door ~= 0) or (gate and gate ~= 0) then
state.action = {"rightclicknode", pos=colpos, prevaction=state.action}
return false
end
end
end
unwholesome = true
-- We've hit something...
-- Apply some damage for bashing into something. Apart from being
-- the right thing to do, it will also kill off things that get
-- stuck in a wall. ;)
state.damage = 0.4
end
if not unwholesome and distance > 1 then
local erk = nil
local ahead = state.speed
if ahead > 1 then ahead = 1 end
local nd = vector.from_speed_yaw(ahead, state.yaw)
local nextpos = vector.add(vector.round(state.pos), nd)
-- Don't look at the ahead position if it's the same node we're
-- already at...
if not vector.equals(vector.round(state.pos), nextpos) then
nextpos.y = nextpos.y + 2
-- How far down we'll look, which needs to be at least this to
-- be able to descend stairs.
local ncount = 6
local diags = "at:" ..minetest.pos_to_string(state.pos)
local notwalkable = 0
local unloaded = false
diags = diags .. " next+1:" ..minetest.pos_to_string(nextpos)
while ncount > 0 do
local nn = minetest.get_node(nextpos).name
diags = diags .. ":" .. nn
if nn == "ignore" then
unloaded = true
break
end
nd = minetest.registered_nodes[nn]
if not nd then
erk = "mysterious " .. nn
break
end
local gg = nd.groups or {}
if gg.water or gg.lava then
erk = nn
break
end
-- Use 'walkable' to decide if we will bump into, and/or can stand
-- on the node. But with a bit of variation. We ignore doors so we
-- bump into them on purpose!?
local iswalkable = nd.walkable
if iswalkable and state.ent.can_use_doors and (
(nd.groups.door and nd.groups.door ~= 0) or
(nd.groups.gate and nd.groups.gate ~= 0)) then
iswalkable = false
end
if iswalkable and ncount < 6 then
break
end
if not iswalkable then
notwalkable = notwalkable + 1
end
nextpos.y = nextpos.y - 1
ncount = ncount - 1
end
if unloaded then
-- If we hit an unloaded block, just wait until it loads.
dbg.v3(state.ent.name.." waiting for unloaded to check ahead")
state.wait = 1
return false
end
if ncount == 0 then
erk = "fall"
elseif notwalkable < 2 then
erk = "bump"
end
if erk then
dbg.v1(state.ent.name.." aborted go due to fear of " ..
erk .. " - " .. diags)
unwholesome = true
end
end
end
if unwholesome then
if distance <= MAX_PATH_DIST then
if state.action.triedpath then
state.action.triedpath = false
dbg.v2(state.ent.name.." is waiting before trying a path again ")
state.action.intermediate = nil
state.action = {"wait", time=30, prevaction=state.action}
return false
end
local spos = vector.new(state.pos)
if state.ent.yoffset then
spos.y = spos.y - state.ent.yoffset
end
local path = minetest.find_path(spos, curdest,
distance + 16, 1, 3, "A*_noprefetch")
if not path then
dbg.v2(state.ent.name.." cannot find path from "..
minetest.pos_to_string(state.pos).." to "..
minetest.pos_to_string(curdest))
if state.action.intermediate then
state.action.intermediate = nil
end
state.wait = 2
return true, false
else
state.action.intermediate = {}
-- Start following the path from the rounded position, in case
-- we're off centre and will collide immediately...
table.insert(state.action.intermediate, vector.round(state.pos))
for i = 2, #path do
table.insert(state.action.intermediate, {x=path[i].x, y=path[i].y -0.5, z=path[i].z})
end
dbg.v3(state.ent.name.." found path to "..minetest.pos_to_string(curdest).." : "..dump(path))
state.action.triedpath = true
return false
end
end
-- We hit something, and we're too far from our destination to try
-- pathfinding. Pick a couple of nearby points and walk to them,
-- then try again
local randomdir = math.random(math.pi * 2)
vec = vector.from_speed_yaw(8, randomdir)
local rpos = {x=math.floor(state.pos.x + vec.x + 0.5), y=state.pos.y, z=math.floor(state.pos.z + vec.z + 0.5)}
randomdir = math.random(math.pi * 2)
vec = vector.from_speed_yaw(8, randomdir)
local rpos2 = {x=math.floor(rpos.x + vec.x + 0.5), y=state.pos.y, z=math.floor(rpos.z + vec.z + 0.5)}
if not state.action.intermediate or #state.action.intermediate > 10 then
state.action.intermediate = {}
end
table.insert(state.action.intermediate, 1, rpos2)
table.insert(state.action.intermediate, 1, rpos)
dbg.v2(state.ent.name.." hit something, far from destination - talking a random detour")
return false
end
if distance < 1.5 then
state.speed = distance * 1.5
else
state.speed = 3
end
-- Apply max speed if there is one
if state.ent.maxspeed and state.speed > state.ent.maxspeed then
state.speed = state.ent.maxspeed
end
state.yaw = vector.get_yaw(state.pos, curdest)
return false
end