Compare commits

..

2 commits

Author SHA1 Message Date
toasterpanic
85eb62a495 Clear crippled and near-death enemies, various optimizations 2026-05-26 23:21:18 -04:00
toasterpanic
8da450edb3 Edit defaults, take burning into account, don't delete player corpses 2026-05-26 19:12:52 -04:00
4 changed files with 129 additions and 76 deletions

View file

@ -5,8 +5,14 @@ Adds better NPC integration for the Garry's Mod addon Z-City. While Z-City has a
- NPCs can now be alive while ragdolled, and can move around in that state. - NPCs can now be alive while ragdolled, and can move around in that state.
- NPCs can collapse due to unconsciousness or pain. - NPCs can collapse due to unconsciousness or pain.
- NPCs can get back up after being ragdolled. - NPCs can get back up after being ragdolled.
- Various performance settings are included, with the biggest one being automatic corpse removal.
This plugin is based off of Kazarei's Euphoria, which in turn is a fork of Fedhoria by Rama. This plugin is based off of Kazarei's Euphoria, which in turn is a fork of Fedhoria by Rama. Credits to them for making the cool thing.
## Known issues
- Kicking NPCs does not knock them down. This is not something I can easily fix -- I would have to override code in Z-City, which I would rather not do.
- NPCs currently instantly get up, with no animation. I plan on remedying this in the future.
## Incompatibilities ## Incompatibilities
@ -18,4 +24,4 @@ No guarantees any of this will become real.
- NPCs will back out of combat when injured. - NPCs will back out of combat when injured.
- Friendly NPCs can heal each other. - Friendly NPCs can heal each other.
- NPCs can heal themselves to a limited degree. - NPCs can heal themselves.

View file

@ -44,6 +44,12 @@ local function PopulatePerformanceSBXToolMenu(pnl)
pnl:CheckBox("Never remove corpses near the player", "zcnpci_disable_corpse_removal_near_player") pnl:CheckBox("Never remove corpses near the player", "zcnpci_disable_corpse_removal_near_player")
pnl:ControlHelp("If on, corpses under the variable above in distance will never be cleared.") pnl:ControlHelp("If on, corpses under the variable above in distance will never be cleared.")
pnl:CheckBox("Treat crippled NPCs as dead", "zcnpci_treat_crippled_as_dead")
pnl:ControlHelp("If on, corpses with amputations or both legs broken will be clearable.")
pnl:CheckBox("Treat near-death NPCs as dead", "zcnpci_treat_near_death_as_dead")
pnl:ControlHelp("If on, corpses near death (severe blood loss, comas, etc.) will be clearable.")
end end
if engine.ActiveGamemode() == "sandbox" then if engine.ActiveGamemode() == "sandbox" then

View file

@ -6,9 +6,13 @@ local always_active_on_player_kill = CreateConVar("zcnpci_always_active_on_play
local remove_corpses = CreateConVar("zcnpci_enable_corpse_removal", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) local remove_corpses = CreateConVar("zcnpci_enable_corpse_removal", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local max_corpses = CreateConVar("zcnpci_max_corpses", 8, 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", 30, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) local max_corpse_time = CreateConVar("zcnpci_max_corpse_time", 120, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local max_corpse_distance = CreateConVar("zcnpci_max_corpse_distance", 3000, 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 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_near_death_as_dead = CreateConVar("zcnpci_treat_near_death_as_dead", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local debug_ragdoll_all = CreateConVar("zcnpci_debug_ragdoll_all", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_PROTECTED))
local last_dmgpos = {} local last_dmgpos = {}
local last_hitgroup = {} local last_hitgroup = {}
@ -16,6 +20,9 @@ local last_attacker = {}
local corpses = {} local corpses = {}
local last_corpse_tick = CurTime()
local last_tick = CurTime()
hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll) hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll)
if !ent.organism then return end if !ent.organism then return end
@ -83,78 +90,107 @@ hook.Add("ScaleNPCDamage", "zcnpci", function(npc, hitgroup, dmginfo)
end) end)
hook.Add("Think", "zcnpci", function() hook.Add("Think", "zcnpci", function()
for i, ent in pairs(corpses) do local do_corpse_loop = (CurTime() - last_corpse_tick) > 2.0
if !IsValid(ent) then local do_general_loop = (CurTime() - last_tick) > 0.1
table.RemoveByValue(corpses, ent)
end
end
if (max_corpses:GetFloat() != -1) and (#corpses > max_corpses:GetFloat()) then if do_general_loop then
if IsValid(corpses[1]) then if do_corpse_loop then
corpses[1]:Remove() last_corpse_tick = CurTime()
end
table.remove(corpses, 1) for i, ent in pairs(corpses) do
end if !IsValid(ent) then
table.RemoveByValue(corpses, ent)
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)
) then
local damage_info = DamageInfo()
damage_info:SetDamage(ent:Health())
damage_info:SetAttacker(ent)
damage_info:SetDamageType( DMG_DIRECT )
damage_info:SetDamageForce(Vector(0, 0, 0))
ent:TakeDamageInfo(damage_info)
end
elseif !ent.organism.alive and remove_corpses then
-- 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 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
break
end end
end end
if nearby_player and IsValid(nearby_player) and no_corpse_removal_near_player:GetBool() then continue if (max_corpses:GetFloat() != -1) and (#corpses > max_corpses:GetFloat()) then
elseif (max_corpse_distance:GetFloat() != -1) and !nearby_player then if IsValid(corpses[1]) then
ent:Remove() corpses[1]:Remove()
continue end
end
if max_corpse_time:GetFloat() != -1 then table.remove(corpses, 1)
if !ent.corpse_timestamp then ent.corpse_timestamp = CurTime() end
elseif (CurTime() - ent.corpse_timestamp) > max_corpse_time:GetFloat() then 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
debug_ragdoll_all:GetBool()
) then
local damage_info = DamageInfo()
damage_info:SetDamage(ent:Health())
damage_info:SetAttacker(ent)
damage_info:SetDamageType( DMG_DIRECT )
damage_info:SetDamageForce(Vector(0, 0, 0))
ent:TakeDamageInfo(damage_info)
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 !clearable then return 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 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
break
end
end
if nearby_player and IsValid(nearby_player) and no_corpse_removal_near_player:GetBool() then continue
elseif (max_corpse_distance:GetFloat() != -1) and !nearby_player then
ent:Remove() ent:Remove()
continue continue
end end
print((CurTime() - ent.corpse_timestamp) > max_corpse_time:GetFloat())
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
print((CurTime() - ent.corpse_timestamp) > max_corpse_time:GetFloat())
end
end end
end end
end end
end) end)
--[[hook.Add("OnNPCKilled", "Fedhoria", function(ent, attacker, inflictor) --[[hook.Add("OnNPCKilled", "Fedhoria", function(ent, attacker, inflictor)

View file

@ -161,6 +161,8 @@ function MODULE:Init()
self.StartDie = nil self.StartDie = nil
self.AnimationRollEndTime = 0 self.AnimationRollEndTime = 0
self.StopProcessing = false self.StopProcessing = false
self.LastThink = CurTime()
self.LastPhysProcess = CurTime()
-- Add damping to all bones. -- Add damping to all bones.
local phys = self:GetPhysicsObject() local phys = self:GetPhysicsObject()
@ -184,14 +186,14 @@ end
---------------------------------------------------------------------------]] ---------------------------------------------------------------------------]]
function MODULE:Think() function MODULE:Think()
if (CurTime() - self.LastThink) < 1 then return end
if !IsValid(target) then return end if !IsValid(target) then return end
print("target is valid") self.LastThink = CurTime()
local phys = target:GetPhysicsObject() local phys = target:GetPhysicsObject()
if !IsValid(phys) or !phys:IsAsleep() then return end
print("phys is valid") if !IsValid(phys) or !phys:IsAsleep() then return end
phys:Wake() phys:Wake()
end end
@ -237,14 +239,18 @@ end
Physics Simulation Hook (FIXED) Physics Simulation Hook (FIXED)
---------------------------------------------------------------------------]] ---------------------------------------------------------------------------]]
function MODULE:PhysicsSimulate(phys, dt) function MODULE:PhysicsSimulate(phys, dt)
phys:Wake()
if self.StopProcessing then return false end if self.StopProcessing then return false end
--print((CurTime() - self.LastPhysProcess) < 0.05)
--if (CurTime() - self.LastPhysProcess) < (1 /) then return false end
self.LastPhysProcess = CurTime()
local cur_time = CurTime() local cur_time = CurTime()
local target = self:GetTarget() local target = self:GetTarget()
if not IsValid(target) then self:Remove(); return false end if not IsValid(target) then self:Remove(); return false end
target.is_npc_corpse = true
if !self.StartDie then self.StartDie = cur_time end if !self.StartDie then self.StartDie = cur_time end
-- Check for active animation -- Check for active animation
@ -281,7 +287,8 @@ function MODULE:PhysicsSimulate(phys, dt)
(minimum_down_timer >= 1.0) and (minimum_down_timer >= 1.0) and
(target.class_in_previous_life != nil) and (target.class_in_previous_life != nil) and
!(target.organism.llegamputated or target.organism.rlegamputated or target.organism.larmamputated or target.organism.rarmamputated) and !(target.organism.llegamputated or target.organism.rlegamputated or target.organism.larmamputated or target.organism.rarmamputated) and
(target.organism.pain <= 80) (target.organism.pain <= 80) and
!target:IsOnFire()
) then ) then
local ent = ents.Create(target.class_in_previous_life) local ent = ents.Create(target.class_in_previous_life)
ent:SetPos(target:GetPos()) ent:SetPos(target:GetPos())
@ -354,10 +361,8 @@ function MODULE:PhysicsSimulate(phys, dt)
local offset_sqr = (pos - self.last_pos):LengthSqr() local offset_sqr = (pos - self.last_pos):LengthSqr()
self.last_pos = pos self.last_pos = pos
if (offset_sqr < (10*10 * dt*dt) and not (ragmod and ragmod:IsRagmodRagdoll(target))) then if offset_sqr < (10*10 * dt*dt) then
self.StartDie = self.StartDie or cur_time self.StartDie = self.StartDie or cur_time
else
--self.StartDie = nil
end end
-- Physics correction after collision -- Physics correction after collision