mirror of
https://github.com/lifestorm/wnsrc.git
synced 2026-02-04 20:23:47 +03:00
635 lines
18 KiB
Lua
635 lines
18 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/
|
|
--]]
|
|
|
|
-- c4 explosive
|
|
|
|
local math = math
|
|
|
|
if SERVER then
|
|
AddCSLuaFile("cl_init.lua")
|
|
AddCSLuaFile("shared.lua")
|
|
end
|
|
|
|
if CLIENT then
|
|
-- this entity can be DNA-sampled so we need some display info
|
|
ENT.Icon = "vgui/ttt/icon_c4"
|
|
ENT.PrintName = "C4"
|
|
|
|
local GetPTranslation = LANG.GetParamTranslation
|
|
local hint_params = {usekey = Key("+use", "USE")}
|
|
|
|
ENT.TargetIDHint = {
|
|
name = "C4",
|
|
hint = "c4_hint",
|
|
fmt = function(ent, txt) return GetPTranslation(txt, hint_params) end
|
|
};
|
|
end
|
|
|
|
C4_WIRE_COUNT = 6
|
|
C4_MINIMUM_TIME = 45
|
|
C4_MAXIMUM_TIME = 600
|
|
|
|
ENT.Type = "anim"
|
|
ENT.Model = Model("models/weapons/w_c4_planted.mdl")
|
|
|
|
ENT.CanHavePrints = true
|
|
ENT.CanUseKey = true
|
|
ENT.Avoidable = true
|
|
|
|
AccessorFunc( ENT, "thrower", "Thrower")
|
|
|
|
AccessorFunc( ENT, "radius", "Radius", FORCE_NUMBER )
|
|
AccessorFunc( ENT, "dmg", "Dmg", FORCE_NUMBER )
|
|
|
|
AccessorFunc( ENT, "arm_time", "ArmTime", FORCE_NUMBER)
|
|
AccessorFunc( ENT, "timer_length", "TimerLength", FORCE_NUMBER)
|
|
|
|
-- Generate accessors for DT vars. This way all consumer code can keep accessing
|
|
-- the vars as they always did, the only difference is that behind the scenes
|
|
-- they are set up as DT vars.
|
|
AccessorFuncDT(ENT, "explode_time", "ExplodeTime")
|
|
AccessorFuncDT(ENT, "armed", "Armed")
|
|
|
|
ENT.Beep = 0
|
|
ENT.DetectiveNearRadius = 300
|
|
ENT.SafeWires = nil
|
|
|
|
function ENT:SetupDataTables()
|
|
self:DTVar("Int", 0, "explode_time")
|
|
self:DTVar("Bool", 0, "armed")
|
|
end
|
|
|
|
function ENT:Initialize()
|
|
self:SetModel(self.Model)
|
|
|
|
if SERVER then
|
|
self:PhysicsInit(SOLID_VPHYSICS)
|
|
end
|
|
self:SetMoveType(MOVETYPE_VPHYSICS)
|
|
self:SetSolid(SOLID_BBOX)
|
|
self:SetCollisionGroup(COLLISION_GROUP_WEAPON)
|
|
|
|
if SERVER then
|
|
self:SetUseType(SIMPLE_USE)
|
|
end
|
|
|
|
self.SafeWires = nil
|
|
self.Beep = 0
|
|
self.DisarmCausedExplosion = false
|
|
|
|
self:SetTimerLength(0)
|
|
self:SetExplodeTime(0)
|
|
self:SetArmed(false)
|
|
if not self:GetThrower() then self:SetThrower(nil) end
|
|
|
|
if not self:GetRadius() then self:SetRadius(1000) end
|
|
if not self:GetDmg() then self:SetDmg(200) end
|
|
|
|
end
|
|
|
|
function ENT:SetDetonateTimer(length)
|
|
self:SetTimerLength(length)
|
|
self:SetExplodeTime( CurTime() + length )
|
|
end
|
|
|
|
|
|
function ENT:UseOverride(activator)
|
|
if IsValid(activator) and activator:IsPlayer() then
|
|
-- Traitors not allowed to disarm other traitor's C4 until he is dead
|
|
local owner = self:GetOwner()
|
|
if self:GetArmed() and owner != activator and activator:GetTraitor() and (IsValid(owner) and owner:Alive() and owner:GetTraitor()) then
|
|
LANG.Msg(activator, "c4_no_disarm")
|
|
return
|
|
end
|
|
|
|
self:ShowC4Config(activator)
|
|
end
|
|
end
|
|
|
|
function ENT.SafeWiresForTime(t)
|
|
local m = t / 60
|
|
|
|
if m > 4 then return 1
|
|
elseif m > 3 then return 2
|
|
elseif m > 2 then return 3
|
|
elseif m > 1 then return 4
|
|
else return 5
|
|
end
|
|
end
|
|
|
|
function ENT:WeldToGround(state)
|
|
if self.IsOnWall then return end
|
|
|
|
if state then
|
|
-- getgroundentity does not work for non-players
|
|
-- so sweep ent downward to find what we're lying on
|
|
local ignore = player.GetAll()
|
|
table.insert(ignore, self)
|
|
|
|
local tr = util.TraceEntity({start=self:GetPos(), endpos=self:GetPos() - Vector(0,0,16), filter=ignore, mask=MASK_SOLID}, self)
|
|
|
|
-- Start by increasing weight/making uncarryable
|
|
local phys = self:GetPhysicsObject()
|
|
if IsValid(phys) then
|
|
-- Could just use a pickup flag for this. However, then it's easier to
|
|
-- push it around.
|
|
self.OrigMass = phys:GetMass()
|
|
phys:SetMass(150)
|
|
end
|
|
|
|
if tr.Hit and (IsValid(tr.Entity) or tr.HitWorld) then
|
|
-- "Attach" to a brush if possible
|
|
if IsValid(phys) and tr.HitWorld then
|
|
phys:EnableMotion(false)
|
|
end
|
|
|
|
-- Else weld to objects we cannot pick up
|
|
local entphys = tr.Entity:GetPhysicsObject()
|
|
if IsValid(entphys) and entphys:GetMass() > CARRY_WEIGHT_LIMIT then
|
|
constraint.Weld(self, tr.Entity, 0, 0, 0, true)
|
|
end
|
|
|
|
-- Worst case, we are still uncarryable
|
|
end
|
|
else
|
|
constraint.RemoveConstraints(self, "Weld")
|
|
local phys = self:GetPhysicsObject()
|
|
if IsValid(phys) then
|
|
phys:EnableMotion(true)
|
|
phys:SetMass(self.OrigMass or 10)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ENT:SphereDamage(dmgowner, center, radius)
|
|
-- It seems intuitive to use FindInSphere here, but that will find all ents
|
|
-- in the radius, whereas there exist only ~16 players. Hence it is more
|
|
-- efficient to cycle through all those players and do a Lua-side distance
|
|
-- check.
|
|
|
|
local r = radius ^ 2 -- square so we can compare with dot product directly
|
|
|
|
|
|
-- pre-declare to avoid realloc
|
|
local d = 0.0
|
|
local diff = nil
|
|
local dmg = 0
|
|
for _, ent in player.Iterator() do
|
|
if IsValid(ent) and ent:Team() == TEAM_TERROR then
|
|
|
|
-- dot of the difference with itself is distance squared
|
|
diff = center - ent:GetPos()
|
|
d = diff:Dot(diff)
|
|
|
|
if d < r then
|
|
-- deadly up to a certain range, then a quick falloff within 100 units
|
|
d = math.max(0, math.sqrt(d) - 490)
|
|
dmg = -0.01 * (d^2) + 125
|
|
|
|
local dmginfo = DamageInfo()
|
|
dmginfo:SetDamage(dmg)
|
|
dmginfo:SetAttacker(dmgowner)
|
|
dmginfo:SetInflictor(self)
|
|
dmginfo:SetDamageType(DMG_BLAST)
|
|
dmginfo:SetDamageForce(center - ent:GetPos())
|
|
dmginfo:SetDamagePosition(ent:GetPos())
|
|
|
|
ent:TakeDamageInfo(dmginfo)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local c4boom = Sound("c4.explode")
|
|
function ENT:Explode(tr)
|
|
hook.Call("TTTC4Explode", nil, self)
|
|
if SERVER then
|
|
self:SetNoDraw(true)
|
|
self:SetSolid(SOLID_NONE)
|
|
|
|
-- pull out of the surface
|
|
if tr.Fraction != 1.0 then
|
|
self:SetPos(tr.HitPos + tr.HitNormal * 0.6)
|
|
end
|
|
|
|
local pos = self:GetPos()
|
|
if util.PointContents(pos) == CONTENTS_WATER or GetRoundState() != ROUND_ACTIVE then
|
|
self:Remove()
|
|
self:SetExplodeTime(0)
|
|
return
|
|
end
|
|
|
|
local dmgowner = self:GetThrower()
|
|
dmgowner = IsValid(dmgowner) and dmgowner or self
|
|
|
|
local r_inner = 750
|
|
local r_outer = self:GetRadius()
|
|
|
|
if self.DisarmCausedExplosion then
|
|
r_inner = r_inner / 2.5
|
|
r_outer = r_outer / 2.5
|
|
end
|
|
|
|
-- damage through walls
|
|
self:SphereDamage(dmgowner, pos, r_inner)
|
|
|
|
-- explosion damage
|
|
util.BlastDamage(self, dmgowner, pos, r_outer, self:GetDmg())
|
|
|
|
local effect = EffectData()
|
|
effect:SetStart(pos)
|
|
effect:SetOrigin(pos)
|
|
-- these don't have much effect with the default Explosion
|
|
effect:SetScale(r_outer)
|
|
effect:SetRadius(r_outer)
|
|
effect:SetMagnitude(self:GetDmg())
|
|
|
|
if tr.Fraction != 1.0 then
|
|
effect:SetNormal(tr.HitNormal)
|
|
end
|
|
|
|
effect:SetOrigin(pos)
|
|
util.Effect("Explosion", effect, true, true)
|
|
util.Effect("HelicopterMegaBomb", effect, true, true)
|
|
|
|
timer.Simple(0.1, function() sound.Play(c4boom, pos, 100, 100) end)
|
|
|
|
-- extra push
|
|
local phexp = ents.Create("env_physexplosion")
|
|
phexp:SetPos(pos)
|
|
phexp:SetKeyValue("magnitude", self:GetDmg())
|
|
phexp:SetKeyValue("radius", r_outer)
|
|
phexp:SetKeyValue("spawnflags", "19")
|
|
phexp:Spawn()
|
|
phexp:Fire("Explode", "", 0)
|
|
|
|
|
|
-- few fire bits to ignite things
|
|
timer.Simple(0.2, function() StartFires(pos, tr, 4, 5, true, dmgowner) end)
|
|
|
|
self:SetExplodeTime(0)
|
|
|
|
SCORE:HandleC4Explosion(dmgowner, self:GetArmTime(), CurTime())
|
|
|
|
self:Remove()
|
|
else
|
|
local spos = self:GetPos()
|
|
local trs = util.TraceLine({start=spos + Vector(0,0,64), endpos=spos + Vector(0,0,-128), filter=self})
|
|
util.Decal("Scorch", trs.HitPos + trs.HitNormal, trs.HitPos - trs.HitNormal)
|
|
|
|
self:SetExplodeTime(0)
|
|
end
|
|
end
|
|
|
|
function ENT:IsDetectiveNear()
|
|
local center = self:GetPos()
|
|
local r = self.DetectiveNearRadius ^ 2
|
|
local d = 0.0
|
|
local diff = nil
|
|
for _, ent in player.Iterator() do
|
|
if IsValid(ent) and ent:IsActiveDetective() then
|
|
-- dot of the difference with itself is distance squared
|
|
diff = center - ent:GetPos()
|
|
d = diff:Dot(diff)
|
|
|
|
if d < r then
|
|
if ent:HasWeapon("weapon_ttt_defuser") then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local beep = Sound("weapons/c4/c4_beep1.wav")
|
|
local MAX_MOVE_RANGE = 1000000 -- sq of 1000
|
|
function ENT:Think()
|
|
if not self:GetArmed() then return end
|
|
|
|
if SERVER then
|
|
local curpos = self:GetPos()
|
|
if self.LastPos and self.LastPos:DistToSqr(curpos) > MAX_MOVE_RANGE then
|
|
self:Disarm(nil)
|
|
return
|
|
end
|
|
self.LastPos = curpos
|
|
end
|
|
|
|
local etime = self:GetExplodeTime()
|
|
if self:GetArmed() and etime != 0 and etime < CurTime() then
|
|
-- find the ground if it's near and pass it to the explosion
|
|
local spos = self:GetPos()
|
|
local tr = util.TraceLine({start=spos, endpos=spos + Vector(0,0,-32), mask=MASK_SHOT_HULL, filter=self:GetThrower()})
|
|
|
|
local success, err = pcall(self.Explode, self, tr)
|
|
if not success then
|
|
-- prevent effect spam on Lua error
|
|
self:Remove()
|
|
|
|
ErrorNoHalt("ERROR CAUGHT: ttt_c4: " .. err .. "\n")
|
|
end
|
|
elseif self:GetArmed() and CurTime() > self.Beep then
|
|
local amp = 48
|
|
|
|
if self:IsDetectiveNear() then
|
|
amp = 65
|
|
|
|
local dlight = CLIENT and DynamicLight(self:EntIndex())
|
|
if dlight then
|
|
dlight.Pos = self:GetPos()
|
|
dlight.r = 255
|
|
dlight.g = 0
|
|
dlight.b = 0
|
|
dlight.Brightness = 1
|
|
dlight.Size = 128
|
|
dlight.Decay = 500
|
|
dlight.DieTime = CurTime() + 0.1
|
|
end
|
|
|
|
elseif SERVER then
|
|
-- volume lower for long fuse times, bottoms at 50 at +5mins
|
|
amp = amp + math.max(0, 12 - (0.03 * self:GetTimerLength()))
|
|
end
|
|
|
|
if SERVER then
|
|
sound.Play(beep, self:GetPos(), amp, 100)
|
|
end
|
|
|
|
local btime = (etime - CurTime()) / 30
|
|
self.Beep = CurTime() + btime
|
|
end
|
|
end
|
|
|
|
function ENT:Defusable()
|
|
return self:GetArmed()
|
|
end
|
|
|
|
-- Timer configuration handlign
|
|
|
|
if SERVER then
|
|
-- Inform traitors about us
|
|
function ENT:SendWarn(armed)
|
|
net.Start("TTT_C4Warn")
|
|
net.WriteUInt(self:EntIndex(), 16)
|
|
net.WriteBit(armed)
|
|
if armed then
|
|
net.WriteVector(self:GetPos())
|
|
net.WriteFloat(self:GetExplodeTime())
|
|
end
|
|
net.Send(GetTraitorFilter(true))
|
|
end
|
|
|
|
function ENT:OnRemove()
|
|
self:SendWarn(false)
|
|
end
|
|
|
|
function ENT:Disarm(ply)
|
|
local owner = self:GetOwner()
|
|
|
|
SCORE:HandleC4Disarm(ply, owner, true)
|
|
|
|
if ply != owner and IsValid(owner) then
|
|
LANG.Msg(owner, "c4_disarm_warn")
|
|
end
|
|
|
|
self:SetExplodeTime(0)
|
|
self:SetArmed(false)
|
|
self:WeldToGround(false)
|
|
self:SendWarn(false)
|
|
|
|
self.DisarmCausedExplosion = false
|
|
end
|
|
|
|
function ENT:FailedDisarm(ply)
|
|
self.DisarmCausedExplosion = true
|
|
|
|
SCORE:HandleC4Disarm(ply, self:GetOwner(), false)
|
|
|
|
-- tiny moment of zen and realization before the bang
|
|
self:SetExplodeTime(CurTime() + 0.1)
|
|
end
|
|
|
|
function ENT:Arm(ply, time)
|
|
|
|
-- Initialize armed state
|
|
self:SetDetonateTimer(time)
|
|
self:SetArmTime(CurTime())
|
|
|
|
self:SetArmed(true)
|
|
self:WeldToGround(true)
|
|
self.DisarmCausedExplosion = false
|
|
|
|
-- ply may be a different player than he who dropped us.
|
|
-- Arming player should be the damage owner = "thrower"
|
|
self:SetThrower(ply)
|
|
-- Owner determines who gets messages and can quick-disarm if traitor,
|
|
-- make that the armer as well for now. Theoretically the dropping player
|
|
-- should also be able to quick-disarm, but that's going to be rare.
|
|
self:SetOwner(ply)
|
|
|
|
-- Wire stuff:
|
|
|
|
self.SafeWires = {}
|
|
|
|
-- list of possible wires to make safe
|
|
local choices = {}
|
|
for i=1, C4_WIRE_COUNT do
|
|
table.insert(choices, i)
|
|
end
|
|
|
|
-- random selection process, lot like traitor selection
|
|
local safe_count = self.SafeWiresForTime(time)
|
|
local safes = {}
|
|
local picked = 0
|
|
while picked < safe_count do
|
|
local pick = math.random(1, #choices)
|
|
local w = choices[pick]
|
|
|
|
if not self.SafeWires[w] then
|
|
self.SafeWires[w] = true
|
|
table.remove(choices, pick)
|
|
|
|
-- owner will end up having the last safe wire on his corpse
|
|
ply.bomb_wire = w
|
|
|
|
picked = picked + 1
|
|
end
|
|
end
|
|
|
|
-- send indicator to traitors
|
|
self:SendWarn(true)
|
|
end
|
|
|
|
function ENT:ShowC4Config(ply)
|
|
-- show menu to player to configure or disarm us
|
|
net.Start("TTT_C4Config")
|
|
net.WriteEntity(self)
|
|
net.Send(ply)
|
|
end
|
|
|
|
local function ReceiveC4Config(ply, cmd, args)
|
|
if not (IsValid(ply) and ply:IsTerror() and #args == 2) then return end
|
|
local idx = tonumber(args[1])
|
|
local time = tonumber(args[2])
|
|
|
|
if not idx or not time then return end
|
|
|
|
local bomb = ents.GetByIndex(idx)
|
|
if IsValid(bomb) and bomb:GetClass() == "ttt_c4" and (not bomb:GetArmed()) then
|
|
|
|
if bomb:GetPos():Distance(ply:GetPos()) > 256 then
|
|
-- These cases should never arise in normal play, so no messages
|
|
return
|
|
elseif time < C4_MINIMUM_TIME or time > C4_MAXIMUM_TIME then
|
|
return
|
|
elseif IsValid(bomb:GetPhysicsObject()) and bomb:GetPhysicsObject():HasGameFlag(FVPHYSICS_PLAYER_HELD) then
|
|
return
|
|
else
|
|
LANG.Msg(ply, "c4_armed")
|
|
|
|
bomb:Arm(ply, time)
|
|
hook.Call("TTTC4Arm", nil, bomb, ply)
|
|
end
|
|
end
|
|
|
|
end
|
|
concommand.Add("ttt_c4_config", ReceiveC4Config)
|
|
|
|
local function SendDisarmResult(ply, bomb, result)
|
|
hook.Call("TTTC4Disarm", nil, bomb, result, ply)
|
|
|
|
net.Start("TTT_C4DisarmResult")
|
|
net.WriteEntity(bomb)
|
|
net.WriteBit(result) -- this way we can squeeze this bit into 16
|
|
net.Send(ply)
|
|
end
|
|
|
|
local function ReceiveC4Disarm(ply, cmd, args)
|
|
if not (IsValid(ply) and ply:IsTerror() and #args == 2) then return end
|
|
local idx = tonumber(args[1])
|
|
local wire = tonumber(args[2])
|
|
|
|
if not idx or not wire then return end
|
|
|
|
local bomb = ents.GetByIndex(idx)
|
|
if IsValid(bomb) and bomb:GetClass() == "ttt_c4" and not bomb.DisarmCausedExplosion and bomb:GetArmed() then
|
|
if bomb:GetPos():Distance(ply:GetPos()) > 256 then
|
|
return
|
|
elseif bomb.SafeWires[wire] or ply:IsTraitor() or ply == bomb:GetOwner() then
|
|
LANG.Msg(ply, "c4_disarmed")
|
|
|
|
bomb:Disarm(ply)
|
|
|
|
-- only case with success net message
|
|
SendDisarmResult(ply, bomb, true)
|
|
else
|
|
SendDisarmResult(ply, bomb, false)
|
|
|
|
-- wrong wire = bomb goes boom
|
|
bomb:FailedDisarm(ply)
|
|
end
|
|
end
|
|
end
|
|
concommand.Add("ttt_c4_disarm", ReceiveC4Disarm)
|
|
|
|
|
|
local function ReceiveC4Pickup(ply, cmd, args)
|
|
if not (IsValid(ply) and ply:IsTerror() and #args == 1) then return end
|
|
local idx = tonumber(args[1])
|
|
|
|
if not idx then return end
|
|
|
|
local bomb = ents.GetByIndex(idx)
|
|
if IsValid(bomb) and bomb:GetClass() == "ttt_c4" and (not bomb:GetArmed()) then
|
|
if bomb:GetPos():Distance(ply:GetPos()) > 256 then
|
|
return
|
|
elseif not ply:CanCarryType(WEAPON_EQUIP1) then
|
|
LANG.Msg(ply, "c4_no_room")
|
|
else
|
|
local prints = bomb.fingerprints or {}
|
|
|
|
hook.Call("TTTC4Pickup", nil, bomb, ply)
|
|
|
|
local wep = ply:Give("weapon_ttt_c4")
|
|
if IsValid(wep) then
|
|
wep.fingerprints = wep.fingerprints or {}
|
|
table.Add(wep.fingerprints, prints)
|
|
|
|
bomb:Remove()
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|
|
concommand.Add("ttt_c4_pickup", ReceiveC4Pickup)
|
|
|
|
|
|
local function ReceiveC4Destroy(ply, cmd, args)
|
|
if not (IsValid(ply) and ply:IsTerror() and #args == 1) then return end
|
|
local idx = tonumber(args[1])
|
|
|
|
if not idx then return end
|
|
|
|
local bomb = ents.GetByIndex(idx)
|
|
if IsValid(bomb) and bomb:GetClass() == "ttt_c4" and (not bomb:GetArmed()) then
|
|
if bomb:GetPos():Distance(ply:GetPos()) > 256 then
|
|
return
|
|
else
|
|
-- spark to show onlookers we destroyed this bomb
|
|
util.EquipmentDestroyed(bomb:GetPos())
|
|
hook.Call("TTTC4Destroyed", nil, bomb, ply)
|
|
|
|
bomb:Remove()
|
|
end
|
|
end
|
|
end
|
|
concommand.Add("ttt_c4_destroy", ReceiveC4Destroy)
|
|
end
|
|
|
|
if CLIENT then
|
|
surface.CreateFont("C4ModelTimer", {
|
|
font = "Default",
|
|
size = 13,
|
|
weight = 0,
|
|
antialias = false
|
|
})
|
|
|
|
|
|
function ENT:GetTimerPos()
|
|
local att = self:GetAttachment(self:LookupAttachment("controlpanel0_ur"))
|
|
if att then
|
|
return att
|
|
else
|
|
local ang = self:GetAngles()
|
|
ang:RotateAroundAxis(self:GetUp(), -90)
|
|
local pos = (self:GetPos() + self:GetForward() * 4.5 +
|
|
self:GetUp() * 9.0 + self:GetRight() * 7.8)
|
|
return { Pos = pos, Ang = ang }
|
|
end
|
|
end
|
|
|
|
local strtime = util.SimpleTime
|
|
local max = math.max
|
|
function ENT:Draw()
|
|
self:DrawModel()
|
|
|
|
if self:GetArmed() then
|
|
local angpos_ur = self:GetTimerPos()
|
|
if angpos_ur then
|
|
cam.Start3D2D(angpos_ur.Pos, angpos_ur.Ang, 0.2)
|
|
draw.DrawText(strtime(max(0, self:GetExplodeTime() - CurTime()), "%02i:%02i"), "C4ModelTimer", -1, 1, COLOR_RED, TEXT_ALIGN_RIGHT)
|
|
cam.End3D2D()
|
|
end
|
|
end
|
|
end
|
|
end
|