zcnpci/lua/zcnpci.lua

403 lines
13 KiB
Lua
Raw Normal View History

include("zcnpci/modules.lua")
2026-05-25 04:57:41 +00:00
local enabled = CreateConVar("zcnpci_enabled", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
2026-05-27 16:13:26 +00:00
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))
2026-05-25 04:57:41 +00:00
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))
2026-05-27 16:13:26 +00:00
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))
2026-05-27 16:13:26 +00:00
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))
2026-05-31 01:35:28 +00:00
local modded_npc_whitelist = CreateConVar("zcnpci_modded_npc_whitelist", "npc_example_class", bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local debug_ragdoll_all = CreateConVar("zcnpci_debug_ragdoll_all", 0, 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)
2026-05-25 04:57:41 +00:00
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()
-- We need to override the default homigrad damage hook so it doesn't cause damage to ragdolls we are tryign to fake
-- Don't particularly like doing this but you gotta do what you gotta do
local homigrad_damage_hook = hook.GetTable().EntityTakeDamage["homigrad-damage"]
hook.Add("EntityTakeDamage", "homigrad-damage", function(ent, dmginfo)
if ent:IsNPC() and ent.organism_no_damage then return false end
homigrad_damage_hook(ent, dmginfo)
end)
hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll)
if !ent.organism then return end
2026-05-25 04:57:41 +00:00
if !enabled:GetBool() then return end
local dmgpos = last_dmgpos[ent]
2026-05-25 04:57:41 +00:00
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
if dmgpos and (phys_bone != -1) then
local phys = ragdoll:GetPhysicsObjectNum(phys_bone)
phys:SetVelocity(ent.npcfakeknockback)
else
ragdoll:GetPhysicsObject():SetVelocity(ent.npcfakeknockback)
end
end
2026-06-01 01:12:19 +00:00
local velocity = ragdoll:GetPhysicsObject():GetVelocity()
-- 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")
ragdoll.respawn_data = {
flags = ent:GetFlags(),
spawn_flags = ent:GetSpawnFlags()
}
2026-05-25 04:57:41 +00:00
timer.Simple(0, function()
if !IsValid(ragdoll) then return end
ragdoll.organism.alive = true
ragdoll:SetRenderMode(RENDERMODE_NORMAL)
2026-05-25 04:57:41 +00:00
zcnpci.StartModule(ragdoll, "stumble_legs", phys_bone, lpos)
2026-05-25 04:57:41 +00:00
last_dmgpos[ent] = nil
end)
end)
hook.Add("HomigradDamage", "zcnpci", function(ent, dmginfo)
if !enabled:GetBool() or !IsValid(dmginfo) then return end
2026-05-30 22:42:24 +00:00
if !ent:IsNPC() then
if !ent.organism or !ent.StartDie then return end
2026-05-25 04:57:41 +00:00
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:GetDamagePosition()
2026-05-25 04:57:41 +00:00
last_attacker[ent] = dmginfo:GetAttacker()
if dmginfo:IsDamageType(DMG_BULLET + DMG_BUCKSHOT + DMG_BLAST) then
2026-05-28 22:24:41 +00:00
if dmginfo:GetDamage() > 3 then
table.insert(npcs_to_fake, ent)
2026-06-01 01:12:19 +00:00
--local attacker_angle = dmginfo:GetAttacker():EyeAngles()
2026-06-01 01:12:19 +00:00
--local normal = attacker_angle:Forward(normal)
2026-06-01 01:12:19 +00:00
--ent.npcfakeknockback = normal * 7 * 3
end
elseif dmginfo:IsDamageType(DMG_CLUB + DMG_SLASH) then
2026-05-31 01:35:28 +00:00
local attacker = dmginfo:GetAttacker()
if !IsValid(attacker) then return end
local fists = attacker:HasWeapon("weapon_hands_sh")
if !fists then fists = attacker:HasWeapon("weapon_hg_coolhands") end
-- Kicks should knock NPCs down
2026-05-31 01:35:28 +00:00
if IsValid(dmginfo:GetInflictor()) and fists and attacker.InLegKick and ((attacker.InLegKick + 0.1) > CurTime()) then
table.insert(npcs_to_fake, ent)
local attacker_angle = dmginfo:GetAttacker():EyeAngles()
local normal = attacker_angle:Forward(normal)
ent.npcfakeknockback = normal * dmginfo:GetDamage() * 135
last_dmgpos[ent] = nil
end
end
2026-05-25 04:57:41 +00:00
end)
hook.Add("ScaleNPCDamage", "zcnpci", function(npc, hitgroup, dmginfo)
if !IsValid(npc) then return end
2026-05-25 04:57:41 +00:00
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
2026-05-27 16:13:26 +00:00
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
2026-05-27 16:13:26 +00:00
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
2026-05-27 16:13:26 +00:00
end
if (distance <= min_corpse_distance:GetFloat()) then
player_too_close = loop_player
break
end
end
2026-05-27 16:13:26 +00:00
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
-- NPC faking is in a seperate area because we want NPCs to fake as soon as they can
for i,ent in pairs(npcs_to_fake) do
ent.organism_no_damage = true
local damage_info = DamageInfo()
damage_info:SetDamage(999999)
damage_info:SetAttacker(ent)
damage_info:SetDamageForce(Vector(0, 0, 0))
ent:TakeDamageInfo(damage_info)
table.remove(npcs_to_fake, 1)
end
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)
2026-05-25 04:57:41 +00:00
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