mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 13:23:46 +03:00
613 lines
13 KiB
Lua
613 lines
13 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/
|
|
--]]
|
|
|
|
AddCSLuaFile()
|
|
|
|
ENT.Type = "anim"
|
|
ENT.Base = "base_gmodentity"
|
|
|
|
ENT.PrintName = "Hunter"
|
|
ENT.Category = "Groundwatch"
|
|
|
|
ENT.Spawnable = true
|
|
ENT.AdminOnly = true
|
|
|
|
ENT.AutomaticFrameAdvance = true
|
|
|
|
ENT.TPOffset = Vector(-300, 0, 0)
|
|
|
|
local MOVE_WALK = 0
|
|
local MOVE_RUN = 1
|
|
|
|
function ENT:SpawnFunction(ply, tr, className)
|
|
if not tr.Hit then
|
|
return
|
|
end
|
|
|
|
local spawnPos = tr.HitPos + tr.HitNormal
|
|
|
|
local ent = ents.Create(className)
|
|
ent:SetPos(spawnPos)
|
|
ent:Spawn()
|
|
ent:Activate()
|
|
|
|
ent.Owner = ply
|
|
|
|
return ent
|
|
end
|
|
|
|
function ENT:Initialize()
|
|
if SERVER then
|
|
self:SetModel("models/hunter.mdl")
|
|
|
|
self:PhysicsInitBox(Vector(-18, -18, 55), Vector(18, 18, 100))
|
|
self:SetMoveType(MOVETYPE_VPHYSICS)
|
|
|
|
self:SetCollisionGroup(COLLISION_GROUP_NPC)
|
|
|
|
self:SetUseType(SIMPLE_USE)
|
|
|
|
local phys = self:GetPhysicsObject()
|
|
|
|
if IsValid(phys) then
|
|
phys:SetMass(500000)
|
|
end
|
|
|
|
self.Driver = ents.Create("prop_vehicle_prisoner_pod")
|
|
self.Driver:SetModel("models/props_lab/cactus.mdl")
|
|
self.Driver:SetPos(self:GetAttachment(9).Pos)
|
|
self.Driver:SetAngles(self:GetAngles())
|
|
self.Driver:SetSolid(SOLID_NONE)
|
|
self.Driver:SetKeyValue("limitview", 0, 0)
|
|
self.Driver:SetNoDraw(true)
|
|
self.Driver:Spawn()
|
|
self.Driver:SetParent(self)
|
|
self.Driver:SetNotSolid(true)
|
|
self.Driver:SetNWEntity("GWEnt", self)
|
|
|
|
self:SetDriver(self.Driver)
|
|
|
|
self:DeleteOnRemove(self.Driver)
|
|
|
|
self:StartMotionController()
|
|
|
|
self:SetMaxHealth(GetConVar("gw_hunter_health"):GetInt())
|
|
self:SetHealth(self:GetMaxHealth())
|
|
|
|
self:SetOverride("drop_down")
|
|
|
|
self.StoredYaw = self:GetAngles().y
|
|
self.StoredAimYaw = 0
|
|
|
|
self.NextShot = 0
|
|
|
|
self.LastPercentage = 0
|
|
self.BleedFromAnus = false
|
|
|
|
self.HeadRadius = (GW.GetStringAttachment(self, "head_center").Pos - GW.GetStringAttachment(self, "head_radius_measure").Pos):Length()
|
|
end
|
|
end
|
|
|
|
function ENT:SetupDataTables()
|
|
self:NetworkVar("Entity", 0, "Driver")
|
|
end
|
|
|
|
function ENT:Think()
|
|
local phys = self:GetPhysicsObject()
|
|
|
|
if IsValid(phys) then
|
|
phys:Wake()
|
|
end
|
|
|
|
if SERVER then
|
|
self:AimGun()
|
|
self:WeaponThink()
|
|
self:BleedThink()
|
|
end
|
|
|
|
self:NextThink(CurTime())
|
|
return true
|
|
end
|
|
|
|
function ENT:HasLOS()
|
|
local ent = self:GetDriver()
|
|
|
|
if not IsValid(ent) then
|
|
return
|
|
end
|
|
|
|
local ply
|
|
|
|
if CLIENT then
|
|
ply = LocalPlayer()
|
|
else
|
|
ply = ent:GetDriver()
|
|
end
|
|
|
|
if IsValid(ply) then
|
|
local hitpos = self:GetHitpos(ply)
|
|
local barrel = self:GetAttachment(6)
|
|
|
|
local dot = barrel.Ang:Forward():Dot((hitpos - barrel.Pos):GetNormalized())
|
|
|
|
if dot >= 0.9 then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function ENT:GetViewData(ply)
|
|
if not IsValid(ply) then
|
|
return
|
|
end
|
|
|
|
local eye = ply:EyeAngles()
|
|
|
|
-- Hours wasted on trying to find what the issue was: 4.5
|
|
-- Hours wasted on trying to fix the issue before finding out the fix was the issue: Too many
|
|
if SERVER then
|
|
eye = self:WorldToLocalAngles(eye) -- Note to self: NEVER subtract angles when you can WorldToLocal/LocalToWorld
|
|
end
|
|
|
|
local thirdperson = ply:GetVehicle():GetThirdPersonMode()
|
|
|
|
local pos, ang
|
|
|
|
if thirdperson then
|
|
local center = self:GetAttachment(9).Pos
|
|
|
|
local trace = util.TraceLine({
|
|
start = center,
|
|
endpos = center + eye:Up() * self.TPOffset.z + eye:Forward() * self.TPOffset.x,
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
})
|
|
|
|
pos = trace.HitPos + trace.HitNormal * 5
|
|
ang = eye
|
|
else
|
|
local entAng = self:GetAngles()
|
|
|
|
entAng.p = 0
|
|
entAng.r = 0
|
|
|
|
pos = self:GetAttachment(6).Pos
|
|
ang = eye
|
|
end
|
|
|
|
return pos, ang
|
|
end
|
|
|
|
function ENT:GetHitpos(ply)
|
|
local pos, ang = self:GetViewData(ply)
|
|
local trace = {
|
|
start = pos,
|
|
endpos = pos + ang:Forward() * 10000,
|
|
filter = {self},
|
|
mask = MASK_SOLID_BRUSHONLY
|
|
}
|
|
|
|
return util.TraceLine(trace).HitPos
|
|
end
|
|
|
|
function ENT:CanPhysgun(ply)
|
|
if ply and ply:IsValid() then
|
|
return ply:IsAdmin()
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
if CLIENT then
|
|
function ENT:FireAnimationEvent(_, _, event)
|
|
local sequence = self:GetSequenceName(self:GetSequence())
|
|
|
|
if (sequence == "canter_all" or sequence == "walk_all") and (event == 6006 or event == 6007) then
|
|
self:EmitSound("NPC_Hunter.Footstep")
|
|
end
|
|
end
|
|
end
|
|
|
|
if SERVER then
|
|
function ENT:Use(ply)
|
|
if IsValid(self.Driver:GetDriver()) then
|
|
return
|
|
end
|
|
|
|
ply:EnterVehicle(self.Driver)
|
|
ply:SetNoDraw(true)
|
|
end
|
|
|
|
function ENT:Eject(ply)
|
|
ply:SetNoDraw(false)
|
|
end
|
|
|
|
function ENT:KeyPress(ply, key)
|
|
if key == IN_RELOAD and not self.AnimEnd then
|
|
-- Scan sounds don't have their own animation
|
|
if ply:KeyDown(IN_WALK) then
|
|
self:EmitSound("NPC_Hunter.Scan")
|
|
|
|
return
|
|
end
|
|
|
|
local anims = {
|
|
"hunter_call_1",
|
|
"hunter_respond_1",
|
|
"hunter_respond_3"
|
|
}
|
|
|
|
local anims_angry = {
|
|
"hunter_angry",
|
|
"hunter_angry_2"
|
|
}
|
|
|
|
local anim
|
|
|
|
if ply:KeyDown(IN_SPEED) then
|
|
anim = anims_angry[math.random(1, #anims_angry)]
|
|
else
|
|
anim = anims[math.random(1, #anims)]
|
|
end
|
|
|
|
self:SetOverride(anim)
|
|
elseif key == IN_ATTACK2 and not self.AnimEnd then
|
|
local anims = {
|
|
"melee_02",
|
|
"meleeleft",
|
|
"meleert"
|
|
}
|
|
|
|
local anim = anims[math.random(1, #anims)]
|
|
|
|
local time = self:SetOverride(anim)
|
|
|
|
timer.Simple(time * 0.5, function()
|
|
if not IsValid(ply) then
|
|
return
|
|
end
|
|
|
|
local pos = self:GetAttachment(4).Pos
|
|
local _, ang = self:GetViewData(ply)
|
|
|
|
local trace = util.TraceHull({
|
|
start = pos,
|
|
endpos = pos + ang:Forward() * 100,
|
|
filter = self,
|
|
mins = Vector(-20, -20, -15),
|
|
maxs = Vector(20, 20, 15),
|
|
mask = MASK_SHOT_HULL
|
|
})
|
|
|
|
if IsValid(trace.Entity) and (trace.Entity:IsPlayer() or trace.Entity:IsNPC() or trace.Entity:Health() > 0) then
|
|
local info = DamageInfo()
|
|
|
|
info:SetAttacker(ply)
|
|
info:SetInflictor(self)
|
|
|
|
info:SetDamageType(bit.bor(DMG_CLUB, DMG_SLASH))
|
|
|
|
info:SetDamagePosition(trace.HitPos)
|
|
|
|
if trace.Entity:IsNPC() then
|
|
info:SetDamage(trace.Entity:Health())
|
|
else
|
|
info:SetDamage(120)
|
|
end
|
|
|
|
info:SetDamageForce(trace.Normal * 10000)
|
|
|
|
trace.Entity:TakeDamageInfo(info)
|
|
end
|
|
end)
|
|
|
|
self:EmitSound("NPC_Hunter.MeleeAnnounce")
|
|
end
|
|
end
|
|
|
|
function ENT:SetOverride(sequence)
|
|
local time = self:SequenceDuration(self:LookupSequence(sequence))
|
|
|
|
if self:EasySetSequence(sequence) then
|
|
self.AnimEnd = CurTime() + time
|
|
end
|
|
|
|
return time
|
|
end
|
|
|
|
function ENT:EasySetSequence(sequence)
|
|
if self:GetSequence() != self:LookupSequence(sequence) then
|
|
self:SetCycle(0)
|
|
self:ResetSequence(sequence)
|
|
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function ENT:AimGun()
|
|
local ply = self.Driver:GetDriver()
|
|
|
|
local pitch = 0
|
|
local yaw = 0
|
|
|
|
if IsValid(ply) then
|
|
-- Thanks wiremod
|
|
local rad2deg = 180 / math.pi
|
|
|
|
local pos, _ = WorldToLocal(self:GetHitpos(ply), self:GetAngles(), self:GetAttachment(11).Pos, self:GetAngles())
|
|
local len = pos:Length()
|
|
|
|
if len < 0.0000001000000 then
|
|
pitch = 0
|
|
else
|
|
pitch = rad2deg * math.asin(pos.z / len)
|
|
end
|
|
|
|
yaw = rad2deg * math.atan2(pos.y, pos.x)
|
|
end
|
|
|
|
local pitchMin, pitchMax = self:GetPoseParameterRange(5)
|
|
local yawMin, yawMax = self:GetPoseParameterRange(4)
|
|
|
|
if yaw > 120 then
|
|
yaw = yaw - 180
|
|
elseif yaw < -120 then
|
|
yaw = yaw + 180
|
|
end
|
|
|
|
yaw = math.Approach(self.StoredAimYaw, yaw, 10)
|
|
|
|
pitch = math.Clamp(pitch, pitchMin, pitchMax)
|
|
yaw = math.Clamp(yaw, yawMin, yawMax)
|
|
|
|
self:SetPoseParameter("body_Pitch", -pitch)
|
|
self:SetPoseParameter("body_yaw", yaw)
|
|
|
|
self.StoredAimYaw = yaw
|
|
end
|
|
|
|
function ENT:WeaponThink()
|
|
local ply = self.Driver:GetDriver()
|
|
|
|
if not IsValid(ply) then
|
|
return
|
|
end
|
|
|
|
if ply:KeyDown(IN_ATTACK) then
|
|
if self.NextShot <= CurTime() and self:HasLOS() and self.IsPlanted and not self.AnimEnd then
|
|
self.NextShot = CurTime() + 0.1
|
|
|
|
self.Attach = not self.Attach
|
|
|
|
local pos = self:GetAttachment(self.Attach and 4 or 5).Pos
|
|
local ang = (self:GetHitpos(ply) - pos):GetNormalized():Angle()
|
|
|
|
local ent = ents.Create("hunter_flechette")
|
|
ent:SetPos(pos)
|
|
ent:SetAngles(ang)
|
|
ent:Spawn()
|
|
ent:SetVelocity(ang:Forward() * 2000)
|
|
ent:SetOwner(self)
|
|
|
|
local effectdata = EffectData()
|
|
effectdata:SetAttachment(self.Attach and 4 or 5)
|
|
effectdata:SetEntity(self)
|
|
|
|
ParticleEffect("hunter_muzzle_flash", pos, ang)
|
|
util.Effect("HunterMuzzleFlash", effectdata)
|
|
|
|
self:EmitSound("NPC_Hunter.FlechetteShoot")
|
|
end
|
|
|
|
if not self.IsPlanted and not self.AnimEnd then
|
|
timer.Simple(self:SetOverride("plant"), function()
|
|
if IsValid(self) then
|
|
self.IsPlanted = true
|
|
end
|
|
end)
|
|
end
|
|
elseif self.IsPlanted then
|
|
timer.Simple(self:SetOverride("unplant"), function()
|
|
if IsValid(self) then
|
|
self.IsPlanted = false
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
function ENT:BleedThink()
|
|
if not self.BleedFromAnus or self.BleedFromAnus > CurTime() then
|
|
return
|
|
end
|
|
|
|
local pos = GW.GetStringAttachment(self, "head_center").Pos
|
|
|
|
local dir = VectorRand()
|
|
dir:Normalize()
|
|
|
|
local ang = dir:Angle()
|
|
|
|
dir = dir * self.HeadRadius
|
|
|
|
ParticleEffect("blood_spurt_synth_01", pos + dir, ang, self)
|
|
|
|
self.BleedFromAnus = CurTime() + math.Rand(0.6, 1.5)
|
|
end
|
|
|
|
function ENT:OnTakeDamage(dmginfo)
|
|
-- TODO: Scale down damage taken during certain animations?
|
|
local ply = self.Driver:GetDriver()
|
|
|
|
if not IsValid(ply) then
|
|
return
|
|
end
|
|
|
|
local health = self:Health()
|
|
local dmg = dmginfo:GetDamage()
|
|
|
|
if self.IsPlanted or self.AnimEnd then
|
|
dmg = dmg * 0.5
|
|
end
|
|
|
|
if health <= 0 then
|
|
return
|
|
end
|
|
|
|
self:SetHealth(health - dmg)
|
|
|
|
health = self:Health()
|
|
|
|
local percentage = (health / self:GetMaxHealth()) * 100
|
|
|
|
if percentage <= 30 and self.LastPercentage > 30 then
|
|
self:EmitSound("NPC_Hunter.Pain")
|
|
|
|
-- Start gushing blood from our... anus or something.
|
|
ParticleEffectAttach("blood_drip_synth_01", PATTACH_POINT_FOLLOW, self, self:LookupAttachment("head_radius_measure"))
|
|
|
|
self.BleedFromAnus = CurTime()
|
|
end
|
|
|
|
if health <= 0 then
|
|
self:EmitSound("NPC_Hunter.Death")
|
|
|
|
local ragdoll = ents.Create("prop_ragdoll")
|
|
ragdoll:SetModel(self:GetModel())
|
|
ragdoll:SetPos(self:GetPos())
|
|
ragdoll:SetAngles(self:GetAngles())
|
|
ragdoll:Spawn()
|
|
ragdoll:Activate()
|
|
ragdoll:GetPhysicsObject():SetVelocity(self:GetPhysicsObject():GetVelocity())
|
|
|
|
ragdoll:SetCollisionGroup(COLLISION_GROUP_WEAPON)
|
|
|
|
self:Remove()
|
|
|
|
return
|
|
end
|
|
|
|
self.LastPercentage = percentage
|
|
end
|
|
|
|
function ENT:PhysicsSimulate(phys, delta)
|
|
local trace = util.TraceHull({
|
|
start = self:GetPos() + Vector(0, 0, 100),
|
|
endpos = self:GetPos() - (Vector(0, 0, 1) * 100),
|
|
filter = table.Add({self}, player.GetAll()),
|
|
mins = Vector(-18, -18, 0),
|
|
maxs = Vector(18, 18, 0)
|
|
})
|
|
|
|
local ply = self.Driver:GetDriver()
|
|
|
|
local vec = Vector()
|
|
local ang = Angle()
|
|
|
|
local speed = MOVE_WALK
|
|
|
|
if IsValid(ply) then
|
|
if ply:KeyDown(IN_SPEED) then
|
|
speed = MOVE_RUN
|
|
end
|
|
|
|
local dir = Vector()
|
|
|
|
if ply:KeyDown(IN_FORWARD) then
|
|
dir:Add(Vector(1, 0, 0))
|
|
end
|
|
|
|
if ply:KeyDown(IN_BACK) then
|
|
dir:Add(Vector(-1, 0, 0))
|
|
end
|
|
|
|
if ply:KeyDown(IN_MOVELEFT) then
|
|
dir:Add(Vector(0, 1, 0))
|
|
end
|
|
|
|
if ply:KeyDown(IN_MOVERIGHT) then
|
|
dir:Add(Vector(0, -1, 0))
|
|
end
|
|
|
|
if dir:Length() > 0 then
|
|
vec = Vector(1, 0, 0)
|
|
ang.y = dir:Angle().y
|
|
end
|
|
|
|
if self.IsPlanted or ply:KeyDown(IN_WALK) then
|
|
ang.y = self.StoredYaw
|
|
else
|
|
ang.y = ang.y + self:WorldToLocalAngles(ply:EyeAngles()).y
|
|
self.StoredYaw = ang.y
|
|
end
|
|
else
|
|
ang.y = self.StoredYaw
|
|
end
|
|
|
|
local mult = 20
|
|
|
|
if speed == MOVE_RUN then
|
|
mult = 30
|
|
end
|
|
|
|
vec = vec * mult
|
|
vec:Rotate(self:GetAngles())
|
|
|
|
if util.QuickTrace(self:GetAttachment(9).Pos + vec, Vector(), self).Fraction != 1 then
|
|
vec = Vector()
|
|
elseif self.AnimEnd or self.IsPlanted then
|
|
vec = Vector()
|
|
end
|
|
|
|
if not trace.Hit then
|
|
self:EasySetSequence("jump_idle")
|
|
elseif self.AnimEnd then
|
|
if self.AnimEnd <= CurTime() then
|
|
self.AnimEnd = nil
|
|
end
|
|
elseif self.IsPlanted then
|
|
if self:HasLOS() then
|
|
self:EasySetSequence("shoot_minigun")
|
|
else
|
|
self:EasySetSequence("idle_planted")
|
|
end
|
|
elseif vec:Length() > 0 then
|
|
if speed == MOVE_RUN then
|
|
self:EasySetSequence("canter_all")
|
|
else
|
|
self:EasySetSequence("walk_all")
|
|
end
|
|
else
|
|
self:EasySetSequence("idle_3")
|
|
end
|
|
|
|
self:SetPoseParameter("move_yaw", math.NormalizeAngle((vec:Angle().y - self:GetAngles().y) * 2))
|
|
|
|
local move = {}
|
|
move.secondstoarrive = 0.1
|
|
move.pos = trace.HitPos + vec
|
|
move.angle = ang
|
|
move.maxangular = math.Remap(self:GetVelocity():Length(), 10, 700, 300, 100)
|
|
move.maxangulardamp = 10000
|
|
move.maxspeed = 12000
|
|
move.maxspeeddamp = 10000
|
|
move.dampfactor = 0.8
|
|
move.teleportdistance = 0
|
|
move.deltatime = delta
|
|
|
|
if trace.Hit then
|
|
phys:ComputeShadowControl(move)
|
|
end
|
|
|
|
self.LastHit = trace.Hit
|
|
end
|
|
end |