mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
478 lines
14 KiB
Lua
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
|