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", "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) 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() if !IsValid(ragdoll) then return end ragdoll.organism.alive = true ragdoll:SetRenderMode(RENDERMODE_NORMAL) 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() print("I'M HIT FOR "..dmginfo:GetDamage()) end return end last_dmgpos[ent] = dmginfo:GetDamagePosition() 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 attacker = dmginfo:GetAttacker() if !IsValid(attacker) then return end local fists = attacker:HasWeapon("weapon_hands_sh") if !fists then fists = dmginfo:GetAttacker():HasWeapon("weapon_hg_coolhands") end local attacker = dmginfo:GetAttacker() -- Kicks should knock NPCs down 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 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 -- 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("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