mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
1255 lines
36 KiB
Lua
1255 lines
36 KiB
Lua
--[[
|
|
| This file was obtained through the combined efforts
|
|
| of Madbluntz & Plymouth Antiquarian Society.
|
|
|
|
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
|
|
| Maloy, DrPepper10 @ RIP, Atle!
|
|
|
|
|
| Visit for more: https://plymouth.thetwilightzone.ru/
|
|
--]]
|
|
|
|
-- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/
|
|
-- All Rights Reserved. Not allowed to be reuploaded.
|
|
local NikNaks = NikNaks
|
|
local util_TraceLine, Vector, table_insert, table_Count, table_SortByKey = util.TraceLine, Vector, table.insert, table.Count, table.SortByKey
|
|
local floor, band = math.floor, bit.band
|
|
local FindEntityHull = util.FindEntityHull
|
|
|
|
NikNaks.NodeGraph = {}
|
|
|
|
--- @class NodeGraph
|
|
local n_meta = {}
|
|
n_meta.__index = n_meta
|
|
n_meta.__tostring = function( self ) return
|
|
"NodeGraph: " .. ( self._file or "New" )
|
|
end
|
|
NikNaks.__metatables["NodeGraph"] = n_meta
|
|
|
|
--- @class ain_node
|
|
local ain_node = {}
|
|
ain_node.__tostring = function( self )
|
|
return "Ain Node: " .. self:GetID()
|
|
end
|
|
ain_node.__index = ain_node
|
|
|
|
--- @class ain_link
|
|
local ain_link = {}
|
|
ain_link.__index = ain_link
|
|
|
|
-- Load AIN file.
|
|
local error_detected = {}
|
|
do
|
|
-- Reads and returns the node from AIN file
|
|
--- @param b BitBuffer
|
|
local function readNode( b )
|
|
--- @class ain_node
|
|
local t = {}
|
|
t.pos = b:ReadVector()
|
|
t.yaw = b:ReadFloat()
|
|
t.flOffsets = {}
|
|
for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
|
|
t.flOffsets[i] = b:ReadFloat() -- Float
|
|
end
|
|
t.nodeType = b:ReadByte() -- Byte
|
|
t.nodeInfo = b:ReadUShort() -- UShort
|
|
t.zone = b:ReadShort()
|
|
|
|
-- Clamp Invalid
|
|
if t.nodeType < NikNaks.NODE_TYPE_INVALID or t.nodeType > ( NikNaks.NODE_TYPE_WATER or 5 ) then
|
|
t.nodeType = NikNaks.NODE_TYPE_INVALID
|
|
end
|
|
|
|
-- Trade down ( Cause Source-nodes fly in the air )
|
|
if t.nodeType == NikNaks.NODE_TYPE_GROUND then
|
|
local trace = util_TraceLine( {
|
|
start = t.pos + Vector( 0, 0, 50 ),
|
|
endpos = t.pos - Vector( 0, 0, 128 ),
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
} )
|
|
-- Sometimes trace-line fails for some reason.
|
|
if trace then
|
|
t.pos = trace.Hit and trace.HitPos or t.pos
|
|
end
|
|
end
|
|
|
|
return setmetatable( t, ain_node )
|
|
end
|
|
|
|
--- Read and returns the link from AIN file.
|
|
--- @param b BitBuffer
|
|
local function readLink( b )
|
|
--- @class ain_link
|
|
--- @field moves number[]
|
|
local l = {}
|
|
l.srcId = b:ReadShort() + 1 -- Short
|
|
l.destId = b:ReadShort() + 1 -- Short
|
|
l.moves = {}
|
|
|
|
for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
|
|
l.moves[i] = b:ReadByte() -- Byte
|
|
end
|
|
|
|
return setmetatable( l, ain_link )
|
|
end
|
|
|
|
--- Parses the link data.
|
|
--- @param self NodeGraph
|
|
--- @param link ain_link
|
|
local function parseLink( self, link )
|
|
local from = self._nodes[link.srcId]
|
|
local to = self._nodes[link.destId]
|
|
|
|
from._connect[#from._connect + 1] = { to, link }
|
|
to._connect[#to._connect + 1] = { from, link }
|
|
if to.zone ~= from.zone then
|
|
error_detected[to] = true
|
|
error_detected[from] = true
|
|
end
|
|
|
|
for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
|
|
local _type = link.moves[i]
|
|
if _type ~= 0 then
|
|
if not from._connect_hull[i] then from._connect_hull[i] = {} end
|
|
if not to._connect_hull[i] then to._connect_hull[i] = {} end
|
|
|
|
table_insert( from._connect_hull[i], { to, _type } )
|
|
table_insert( to._connect_hull[i], { from, _type } )
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Easy Add function
|
|
local function add( tab, x, y, node )
|
|
if not tab[x] then tab[x] = {} end
|
|
|
|
local c = tab[x][y]
|
|
if not c then
|
|
c = {}
|
|
tab[x][y] = c
|
|
end
|
|
|
|
c[#c + 1] = node
|
|
end
|
|
|
|
--- Function to add the node to a nodegraph.
|
|
--- @param self NodeGraph
|
|
--- @param node ain_node
|
|
local function addNodeToGraph( self, node )
|
|
local ng = self._nodegraph
|
|
local p = node.pos
|
|
local xf, yf = p.x / 1000, p.y / 1000
|
|
local x, y = floor( xf ), floor( yf )
|
|
add( ng, x, y, node )
|
|
|
|
local L = xf % 1 < 0.5
|
|
local B = yf % 1 < 0.5
|
|
|
|
if L then
|
|
add( ng, x - 1, y, node )
|
|
if B then
|
|
add( ng, x, y - 1, node )
|
|
add( ng, x - 1, y - 1, node )
|
|
else
|
|
add( ng, x, y + 1, node )
|
|
add( ng, x - 1, y + 1, node )
|
|
end
|
|
else
|
|
add( ng, x + 1, y, node )
|
|
if B then
|
|
add( ng, x, y - 1, node )
|
|
add( ng, x + 1, y - 1, node )
|
|
else
|
|
add( ng, x, y + 1, node )
|
|
add( ng, x + 1, y + 1, node )
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- Tries to patch the table error_detected
|
|
local function scan( node, tab, zone )
|
|
for _, t in pairs( node:GetConnections() ) do
|
|
local n = t[1]
|
|
if not tab[n:GetID()] then
|
|
tab[n:GetID()] = n
|
|
|
|
local z = n:GetZone()
|
|
zone[z] = ( zone[z] or 0 ) + 1
|
|
scan( t[1], tab, zone )
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @return number
|
|
local function patchZones()
|
|
local fixes = 0
|
|
|
|
for _ = 1, table_Count( error_detected ) do
|
|
local node = next( error_detected )
|
|
if not node then break end
|
|
local tab, zone = {}, {}
|
|
scan( node, tab, zone )
|
|
|
|
-- Take the most commen zone
|
|
zone = table_SortByKey( zone )[1]
|
|
|
|
for _, n in pairs( tab ) do
|
|
if n.zone ~= zone then
|
|
fixes = fixes + 1
|
|
n.zone = zone
|
|
end
|
|
error_detected[n] = nil
|
|
end
|
|
end
|
|
|
|
return fixes
|
|
end
|
|
|
|
-- Load map data and add it to the nodegraph.
|
|
local l = {
|
|
-- ["info_hint"] = true,
|
|
["info_node_hint"] = NikNaks.NODE_TYPE_GROUND,
|
|
["info_node_air_hint"] = NikNaks.NODE_TYPE_AIR,
|
|
["info_node_climb"] = NikNaks.NODE_TYPE_CLIMB
|
|
}
|
|
|
|
local function parseEntity( self, ent )
|
|
if not ent.classname then return end
|
|
|
|
local _type = l[ent.classname]
|
|
if not _type then return end
|
|
|
|
-- local node = self:GetNode(v.nodeid)
|
|
-- For some reason, node ID won't match the entities. I guess there are some internal magic to correct this,
|
|
-- but our only way is to use the position-data instead. Note that Z position might change cause of trace.
|
|
local node = self:FindNode( ent.origin, _type )
|
|
|
|
if not node then
|
|
ErrorNoHaltWithStack( "Unable to locate " .. _type .. "'s node!" )
|
|
else
|
|
node.hint = ent
|
|
end
|
|
end
|
|
|
|
local function parseMap( self )
|
|
for _, v in pairs( NikNaks.CurrentMap:GetEntities() ) do
|
|
parseEntity( self, v )
|
|
end
|
|
end
|
|
|
|
local thisMap = "maps/graphs/" .. game.GetMap() .. ".ain"
|
|
|
|
--- Loads the NodeGraph from a file. Note: Can output "AIN_ERROR_ZONEPATCH", if the file had invalid zones that got patched.
|
|
--- @param fileName string
|
|
--- @return NodeGraph|nil
|
|
--- @return number AIN_ERROR_*
|
|
local function loadAin( fileName )
|
|
-- Make sure you can't create a nodegraph if entities hasn't been initialised
|
|
assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" )
|
|
|
|
if not fileName and _nodeG then return _nodeG end
|
|
if not fileName then fileName = "maps/graphs/" .. game.GetMap() .. ".ain" end
|
|
if not string.match( fileName, "%.ain$" ) and not string.match( fileName, "%.dat$" ) then
|
|
-- Add file type
|
|
fileName = fileName .. ".ain"
|
|
end
|
|
|
|
--if thisMapObject and fileName == thisMap then return thisMapObject end
|
|
|
|
if not file.Exists( fileName, "GAME" ) then return end
|
|
|
|
--- @type BitBuffer
|
|
local b = NikNaks.BitBuffer.OpenFile( fileName, "GAME" )
|
|
|
|
-- Create new NG object
|
|
--- @class NodeGraph
|
|
local n = {}
|
|
n._version = b:ReadLong()
|
|
n._map_version = b:ReadLong()
|
|
if n._version ~= 37 then -- This is an old / newer AIN file.
|
|
local s = n._version > 37 and "newer" or "older"
|
|
print( "[NodeGraph]: This .AIN version is " .. s .. ", not the supported 37!" )
|
|
return nil, NikNaks.AIN_ERROR_VERSIONNUM
|
|
end
|
|
|
|
n._file = fileName
|
|
|
|
--- @type ain_node[]
|
|
n._nodes = {}
|
|
|
|
--- @type ain_link[]
|
|
n._links = {}
|
|
|
|
n._lookup = {}
|
|
n._nodegraph = {} -- This is a custom table to speed up finding nearby notes. Locating notes is one of the costly things.
|
|
setmetatable( n, n_meta )
|
|
|
|
-- Load nodes
|
|
local num_nodes = b:ReadLong()
|
|
error_detected = {}
|
|
for i = 1, num_nodes do -- No limitsh ere
|
|
--- @class ain_node
|
|
local a = readNode( b )
|
|
a._id = i
|
|
a._connect = {} -- A list of nodes connect to this.
|
|
a._connect_hull = {} -- A list of nodes connect to this, by hull.
|
|
n._nodes[i] = a
|
|
addNodeToGraph( n, a )
|
|
end
|
|
|
|
-- Read Links
|
|
local num_links = b:ReadLong()
|
|
for i = 1, num_links do -- No limits here
|
|
--- @class ain_link
|
|
local link = readLink( b )
|
|
link._id = i
|
|
n._links[i] = link
|
|
parseLink( n, link )
|
|
end
|
|
|
|
-- Read lookup
|
|
for i = 1, num_nodes do
|
|
n._lookup[i] = b:ReadLong()
|
|
end
|
|
|
|
local err = table.Count( error_detected )
|
|
|
|
if err > 0 then
|
|
print( "NodeGraph: Detected zone errors in " .. fileName .. "!" )
|
|
print( "NodeGraph: Patching zones .." )
|
|
local s = SysTime()
|
|
local fixed = patchZones() or 0
|
|
print( string.format( "NodeGraph: Took %fms to fix " .. fixed .. " nodes.", SysTime() - s ) )
|
|
end
|
|
|
|
if fileName == thisMap then
|
|
parseMap( n )
|
|
-- thisMapObject = n
|
|
end
|
|
|
|
--local leftOver = b:Read()
|
|
--print("LEFTOVER: ", string.byte(leftOver), #leftOver)
|
|
return n, err > 0 and NikNaks.AIN_ERROR_PATCHEDDATA
|
|
end
|
|
NikNaks.NodeGraph.LoadAin = loadAin
|
|
|
|
local varNG
|
|
--- Returns the nodegraph for the current map and caches it
|
|
--- @return NodeGraph|boolean
|
|
--- @return number? AIN_ERROR_*
|
|
function NikNaks.NodeGraph.GetMap()
|
|
assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" )
|
|
if varNG ~= nil then return varNG end
|
|
|
|
local a, err = loadAin()
|
|
varNG = a or false
|
|
return varNG, err
|
|
end
|
|
end
|
|
|
|
-- Node Meta
|
|
do
|
|
function ain_node:__tostring()
|
|
return "ain_node [" .. self:GetID() .. "]"
|
|
end
|
|
|
|
--- Returns the node ID.
|
|
function ain_node:GetID()
|
|
return self._id or -1
|
|
end
|
|
|
|
--- Returns true if the node is valid.
|
|
function ain_node:IsValid()
|
|
return self._id >= 0 and self:GetType() > NikNaks.NODE_TYPE_DELETED
|
|
end
|
|
|
|
--- Returns the node position.
|
|
--- @return Vector
|
|
function ain_node:GetPos( hull )
|
|
if not hull then hull = 0 end
|
|
return self.pos + Vector( 0, 0, self.flOffsets[hull] )
|
|
end
|
|
|
|
--- Returns the node YAW.
|
|
function ain_node:GetYaw()
|
|
return self.yaw
|
|
end
|
|
|
|
--- Returns the node type.
|
|
--- @return number NODE_TYPE
|
|
function ain_node:GetType()
|
|
return self.nodeType
|
|
end
|
|
|
|
--- Returns the node info.
|
|
function ain_node:GetInfo()
|
|
return self.nodeInfo
|
|
end
|
|
|
|
--- Returns the node zone. It should match any notes connected to this one.
|
|
function ain_node:GetZone()
|
|
return self.zone
|
|
end
|
|
end
|
|
|
|
-- Link Meta
|
|
do
|
|
function ain_link:__tostring()
|
|
return "ain_link [" .. self:GetID() .. "]"
|
|
end
|
|
|
|
--- Returns the link ID.
|
|
function ain_link:GetID()
|
|
return self._id or -1
|
|
end
|
|
|
|
--- Returns the move bitflags.
|
|
--- @param HULL number
|
|
function ain_link:GetMove( HULL )
|
|
return self.moves[HULL or 0]
|
|
end
|
|
|
|
--- Checks to see if it has any of said move flag
|
|
--- @param HULL number
|
|
--- @param flag number
|
|
function ain_link:HasMoveFlag( HULL, flag )
|
|
return band( self.moves[HULL or 0], flag ) ~= 0
|
|
end
|
|
|
|
--- Returns the node ID of the source node.
|
|
function ain_link:GetSrcID()
|
|
return self.srcId
|
|
end
|
|
|
|
--- Returns the node IF of the distination node.
|
|
function ain_link:GetDestID()
|
|
return self.destId
|
|
end
|
|
end
|
|
|
|
-- Special Node Meta
|
|
do
|
|
--- Returns all the connections from this node.
|
|
--- @return table
|
|
function ain_node:GetConnections()
|
|
return self._connect
|
|
end
|
|
|
|
local e_t = {}
|
|
--- Returns all the connections from this node, with said hull that aren't invalid.
|
|
--- @param hull number
|
|
--- @return table
|
|
function ain_node:GetConnectionsByHull( hull )
|
|
return self._connect_hull[hull] or e_t
|
|
end
|
|
|
|
--- Returns true if the node_type match.
|
|
--- @param NODE_TYPE number
|
|
--- @return boolean
|
|
function ain_node:IsNodeType( NODE_TYPE )
|
|
if NODE_TYPE == NikNaks.NODE_TYPE_ANY then return true end
|
|
if self.nodeType == NODE_TYPE then return true end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Get Grid Nodes
|
|
do
|
|
local function scan( node, tab )
|
|
for _, t in pairs( node:GetConnections() ) do
|
|
if not tab[t[1]:GetID()] then
|
|
tab[t[1]:GetID()] = t[1]
|
|
scan( t[1], tab )
|
|
end
|
|
end
|
|
end
|
|
|
|
--- A function returning all nodes connected to this one. A bit costly.
|
|
--- @return table
|
|
function ain_node:GetAllGridNodes()
|
|
local p = {}
|
|
local tab = {}
|
|
scan( self, tab )
|
|
|
|
for _, node in pairs( tab ) do
|
|
p[#p + 1] = node
|
|
end
|
|
|
|
return p
|
|
end
|
|
end
|
|
|
|
-- NodeGraph
|
|
do
|
|
-- Adds a dis-cost if node isn't visible. 1 = clear, 10 = blocked
|
|
local function traceCheck( from, to )
|
|
local trace = util_TraceLine( {
|
|
start = from,
|
|
endpos = to,
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
} )
|
|
|
|
if not trace then return false end
|
|
|
|
return not trace.Hit and trace.Fraction < 1
|
|
end
|
|
|
|
--- Returns the AIN version. Should be 37.
|
|
function n_meta:GetVersion()
|
|
return self._version
|
|
end
|
|
|
|
--- Returns the AIN map-version.
|
|
function n_meta:GetMapVersion()
|
|
return self._map_version
|
|
end
|
|
|
|
--- Returns the given ain_node at said ID.
|
|
--- @param id number
|
|
function n_meta:GetNode( id )
|
|
return self._nodes[id]
|
|
end
|
|
|
|
--- Returns a list of all nodes. With the ID as keys.
|
|
function n_meta:GetAllNodes()
|
|
return self._nodes
|
|
end
|
|
|
|
--- Returns the nearest node
|
|
--- @param position Vector
|
|
--- @param NODE_TYPE? number
|
|
--- @param Zone? number
|
|
--- @return ain_node
|
|
function n_meta:FindNode( position, NODE_TYPE, Zone, HULL )
|
|
NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
|
|
local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
|
|
local c, v
|
|
|
|
-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
|
|
local ng = self._nodegraph[x] and self._nodegraph[x][y]
|
|
if ng then
|
|
for _, node in pairs( ng ) do
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
|
|
if not vis then continue end
|
|
local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
|
|
if v then return v end
|
|
end
|
|
|
|
-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
|
|
for _, node in pairs( self._nodes ) do
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
|
|
local d = position:DistToSqr( node:GetPos() )
|
|
|
|
if not vis then
|
|
d = d * 100
|
|
end
|
|
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
|
|
return v
|
|
end
|
|
|
|
--- Returns the nearest node with a connection matching the hull.
|
|
--- @param position Vector
|
|
--- @param NODE_TYPE? number
|
|
--- @param HULL number
|
|
--- @param Zone? number
|
|
--- @return ain_node
|
|
function n_meta:FindNodeWithHull( position, NODE_TYPE, Zone, HULL )
|
|
NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
|
|
local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
|
|
local c, v
|
|
|
|
-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
|
|
local ng = self._nodegraph[x] and self._nodegraph[x][y]
|
|
if ng then
|
|
for _, node in pairs( ng ) do
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
if #node._connect_hull[HULL] < 1 then continue end
|
|
|
|
local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
|
|
if v then return v end
|
|
end
|
|
|
|
-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
|
|
for _, node in pairs( self._nodes ) do
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
if #node._connect_hull[HULL] < 1 then continue end
|
|
|
|
local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
return v
|
|
end
|
|
|
|
--- Returns the nearest node with said HintType.
|
|
--- @param position Vector
|
|
--- @param NODE_TYPE? number
|
|
--- @param HintType number
|
|
--- @param HintGroup? number
|
|
--- @param Zone? number
|
|
--- @return ain_node
|
|
function n_meta:FindHintNode( position, NODE_TYPE, HintType, HintGroup, Zone, HULL )
|
|
NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
|
|
local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
|
|
local c, v
|
|
|
|
-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
|
|
local ng = self._nodegraph[x] and self._nodegraph[x][y]
|
|
if ng then
|
|
for _, node in pairs( ng ) do
|
|
if not node.hint or node.hint.hinttype ~= HintType then continue end
|
|
if HintGroup and node.hint.group ~= HintGroup then continue end
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
|
|
local d = position:DistToSqr( node:GetPos( HULL ) )
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
|
|
if v then return v end
|
|
end
|
|
|
|
-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
|
|
for _, node in pairs( self._nodes ) do
|
|
if not node.hint or node.hint.hinttype ~= HintType then continue end
|
|
if HintGroup and node.hint.group ~= HintGroup then continue end
|
|
if Zone and node.zone ~= Zone then continue end
|
|
if not node:IsNodeType( NODE_TYPE ) then continue end
|
|
|
|
local d = position:DistToSqr( node:GetPos( HULL ) )
|
|
if not c or d < c then
|
|
c = d
|
|
v = node
|
|
end
|
|
end
|
|
|
|
return v
|
|
end
|
|
|
|
local MAX_NODES = MAX_NODES or 4096 -- This is the limit for Source, not NikNaks.
|
|
--- Returns the nodegraph as a BitBuffer.
|
|
--- @return BitBuffer
|
|
function n_meta:SaveToBuf()
|
|
local b = NikNaks.BitBuffer()
|
|
b:WriteLong( self:GetVersion() ) -- Should be 37
|
|
b:WriteLong( self._map_version )
|
|
|
|
-- Write node num
|
|
local note_num = #self._nodes
|
|
if note_num > MAX_NODES then
|
|
print( "[NodeGrpah]: Warning! Reached over the default MAX_NODES limits. " .. fileName .. " will only work with NikNak's pathfinding." )
|
|
end
|
|
|
|
b:WriteLong( note_num )
|
|
|
|
for i = 1, note_num do
|
|
local node = self._nodes[i]
|
|
b:WriteVector( node.pos )
|
|
b:WriteFloat( node.yaw )
|
|
for j = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
|
|
b:WriteFloat( node.flOffsets[j] )
|
|
end
|
|
b:WriteByte( node.nodeType )
|
|
b:WriteUShort( node.nodeInfo )
|
|
b:WriteShort( node.zone )
|
|
end
|
|
|
|
-- Write links. No limits I know of.
|
|
local num = #self._links
|
|
b:WriteLong( num )
|
|
for i = 1, num do
|
|
local l = self._links[i]
|
|
b:WriteShort( l.srcId - 1 )
|
|
b:WriteShort( l.destId - 1 )
|
|
for ii = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
|
|
b:WriteByte( l.moves[ii] )
|
|
end
|
|
end
|
|
|
|
-- Write lookup
|
|
for i = 1, note_num do
|
|
b:WriteLong( self._lookup[i] )
|
|
end
|
|
|
|
return b
|
|
end
|
|
|
|
--- Saves the nodegraph to a file.
|
|
--- @param filePath string
|
|
function n_meta:SaveAin( filePath )
|
|
filePath = filePath or self._file or "nodegraph"
|
|
if not string.match( filePath, ".dat$" ) then filePath = filePath .. ".dat" end -- Add file type. We use .dat to stop users from opening it in notepad.
|
|
self:SaveToBuf():SaveToFile( filePath )
|
|
print( "[NodeGraph]: Saved to " .. tostring( "data/" .. filePath ) )
|
|
end
|
|
|
|
--- Overrides and generates all the zones in the nodegraph. This can fix zone-errors.
|
|
function n_meta:GenerateZones()
|
|
-- Load all nodes to list.
|
|
local s = SysTime()
|
|
local all_nodes = {}
|
|
local nodes = self._nodes
|
|
for i = 1, #nodes do
|
|
local node = nodes[i]
|
|
all_nodes[node:GetID()] = node
|
|
end
|
|
|
|
-- For each node ..
|
|
local zone = 0
|
|
for _ = 1, #nodes do
|
|
local id, node = next( all_nodes )
|
|
if not id then break end -- No more nodes left.
|
|
all_nodes[id] = nil -- Remove this node
|
|
node.zone = zone
|
|
|
|
-- For each node connected to this one, set the zone to match.
|
|
for _, con in pairs( node:GetAllConnections() ) do
|
|
con.zone = zone
|
|
all_nodes[con:GetID()] = nil -- Remove said node from the lookup table.
|
|
end
|
|
|
|
zone = zone + 1
|
|
end
|
|
|
|
print( string.format( "[NodeGraph] Generated zones within: %f", SysTime() - s ) )
|
|
end
|
|
end
|
|
|
|
-- A* PathFinder Node functions
|
|
do
|
|
local cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {}
|
|
function ain_node:ClearSearchLists()
|
|
cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {}
|
|
end
|
|
|
|
--- Sets the cost for pathfinding.
|
|
--- @param cost number
|
|
function ain_node:SetCostSoFar( cost )
|
|
cost_l[self:GetID()] = cost
|
|
end
|
|
|
|
--- Returns the cost so far.
|
|
--- @return number
|
|
function ain_node:GetCostSoFar()
|
|
return cost_l[self:GetID()] or -1
|
|
end
|
|
|
|
--- Sets the total cost.
|
|
--- @param cost number
|
|
function ain_node:SetTotalCost( cost )
|
|
t_cost[self:GetID()] = cost
|
|
end
|
|
|
|
--- Returns the total cost.
|
|
--- @return number
|
|
function ain_node:GetTotalCost()
|
|
return t_cost[self:GetID()] or -1
|
|
end
|
|
|
|
---Adds the node to the open list.
|
|
function ain_node:AddToOpenList()
|
|
open_list[#open_list + 1] = self
|
|
end
|
|
|
|
--- Returns true if the node is on the open list.
|
|
--- @return boolean
|
|
function ain_node:IsOpen()
|
|
for i = 1, #open_list do
|
|
if open_list[i]:GetID() == self:GetID() then return true end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- Returns true if the open list is empty.
|
|
--- @return boolean
|
|
function ain_node:IsOpenListEmpty()
|
|
if next( open_list ) then return false end
|
|
return true
|
|
end
|
|
|
|
local function sorter( a, b )
|
|
return a:GetTotalCost() < b:GetTotalCost()
|
|
end
|
|
|
|
--- Updates the open list.
|
|
function ain_node:UpdateOnOpenList()
|
|
table.sort( open_list, sorter )
|
|
end
|
|
|
|
--- Pops the open list and returns the kiwest total cost node.
|
|
function ain_node:PopOpenList()
|
|
return table.remove( open_list, 1 )
|
|
end
|
|
|
|
--- Adds the node to the closed list.
|
|
function ain_node:AddToClosedList()
|
|
closed_list[self:GetID()] = true
|
|
end
|
|
|
|
--- Returns true if the node is within the closed list.
|
|
--- @return boolean
|
|
function ain_node:IsClosed()
|
|
return closed_list[self:GetID()] or false
|
|
end
|
|
|
|
--- Removes the node from the closed list.
|
|
function ain_node:RemoveFromClosedList()
|
|
closed_list[self:GetID()] = nil
|
|
end
|
|
|
|
--- Sets the move-type to this node.
|
|
--- @param CAP_MOVE number
|
|
function ain_node:SetMoveType( CAP_MOVE )
|
|
move_list[self:GetID()] = CAP_MOVE
|
|
end
|
|
|
|
--- Returns the move-type to this node.
|
|
--- @return number
|
|
function ain_node:GetMoveType()
|
|
return move_list[self:GetID()]
|
|
end
|
|
end
|
|
|
|
-- A* PathFinder
|
|
local LPFMeta = NikNaks.__metatables["LPathFollower"]
|
|
do
|
|
local function heuristic_cost_estimate( start, goal, HULL )
|
|
-- Perhaps play with some calculations on which corner is closest/farthest or whatever
|
|
return start:GetPos( HULL ):Distance( goal:GetPos( HULL ) )
|
|
end
|
|
|
|
local function reconstruct_path( cameFrom, current )
|
|
local total_path = { current }
|
|
while ( cameFrom[current] ) do
|
|
current = cameFrom[current]
|
|
table.insert( total_path, current )
|
|
end
|
|
|
|
return total_path
|
|
end
|
|
|
|
-- Finds the best move option
|
|
local function getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier )
|
|
if canFly and band( moveoptions, NikNaks.CAP_MOVE_FLY ) ~= 0 then -- Flying seems to always be the best option
|
|
return NikNaks.CAP_MOVE_FLY, 1
|
|
end
|
|
|
|
-- We like to jump more than walking
|
|
if canJump and JumpMultiplier < 1 and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then
|
|
return NikNaks.CAP_MOVE_JUMP, JumpMultiplier
|
|
end
|
|
|
|
if canWalk and band( moveoptions, NikNaks.CAP_MOVE_GROUND ) ~= 0 then
|
|
return NikNaks.CAP_MOVE_GROUND, 1
|
|
elseif canClimb and band( moveoptions, NikNaks.CAP_MOVE_CLIMB ) ~= 0 then -- %20 climb cost
|
|
return NikNaks.CAP_MOVE_CLIMB, ClimbMultiplier
|
|
elseif canJump and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then
|
|
return NikNaks.CAP_MOVE_JUMP, JumpMultiplier
|
|
end
|
|
end
|
|
|
|
--[[
|
|
Tries to A* pathfind to the location.
|
|
true = Same Nodes
|
|
false = Unable to pathfind at all
|
|
table = List of nodes from goal towards the start
|
|
]]
|
|
local function AStart( node_start, node_goal, HULL, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance )
|
|
if not node_start or not node_goal then return false end
|
|
if node_start == node_goal then return true end
|
|
|
|
local band = band
|
|
node_start:ClearSearchLists()
|
|
node_start:AddToOpenList()
|
|
|
|
local cameFrom = {}
|
|
node_start:SetCostSoFar( 0 )
|
|
node_start:SetTotalCost( heuristic_cost_estimate( node_start, node_goal, HULL ) )
|
|
node_start:UpdateOnOpenList()
|
|
|
|
local canWalk = band( BitCapability, NikNaks.CAP_MOVE_GROUND ) ~= 0
|
|
local canFly = band( BitCapability, NikNaks.CAP_MOVE_FLY ) ~= 0
|
|
local canClimb = band( BitCapability, NikNaks.CAP_MOVE_CLIMB ) ~= 0
|
|
local canJump = band( BitCapability, NikNaks.CAP_MOVE_JUMP ) ~= 0
|
|
|
|
while not node_start:IsOpenListEmpty() do
|
|
local current = node_start:PopOpenList()
|
|
if ( current == node_goal ) then
|
|
return reconstruct_path( cameFrom, current )
|
|
end
|
|
|
|
current:AddToClosedList()
|
|
|
|
for _, tab in pairs( current:GetConnectionsByHull( HULL ) ) do
|
|
local neighbor = tab[1]
|
|
if not neighbor then continue end
|
|
|
|
local moveoptions = band( BitCapability, tab[2] or 0 )
|
|
if moveoptions == 0 then continue end -- Unable to use this link. No options
|
|
|
|
local CAP_MOVE, Multi = getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier )
|
|
if not CAP_MOVE then continue end
|
|
|
|
-- Cost calculator
|
|
local newCostSoFar
|
|
if not generator then -- Custom generator
|
|
newCostSoFar = current:GetCostSoFar() + heuristic_cost_estimate( current, neighbor, HULL ) * Multi
|
|
else -- Default generator
|
|
-- TODO: Elevator? Check L4D elevator maps and what they are.
|
|
newCostSoFar = current:GetCostSoFar() + generator( current, neighbor, CAP_MOVE, BitCapability, heuristic_cost_estimate( current, neighbor, HULL ) * Multi )
|
|
end
|
|
|
|
if newCostSoFar < 0 or MaxDistance and newCostSoFar > MaxDistance then -- Check if we went over max-distance
|
|
continue
|
|
end
|
|
|
|
if ( neighbor:IsOpen() or neighbor:IsClosed() ) and neighbor:GetCostSoFar() <= newCostSoFar then
|
|
-- This node is already open/close and the cost is shorter
|
|
continue
|
|
else
|
|
neighbor:SetCostSoFar( newCostSoFar );
|
|
neighbor:SetTotalCost( newCostSoFar + heuristic_cost_estimate( neighbor, node_goal, HULL ) )
|
|
neighbor:SetMoveType( CAP_MOVE )
|
|
|
|
if ( neighbor:IsClosed() ) then
|
|
neighbor:RemoveFromClosedList()
|
|
end
|
|
|
|
if ( neighbor:IsOpen() ) then
|
|
-- This area is already on the open list, update its position in the list to keep costs sorted
|
|
neighbor:UpdateOnOpenList()
|
|
else
|
|
neighbor:AddToOpenList()
|
|
end
|
|
|
|
cameFrom[neighbor] = current
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
--- A* pathfinding using the NodeGraph.
|
|
--- @param start_pos Vector|Entity
|
|
--- @param end_pos Vector|Entity
|
|
--- @param NODE_TYPE? number
|
|
--- @param options? table
|
|
--- @param HULL_SIZE? number
|
|
--- @param generator? function -- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length )
|
|
--- @return LPathFollower|boolean
|
|
function n_meta:PathFind( start_pos, end_pos, NODE_TYPE, options, HULL_SIZE, generator )
|
|
if UseZone == nil then UseZone = true end
|
|
if not options then options = {} end
|
|
|
|
local MaxDistance = options.MaxDistance or 100000
|
|
local BitCapability = options.BitCapability or ( NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND ) -- Make default walk, unless NODE_TYPE is fly )
|
|
local JumpMultiplier = options.JumpMultiplier or 1.4
|
|
local ClimbMultiplier = options.ClimbMultiplier or 1.2
|
|
|
|
if not JumpMultiplier then JumpMultiplier = 1.4 -- Default, make it kinda hate jumping around
|
|
elseif JumpMultiplier < 0.3 then JumpMultiplier = 0.3 end -- Make sure it can't go below 0.3. Can inf loop if so.
|
|
|
|
if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end -- Default node: Ground.
|
|
|
|
-- Entity checks
|
|
local ent = start_pos.OBBMins and start_pos
|
|
local ent_e = end_pos.OBBMins and end_pos
|
|
|
|
if ent then
|
|
start_pos = ent:GetPos()
|
|
if not HULL_SIZE then
|
|
if ent.GetHullType then
|
|
HULL_SIZE = ent:GetHullType() -- Get the hulltype from the hulltype function, if that is the starting entity.
|
|
else
|
|
HULL_SIZE = FindEntityHull( ent ) or 0 -- Get the hull size from the OBB, use Hull Human as fallback
|
|
end
|
|
end
|
|
elseif not HULL_SIZE then
|
|
HULL_SIZE = 0 -- Hull Human
|
|
end
|
|
|
|
if ent_e then
|
|
end_pos = ent_e:GetPos() + ent_e:OBBCenter()
|
|
end
|
|
|
|
-- Find the start and end node.
|
|
local start_node = self:FindNode( start_pos, NODE_TYPE )
|
|
if not start_node then return false end
|
|
|
|
local offset = start_pos - start_node:GetPos( HULL_SIZE ) -- Sway the position a bit for the node to be located
|
|
local end_node = self:FindNode( end_pos + offset, NODE_TYPE, start_node:GetZone() ) -- Find an end-node. matching the starting node's zone.
|
|
if not end_node then return false end
|
|
|
|
-- Path find to location
|
|
local t = AStart( start_node, end_node, HULL_SIZE, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance )
|
|
local def_cap = NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND
|
|
|
|
if t == false then -- Unable to pathfind to location
|
|
return false
|
|
elseif t == true then -- Same location, return an "empty" path object.
|
|
local p = LPFMeta.CreatePathFollower( start_pos )
|
|
local s = p:AddSegment( start_pos, end_pos, 0, def_cap )
|
|
s.target_ent = ent_e -- The last position of the pathfollower, is an entity.
|
|
p._generator = generator
|
|
p._MaxDistance = MaxDistance
|
|
return p
|
|
else -- A table of locations
|
|
local p = LPFMeta.CreatePathFollower( start_pos )
|
|
local lP = start_pos
|
|
|
|
-- Add end pos
|
|
for i = #t, 1, -1 do
|
|
local node = t[i]
|
|
local s = p:AddSegment( lP, node:GetPos(), 0, node:GetMoveType() or def_cap )
|
|
s.node = node
|
|
lP = t[i]:GetPos()
|
|
end
|
|
|
|
local s = p:AddSegment( lP, end_pos, 0, def_cap )
|
|
s.target_ent = ent_e
|
|
p._generator = generator
|
|
p._MaxDistance = MaxDistance
|
|
|
|
return p
|
|
end
|
|
end
|
|
|
|
--- A cheap lookup function. Checks to see if we can reach the position using nearby nodes.
|
|
--- Note that this use zones and might have false positives on maps with a broken NodeGraph.
|
|
--- @param start_pos Vector
|
|
--- @param end_pos Vector
|
|
--- @param NODE_TYPE? number
|
|
--- @param HULL_SIZE? number
|
|
--- @param max_dis? number -- Distance to nearest node
|
|
--- @return boolean
|
|
function n_meta:CanMaybeReach( start_pos, end_pos, NODE_TYPE, HULL_SIZE, max_dis )
|
|
if not HULL_SIZE then HULL_SIZE = 0 end
|
|
if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end
|
|
|
|
local a = self:FindNode( start_pos, NODE_TYPE )
|
|
if not a then
|
|
return false
|
|
elseif max_dis and a:GetPos():Distance( start_pos ) > max_dis then
|
|
return false
|
|
end
|
|
|
|
local b = self:FindNode( end_pos, NODE_TYPE, a:GetZone() )
|
|
if not b then
|
|
return false
|
|
elseif max_dis and b:GetPos():Distance( start_pos ) > max_dis then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
end
|
|
|
|
-- ASync. Calculates 10 paths pr tick.
|
|
do
|
|
local async, run = {}, false
|
|
local function remove_hook()
|
|
if not run then return end
|
|
run = false
|
|
hook.Remove( "Think","ain_apath" )
|
|
end
|
|
|
|
local function add_hook()
|
|
if run then return end
|
|
run = true
|
|
|
|
hook.Add( "Think", "ain_apath", function()
|
|
local n = #async
|
|
if n < 1 then
|
|
return remove_hook() -- None left to calculate
|
|
end
|
|
|
|
for i = n, 1, -1 do
|
|
local ok, message = coroutine.resume( async[i] )
|
|
if ( ok == false ) then
|
|
ErrorNoHalt( " Error: ", message, "\n" )
|
|
table.remove( async, 1 )
|
|
elseif message then
|
|
table.remove( async, 1 )
|
|
end
|
|
end
|
|
end )
|
|
end
|
|
|
|
--- A* pathfinding using the NodeGraph. Returns the result in the callback. Calculates 20 paths pr tick.
|
|
--- @param start_pos Vector|Entity
|
|
--- @param end_pos Vector|Entity
|
|
--- @param callback function -- Returns the result. LPathFollower or false
|
|
--- @param NODE_TYPE? number
|
|
--- @param options? table
|
|
--- @param HULL_SIZE? number
|
|
--- @param generator? function -- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length )
|
|
function n_meta:PathFindASync( start_pos, end_pos, callback, NODE_TYPE, options, HULL_SIZE, generator )
|
|
add_hook() -- Make sure the async runs
|
|
local count = 0
|
|
|
|
local function awaitGen( current, neighbor, CAP_MOVE, elevator, h_cost_estimate )
|
|
count = count + 1
|
|
if count > 20 then
|
|
count = 0
|
|
coroutine.yield()
|
|
end
|
|
if generator then return generator( current, neighbor, CAP_MOVE, elevator, h_cost_estimate ) end
|
|
return current:GetCostSoFar() + h_cost_estimate
|
|
end
|
|
|
|
table.insert( async, coroutine.create( function()
|
|
callback( self:PathFind( start_pos, end_pos, NODE_TYPE, HULL_SIZE, BitCapability, JumpMultiplier, awaitGen, MaxDistance, UseZone ) )
|
|
return true
|
|
end ) )
|
|
end
|
|
end
|
|
|
|
if SERVER then return end
|
|
do
|
|
local c = {
|
|
[NikNaks.NODE_TYPE_AIR ] = Color( 155, 155, 255 ),
|
|
[NikNaks.NODE_TYPE_GROUND ] = Color( 155, 255, 155 ),
|
|
--[NODE_TYPE_WATER ]= Color(0,0,255) ,
|
|
[NikNaks.NODE_TYPE_CLIMB ] = Color( 155, 155, 155 )
|
|
}
|
|
local l = {}
|
|
for i = 0, 9 do
|
|
l[i] = Material( "sprites/key_" .. i )
|
|
end
|
|
|
|
local hType = {
|
|
[0] = "None",
|
|
[2] = "Window",
|
|
[12] = "Act Busy",
|
|
[13] = "Visually Interesting",
|
|
[14] = "Visually Interesting(Dont aim)",
|
|
[15] = "Inhibit Combine Mines",
|
|
[16] = "Visually Interesting (Stealth mode)",
|
|
[100] = "Crouch Cover Medium", -- Angles + FOV is important
|
|
[101] = "Crouch Cover Low", -- Angles + FOV is important
|
|
[102] = "Waste Scanner Spawn",
|
|
[103] = "Entrance / Exit Pinch (Cut content from Antlion guard)",
|
|
[104] = "Guard Point",
|
|
[105] = "Enemy Disadvantage Point",
|
|
[106] = "Health Kit (Cut content from npc_assassin)",
|
|
[400] = "Antlion: Burrow Point",
|
|
[401] = "Antlion: Thumper Flee Point",
|
|
[450] = "Headcrab: Burrow Point",
|
|
[451] = "Headcrab: Exit Pod Point",
|
|
[500] = "Roller: Patrol Point",
|
|
[501] = "Roller: Cleanup Spot",
|
|
[700] = "Crow: Fly to point",
|
|
[701] = "Crow: Perch point",
|
|
[900] = "Follower: Wait point",
|
|
[901] = "Override jump permission",
|
|
[902] = "Player squad transition point",
|
|
[903] = "NPC exit point",
|
|
[904] = "Strider mnode",
|
|
[950] = "Player Ally: Push away destination",
|
|
[951] = "Player Ally: Fear withdrawal destination",
|
|
[1000] = "HL1 World: Machinery",
|
|
[1001] = "HL1 World: Blinking Light",
|
|
[1002] = "HL1 World: Human Blood",
|
|
[1003] = "HL1 World: Alien Blood",
|
|
}
|
|
|
|
local function getH( num )
|
|
if not num then return "?" end
|
|
if hType[num] then
|
|
return hType[num] .. "[" .. num .. "]"
|
|
end
|
|
return num
|
|
end
|
|
|
|
function ain_node:DebugRender( size )
|
|
--if self.zone ~= 4 then return end
|
|
size = size or 32
|
|
|
|
if self.hint then
|
|
render.SetMaterial( l[self.zone % 10] )
|
|
render.DrawSprite( self:GetPos(), size, size, HSVToColor( ( CurTime() * 420 ) % 360, 0.5, 0.5 ) )
|
|
|
|
if LocalPlayer():GetPos():DistToSqr( self:GetPos() ) < 40000 then
|
|
local angle = EyeAngles()
|
|
angle:RotateAroundAxis( angle:Up(), -90 )
|
|
angle:RotateAroundAxis( angle:Forward(), 90 )
|
|
cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.1 )
|
|
draw.DrawText( "HintType: " .. getH( self.hint.hinttype ), "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER )
|
|
|
|
if self.hint.targetnode and self.hint.targetnode > -1 then
|
|
draw.DrawText( "Target node: " .. self.hint.targetnode, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
|
|
elseif ( self.hint.hinttype or 0 ) > 0 or not self.hint.group then
|
|
draw.DrawText( "ID node: " .. self.hint.nodeid, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
|
|
else
|
|
draw.DrawText( "Group node: " .. self.hint.group, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
|
|
end
|
|
cam.End3D2D()
|
|
end
|
|
elseif self.zone <= 9 then
|
|
render.SetMaterial( l[self.zone % 10] )
|
|
render.DrawSprite( self:GetPos() + Vector( 0, 0, 15 ), size, size, c[self:GetType()] )
|
|
else
|
|
local angle = EyeAngles()
|
|
angle:RotateAroundAxis( angle:Up(), -90 )
|
|
angle:RotateAroundAxis( angle:Forward(), 90 )
|
|
cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.5 )
|
|
draw.DrawText( "" .. self.zone, "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER )
|
|
cam.End3D2D()
|
|
end
|
|
|
|
local h = 0
|
|
for _, v in pairs( self._connect ) do
|
|
if v[2]:HasMoveFlag( h, NikNaks.CAP_MOVE_GROUND ) then -- or v[2]:HasMoveFlag( h, CAP_MOVE_FLY ) then
|
|
render.DrawLine( self:GetPos(), v[1]:GetPos(), c[self:GetType()] )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function n_meta:DebugRender()
|
|
local lp = LocalPlayer()
|
|
if not lp then return end
|
|
local x, y = math.floor( lp:GetPos().x / 1000 ), math.floor( lp:GetPos().y / 1000 )
|
|
|
|
-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
|
|
local ng = self._nodegraph[x] and self._nodegraph[x][y]
|
|
if not ng then return end
|
|
|
|
for _, v in pairs( ng ) do
|
|
v:DebugRender()
|
|
end
|
|
end
|