--[[ | 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 CurTime, setmetatable, IsValid = CurTime, setmetatable, IsValid local min, max, cos = math.min, math.max, math.cos --- @class LPathFollower --- @field _segments LPathFollowerSegment[] local meta = {} meta.__index = meta meta.__tostring = function( self ) return "LPathFollower Age: " .. self:GetAge() end NikNaks.__metatables["LPathFollower"] = meta --[[ t._segments = {} t._entity = nil -- Overrides the last position in the segments t._age = CurTime() ]] --- Creates an empty path-follower --- @param start_pos Vector --- @return LPathFollower function meta.CreatePathFollower( start_pos ) --- @class LPathFollower local t = {} t._segments = {} t._start = start_pos t._length = 0 t._age = CurTime() t._valid = true t._cursor = 1 t._cursor_dis = 0 t._cursor_lef = 0 return setmetatable( t, meta ) end --- Adds a segment. --- @param from Vector --- @param to Vector --- @param curvature number --- @param move_type number --- @return LPathFollowerSegment function meta:AddSegment( from, to, curvature, move_type ) --- @class LPathFollowerSegment local seg = {} seg.curvature = curvature or 0 seg.move_type = move_type or 1 seg.distanceFromStart = from:Distance( to ) local n = ( to - from ):GetNormalized() seg.forward = n seg.yaw = ( -n ):Angle().y if n.z > 0.7 or n.z < -0.7 then self.how = 9 else if n.y < -0.5 then -- North seg.how = 0 elseif n.y > 0.5 then -- South seg.how = 2 elseif n.x > 0.5 then -- East seg.how = 1 else -- West seg.how = 3 end end seg.length = from:Distance( to ) seg.s_lengh = self._length seg.pos = to --seg.ladder --seg.node --seg.area --seg.nna self._length = self._length + seg.length self._segments[#self._segments + 1] = seg return seg end -- Default easy functions do --- Returns the length of the path. function meta:GetLength() return self._length or 0 end --- Returns the first segment. function meta:FirstSegment() return self._segments[1] end --- Returns the last segment. function meta:LastSegment() return self._segments[#self._segments] end --- Returns all segments. function meta:GetAllSegments() return self._segments end --- Returns the age of the path. function meta:GetAge() return CurTime() - self._age end --- Resets the age of the path. function meta:ResetAge() self._age = CurTime() end --- Returns true if the path is valid. function meta:IsValid() return self._valid or false end --- Invalidates the path. function meta:Invalidate() self._valid = false end --- Returns the starting position of the path. function meta:GetStart() return self._start end --- Returns the ending position of the path (Note, will update if the target is an entity). function meta:GetEnd() if IsValid( self.target_ent ) then return self.target_ent:GetPos() end return self._segments[#self._segments].pos end end -- Cursor local findClosestSeg do --- Returns the cursor position. --- @return number --- @return number local function findCursor( self, distance ) local q = self._segments distance = min( self._length, distance ) for i = 1, #q do if distance <= q[i].s_lengh + q[i].length then return i, distance - ( q[i].s_lengh + q[i].length ) end end return 1, distance end --- Returns the closest segment and its index. --- @param position Vector --- @return LPathFollowerSegment --- @return number findClosestSeg = function( self, position ) local c, d, q for i, seg in pairs( self._segments ) do local dis = seg.pos:DistToSqr( position ) if not c or c > dis then c = dis d = seg q = i end end return d, q end --- Returns the closest position along the path to said position. --- @param position Vector --- @return Vector local function findClosestBetween( A, dir, position, maxLength ) local v = position - A local d = v:Dot( dir ) return A + dir * max( 0, min( d, maxLength ) ) end --- Returns the position on the path by given distance --- @param distance number --- @return Vector function meta:GetPositionOnPath( distance ) local seg_id, lef = findCursor( self, distance ) local t = self._segments local seg = t[seg_id] return seg.pos + seg.forward * lef end --- Returns the closest position along the path to said position. --- @param position Vector --- @return Vector function meta:GetClosestPosition( position ) -- Locate the closest points local seg, seg_id = findClosestSeg( self, position ) if not seg then return self:GetStart() end -- Fallback to the start pos local max_seg = #self._segments if seg_id <= 1 then local n_seg = self._segments[seg_id + 1] local v1 = findClosestBetween( self:GetStart(), seg.forward, position, seg.length ) local v2 = findClosestBetween( seg.pos, n_seg.forward, position, n_seg.length ) if v1:DistToSqr( position ) > v2:DistToSqr( position ) then return v2 else return v1 end elseif seg_id >= max_seg then local s_seg = self._segments[max_seg - 1] return findClosestBetween( s_seg.pos, seg.forward, position, seg.length ) else local p_seg = self._segments[seg_id - 1] local n_seg = self._segments[seg_id + 1] local v1 = findClosestBetween( p_seg.pos, seg.forward, position, seg.length ) local v2 = findClosestBetween( seg.pos, n_seg.forward, position, n_seg.length ) if v1:DistToSqr( position ) > v2:DistToSqr( position ) then return v2 else return v1 end end end --- Moves the cursor to the start of the path. function meta:MoveCursorToStart() self._cursor_dis = 0 self._cursor_lef = 0 self._cursor = 1 end --- Moves the cursor to the end of the path. function meta:MoveCursorToEnd() self._cursor_dis = self:GetLength() self._cursor = #self._segments self._cursor_lef = self._segments[self._cursor].length end --- Returns the cursor progress along the path --- @return number function meta:GetCursorPosition() return self._cursor_dis end --- Moves the cursor to said distance. --- @param distance number function meta:MoveCursorTo( distance ) self._cursor_dis = distance local seg_id, lef = findCursor( self, distance ) self._cursor = seg_id self._cursor_lef = lef end --- Moves the cursor said distance --- @param distance number function meta:MoveCursor( distance ) self._cursor_dis = self._cursor_dis + distance local nd = self._cursor_lef + distance local seg = self._segments[self._cursor] if nd < 0 or nd > seg.length then -- New segment local seg_id, lef = findCursor( self, self._cursor_dis ) self._cursor = seg_id self._cursor_lef = lef else self._cursor_lef = nd end end --- Returns the closest sequence. --- @param position Vector --- @return LPathFollowerSegment --- @return number function meta:FindClosestSeg( position ) return findClosestSeg( self, position ) end --- Returns the distance from the path. --- @param position Vector --- @return number function meta:FindDistanceFromPath( position ) return self:GetClosestPosition( position ):Distance( position ) end end -- NPC stuff do --[[ By default in Gmod, you're reuired to create a pathfind object, and then compute it. However I've found many situations where I could reuse a path. I.e a group of NPC's. So to stick to the closest "Gmod way", path:Update can be called on multiple entities and got a goal argument. C function here: https://github.com/Joshua-Ashton/Source-PlusPlus/blob/4056819cea889d73626a1cbc09518b2f8ba5dda4/src/game/shared/cstrike/bot/nav_path.cpp#L472 ]] function meta:Update( ent, toleranceSqrt ) -- Make sure it is a valid entity and it has loco if not IsValid( ent ) or not ent.loco then return true end local entPos = ent:GetPos() local seq, id = nil, ent._cursor -- Located the closest segment, and use that if not id then seq, id = findClosestSeg( self, entPos ) ent._cursor = id else seq = self._segments[id] end if not seq then return true end -- No segment, we must have reached the end local goal = seq.pos if goal:DistToSqr( entPos ) < toleranceSqrt then if id >= #self._segments then return true end -- Reached the end id = id + 1 seq = self._segments[id] goal = seq.pos ent._cursor = ent._cursor + 1 end ent.loco:Approach( goal, 1 ) ent.loco:FaceTowards( goal ) ent:SetAngles( Angle( 0, ( ent:GetPos() - goal ):Angle().y, 0 ) ) end end -- NET do function NikNaks.net.WritePath( path ) local n = #path._segments net.WriteUInt( n, 16 ) net.WriteFloat( path._age ) net.WriteVector( path._start ) for i = 1, n do local seg = path._segments[i] net.WriteVector( seg.pos - seg.length * seg.forward ) net.WriteVector( seg.pos ) net.WriteFloat( seg.curvature ) net.WriteUInt( seg.move_type, 8 ) end end function NikNaks.net.ReadPath() local n = net.ReadUInt( 16 ) local age = net.ReadFloat() local path = meta.CreatePathFollower( net.ReadVector() ) path._age = age for _ = 1, n do path:AddSegment( net.ReadVector(), net.ReadVector(), net.ReadFloat(), net.ReadUInt( 8 ) ) end return path end end -- Debug do local mat_goal = Material("editor/assault_rally") local mat_start = Material("effects/powerup_agility_hud") local tMat = Material("effects/bluelaser1") local m = Material("hud/arrow_big") local point = Material("hud/freezecam_callout_arrow") local cir = Material("hud/cart_point_neutral_opaque") local mat_arrow = Material("vgui/glyph_expand") local mat_solider = Material("hud/bomb_carried") local jump_man = Material("hud/death_wheel_1") local col_walk = color_white local col_climb = Color(155,55,155) local col_fly = Color(55,55,255) local col_jump = Color(125, 55, 255) local mov = bit.bor(NikNaks.CAP_MOVE_GROUND, NikNaks.CAP_MOVE_CLIMB, NikNaks.CAP_MOVE_JUMP) local point_b = Material("hud/cart_point_blue") local point_c = Material("hud/expanding_vert_middle_blue_bg") function meta:DebugRender() local l for i, seg in ipairs(self:GetAllSegments()) do render.SetMaterial(cir) render.DrawSprite( seg.pos, 16, 16 ) if l then local col = color_white if seg.move_type == NikNaks.CAP_MOVE_CLIMB then col = col_climb elseif seg.move_type == NikNaks.CAP_MOVE_FLY then col = col_fly elseif seg.move_type == NikNaks.CAP_MOVE_JUMP then col = col_jump end local d = seg.pos:Distance(l) local n = (SysTime() * 2) % 1 m:SetVector("$color",Vector(col.r,col.g,col.b) / 255) render.SetMaterial(m) render.DrawBeam( seg.pos, l, 30, n, d / 40 + n, col ) render.SetMaterial(point) render.DrawBeam( l + seg.forward * 10,l + seg.forward * 30, 20, 0, 1) end l = seg.pos end local num = max(1, self:GetLength() / 800) for i = 1, num do local dis = (CurTime() * 100 + i * 800) % self:GetLength() local pos = self:GetPositionOnPath( dis ) local q = 1 + cos(SysTime() * 10 + i) * 0.1 render.SetMaterial(mat_arrow) render.DrawBeam( pos + Vector(0,0,16), pos, 16, q - 1, q, color_white ) render.SetMaterial(mat_solider) render.DrawSprite( pos + Vector(0,0,20), 16, 16 ) end end end