509 lines
21 KiB
Lua
509 lines
21 KiB
Lua
AddCSLuaFile("cl_init.lua")
|
|
AddCSLuaFile("shared.lua")
|
|
include("shared.lua")
|
|
|
|
local enabled = CreateConVar("gc_friendlyscanner_enabled", "1", FCVAR_ARCHIVE, "Enable friendly healing scanners")
|
|
local healRadius = CreateConVar("gc_friendlyscanner_healradius", "360", FCVAR_ARCHIVE, "Friendly scanner healing radius")
|
|
local patientSearchRadius = CreateConVar("gc_friendlyscanner_searchradius", "700", FCVAR_ARCHIVE, "Radius in which the friendly scanner looks for hurt players")
|
|
local healRate = CreateConVar("gc_friendlyscanner_healrate", "15", FCVAR_ARCHIVE, "Friendly scanner health restored per second")
|
|
local respawnTime = CreateConVar("gc_friendlyscanner_respawntime", "5", FCVAR_ARCHIVE, "Friendly scanner respawn delay")
|
|
|
|
local hoverHeight = 97
|
|
local patrolRadius = 250
|
|
local flightHullMins = Vector(-34, -34, -22)
|
|
local flightHullMaxs = Vector(34, 34, 22)
|
|
local worldUp = Vector(0, 0, 1)
|
|
local farewellMessages = {
|
|
"I will get you for that, %s!",
|
|
"That was by accident, right, %s?",
|
|
"I was the yellow scanner, %s. The yellow one!",
|
|
"Remember: medics have feelings too, %s.",
|
|
"My repair bill is going straight to %s.",
|
|
"Five seconds, %s. Then we are having a talk.",
|
|
"I hope your next medkit rolls away from you, %s.",
|
|
"Diagnostic result: %s is hazardous to helpful machinery."
|
|
}
|
|
|
|
local function ClampVectorLength(value, maximum)
|
|
local length = value:Length()
|
|
if length <= maximum or length <= 0 then return value end
|
|
return value * (maximum / length)
|
|
end
|
|
|
|
local function IsScanner(ent)
|
|
return IsValid(ent) and ent:GetClass() == "feedc0de_friendly_scanner"
|
|
end
|
|
|
|
function ENT:SpawnFunction(ply, trace, className)
|
|
if not trace.Hit then return end
|
|
local scanner = ents.Create(className)
|
|
if not IsValid(scanner) then return end
|
|
local position = trace.HitPos + trace.HitNormal * hoverHeight
|
|
scanner:SetPos(position)
|
|
scanner:SetAngles(Angle(0, IsValid(ply) and ply:EyeAngles().y or 0, 0))
|
|
scanner:Spawn()
|
|
scanner:Activate()
|
|
scanner:SetSpawnAnchor(position)
|
|
if IsValid(ply) then scanner:SetCreator(ply) end
|
|
return scanner
|
|
end
|
|
|
|
function ENT:Initialize()
|
|
self:SetModel("models/combine_scanner.mdl")
|
|
self:PhysicsInit(SOLID_VPHYSICS)
|
|
self:SetMoveType(MOVETYPE_VPHYSICS)
|
|
self:SetSolid(SOLID_VPHYSICS)
|
|
self:SetCollisionGroup(COLLISION_GROUP_NONE)
|
|
self:SetUseType(SIMPLE_USE)
|
|
self:SetRenderMode(RENDERMODE_TRANSCOLOR)
|
|
self:SetColor(Color(255, 210, 55))
|
|
self:SetMaxHealth(100)
|
|
self:SetHealth(100)
|
|
self:SetHealingTarget(NULL)
|
|
if self:GetSpawnAnchor() == vector_origin then self:SetSpawnAnchor(self:GetPos()) end
|
|
|
|
local physics = self:GetPhysicsObject()
|
|
if not IsValid(physics) then
|
|
ErrorNoHalt("[feedc0de_scanner] Scanner model has no VPhysics object\n")
|
|
self:Remove()
|
|
return
|
|
end
|
|
physics:EnableGravity(false)
|
|
physics:EnableDrag(true)
|
|
physics:SetMass(12)
|
|
physics:SetDamping(0.5, 1.8)
|
|
physics:Wake()
|
|
|
|
self.NextControllerThink = CurTime()
|
|
self.LastControllerThink = CurTime()
|
|
self.NextDestinationAt = 0
|
|
self.HealRemainder = 0
|
|
self.NextIdleSound = CurTime() + math.Rand(3, 7)
|
|
self:SetNW2Bool("CustomCoopFriendlyScanner", true)
|
|
|
|
self.FlightSound = CreateSound(self, "NPC_CScanner.FlyLoop")
|
|
if self.FlightSound then self.FlightSound:PlayEx(0.55, 100) end
|
|
|
|
-- NPC-list spawning places scripted entities directly on the floor and
|
|
-- does not necessarily call SpawnFunction. Establish the desired anchor
|
|
-- from a downward floor sensor after the physics object exists.
|
|
timer.Simple(0, function()
|
|
if not IsValid(self) or self.Destroyed then return end
|
|
local hoverPoint = self:FindFloorHoverPoint(self:GetPos())
|
|
if isvector(hoverPoint) then self:SetPatrolAnchor(hoverPoint, false) end
|
|
end)
|
|
end
|
|
|
|
function ENT:SetPatrolAnchor(position, moveThere)
|
|
if not isvector(position) then return end
|
|
self:SetSpawnAnchor(position)
|
|
self.PatrolDestination = nil
|
|
self.FocusPlayer = nil
|
|
self.NextDestinationAt = 0
|
|
local physics = self:GetPhysicsObject()
|
|
if IsValid(physics) then
|
|
physics:SetVelocity(vector_origin)
|
|
physics:AddAngleVelocity(-physics:GetAngleVelocity())
|
|
end
|
|
if moveThere then self:SetPos(position) end
|
|
end
|
|
|
|
function ENT:FindFloorHoverPoint(position)
|
|
local anchor = self:GetSpawnAnchor()
|
|
local probeZ = math.max(position.z, self:GetPos().z)
|
|
local floorTrace = util.TraceLine({
|
|
start = Vector(position.x, position.y, probeZ),
|
|
endpos = Vector(position.x, position.y, probeZ - 512),
|
|
filter = self,
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
})
|
|
if not floorTrace.Hit or floorTrace.StartSolid or floorTrace.HitNormal.z <= 0.45 then
|
|
return Vector(position.x, position.y, anchor.z)
|
|
end
|
|
|
|
local floorZ = floorTrace.HitPos.z
|
|
local ceilingTrace = util.TraceLine({
|
|
start = Vector(position.x, position.y, floorZ + 20),
|
|
endpos = Vector(position.x, position.y, floorZ + hoverHeight + 64),
|
|
filter = self,
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
})
|
|
local targetZ = floorZ + hoverHeight
|
|
if ceilingTrace.Hit then targetZ = math.min(targetZ, ceilingTrace.HitPos.z - 24) end
|
|
targetZ = math.max(targetZ, floorZ + 30)
|
|
return Vector(position.x, position.y, targetZ)
|
|
end
|
|
|
|
function ENT:PlayersNearAnchor(maximumDistance)
|
|
local result = {}
|
|
local maximumDistanceSquared = maximumDistance * maximumDistance
|
|
for _, ply in ipairs(player.GetHumans()) do
|
|
if ply:Alive() and ply:GetPos():DistToSqr(self:GetSpawnAnchor()) <= maximumDistanceSquared then
|
|
result[#result + 1] = ply
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
function ENT:FindHealingPatient()
|
|
local maximumDistance = math.max(patientSearchRadius:GetFloat(), healRadius:GetFloat(), 0)
|
|
local maximumDistanceSquared = maximumDistance * maximumDistance
|
|
local closest
|
|
local closestDistance
|
|
for _, ply in ipairs(player.GetHumans()) do
|
|
local distance = self:GetPos():DistToSqr(ply:GetPos())
|
|
local maximumHealth = math.max(ply:GetMaxHealth(), 100)
|
|
if ply:Alive()
|
|
and ply:Health() > 0
|
|
and ply:Health() < maximumHealth
|
|
and distance <= maximumDistanceSquared
|
|
and (not closestDistance or distance < closestDistance)
|
|
then
|
|
-- Acquisition deliberately ignores line of sight. The scanner
|
|
-- must first approach patients around spawn-room corners; the
|
|
-- actual healing beam still requires a clear trace.
|
|
closest = ply
|
|
closestDistance = distance
|
|
end
|
|
end
|
|
return closest
|
|
end
|
|
|
|
function ENT:CanHealPatient(patient)
|
|
if not IsValid(patient) or not patient:IsPlayer() or not patient:Alive() then return false end
|
|
local maximumDistance = math.max(healRadius:GetFloat(), 0)
|
|
if self:GetPos():DistToSqr(patient:GetPos()) > maximumDistance * maximumDistance then return false end
|
|
local trace = util.TraceLine({
|
|
start = self:WorldSpaceCenter(),
|
|
endpos = patient:WorldSpaceCenter(),
|
|
filter = { self, patient },
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
})
|
|
return not trace.Hit
|
|
end
|
|
|
|
function ENT:ChooseDestination(preferredPlayer)
|
|
local nearbyPlayers = self:PlayersNearAnchor(380)
|
|
local focus = IsValid(preferredPlayer) and preferredPlayer
|
|
or (#nearbyPlayers > 0 and nearbyPlayers[math.random(#nearbyPlayers)] or nil)
|
|
self.FocusPlayer = focus
|
|
|
|
for attempt = 1, 24 do
|
|
local destination
|
|
if IsValid(focus) and (IsValid(preferredPlayer) or math.Rand(0, 1) < 0.65) then
|
|
local angle = math.Rand(0, math.pi * 2)
|
|
local offset = Vector(math.cos(angle), math.sin(angle), 0) * math.Rand(55, 90)
|
|
destination = self:FindFloorHoverPoint(focus:GetPos() + offset)
|
|
else
|
|
local angle = math.Rand(0, math.pi * 2)
|
|
local offset = Vector(math.cos(angle), math.sin(angle), 0) * math.Rand(35, patrolRadius)
|
|
destination = self:FindFloorHoverPoint(self:GetSpawnAnchor() + offset)
|
|
end
|
|
|
|
local fromAnchor = destination - self:GetSpawnAnchor()
|
|
local horizontal = Vector(fromAnchor.x, fromAnchor.y, 0)
|
|
if horizontal:Length() > patrolRadius then
|
|
horizontal = horizontal:GetNormalized() * patrolRadius
|
|
destination.x = self:GetSpawnAnchor().x + horizontal.x
|
|
destination.y = self:GetSpawnAnchor().y + horizontal.y
|
|
end
|
|
|
|
local clear = util.TraceHull({
|
|
start = destination,
|
|
endpos = destination,
|
|
mins = flightHullMins,
|
|
maxs = flightHullMaxs,
|
|
filter = self,
|
|
mask = MASK_SOLID
|
|
})
|
|
local route = util.TraceHull({
|
|
start = self:GetPos(),
|
|
endpos = destination,
|
|
mins = flightHullMins,
|
|
maxs = flightHullMaxs,
|
|
filter = self,
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
})
|
|
local sufficientlyFar = self:GetPos():DistToSqr(destination) >= 70 * 70
|
|
if not clear.StartSolid
|
|
and not route.Hit
|
|
and util.IsInWorld(destination)
|
|
and (IsValid(preferredPlayer) or sufficientlyFar or attempt > 16)
|
|
then
|
|
self.PatrolDestination = destination
|
|
local path = destination - self:GetPos()
|
|
local horizontalPath = Vector(path.x, path.y, 0)
|
|
if horizontalPath:LengthSqr() > 1 then
|
|
horizontalPath:Normalize()
|
|
self.SwayAxis = Vector(-horizontalPath.y, horizontalPath.x, 0)
|
|
end
|
|
self.HoldPosition = nil
|
|
self.NextDestinationAt = CurTime() + math.Rand(5, 9)
|
|
return
|
|
end
|
|
end
|
|
|
|
self.PatrolDestination = nil
|
|
self.HoldPosition = self:GetPos()
|
|
self.NextDestinationAt = CurTime() + 1
|
|
end
|
|
|
|
function ENT:ApplyFlightController(targetPosition, lookDirection, elapsed, feedForwardVelocity, lookingAtPlayer)
|
|
local physics = self:GetPhysicsObject()
|
|
if not IsValid(physics) or self.PhysgunHeld then return end
|
|
|
|
feedForwardVelocity = feedForwardVelocity or vector_origin
|
|
local positionError = targetPosition - self:GetPos()
|
|
local horizontalError = Vector(positionError.x, positionError.y, 0)
|
|
local horizontalFeedForward = Vector(feedForwardVelocity.x, feedForwardVelocity.y, 0)
|
|
local desiredHorizontal = ClampVectorLength(horizontalError * 0.55 + horizontalFeedForward, 36)
|
|
local desiredVelocity = Vector(
|
|
desiredHorizontal.x,
|
|
desiredHorizontal.y,
|
|
math.Clamp(positionError.z * 0.5 + feedForwardVelocity.z, -12, 12)
|
|
)
|
|
local velocityError = desiredVelocity - physics:GetVelocity()
|
|
-- Strong enough to recover promptly after a barrel or explosion launches
|
|
-- it, while remaining an ordinary force-driven VPhysics body that can be
|
|
-- displaced and spun by the collision itself.
|
|
-- ApplyForceCenter changes VPhysics velocity during the current physics
|
|
-- step. Treating these as continuous-force gains above 1 makes velocity
|
|
-- overshoot and reverse every update. Correct only a small, elapsed-time
|
|
-- scaled fraction of the error per step for critically damped motion.
|
|
local horizontalGain = math.min(elapsed * 3.5, 0.2)
|
|
local verticalGain = math.min(elapsed * 4, 0.24)
|
|
local force = Vector(
|
|
velocityError.x * physics:GetMass() * horizontalGain,
|
|
velocityError.y * physics:GetMass() * horizontalGain,
|
|
velocityError.z * physics:GetMass() * verticalGain
|
|
)
|
|
force = ClampVectorLength(force, physics:GetMass() * 40)
|
|
physics:ApplyForceCenter(force)
|
|
|
|
if lookDirection:LengthSqr() > 1 then
|
|
local lookAngles = lookDirection:Angle()
|
|
local wavePitch = lookingAtPlayer and 0 or math.Clamp(-feedForwardVelocity.z * 0.8, -8, 8)
|
|
local desiredPitch = math.Clamp(math.NormalizeAngle(lookAngles.p) + wavePitch, -15, 40)
|
|
local desiredAngles = Angle(desiredPitch, lookAngles.y, 0)
|
|
local yawError = math.AngleDifference(desiredAngles.y, self:GetAngles().y)
|
|
local angularVelocity = physics:GetAngleVelocity()
|
|
local desiredYawVelocity = math.Clamp(yawError * 1.6, -75, 75)
|
|
|
|
local currentUp = self:GetUp()
|
|
local desiredUp = desiredAngles:Up()
|
|
local alignment = math.Clamp(currentUp:Dot(desiredUp), -1, 1)
|
|
local tiltAngle = math.deg(math.acos(alignment))
|
|
local rightingAxis = currentUp:Cross(desiredUp)
|
|
if rightingAxis:LengthSqr() < 0.001 and alignment < 0 then
|
|
rightingAxis = self:GetRight()
|
|
elseif rightingAxis:LengthSqr() > 0 then
|
|
rightingAxis:Normalize()
|
|
end
|
|
|
|
-- Normal steering stays soft, but a collision that rolls the scanner
|
|
-- far over gets a decisive recovery. Suppress most yaw authority
|
|
-- while badly tilted so turning cannot compete with self-righting.
|
|
local severeTilt = tiltAngle > 25
|
|
local yawAuthority = math.Clamp((alignment - 0.15) / 0.7, 0, 1)
|
|
local rightingSpeed = math.min(tiltAngle * (severeTilt and 5 or 3), 220)
|
|
local desiredWorldAngularVelocity = rightingAxis * rightingSpeed
|
|
+ worldUp * desiredYawVelocity * yawAuthority
|
|
local desiredAngularVelocity = physics:WorldToLocalVector(desiredWorldAngularVelocity)
|
|
local correctionGain = math.min(elapsed * (severeTilt and 9 or 4.5), severeTilt and 0.55 or 0.25)
|
|
local correctionLimit = severeTilt and 30 or 12
|
|
local correction = (desiredAngularVelocity - angularVelocity) * correctionGain
|
|
physics:AddAngleVelocity(ClampVectorLength(correction, correctionLimit))
|
|
end
|
|
end
|
|
|
|
function ENT:Think()
|
|
if self.Destroyed then return false end
|
|
local now = CurTime()
|
|
if (self.NextControllerThink or 0) > now then
|
|
self:NextThink(self.NextControllerThink)
|
|
return true
|
|
end
|
|
local elapsed = math.Clamp(now - (self.LastControllerThink or now), 0.01, 0.1)
|
|
self.LastControllerThink = now
|
|
self.NextControllerThink = now + 0.05
|
|
self:NextThink(self.NextControllerThink)
|
|
|
|
if not enabled:GetBool() then
|
|
self:Remove()
|
|
return true
|
|
end
|
|
|
|
local patient = self:FindHealingPatient()
|
|
local canHeal = self:CanHealPatient(patient)
|
|
self:SetHealingTarget(canHeal and patient or NULL)
|
|
if IsValid(patient) and self.FocusPlayer ~= patient then
|
|
self:ChooseDestination(patient)
|
|
elseif not self.PatrolDestination and (self.NextDestinationAt or 0) <= now then
|
|
self:ChooseDestination(IsValid(patient) and patient or nil)
|
|
end
|
|
|
|
local wavePhase = now + self:EntIndex()
|
|
local wave = math.sin(wavePhase) * 10
|
|
local waveVelocity = math.cos(wavePhase) * 10
|
|
local sway = math.cos(wavePhase) * 6
|
|
local swayVelocity = -math.sin(wavePhase) * 6
|
|
local swayAxis = self.SwayAxis or Vector(self:GetRight().x, self:GetRight().y, 0):GetNormalized()
|
|
local targetPosition = self.PatrolDestination or self.HoldPosition or self:GetSpawnAnchor()
|
|
targetPosition = Vector(targetPosition.x, targetPosition.y, targetPosition.z + wave) + swayAxis * sway
|
|
local feedForwardVelocity = swayAxis * swayVelocity + Vector(0, 0, waveVelocity)
|
|
|
|
local travel = targetPosition - self:GetPos()
|
|
if travel:LengthSqr() < 12 * 12 and self.PatrolDestination then
|
|
self.HoldPosition = self.PatrolDestination
|
|
self.PatrolDestination = nil
|
|
self.NextDestinationAt = now + math.Rand(1.5, 3)
|
|
elseif travel:LengthSqr() > 1 then
|
|
local obstacle = util.TraceHull({
|
|
start = self:GetPos(),
|
|
endpos = self:GetPos() + travel:GetNormalized() * math.min(travel:Length(), 42),
|
|
mins = flightHullMins,
|
|
maxs = flightHullMaxs,
|
|
filter = self,
|
|
mask = MASK_SOLID
|
|
})
|
|
if obstacle.Hit then
|
|
self.PatrolDestination = nil
|
|
self.HoldPosition = self:GetPos()
|
|
self.NextDestinationAt = now + 0.8
|
|
targetPosition = self.HoldPosition
|
|
feedForwardVelocity = vector_origin
|
|
travel = targetPosition - self:GetPos()
|
|
end
|
|
end
|
|
|
|
local horizontalTravel = Vector(travel.x, travel.y, 0)
|
|
local lookDirection
|
|
local lookingAtPlayer = false
|
|
if IsValid(patient) then
|
|
lookDirection = patient:EyePos() - self:WorldSpaceCenter()
|
|
lookingAtPlayer = true
|
|
elseif IsValid(self.FocusPlayer) then
|
|
lookDirection = self.FocusPlayer:EyePos() - self:WorldSpaceCenter()
|
|
lookingAtPlayer = true
|
|
elseif horizontalTravel:LengthSqr() > 12 * 12 then
|
|
lookDirection = travel
|
|
else
|
|
lookDirection = Vector(self:GetForward().x, self:GetForward().y, 0)
|
|
end
|
|
self:ApplyFlightController(targetPosition, lookDirection, elapsed, feedForwardVelocity, lookingAtPlayer)
|
|
|
|
self:SetPoseParameter("dynamo_wheel", math.NormalizeAngle(now * 180))
|
|
self:SetPoseParameter("tail_control", 0)
|
|
self:SetPoseParameter("alert_control", IsValid(patient) and 1 or 0.25)
|
|
self:SetPoseParameter("flex_vert", 0)
|
|
self:SetPoseParameter("flex_horz", 0)
|
|
|
|
if (self.NextIdleSound or 0) <= now then
|
|
self.NextIdleSound = now + math.Rand(6, 12)
|
|
self:EmitSound("NPC_CScanner.Idle", 65, math.random(96, 106), 0.42)
|
|
end
|
|
|
|
if canHeal then
|
|
self.HealRemainder = (self.HealRemainder or 0) + math.max(healRate:GetFloat(), 0) * elapsed
|
|
local health = math.floor(self.HealRemainder)
|
|
if health > 0 then
|
|
self.HealRemainder = self.HealRemainder - health
|
|
patient:SetHealth(math.min(patient:Health() + health, math.max(patient:GetMaxHealth(), 100)))
|
|
end
|
|
if (self.NextHealSound or 0) <= now then
|
|
self.NextHealSound = now + 1.2
|
|
self:EmitSound("items/smallmedkit1.wav", 58, math.random(105, 118), 0.28)
|
|
end
|
|
else
|
|
self.HealRemainder = 0
|
|
end
|
|
return true
|
|
end
|
|
|
|
function ENT:OnTakeDamage(damageInfo)
|
|
local physics = self:GetPhysicsObject()
|
|
if self.Destroyed then
|
|
if IsValid(physics) and damageInfo:GetDamageForce():LengthSqr() > 0 then
|
|
physics:ApplyForceOffset(damageInfo:GetDamageForce(), damageInfo:GetDamagePosition())
|
|
physics:Wake()
|
|
end
|
|
return
|
|
end
|
|
|
|
if self.PhysgunHeld
|
|
or (self.PhysgunProtectedUntil or 0) > CurTime()
|
|
or damageInfo:IsDamageType(DMG_CRUSH)
|
|
then
|
|
return
|
|
end
|
|
|
|
if IsValid(physics) and damageInfo:GetDamageForce():LengthSqr() > 0 then
|
|
physics:ApplyForceOffset(damageInfo:GetDamageForce(), damageInfo:GetDamagePosition())
|
|
end
|
|
self:SetHealth(self:Health() - math.max(damageInfo:GetDamage(), 0))
|
|
if self:Health() > 0 or self.Destroyed then return end
|
|
self.Destroyed = true
|
|
|
|
local killer = IsValid(damageInfo:GetAttacker()) and damageInfo:GetAttacker() or damageInfo:GetInflictor()
|
|
if IsValid(killer) and not killer:IsPlayer() and IsValid(killer:GetOwner()) then killer = killer:GetOwner() end
|
|
if IsValid(killer) and killer:IsPlayer() then
|
|
PrintMessage(HUD_PRINTTALK, "[Friendly Scanner] " .. string.format(
|
|
farewellMessages[math.random(#farewellMessages)], killer:Nick()
|
|
))
|
|
else
|
|
PrintMessage(HUD_PRINTTALK, "[Friendly Scanner] Tell my charging station I loved it.")
|
|
end
|
|
|
|
local anchor = self:GetSpawnAnchor()
|
|
local managed = self:GetNW2Bool("CustomCoopGamemodeManaged", false)
|
|
local effect = EffectData()
|
|
effect:SetOrigin(self:WorldSpaceCenter())
|
|
util.Effect("cball_explode", effect, true, true)
|
|
self:EmitSound("npc/scanner/scanner_explode_crash2.wav", 72, 105, 0.8)
|
|
|
|
-- Leave the destroyed scanner behind as an ordinary physical wreck. The
|
|
-- controller no longer fights gravity or impact forces, so explosions,
|
|
-- props and players can throw it around naturally.
|
|
self:SetHealingTarget(NULL)
|
|
self:SetNW2Bool("CustomCoopFriendlyScanner", false)
|
|
self:SetNW2Bool("CustomCoopGamemodeManaged", false)
|
|
self:SetNW2Bool("CustomCoopScannerWreck", true)
|
|
self:SetColor(Color(90, 82, 68))
|
|
if IsValid(physics) then
|
|
physics:EnableGravity(true)
|
|
physics:EnableDrag(true)
|
|
physics:SetDamping(0.05, 0.08)
|
|
physics:SetMass(18)
|
|
physics:Wake()
|
|
local damageForce = damageInfo:GetDamageForce()
|
|
if damageForce:LengthSqr() > 0 then
|
|
physics:ApplyForceOffset(damageForce, damageInfo:GetDamagePosition())
|
|
end
|
|
end
|
|
SafeRemoveEntityDelayed(self, 25)
|
|
hook.Run("Feedc0deFriendlyScannerDestroyed", self, anchor, managed)
|
|
end
|
|
|
|
function ENT:OnRemove()
|
|
if self.FlightSound then
|
|
self.FlightSound:Stop()
|
|
self.FlightSound = nil
|
|
end
|
|
end
|
|
|
|
hook.Add("PhysgunPickup", "Feedc0deFriendlyScannerPhysgunPickup", function(_, ent)
|
|
if not IsScanner(ent) then return end
|
|
ent.PhysgunHeld = true
|
|
ent.PhysgunProtectedUntil = CurTime() + 1
|
|
ent.PatrolDestination = nil
|
|
return true
|
|
end)
|
|
|
|
hook.Add("PhysgunDrop", "Feedc0deFriendlyScannerPhysgunDrop", function(_, ent)
|
|
if not IsScanner(ent) then return end
|
|
ent.PhysgunHeld = false
|
|
ent.PhysgunProtectedUntil = CurTime() + 2
|
|
local hoverPoint = ent:FindFloorHoverPoint(ent:GetPos())
|
|
ent:SetPatrolAnchor(isvector(hoverPoint) and hoverPoint or ent:GetPos(), false)
|
|
end)
|