380 lines
No EOL
12 KiB
Lua
380 lines
No EOL
12 KiB
Lua
include("zcnpci/modules.lua")
|
|
|
|
local enabled = CreateConVar("zcnpci_enabled", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local active_range = CreateConVar("zcnpci_active_range", 32768, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local always_active_on_player_kill = CreateConVar("zcnpci_always_active_on_player_kill", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local ticks_per_second = CreateConVar("zcnpci_tps", 20, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
|
|
local remove_corpses = CreateConVar("zcnpci_enable_corpse_removal", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local corpse_loop_time = CreateConVar("zcnpci_corpse_loop_time", 2.0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local max_corpses = CreateConVar("zcnpci_max_corpses", 8, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local max_corpse_time = CreateConVar("zcnpci_max_corpse_time", 120, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local min_corpse_distance = CreateConVar("zcnpci_min_corpse_distance", 500, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local max_corpse_distance = CreateConVar("zcnpci_max_corpse_distance", 2500, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local no_corpse_removal_near_player = CreateConVar("zcnpci_disable_corpse_removal_near_player", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local treat_crippled_as_dead = CreateConVar("zcnpci_treat_crippled_as_dead", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local treat_unconscious_as_dead = CreateConVar("zcnpci_treat_unconscious_as_dead", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local treat_near_death_as_dead = CreateConVar("zcnpci_treat_near_death_as_dead", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
|
|
local allow_extended_base_npcs = CreateConVar("zcnpci_allow_extended_base_npcs", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local allow_modded_npcs = CreateConVar("zcnpci_allow_modded_npcs", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local auto_enable_humanoid_npcs = CreateConVar("zcnpci_auto_enable_humanoid_npcs", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
local modded_npc_whitelist = CreateConVar("zcnpci_modded_npc_whitelist", "nb_example", bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
|
|
-- The way I have to set up the npc whitelist client-side makes it so the host can't configure it without console commands, so we have to do this shit
|
|
local set_modded_npc_whitelist = concommand.Add("zcnpci_set_modded_npc_whitelist", function(ply, cmd, args)
|
|
if !IsValid(ply) or !args[1] then return end
|
|
|
|
if !ply:IsSuperAdmin() then return end
|
|
|
|
modded_npc_whitelist:SetString(args[1])
|
|
end)
|
|
|
|
local debug_ragdoll_all = CreateConVar("zcnpci_debug_ragdoll_all", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
|
|
|
|
local last_dmgpos = {}
|
|
local last_hitgroup = {}
|
|
local last_attacker = {}
|
|
|
|
local npcs_to_fake = {}
|
|
|
|
-- These NPCs do not have organisms by default, despite being humanoid characters built into the game.
|
|
local base_npc_whitelist = {
|
|
"npc_kleiner",
|
|
"npc_breen",
|
|
"npc_barney",
|
|
"npc_alyx",
|
|
"npc_eli",
|
|
"npc_gman",
|
|
"npc_magnusson",
|
|
"npc_mossman",
|
|
"npc_odessa",
|
|
"npc_monk"
|
|
}
|
|
|
|
local corpses = {}
|
|
|
|
local last_corpse_tick = CurTime()
|
|
local last_tick = CurTime()
|
|
|
|
hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll)
|
|
if !ent.organism then return end
|
|
|
|
if !enabled:GetBool() then return end
|
|
local dmgpos = last_dmgpos[ent]
|
|
|
|
local active_by_default = false
|
|
|
|
if always_active_on_player_kill:GetBool() then
|
|
if last_attacker[ent] and last_attacker[ent]:IsPlayer() then active_by_default = true end
|
|
end
|
|
|
|
if !active_by_default then
|
|
local entity_position = ragdoll:GetPos()
|
|
local in_range = false
|
|
local players = player.GetAll()
|
|
|
|
for i, loop_player in pairs(players) do
|
|
local player_position = loop_player:GetPos()
|
|
|
|
local distance = entity_position:Distance(player_position)
|
|
|
|
if distance <= active_range:GetFloat() then
|
|
in_range = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if !in_range then return end
|
|
end
|
|
|
|
local phys_bone, lpos
|
|
|
|
if dmgpos then
|
|
phys_bone = ragdoll:GetClosestPhysBone(dmgpos)
|
|
if phys_bone then
|
|
local phys = ragdoll:GetPhysicsObjectNum(phys_bone)
|
|
lpos = phys:WorldToLocal(dmgpos)
|
|
end
|
|
end
|
|
|
|
ragdoll.class_in_previous_life = ent:GetClass()
|
|
|
|
if !ragdoll.organism then
|
|
ragdoll.inventory = {}
|
|
ragdoll.inventory.Weapons = {}
|
|
|
|
hg.organism.Add(ragdoll)
|
|
table.Merge(ragdoll.organism, ent.organism)
|
|
|
|
hook.Run("RagdollDeath", ent, ragdoll)
|
|
|
|
ragdoll.organism.owner = ragdoll
|
|
ragdoll:CallOnRemove("organism", hg.organism.Remove, ragdoll)
|
|
ragdoll.organism.owner.fullsend = true
|
|
hg.send_bareinfo(ragdoll.organism)
|
|
|
|
ent.organism = nil
|
|
end
|
|
|
|
if ent.npcfakeknockback then
|
|
ragdoll:GetPhysicsObject():SetVelocity(ent.npcfakeknockback)
|
|
end
|
|
|
|
-- For some reason, citizen types such as rebels, medics, refugees, etc. use a keyvalue.
|
|
-- That keyvalue does not transfer over to the ragdoll, despite it looking like the NPC,
|
|
-- and you have to manually save it to the ragdoll to be able to make it work.
|
|
-- Why is it like this? Fuck if I know, I just know I don't wanna deal with it again.
|
|
ragdoll.citizentype = ent:GetInternalVariable("citizentype")
|
|
|
|
timer.Simple(0, function()
|
|
ragdoll.organism.alive = true
|
|
|
|
if !IsValid(ragdoll) then return end
|
|
|
|
zcnpci.StartModule(ragdoll, "stumble_legs", phys_bone, lpos)
|
|
last_dmgpos[ent] = nil
|
|
end)
|
|
end)
|
|
|
|
hook.Add("HomigradDamage", "zcnpci", function(ent, dmginfo)
|
|
if !enabled:GetBool() or !IsValid(dmginfo) then return end
|
|
if !ent:IsNPC() then
|
|
if !ent.organism or !ent.StartDie then return end
|
|
|
|
if dmginfo:IsDamageType(DMG_BULLET + DMG_BUCKSHOT + DMG_BLAST + DMG_CLUB + DMG_SLASH + DMG_GENERIC) then
|
|
-- Reset ragdoll get up timer -- don't want someone getting up mid-curbstomp
|
|
ent.StartDie = CurTime()
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
last_dmgpos[ent] = dmginfo:GetDamage()
|
|
last_attacker[ent] = dmginfo:GetAttacker()
|
|
|
|
if dmginfo:IsDamageType(DMG_BULLET + DMG_BUCKSHOT + DMG_BLAST) then
|
|
if dmginfo:GetDamage() > 3 then
|
|
table.insert(npcs_to_fake, ent)
|
|
end
|
|
elseif dmginfo:IsDamageType(DMG_CLUB + DMG_SLASH) then
|
|
local fists = dmginfo:GetAttacker():GetWeapon("weapon_hands_sh")
|
|
if !IsValid(fists) then fists = dmginfo:GetAttacker():GetWeapon("weapon_hg_coolhands") end
|
|
|
|
local attacker = dmginfo:GetAttacker()
|
|
|
|
print(fists.InLegKick)
|
|
|
|
-- Kicks should knock NPCs down
|
|
if IsValid(dmginfo:GetInflictor()) and IsValid(fists) and attacker.InLegKick and ((attacker.InLegKick + 0.1) > CurTime()) then
|
|
table.insert(npcs_to_fake, ent)
|
|
print("ADD TO LIST!!!")
|
|
|
|
local attacker_angle = dmginfo:GetAttacker():EyeAngles()
|
|
|
|
local normal = attacker_angle:Forward(normal)
|
|
|
|
ent.npcfakeknockback = normal * dmginfo:GetDamage() * 135
|
|
end
|
|
end
|
|
end)
|
|
|
|
hook.Add("ScaleNPCDamage", "zcnpci", function(npc, hitgroup, dmginfo)
|
|
if !IsValid(npc) then return end
|
|
last_hitgroup[npc] = hitgroup
|
|
end)
|
|
|
|
hook.Add("Tick", "zcnpci", function()
|
|
local do_corpse_loop = (CurTime() - last_corpse_tick) > corpse_loop_time:GetFloat()
|
|
local do_general_loop = (CurTime() - last_tick) > (1 / ticks_per_second:GetInt())
|
|
|
|
if do_general_loop then
|
|
last_tick = CurTime()
|
|
|
|
if do_corpse_loop then
|
|
last_corpse_tick = CurTime()
|
|
|
|
for i, ent in pairs(corpses) do
|
|
if !IsValid(ent) then
|
|
table.RemoveByValue(corpses, ent)
|
|
end
|
|
end
|
|
|
|
if (max_corpses:GetFloat() != -1) and (#corpses > max_corpses:GetFloat()) then
|
|
if IsValid(corpses[1]) then
|
|
corpses[1]:Remove()
|
|
end
|
|
|
|
table.remove(corpses, 1)
|
|
end
|
|
end
|
|
for i, ent in pairs(ents.GetAll()) do
|
|
if !IsValid(ent) then continue end
|
|
if !ent.organism then continue end
|
|
|
|
if ent:IsNPC() then
|
|
-- Knock them down if something is off
|
|
if (
|
|
((ent.organism.lleg >= 1) and (ent.organism.rleg >= 1)) or
|
|
ent.organism.llegamputated or
|
|
ent.organism.rlegamputated or
|
|
(ent.organism.consciousness <= 0.3) or
|
|
(ent.organism.pain > 90) or
|
|
ent:IsOnFire() or
|
|
ent.neednpcfake or
|
|
debug_ragdoll_all:GetBool()
|
|
) then
|
|
table.insert(npcs_to_fake, ent)
|
|
end
|
|
elseif do_corpse_loop and remove_corpses and ent.is_npc_corpse then
|
|
local clearable = false
|
|
|
|
if !ent.organism.alive then clearable = true end
|
|
if treat_crippled_as_dead:GetBool() and (
|
|
ent.organism.llegamputated or ent.organism.rlegamputated or ent.organism.larmamputated or ent.organism.rarmamputated or
|
|
((ent.organism.lleg >= 1) and (ent.organism.rleg >= 1))
|
|
) then clearable = true end
|
|
if treat_near_death_as_dead:GetBool() and (
|
|
ent.organism.heartstop or
|
|
(ent.organism.brain > 0.6) or
|
|
!ent.organism.lungsfunction or
|
|
(ent.organism.blood < 3000) or
|
|
(ent.organism.pulse < 10)
|
|
) then clearable = true end
|
|
if treat_unconscious_as_dead:GetBool() and (ent.organism.consciousness <= 0.3)
|
|
then clearable = true end
|
|
|
|
if !clearable then continue end
|
|
|
|
-- Add to corpse list if not there already
|
|
if !table.HasValue(corpses, ent) then
|
|
table.insert(corpses, ent)
|
|
end
|
|
|
|
-- Get nearest player for future checks
|
|
local nearby_player
|
|
local player_too_close
|
|
local players = player.GetAll()
|
|
|
|
for i, loop_player in pairs(players) do
|
|
local player_position = loop_player:GetPos()
|
|
|
|
local distance = ent:GetPos():Distance(player_position)
|
|
|
|
if (distance <= max_corpse_distance:GetFloat()) or (max_corpse_distance:GetFloat() == -1) then
|
|
nearby_player = loop_player
|
|
end
|
|
|
|
if (distance <= min_corpse_distance:GetFloat()) then
|
|
player_too_close = loop_player
|
|
break
|
|
end
|
|
end
|
|
|
|
if player_too_close and IsValid(player_too_close) then continue
|
|
elseif (max_corpse_distance:GetFloat() != -1) and !nearby_player then
|
|
ent:Remove()
|
|
continue
|
|
end
|
|
|
|
if max_corpse_time:GetFloat() != -1 then
|
|
if !ent.corpse_timestamp then ent.corpse_timestamp = CurTime()
|
|
elseif (CurTime() - ent.corpse_timestamp) > max_corpse_time:GetFloat() then
|
|
ent:Remove()
|
|
continue
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
PrintTable(npcs_to_fake)
|
|
|
|
-- NPC faking is in a seperate area because we want NPCs to ragdoll as soon as they can
|
|
for i,ent in pairs(npcs_to_fake) do
|
|
local damage_info = DamageInfo()
|
|
damage_info:SetDamage(0)
|
|
damage_info:SetAttacker(ent)
|
|
damage_info:SetDamageType( DMG_DIRECT )
|
|
damage_info:SetDamageForce(Vector(0, 0, 0))
|
|
|
|
ent:BecomeRagdoll(damage_info)
|
|
|
|
table.remove(npcs_to_fake, 1)
|
|
end
|
|
end)
|
|
|
|
--[[hook.Add("OnNPCKilled", "Fedhoria", function(ent, attacker, inflictor)
|
|
if (!enabled:GetBool() or !npcs:GetBool()) then return end
|
|
if (!ent:IsNPC() or dmginfo:GetDamage() < ent:Health()) then return end
|
|
|
|
last_dmgpos[ent] = dmginfo:GetDamagePosition()
|
|
end)]]
|
|
|
|
hook.Add("OnEntityCreated", "zcnpci", function(ent)
|
|
if !IsValid(ent) then return end
|
|
|
|
local add_organism = false
|
|
|
|
if allow_modded_npcs:GetBool() then
|
|
local modded_npc_whitelist_string = modded_npc_whitelist:GetString()
|
|
modded_npc_whitelist_string = string.Replace(modded_npc_whitelist_string, "\n", " ")
|
|
|
|
local modded_npc_whitelist_table = string.Split(modded_npc_whitelist_string, " ")
|
|
|
|
if table.HasValue(modded_npc_whitelist_table, ent:GetClass()) then add_organism = true end
|
|
end
|
|
|
|
if allow_extended_base_npcs:GetBool() and table.HasValue(base_npc_whitelist, ent:GetClass()) then add_organism = true end
|
|
|
|
if add_organism then
|
|
hg.organism.Add(ent)
|
|
hg.organism.Clear(ent.organism)
|
|
ent.organism.fakePlayer = true
|
|
end
|
|
end)
|
|
|
|
local once = true
|
|
|
|
local PLAYER = FindMetaTable("Player")
|
|
|
|
local oldCreateRagdoll = PLAYER.CreateRagdoll
|
|
|
|
local dolls = {}
|
|
|
|
local function CreateRagdoll(self)
|
|
SafeRemoveEntity(dolls[self])
|
|
|
|
local ragdoll = ents.Create("prop_ragdoll")
|
|
ragdoll:SetModel(self:GetModel())
|
|
ragdoll:SetPos(self:GetPos())
|
|
ragdoll:SetAngles(self:GetAngles())
|
|
ragdoll:Spawn()
|
|
|
|
ragdoll:SetSkin(self:GetSkin())
|
|
|
|
for i = 0, self:GetNumBodyGroups() - 1 do
|
|
ragdoll:SetBodygroup(i, self:GetBodygroup(i))
|
|
end
|
|
|
|
for i = 0, ragdoll:GetPhysicsObjectCount()-1 do
|
|
local phys = ragdoll:GetPhysicsObjectNum(i)
|
|
local bone = ragdoll:TranslatePhysBoneToBone(i)
|
|
local matrix = self:GetBoneMatrix(bone)
|
|
local pos, ang = matrix:GetTranslation(), matrix:GetAngles()--self:GetBonePosition(bone)
|
|
phys:SetPos(pos)
|
|
phys:SetAngles(ang)
|
|
phys:SetVelocity(self:GetVelocity())
|
|
end
|
|
|
|
self:SpectateEntity(ragdoll)
|
|
self:Spectate(OBS_MODE_CHASE)
|
|
|
|
dolls[self] = ragdoll
|
|
end
|
|
|
|
local oldGetRagdollEntity = PLAYER.GetRagdollEntity
|
|
|
|
local function GetRagdollEntity(self)
|
|
return dolls[self] or NULL
|
|
end |