Files
wnsrc/gamemodes/terrortown/gamemode/corpse.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

478 lines
14 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/
--]]
---- Corpse functions
-- namespaced because we have no ragdoll metatable
CORPSE = {}
include("corpse_shd.lua")
--- networked data abstraction layer
local dti = CORPSE.dti
function CORPSE.SetFound(rag, state)
--rag:SetNWBool("found", state)
rag:SetDTBool(dti.BOOL_FOUND, state)
end
function CORPSE.SetPlayerNick(rag, ply_or_name)
-- don't have datatable strings, so use a dt entity for common case of
-- still-connected player, and if the player is gone, fall back to nw string
local name = ply_or_name
if IsValid(ply_or_name) then
name = ply_or_name:Nick()
rag:SetDTEntity(dti.ENT_PLAYER, ply_or_name)
end
rag:SetNWString("nick", name)
end
function CORPSE.SetCredits(rag, credits)
--rag:SetNWInt("credits", credits)
rag:SetDTInt(dti.INT_CREDITS, credits)
end
--- ragdoll creation and search
-- If detective mode, announce when someone's body is found
local bodyfound = CreateConVar("ttt_announce_body_found", "1")
function GM:TTTCanIdentifyCorpse(ply, corpse, was_traitor)
-- return true to allow corpse identification, false to disallow
return true
end
local function IdentifyBody(ply, rag)
if not ply:IsTerror() then return end
-- simplified case for those who die and get found during prep
if GetRoundState() == ROUND_PREP then
CORPSE.SetFound(rag, true)
return
end
if not hook.Run("TTTCanIdentifyCorpse", ply, rag, (rag.was_role == ROLE_TRAITOR)) then
return
end
local finder = ply:Nick()
local nick = CORPSE.GetPlayerNick(rag, "")
local traitor = (rag.was_role == ROLE_TRAITOR)
-- Announce body
if bodyfound:GetBool() and not CORPSE.GetFound(rag, false) then
local roletext = nil
local role = rag.was_role
if role == ROLE_TRAITOR then
roletext = "body_found_t"
elseif role == ROLE_DETECTIVE then
roletext = "body_found_d"
else
roletext = "body_found_i"
end
LANG.Msg("body_found", {finder = finder,
victim = nick,
role = LANG.Param(roletext)})
end
-- Register find
if not CORPSE.GetFound(rag, false) then
-- will return either false or a valid ply
local deadply = player.GetBySteamID64(rag.sid64)
if deadply then
deadply:SetNWBool("body_found", true)
if traitor then
-- update innocent's list of traitors
SendConfirmedTraitors(GetInnocentFilter(false))
end
SCORE:HandleBodyFound(ply, deadply)
end
hook.Call( "TTTBodyFound", GAMEMODE, ply, deadply, rag )
CORPSE.SetFound(rag, true)
end
-- Handle kill list
for k, vicsid64 in ipairs(rag.kills) do
-- filter out disconnected
local vic = player.GetBySteamID64(vicsid64)
-- is this an unconfirmed dead?
if IsValid(vic) and (not vic:GetNWBool("body_found", false)) then
LANG.Msg("body_confirm", {finder = finder, victim = vic:Nick()})
-- update scoreboard status
vic:SetNWBool("body_found", true)
end
end
end
-- Covert identify concommand for traitors
local function IdentifyCommand(ply, cmd, args)
if not IsValid(ply) then return end
if #args != 2 then return end
local eidx = tonumber(args[1])
local id = tonumber(args[2])
if (not eidx) or (not id) then return end
if (not ply.search_id) or ply.search_id.id != id or ply.search_id.eidx != eidx then
ply.search_id = nil
return
end
ply.search_id = nil
local rag = Entity(eidx)
if IsValid(rag) and rag.player_ragdoll and rag:GetPos():Distance(ply:GetPos()) < 128 then
if not CORPSE.GetFound(rag, false) then
IdentifyBody(ply, rag)
end
end
end
concommand.Add("ttt_confirm_death", IdentifyCommand)
-- Call detectives to a corpse
local function CallDetective(ply, cmd, args)
if not IsValid(ply) then return end
if #args != 1 then return end
if not ply:IsActive() then return end
local eidx = tonumber(args[1])
if not eidx then return end
local rag = Entity(eidx)
if not (IsValid(rag) and rag.player_ragdoll) then return end
if ((rag.last_detective_call or 0) < (CurTime() - 5)) and (rag:GetPos():Distance(ply:GetPos()) < 128) then
rag.last_detective_call = CurTime()
if CORPSE.GetFound(rag, false) then
-- show indicator to detectives
net.Start("TTT_CorpseCall")
net.WriteVector(rag:GetPos())
net.Send(GetDetectiveFilter(true))
LANG.Msg("body_call", {player = ply:Nick(),
victim = CORPSE.GetPlayerNick(rag, "someone")})
else
LANG.Msg(ply, "body_call_error")
end
end
end
concommand.Add("ttt_call_detective", CallDetective)
local function bitsRequired(num)
local bits, max = 0, 1
while max <= num do
bits = bits + 1
max = max + max
end
return bits
end
local plyBits = bitsRequired(game.MaxPlayers()) -- first game.MaxPlayers() of entities are for players.
function GM:TTTCanSearchCorpse(ply, corpse, is_covert, is_long_range, was_traitor)
-- return true to allow corpse search, false to disallow.
return true
end
-- Send a usermessage to client containing search results
function CORPSE.ShowSearch(ply, rag, covert, long_range)
if not IsValid(ply) or not IsValid(rag) then return end
if rag:IsOnFire() then
LANG.Msg(ply, "body_burning")
return
end
if not hook.Run("TTTCanSearchCorpse", ply, rag, covert, long_range, (rag.was_role == ROLE_TRAITOR)) then
return
end
-- init a heap of data we'll be sending
local nick = CORPSE.GetPlayerNick(rag)
local traitor = (rag.was_role == ROLE_TRAITOR)
local role = rag.was_role
local eq = rag.equipment or EQUIP_NONE
local c4 = rag.bomb_wire or 0
local dmg = rag.dmgtype or DMG_GENERIC
local wep = rag.dmgwep or ""
local words = rag.last_words or ""
local hshot = rag.was_headshot or false
local dtime = rag.time or 0
local owner = player.GetBySteamID64(rag.sid64)
owner = IsValid(owner) and owner:EntIndex() or 0
-- basic sanity check
if nick == nil or eq == nil or role == nil then return end
if DetectiveMode() and not covert then
IdentifyBody(ply, rag)
end
local credits = CORPSE.GetCredits(rag, 0)
if ply:IsActiveSpecial() and credits > 0 and (not long_range) then
LANG.Msg(ply, "body_credits", {num = credits})
ply:AddCredits(credits)
CORPSE.SetCredits(rag, 0)
ServerLog(ply:Nick() .. " took " .. credits .. " credits from the body of " .. nick .. "\n")
SCORE:HandleCreditFound(ply, nick, credits)
end
-- time of death relative to current time (saves bits)
if dtime != 0 then
dtime = math.Round(CurTime() - dtime)
end
-- identifier so we know whether a ttt_confirm_death was legit
ply.search_id = { eidx = rag:EntIndex(), id = rag:EntIndex() + dtime }
-- time of dna sample decay relative to current time
local stime = 0
if rag.killer_sample then
stime = math.max(0, rag.killer_sample.t - CurTime())
end
-- build list of people this traitor killed
local kill_entids = {}
for k, vicsid64 in ipairs(rag.kills) do
-- also send disconnected players as a marker
local vic = player.GetBySteamID64(vicsid64)
table.insert(kill_entids, IsValid(vic) and vic:EntIndex() or 0)
end
local lastid = 0
if rag.lastid and ply:IsActiveDetective() then
-- if the person this victim last id'd has since disconnected, send 0 to
-- indicate this
lastid = IsValid(rag.lastid.ent) and rag.lastid.ent:EntIndex() or 0
end
-- Send a message with basic info
net.Start("TTT_RagdollSearch")
net.WriteUInt(rag:EntIndex(), 16) -- 16 bits
net.WriteUInt(owner, plyBits) -- 128 max players. ( 8 bits )
net.WriteString(nick)
net.WriteUInt(eq, bitsRequired(EQUIP_MAX)) -- Equipment ( default: 3 bits )
net.WriteUInt(role, 2) -- ( 2 bits )
net.WriteUInt(c4, bitsRequired(C4_WIRE_COUNT)) -- 0 -> 2^bits ( default c4: 3 bits )
net.WriteUInt(dmg, 30) -- DMG_BUCKSHOT is the highest. ( 30 bits )
net.WriteString(wep)
net.WriteBool(hshot) -- ( 1 bit )
net.WriteUInt(dtime, 15)
net.WriteUInt(stime, 15)
net.WriteUInt(#kill_entids, 8)
for k, idx in ipairs(kill_entids) do
net.WriteUInt(idx, plyBits)
end
net.WriteUInt(lastid, plyBits)
-- Who found this, so if we get this from a detective we can decide not to
-- show a window
net.WriteUInt(ply:EntIndex(), plyBits)
net.WriteString(words)
-- 93 + string data + plyBits * (3 + #kill_entids)
-- If found by detective, send to all, else just the finder
if ply:IsActiveDetective() then
net.Broadcast()
else
net.Send(ply)
end
end
-- Returns a sample for use in dna scanner if the kill fits certain constraints,
-- else returns nil
local function GetKillerSample(victim, attacker, dmg)
-- only guns and melee damage, not explosions
if not (dmg:IsBulletDamage() or dmg:IsDamageType(DMG_SLASH) or dmg:IsDamageType(DMG_CLUB)) then
return nil
end
if not (IsValid(victim) and IsValid(attacker) and attacker:IsPlayer()) then return end
-- NPCs for which a player is damage owner (meaning despite the NPC dealing
-- the damage, the attacker is a player) should not cause the player's DNA to
-- end up on the corpse.
local infl = dmg:GetInflictor()
if IsValid(infl) and infl:IsNPC() then return end
local dist = victim:GetPos():Distance(attacker:GetPos())
if dist > GetConVar("ttt_killer_dna_range"):GetFloat() then return nil end
local sample = {}
sample.killer = attacker
sample.killer_sid = attacker:SteamID() -- backwards compatibility; use sample.killer_sid64 instead
sample.killer_sid64 = attacker:SteamID64()
sample.victim = victim
sample.t = CurTime() + (-1 * (0.019 * dist)^2 + GetConVar("ttt_killer_dna_basetime"):GetFloat())
return sample
end
local crimescene_keys = {"Fraction", "HitBox", "Normal", "HitPos", "StartPos"}
local poseparams = {
"aim_yaw", "move_yaw", "aim_pitch",
-- "spine_yaw", "head_yaw", "head_pitch"
};
local function GetSceneDataFromPlayer(ply)
local data = {
pos = ply:GetPos(),
ang = ply:GetAngles(),
sequence = ply:GetSequence(),
cycle = ply:GetCycle()
};
for _, param in pairs(poseparams) do
data[param] = ply:GetPoseParameter(param)
end
return data
end
local function GetSceneData(victim, attacker, dmginfo)
-- only for guns for now, hull traces don't work well etc
if not dmginfo:IsBulletDamage() then return end
local scene = {}
if victim.hit_trace then
scene.hit_trace = table.CopyKeys(victim.hit_trace, crimescene_keys)
else
return scene
end
scene.victim = GetSceneDataFromPlayer(victim)
if IsValid(attacker) and attacker:IsPlayer() then
scene.killer = GetSceneDataFromPlayer(attacker)
local att = attacker:LookupAttachment("anim_attachment_RH")
local angpos = attacker:GetAttachment(att)
if not angpos then
scene.hit_trace.StartPos = attacker:GetShootPos()
else
scene.hit_trace.StartPos = angpos.Pos
end
end
return scene
end
local rag_collide = CreateConVar("ttt_ragdoll_collide", "0")
-- Creates client or server ragdoll depending on settings
function CORPSE.Create(ply, attacker, dmginfo)
if not IsValid(ply) then return end
local efn = ply.effect_fn
ply.effect_fn = nil
local rag = ents.Create("prop_ragdoll")
if not IsValid(rag) then return nil end
rag:SetPos(ply:GetPos())
rag:SetModel(ply:GetModel())
rag:SetSkin(ply:GetSkin())
for key, value in pairs(ply:GetBodyGroups()) do
rag:SetBodygroup(value.id, ply:GetBodygroup(value.id))
end
rag:SetAngles(ply:GetAngles())
rag:SetColor(ply:GetColor())
rag:Spawn()
rag:Activate()
-- nonsolid to players, but can be picked up and shot
rag:SetCollisionGroup(rag_collide:GetBool() and COLLISION_GROUP_WEAPON or COLLISION_GROUP_DEBRIS_TRIGGER)
-- flag this ragdoll as being a player's
rag.player_ragdoll = true
rag.sid64 = ply:SteamID64()
rag.sid = ply:SteamID() -- backwards compatibility; use rag.sid64 instead
rag.uqid = ply:UniqueID() -- backwards compatibility; use rag.sid64 instead
-- network data
CORPSE.SetPlayerNick(rag, ply)
CORPSE.SetFound(rag, false)
CORPSE.SetCredits(rag, ply:GetCredits())
-- if someone searches this body they can find info on the victim and the
-- death circumstances
rag.equipment = ply:GetEquipmentItems()
rag.was_role = ply:GetRole()
rag.bomb_wire = ply.bomb_wire
rag.dmgtype = dmginfo:GetDamageType()
local wep = util.WeaponFromDamage(dmginfo)
rag.dmgwep = IsValid(wep) and wep:GetClass() or ""
rag.was_headshot = (ply.was_headshot and dmginfo:IsBulletDamage())
rag.time = CurTime()
rag.kills = table.Copy(ply.kills)
rag.killer_sample = GetKillerSample(ply, attacker, dmginfo)
-- crime scene data
rag.scene = GetSceneData(ply, attacker, dmginfo)
-- position the bones
local num = rag:GetPhysicsObjectCount()-1
local v = ply:GetVelocity()
-- bullets have a lot of force, which feels better when shooting props,
-- but makes bodies fly, so dampen that here
if dmginfo:IsDamageType(DMG_BULLET) or dmginfo:IsDamageType(DMG_SLASH) then
v = v / 5
end
for i=0, num do
local bone = rag:GetPhysicsObjectNum(i)
if IsValid(bone) then
local bp, ba = ply:GetBonePosition(rag:TranslatePhysBoneToBone(i))
if bp and ba then
bone:SetPos(bp)
bone:SetAngles(ba)
end
-- not sure if this will work:
bone:SetVelocity(v)
end
end
-- create advanced death effects (knives)
if efn then
-- next frame, after physics is happy for this ragdoll
timer.Simple(0, function() if IsValid(rag) then efn(rag) end end)
end
hook.Run("TTTOnCorpseCreated", rag, ply)
return rag -- we'll be speccing this
end