--[[ | 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/ --]] ---- Player spawning/dying local math = math local table = table local player = player local timer = timer local pairs = pairs CreateConVar("ttt_bots_are_spectators", "0", FCVAR_ARCHIVE) CreateConVar("ttt_dyingshot", "0") CreateConVar("ttt_killer_dna_range", "550") CreateConVar("ttt_killer_dna_basetime", "100") -- First spawn on the server function GM:PlayerInitialSpawn( ply ) if not GAMEMODE.cvar_init then GAMEMODE:InitCvars() end ply:InitialSpawn() local rstate = GetRoundState() or ROUND_WAIT -- We should update the traitor list, if we are not about to send it if rstate <= ROUND_PREP then SendTraitorList(GetTraitorFilter()) SendConfirmedTraitors(GetInnocentFilter()) SendDetectiveList() end -- Game has started, tell this guy where the round is at if rstate != ROUND_WAIT then SendRoundState(rstate, ply) SendConfirmedTraitors(ply) SendDetectiveList(ply) end -- Handle spec bots if ply:IsBot() and GetConVar("ttt_bots_are_spectators"):GetBool() then ply:SetTeam(TEAM_SPEC) ply:SetForceSpec(true) end end function GM:NetworkIDValidated( name, steamid ) -- edge case where player authed after initspawn for _, p in player.Iterator() do if IsValid(p) and p:SteamID() == steamid and p.delay_karma_recall then KARMA.LateRecallAndSet(p) return end end end function GM:PlayerSpawn(ply) -- stop bleeding util.StopBleeding(ply) -- Some spawns may be tilted ply:ResetViewRoll() -- Clear out stuff like whether we ordered guns or what bomb code we used ply:ResetRoundFlags() -- latejoiner, send him some info if GetRoundState() == ROUND_ACTIVE then SendRoundState(GetRoundState(), ply) end ply.spawn_nick = ply:Nick() ply.has_spawned = true -- let the client do things on spawn net.Start("TTT_PlayerSpawned") net.WriteBit(ply:IsSpec()) net.Send(ply) if ply:IsSpec() then ply:StripAll() ply:Spectate(OBS_MODE_ROAMING) return end ply:UnSpectate() -- ye olde hooks hook.Call("PlayerLoadout", GAMEMODE, ply) hook.Call("PlayerSetModel", GAMEMODE, ply) hook.Call("TTTPlayerSetColor", GAMEMODE, ply) ply:SetupHands() SCORE:HandleSpawn(ply) end function GM:PlayerSetHandsModel( pl, ent ) local simplemodel = player_manager.TranslateToPlayerModelName(pl:GetModel()) local info = player_manager.TranslatePlayerHands(simplemodel) if info then ent:SetModel(info.model) ent:SetSkin(info.skin) ent:SetBodyGroups(info.body) end end function GM:IsSpawnpointSuitable(ply, spwn, force, rigged) if not IsValid(ply) or not ply:IsTerror() then return true end if not rigged and (not IsValid(spwn) or not spwn:IsInWorld()) then return false end -- spwn is normally an ent, but we sometimes use a vector for jury rigged -- positions local pos = rigged and spwn or spwn:GetPos() if not util.IsInWorld(pos) then return false end local blocking = ents.FindInBox(pos + Vector( -16, -16, 0 ), pos + Vector( 16, 16, 64 )) for k, p in ipairs(blocking) do if IsValid(p) and p:IsPlayer() and p:IsTerror() and p:Alive() then if force then p:Kill() else return false end end end return true end local SpawnTypes = {"info_player_deathmatch", "info_player_combine", "info_player_rebel", "info_player_counterterrorist", "info_player_terrorist", "info_player_axis", "info_player_allies", "gmod_player_start", "info_player_teamspawn"} function GetSpawnEnts(shuffled, force_all) local tbl = {} for k, classname in ipairs(SpawnTypes) do for _, e in ipairs(ents.FindByClass(classname)) do if IsValid(e) and (not e.BeingRemoved) then table.insert(tbl, e) end end end -- Don't use info_player_start unless absolutely necessary, because eg. TF2 -- uses it for observer starts that are in places where players cannot really -- spawn well. At all. if force_all or #tbl == 0 then for _, e in ipairs(ents.FindByClass("info_player_start")) do if IsValid(e) and (not e.BeingRemoved) then table.insert(tbl, e) end end end if shuffled then table.Shuffle(tbl) end return tbl end -- Generate points next to and above the spawn that we can test for suitability local function PointsAroundSpawn(spwn) if not IsValid(spwn) then return {} end local pos = spwn:GetPos() local w, h = 36, 72 -- bit roomier than player hull -- all rigged positions -- could be done without typing them out, but would take about as much time return { pos + Vector( w, 0, 0), pos + Vector( 0, w, 0), pos + Vector( w, w, 0), pos + Vector(-w, 0, 0), pos + Vector( 0, -w, 0), pos + Vector(-w, -w, 0), pos + Vector(-w, w, 0), pos + Vector( w, -w, 0) --pos + Vector( 0, 0, h) -- just in case we're outside }; end function GM:PlayerSelectSpawn(ply) if (not self.SpawnPoints) or (table.IsEmpty(self.SpawnPoints)) or (not IsTableOfEntitiesValid(self.SpawnPoints)) then self.SpawnPoints = GetSpawnEnts(true, false) -- One might think that we have to regenerate our spawnpoint -- cache. Otherwise, any rigged spawn entities would not get reused, and -- MORE new entities would be made instead. In reality, the map cleanup at -- round start will remove our rigged spawns, and we'll have to create new -- ones anyway. end if table.IsEmpty(self.SpawnPoints) then Error("No spawn entity found!\n") return end -- Just always shuffle, it's not that costly and should help spawn -- randomness. table.Shuffle(self.SpawnPoints) -- Optimistic attempt: assume there are sufficient spawns for all and one is -- free for k, spwn in pairs(self.SpawnPoints) do if self:IsSpawnpointSuitable(ply, spwn, false) then return spwn end end -- That did not work, so now look around spawns local picked = nil for k, spwn in pairs(self.SpawnPoints) do picked = spwn -- just to have something if all else fails -- See if we can jury rig a spawn near this one local rigged = PointsAroundSpawn(spwn) for _, rig in pairs(rigged) do if self:IsSpawnpointSuitable(ply, rig, false, true) then local rig_spwn = ents.Create("info_player_terrorist") if IsValid(rig_spwn) then rig_spwn:SetPos(rig) rig_spwn:Spawn() ErrorNoHalt("TTT WARNING: Map has too few spawn points, using a rigged spawn for ".. tostring(ply) .. "\n") self.HaveRiggedSpawn = true return rig_spwn end end end end -- Last attempt, force one for k, spwn in pairs(self.SpawnPoints) do if self:IsSpawnpointSuitable(ply, spwn, true) then return spwn end end return picked end function GM:PlayerSetModel(ply) local mdl = GAMEMODE.playermodel or "models/player/phoenix.mdl" util.PrecacheModel(mdl) ply:SetModel(mdl) -- Always clear color state, may later be changed in TTTPlayerSetColor ply:SetColor(COLOR_WHITE) end function GM:TTTPlayerSetColor(ply) local clr = COLOR_WHITE if GAMEMODE.playercolor then -- If this player has a colorable model, always use the same color as all -- other colorable players, so color will never be the factor that lets -- you tell players apart. clr = GAMEMODE.playercolor end ply:SetPlayerColor( Vector( clr.r/255.0, clr.g/255.0, clr.b/255.0 ) ) end -- Only active players can use kill cmd function GM:CanPlayerSuicide(ply) return ply:IsTerror() end function GM:PlayerSwitchFlashlight(ply, on) if not IsValid(ply) then return false end -- add the flashlight "effect" here, and then deny the switch -- this prevents the sound from playing, fixing the exploit -- where weapon sound could be silenced using the flashlight sound if (not on) or ply:IsTerror() then if on then ply:AddEffects(EF_DIMLIGHT) else ply:RemoveEffects(EF_DIMLIGHT) end end return false end function GM:PlayerSpray(ply) if not IsValid(ply) or not ply:IsTerror() then return true -- block end end function GM:PlayerUse(ply, ent) return ply:IsTerror() end function GM:KeyPress(ply, key) if not IsValid(ply) then return end -- Spectator keys if ply:IsSpec() and not ply:GetRagdollSpec() then if ply.propspec then return PROPSPEC.Key(ply, key) end ply:ResetViewRoll() if key == IN_ATTACK then -- snap to random guy ply:Spectate(OBS_MODE_ROAMING) ply:SetEyeAngles(angle_zero) -- After exiting propspec, this could be set to awkward values ply:SpectateEntity(nil) local alive = util.GetAlivePlayers() if #alive < 1 then return end local target = table.Random(alive) if IsValid(target) then ply:SetPos(target:EyePos()) ply:SetEyeAngles(target:EyeAngles()) end elseif key == IN_ATTACK2 then -- spectate either the next guy or a random guy in chase local target = util.GetNextAlivePlayer(ply:GetObserverTarget()) if IsValid(target) then ply:Spectate(ply.spec_mode or OBS_MODE_CHASE) ply:SpectateEntity(target) end elseif key == IN_DUCK then local pos = ply:GetPos() local ang = ply:EyeAngles() local target = ply:GetObserverTarget() if IsValid(target) and target:IsPlayer() and ply:GetObserverMode() != OBS_MODE_ROAMING then -- Only set the spectator's position to the player they are spectating if they are in chase or eye mode. They can use the reload key if they want to return to the person they're spectating pos = target:EyePos() ang = target:EyeAngles() end -- reset ply:Spectate(OBS_MODE_ROAMING) ply:SpectateEntity(nil) ply:SetPos(pos) ply:SetEyeAngles(ang) return true elseif key == IN_JUMP then -- unfuck if you're on a ladder etc if (ply:GetMoveType() != MOVETYPE_NOCLIP) then ply:SetMoveType(MOVETYPE_NOCLIP) end elseif key == IN_RELOAD then local tgt = ply:GetObserverTarget() if not IsValid(tgt) or not tgt:IsPlayer() then return end if not ply.spec_mode or ply.spec_mode == OBS_MODE_CHASE then ply.spec_mode = OBS_MODE_IN_EYE elseif ply.spec_mode == OBS_MODE_IN_EYE then ply.spec_mode = OBS_MODE_CHASE end -- roam stays roam ply:Spectate(ply.spec_mode) end end end function GM:KeyRelease(ply, key) if key == IN_USE and IsValid(ply) and ply:IsTerror() then -- see if we need to do some custom usekey overriding local tr = util.TraceLine({ start = ply:GetShootPos(), endpos = ply:GetShootPos() + ply:GetAimVector() * 84, filter = ply, mask = MASK_SHOT }); if tr.Hit and IsValid(tr.Entity) then if tr.Entity.CanUseKey and tr.Entity.UseOverride then local phys = tr.Entity:GetPhysicsObject() if IsValid(phys) and not phys:HasGameFlag(FVPHYSICS_PLAYER_HELD) then tr.Entity:UseOverride(ply) return true else -- do nothing, can't +use held objects return true end elseif tr.Entity.player_ragdoll then CORPSE.ShowSearch(ply, tr.Entity, (ply:KeyDown(IN_WALK) or ply:KeyDownLast(IN_WALK))) return true end end end end -- Normally all dead players are blocked from IN_USE on the server, meaning we -- can't let them search bodies. This sucks because searching bodies is -- fun. Hence on the client we override +use for specs and use this instead. local function SpecUseKey(ply, cmd, arg) if IsValid(ply) and ply:IsSpec() then -- longer range than normal use local tr = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 128, ply) if tr.Hit and IsValid(tr.Entity) then if tr.Entity.player_ragdoll then if not ply:KeyDown(IN_WALK) then CORPSE.ShowSearch(ply, tr.Entity) else ply:Spectate(OBS_MODE_IN_EYE) ply:SpectateEntity(tr.Entity) end elseif tr.Entity:IsPlayer() and tr.Entity:IsActive() then ply:Spectate(ply.spec_mode or OBS_MODE_CHASE) ply:SpectateEntity(tr.Entity) else PROPSPEC.Target(ply, tr.Entity) end end end end concommand.Add("ttt_spec_use", SpecUseKey) function GM:PlayerDisconnected(ply) -- Prevent the disconnecter from being in the resends if IsValid(ply) then ply:SetRole(ROLE_NONE) end if GetRoundState() != ROUND_PREP then -- Keep traitor entindices in sync on traitor clients SendTraitorList(GetTraitorFilter(false), nil) -- Same for confirmed traitors on innocent clients SendConfirmedTraitors(GetInnocentFilter(false)) SendDetectiveList() end if KARMA.IsEnabled() then KARMA.Remember(ply) end end ---- Death affairs local function CreateDeathEffect(ent, marked) local pos = ent:GetPos() + Vector(0, 0, 20) local jit = 35.0 local jitter = Vector(math.Rand(-jit, jit), math.Rand(-jit, jit), 0) util.PaintDown(pos + jitter, "Blood", ent) if marked then util.PaintDown(pos, "Cross", ent) end end local deathsounds = { Sound("player/death1.wav"), Sound("player/death2.wav"), Sound("player/death3.wav"), Sound("player/death4.wav"), Sound("player/death5.wav"), Sound("player/death6.wav"), Sound("vo/npc/male01/pain07.wav"), Sound("vo/npc/male01/pain08.wav"), Sound("vo/npc/male01/pain09.wav"), Sound("vo/npc/male01/pain04.wav"), Sound("vo/npc/Barney/ba_pain06.wav"), Sound("vo/npc/Barney/ba_pain07.wav"), Sound("vo/npc/Barney/ba_pain09.wav"), Sound("vo/npc/Barney/ba_ohshit03.wav"), --heh Sound("vo/npc/Barney/ba_no01.wav"), Sound("vo/npc/male01/no02.wav"), Sound("hostage/hpain/hpain1.wav"), Sound("hostage/hpain/hpain2.wav"), Sound("hostage/hpain/hpain3.wav"), Sound("hostage/hpain/hpain4.wav"), Sound("hostage/hpain/hpain5.wav"), Sound("hostage/hpain/hpain6.wav") }; local function PlayDeathSound(victim) if not IsValid(victim) then return end sound.Play(table.Random(deathsounds), victim:GetShootPos(), 90, 100) end -- See if we should award credits now local function CheckCreditAward(victim, attacker) if GetRoundState() != ROUND_ACTIVE then return end if not IsValid(victim) then return end -- DETECTIVE AWARD if IsValid(attacker) and attacker:IsPlayer() and attacker:IsActiveDetective() and victim:IsTraitor() then local amt = GetConVar("ttt_det_credits_traitordead"):GetInt() for _, ply in player.Iterator() do if ply:IsActiveDetective() then ply:AddCredits(amt) end end LANG.Msg(GetDetectiveFilter(true), "credit_det_all", {num = amt}) end -- TRAITOR AWARD if (not victim:IsTraitor()) and (not GAMEMODE.AwardedCredits or GetConVar("ttt_credits_award_repeat"):GetBool()) then local inno_alive = 0 local inno_dead = 0 local inno_total = 0 for _, ply in player.Iterator() do if not ply:GetTraitor() then if ply:IsTerror() then inno_alive = inno_alive + 1 elseif ply:IsDeadTerror() then inno_dead = inno_dead + 1 end end end -- we check this at the death of an innocent who is still technically -- Alive(), so add one to dead count and sub one from living inno_dead = inno_dead + 1 inno_alive = math.max(inno_alive - 1, 0) inno_total = inno_dead + inno_alive -- Only repeat-award if we have reached the pct again since last time if GAMEMODE.AwardedCredits then inno_dead = inno_dead - GAMEMODE.AwardedCreditsDead end local pct = inno_dead / inno_total if pct >= GetConVar("ttt_credits_award_pct"):GetFloat() then -- Traitors have killed sufficient people to get an award local amt = GetConVar("ttt_credits_award_size"):GetInt() -- If size is 0, awards are off if amt > 0 then LANG.Msg(GetTraitorFilter(true), "credit_tr_all", {num = amt}) for _, ply in player.Iterator() do if ply:IsActiveTraitor() then ply:AddCredits(amt) end end end GAMEMODE.AwardedCredits = true GAMEMODE.AwardedCreditsDead = inno_dead + GAMEMODE.AwardedCreditsDead end end end function GM:DoPlayerDeath(ply, attacker, dmginfo) if ply:IsSpec() then return end -- Experimental: Fire a last shot if ironsighting and not headshot if GetConVar("ttt_dyingshot"):GetBool() then local wep = ply:GetActiveWeapon() if IsValid(wep) and wep.DyingShot and not ply.was_headshot and dmginfo:IsBulletDamage() then local fired = wep:DyingShot() if fired then return end end -- Note that funny things can happen here because we fire a gun while the -- player is dead. Specifically, this DoPlayerDeath is run twice for -- him. This is ugly, and we have to return the first one to prevent crazy -- shit. end -- Drop all weapons for k, wep in ipairs(ply:GetWeapons()) do WEPS.DropNotifiedWeapon(ply, wep, true) -- with ammo in them wep:DampenDrop() end if IsValid(ply.hat) then ply.hat:Drop() end -- Create ragdoll and hook up marking effects local rag = CORPSE.Create(ply, attacker, dmginfo) ply.server_ragdoll = rag -- nil if clientside CreateDeathEffect(ply, false) util.StartBleeding(rag, dmginfo:GetDamage(), 15) -- Score only when there is a round active. if GetRoundState() == ROUND_ACTIVE then SCORE:HandleKill(ply, attacker, dmginfo) if IsValid(attacker) and attacker:IsPlayer() then attacker:RecordKill(ply) DamageLog(Format("KILL:\t %s [%s] killed %s [%s]", attacker:Nick(), attacker:GetRoleString(), ply:Nick(), ply:GetRoleString())) else DamageLog(Format("KILL:\t killed %s [%s]", ply:Nick(), ply:GetRoleString())) end KARMA.Killed(attacker, ply, dmginfo) end -- Clear out any weapon or equipment we still have ply:StripAll() -- Tell the client to send their chat contents ply:SendLastWords(dmginfo) local killwep = util.WeaponFromDamage(dmginfo) -- headshots, knife damage, and weapons tagged as silent all prevent death -- sound from occurring if not (ply.was_headshot or dmginfo:IsDamageType(DMG_SLASH) or (IsValid(killwep) and killwep.IsSilent)) then PlayDeathSound(ply) end --- Credits CheckCreditAward(ply, attacker) -- Check for T killing D or vice versa if IsValid(attacker) and attacker:IsPlayer() then local reward = 0 if attacker:IsActiveTraitor() and ply:GetDetective() then reward = GetConVar("ttt_credits_detectivekill"):GetInt() elseif attacker:IsActiveDetective() and ply:GetTraitor() then reward = GetConVar("ttt_det_credits_traitorkill"):GetInt() end if reward > 0 then attacker:AddCredits(reward) LANG.Msg(attacker, "credit_kill", {num = reward, role = LANG.NameParam(ply:GetRoleString())}) end end end function GM:PlayerDeath(victim, infl, attacker) -- stop bleeding util.StopBleeding(victim) -- tell no one self:PlayerSilentDeath(victim) victim:SetTeam(TEAM_SPEC) victim:Freeze(false) victim:SetRagdollSpec(true) victim:Spectate(OBS_MODE_IN_EYE) local rag_ent = victim.server_ragdoll or victim:GetRagdollEntity() victim:SpectateEntity(rag_ent) victim:Flashlight(false) victim:Extinguish() net.Start("TTT_PlayerDied") net.Send(victim) if HasteMode() and GetRoundState() == ROUND_ACTIVE then IncRoundEnd(GetConVar("ttt_haste_minutes_per_death"):GetFloat() * 60) end end -- kill hl2 beep function GM:PlayerDeathSound() return true end function GM:SpectatorThink(ply) -- when spectating a ragdoll after death if ply:GetRagdollSpec() then local to_switch, to_chase, to_roam = 2, 5, 8 local elapsed = CurTime() - ply.spec_ragdoll_start local clicked = ply:KeyPressed(IN_ATTACK) -- After first click, go into chase cam, then after another click, to into -- roam. If no clicks made, go into chase after X secs, and roam after Y. -- Don't switch for a second in case the player was shooting when he died, -- this would make him accidentally switch out of ragdoll cam. local m = ply:GetObserverMode() if (m == OBS_MODE_CHASE and clicked) or elapsed > to_roam then -- free roam mode ply:SetRagdollSpec(false) ply:Spectate(OBS_MODE_ROAMING) -- move to spectator spawn if mapper defined any local spec_spawns = ents.FindByClass("ttt_spectator_spawn") if spec_spawns and #spec_spawns > 0 then local spawn = table.Random(spec_spawns) ply:SetPos(spawn:GetPos()) ply:SetEyeAngles(spawn:GetAngles()) end elseif (m == OBS_MODE_IN_EYE and clicked and elapsed > to_switch) or elapsed > to_chase then -- start following ragdoll ply:Spectate(OBS_MODE_CHASE) end if not IsValid(ply.server_ragdoll) then ply:SetRagdollSpec(false) end -- when roaming and messing with ladders elseif ply:GetMoveType() < MOVETYPE_NOCLIP and ply:GetMoveType() > 0 or ply:GetMoveType() == MOVETYPE_LADDER then ply:Spectate(OBS_MODE_ROAMING) end -- when speccing a player if ply:GetObserverMode() != OBS_MODE_ROAMING and (not ply.propspec) and (not ply:GetRagdollSpec()) then local tgt = ply:GetObserverTarget() if IsValid(tgt) and tgt:IsPlayer() then if (not tgt:IsTerror()) or (not tgt:Alive()) then -- stop speccing as soon as target dies ply:Spectate(OBS_MODE_ROAMING) ply:SpectateEntity(nil) elseif GetRoundState() == ROUND_ACTIVE then -- Sync position to target. Uglier than parenting, but unlike -- parenting this is less sensitive to breakage: if we are -- no longer spectating, we will never sync to their position. ply:SetPos(tgt:GetPos()) end end end end GM.PlayerDeathThink = GM.SpectatorThink function GM:PlayerTraceAttack(ply, dmginfo, dir, trace) if IsValid(ply.hat) and trace.HitGroup == HITGROUP_HEAD then ply.hat:Drop(dir) end ply.hit_trace = trace return false end function GM:ScalePlayerDamage(ply, hitgroup, dmginfo) if dmginfo:IsBulletDamage() and ply:HasEquipmentItem(EQUIP_ARMOR) then -- Body armor nets you a damage reduction. dmginfo:ScaleDamage(0.7) end ply.was_headshot = false -- actual damage scaling if hitgroup == HITGROUP_HEAD then -- headshot if it was dealt by a bullet ply.was_headshot = dmginfo:IsBulletDamage() local wep = util.WeaponFromDamage(dmginfo) if IsValid(wep) then local s = wep:GetHeadshotMultiplier(ply, dmginfo) or 2 dmginfo:ScaleDamage(s) end elseif (hitgroup == HITGROUP_LEFTARM or hitgroup == HITGROUP_RIGHTARM or hitgroup == HITGROUP_LEFTLEG or hitgroup == HITGROUP_RIGHTLEG or hitgroup == HITGROUP_GEAR ) then dmginfo:ScaleDamage(0.55) end -- Keep ignite-burn damage etc on old levels if (dmginfo:IsDamageType(DMG_DIRECT) or dmginfo:IsExplosionDamage() or dmginfo:IsDamageType(DMG_FALL) or dmginfo:IsDamageType(DMG_PHYSGUN)) then dmginfo:ScaleDamage(2) end end -- The GetFallDamage hook does not get called until around 600 speed, which is a -- rather high drop already. Hence we do our own fall damage handling in -- OnPlayerHitGround. function GM:GetFallDamage(ply, speed) return 0 end local fallsounds = { Sound("player/damage1.wav"), Sound("player/damage2.wav"), Sound("player/damage3.wav") }; function GM:OnPlayerHitGround(ply, in_water, on_floater, speed) if in_water or speed < 450 or not IsValid(ply) then return end -- Everything over a threshold hurts you, rising exponentially with speed local damage = math.pow(0.05 * (speed - 420), 1.75) -- I don't know exactly when on_floater is true, but it's probably when -- landing on something that is in water. if on_floater then damage = damage / 2 end -- if we fell on a dude, that hurts (him) local ground = ply:GetGroundEntity() if IsValid(ground) and ground:IsPlayer() then if math.floor(damage) > 0 then local att = ply -- if the faller was pushed, that person should get attrib local push = ply.was_pushed if push then -- TODO: move push time checking stuff into fn? if math.max(push.t or 0, push.hurt or 0) > CurTime() - 4 then att = push.att end end local dmg = DamageInfo() if att == ply then -- hijack physgun damage as a marker of this type of kill dmg:SetDamageType(DMG_CRUSH + DMG_PHYSGUN) else -- if attributing to pusher, show more generic crush msg for now dmg:SetDamageType(DMG_CRUSH) end dmg:SetAttacker(att) dmg:SetInflictor(att) dmg:SetDamageForce(Vector(0,0,-1)) dmg:SetDamage(damage) ground:TakeDamageInfo(dmg) end -- our own falling damage is cushioned damage = damage / 3 end if math.floor(damage) > 0 then local dmg = DamageInfo() dmg:SetDamageType(DMG_FALL) dmg:SetAttacker(game.GetWorld()) dmg:SetInflictor(game.GetWorld()) dmg:SetDamageForce(Vector(0,0,1)) dmg:SetDamage(damage) ply:TakeDamageInfo(dmg) -- play CS:S fall sound if we got somewhat significant damage if damage > 5 then sound.Play(table.Random(fallsounds), ply:GetShootPos(), 55 + math.Clamp(damage, 0, 50), 100) end end end local ttt_postdm = CreateConVar("ttt_postround_dm", "0", FCVAR_NOTIFY) function GM:AllowPVP() local rs = GetRoundState() return not (rs == ROUND_PREP or (rs == ROUND_POST and not ttt_postdm:GetBool())) end -- No damage during prep, etc function GM:EntityTakeDamage(ent, dmginfo) if not IsValid(ent) then return end local att = dmginfo:GetAttacker() if not GAMEMODE:AllowPVP() then -- if player vs player damage, or if damage versus a prop, then zero if (ent:IsExplosive() or (ent:IsPlayer() and IsValid(att) and att:IsPlayer())) then dmginfo:ScaleDamage(0) dmginfo:SetDamage(0) end elseif ent:IsPlayer() then GAMEMODE:PlayerTakeDamage(ent, dmginfo:GetInflictor(), att, dmginfo:GetDamage(), dmginfo) elseif ent:IsExplosive() then -- When a barrel hits a player, that player damages the barrel because -- Source physics. This gives stupid results like a player who gets hit -- with a barrel being blamed for killing himself or even his attacker. if IsValid(att) and att:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH) and IsValid(ent:GetPhysicsAttacker()) then dmginfo:SetAttacker(ent:GetPhysicsAttacker()) dmginfo:ScaleDamage(0) dmginfo:SetDamage(0) end elseif ent.is_pinned and ent.OnPinnedDamage then ent:OnPinnedDamage(dmginfo) dmginfo:SetDamage(0) end end function GM:PlayerTakeDamage(ent, infl, att, amount, dmginfo) -- Change damage attribution if necessary if infl or att then local hurter, owner, owner_time -- fall back to the attacker if there is no inflictor if IsValid(infl) then hurter = infl elseif IsValid(att) then hurter = att end -- have a damage owner? if hurter and IsValid(hurter:GetDamageOwner()) then owner, owner_time = hurter:GetDamageOwner() -- barrel bangs can hurt us even if we threw them, but that's our fault elseif hurter and ent == hurter:GetPhysicsAttacker() and dmginfo:IsDamageType(DMG_BLAST) then owner = ent elseif hurter and hurter:IsVehicle() and IsValid(hurter:GetDriver()) then owner = hurter:GetDriver() end -- if we were hurt by a trap OR by a non-ply ent, and we were pushed -- recently, then our pusher is the attacker if owner_time or (not IsValid(att)) or (not att:IsPlayer()) then local push = ent.was_pushed if push and IsValid(push.att) and push.t then -- push must be within the last 5 seconds, and must be done -- after the trap was enabled (if any) owner_time = owner_time or 0 local t = math.max(push.t or 0, push.hurt or 0) if t > owner_time and t > CurTime() - 4 then owner = push.att -- pushed by a trap? if IsValid(push.infl) then dmginfo:SetInflictor(push.infl) end -- for slow-hurting traps we do leech-like damage timing push.hurt = CurTime() end end end -- if we are being hurt by a physics object, we will take damage from -- the world entity as well, which screws with damage attribution so we -- need to detect and work around that if IsValid(owner) and dmginfo:IsDamageType(DMG_CRUSH) then -- we should be able to use the push system for this, as the cases are -- similar: event causes future damage but should still be attributed -- physics traps can also push you to your death, for example local push = ent.was_pushed or {} -- if we already blamed this on a pusher, no need to do more -- else we override whatever was in was_pushed with info pointing -- at our damage owner if push.att != owner then owner_time = owner_time or CurTime() push.att = owner push.t = owner_time push.hurt = CurTime() -- store the current inflictor so that we can attribute it as the -- trap used by the player in the event if IsValid(infl) then push.infl = infl end -- make sure this is set, for if we created a new table ent.was_pushed = push end end -- make the owner of the damage the attacker att = IsValid(owner) and owner or att dmginfo:SetAttacker(att) end -- scale phys damage caused by props if dmginfo:IsDamageType(DMG_CRUSH) and IsValid(att) then -- player falling on player, or player hurt by prop? if not dmginfo:IsDamageType(DMG_PHYSGUN) then -- this is prop-based physics damage dmginfo:ScaleDamage(0.25) -- if the prop is held, no damage if IsValid(infl) and IsValid(infl:GetOwner()) and infl:GetOwner():IsPlayer() then dmginfo:ScaleDamage(0) dmginfo:SetDamage(0) end end end -- handle fire attacker if ent.ignite_info and dmginfo:IsDamageType(DMG_DIRECT) then local datt = dmginfo:GetAttacker() if (not IsValid(datt)) or (not datt:IsPlayer()) then local ignite = ent.ignite_info if IsValid(ignite.att) and IsValid(ignite.infl)then dmginfo:SetAttacker(ignite.att) dmginfo:SetInflictor(ignite.infl) end end end -- try to work out if this was push-induced leech-water damage (common on -- some popular maps like dm_island17) if ent.was_pushed and ent == att and dmginfo:GetDamageType() == DMG_GENERIC and util.BitSet(util.PointContents(dmginfo:GetDamagePosition()), CONTENTS_WATER) then local t = math.max(ent.was_pushed.t or 0, ent.was_pushed.hurt or 0) if t > CurTime() - 3 then dmginfo:SetAttacker(ent.was_pushed.att) ent.was_pushed.hurt = CurTime() end end -- start painting blood decals util.StartBleeding(ent, dmginfo:GetDamage(), 5) -- general actions for pvp damage if ent != att and IsValid(att) and att:IsPlayer() and GetRoundState() == ROUND_ACTIVE and math.floor(dmginfo:GetDamage()) > 0 then -- scale everything to karma damage factor except the knife, because it -- assumes a kill if not dmginfo:IsDamageType(DMG_SLASH) then dmginfo:ScaleDamage(att:GetDamageFactor()) end -- process the effects of the damage on karma KARMA.Hurt(att, ent, dmginfo) DamageLog(Format("DMG: \t %s [%s] damaged %s [%s] for %d dmg", att:Nick(), att:GetRoleString(), ent:Nick(), ent:GetRoleString(), math.Round(dmginfo:GetDamage()))) end end function GM:OnNPCKilled() end -- Drowning and such local tm = nil function GM:Tick() -- three cheers for micro-optimizations for _, ply in player.Iterator() do tm = ply:Team() if tm == TEAM_TERROR and ply:Alive() then -- Drowning if ply:WaterLevel() == 3 then if ply:IsOnFire() then ply:Extinguish() end if ply.drowning then if ply.drowning < CurTime() then local dmginfo = DamageInfo() dmginfo:SetDamage(15) dmginfo:SetDamageType(DMG_DROWN) dmginfo:SetAttacker(game.GetWorld()) dmginfo:SetInflictor(game.GetWorld()) dmginfo:SetDamageForce(Vector(0,0,1)) ply:TakeDamageInfo(dmginfo) -- have started drowning properly ply.drowning = CurTime() + 1 end else -- will start drowning soon ply.drowning = CurTime() + 8 end else ply.drowning = nil end -- Run DNA Scanner think also when it is not deployed if IsValid(ply.scanner_weapon) and wep != ply.scanner_weapon then ply.scanner_weapon:Think() end elseif tm == TEAM_SPEC then if ply.propspec then PROPSPEC.Recharge(ply) if IsValid(ply:GetObserverTarget()) then ply:SetPos(ply:GetObserverTarget():GetPos()) end end -- if spectators are alive, ie. they picked spectator mode, then -- DeathThink doesn't run, so we have to SpecThink here if ply:Alive() then self:SpectatorThink(ply) end end end end function GM:ShowHelp(ply) if IsValid(ply) then ply:ConCommand("ttt_helpscreen") end end function GM:PlayerRequestTeam(ply, teamid) end -- Implementing stuff that should already be in gmod, chpt. 389 function GM:PlayerEnteredVehicle(ply, vehicle, role) if IsValid(vehicle) then vehicle:SetNWEntity("ttt_driver", ply) end end function GM:PlayerLeaveVehicle(ply, vehicle) if IsValid(vehicle) then -- setting nil will not do anything, so bogusify vehicle:SetNWEntity("ttt_driver", vehicle) end end function GM:AllowPlayerPickup(ply, obj) return false end function GM:PlayerShouldTaunt(ply, actid) -- Disable taunts, we don't have a system for them (camera freezing etc). -- Mods/plugins that add such a system should override this. return false end