-- 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