Compare commits

..

No commits in common. "02557dacd1b94f351211b7428fa4f0c6463c5ea5" and "4c4d59838a79a27b59787cc70a7fdf31d2a749c9" have entirely different histories.

8 changed files with 101 additions and 340 deletions

View file

@ -1,16 +1,14 @@
# Z-City NPC Integration
Adds better NPC integration for the Garry's Mod addon Z-City.
While Z-City has a convar that enables the custom health system on NPCs, it really isn't the best or most bug-free thing in the world. This has irritated me since I first started using Z-City. Therefore, I've decided to take care of the problem myself with my own plugin.
Adds better NPC integration for the Garry's Mod addon Z-City. While Z-City has a convar that enables the custom health system on NPCs, it really isn't the best or most advanced thing in the world. This has irritated me since I first started using Z-City. Therefore, I've decided to take care of the problem myself with my own plugin.
- NPCs can be ragdolled like players can, and will wiggle around on the ground. They can get back up, too!
- It takes all elements of Z-City's health system into account, such as unconsciousness and pain.
- NPCs are responsive to damage from many sources -- bullets, fire, melee (especially kicks!) and more.
- NPCs can also see and target downed NPCs, and will prioritize standing NPCs over them when needed.
- Everything is configurable, so you can modify and disable features as you like to create your preferred experience.
- Various configurable settings are included, allowing you to tweak the mod to your liking.
This mod is built to perform reasonably well, thus it includes optimizations, configurable performance settings, and an automatic corpse cleanup tool.
This mod is built to perform reasonably well, thus it includes optimizations, configurable performance settings, and an automatic corpse remover.
This addon 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.
@ -36,16 +34,16 @@ It is, however, not compatbile with:
MUST BE DONE BEFORE RELEASE:
- Spinal injuries
- NPCs should not unfake when moved around
- Fire / falling / pain reaction (more rapid flailing)
- NPCs should not unfake if being grabbed
- Fix combine targeting players when ai_ignoreplayers = 1. This is caused by an npc_bullseye that is always on the player. I don't know why it's always there but I don't like it. Will probably override it with my own npc_ragdoll_target entity.
Stuff to do:
- Posturing (like when you get hit) more similar to Z-City
- Facial expressions
- Generally improve stumbling (foot stepping)
- Stop NPCs from dropping their guns maybe?
- Stop NPCs from dropping their guns (that's not a good thing to do)
Post release ideas:

View file

@ -53,12 +53,6 @@ local function PopulateRagdollSBXToolMenu(pnl)
pnl:NumSlider("Wound grab time", "zcnpci_woundgrab_time", 0, 10, 3)
pnl:ControlHelp("How long the ragdoll should hold its wound.")
pnl:NumSlider("Writhing strength", "zcnpci_writhing_strength", 0, 5, 3)
pnl:ControlHelp("How hard should the ragdoll wriggle/writhe. 1 is default.")
pnl:NumSlider("Death timer", "zcnpci_death_timer", -1, 600, 3)
pnl:ControlHelp("After an NPC is ragdolled for longer than this timer, they will die instantly. Set to -1 to disable.")
pnl:CheckBox("Allow getting up", "zcnpci_unfake_enabled")
pnl:ControlHelp("If enabled, NPCs will get back up if able.")
@ -67,9 +61,6 @@ local function PopulateRagdollSBXToolMenu(pnl)
pnl:NumSlider("Minimum down time", "zcnpci_down_time", 0, 20, 3)
pnl:ControlHelp("The minimum amount of time the ragdoll should be down before trying to get back up.")
pnl:NumSlider("Movement sensitivity", "zcnpci_movement_sensitivity", -1, 256, 3)
pnl:ControlHelp("How far does the NPC have to move to reset the getting up timer. Set to -1 to disable.")
end
local function PopulatePerformanceSBXToolMenu(pnl)

View file

@ -15,7 +15,7 @@ local function UpdateRelationship(ent, me)
if !IsValid(ent) or !ent:IsNPC() or (ent:GetClass() == "npc_ragdoll_target") then return end
if ent:Disposition(me.dummy_entity) == D_HT then
--print("Ragdoll target of class "..me.ragdoll_to_follow.class_in_previous_life.." is hated by a "..ent:GetClass())
print("Ragdoll target of class "..me.ragdoll_to_follow.class_in_previous_life.." is hated by a "..ent:GetClass())
ent:AddEntityRelationship(me, D_HT, -1)
else
ent:AddEntityRelationship(me, D_NU, -1)
@ -84,7 +84,7 @@ function ENT:Think()
(ragdoll.organism.critical)
)
if (should_stop_targeting != self.last_stop_targeting) then
if true or (should_stop_targeting != self.last_stop_targeting) then
self.last_stop_targeting = should_stop_targeting
if should_stop_targeting then

View file

@ -4,6 +4,8 @@ ENT.Type = "anim"
ENT.Base = "base_anim"
ENT.AutomaticFrameAdvance = true
local bones_to_animate = {
"ValveBiped.Bip01_Pelvis",
"ValveBiped.Bip01_Spine2",
@ -22,15 +24,6 @@ local bones_to_animate = {
"ValveBiped.Bip01_L_Hand",
}
-- Copy pasted directly from the GMOD wiki with small edits
local function SetRagdollPos(ragdoll, pos)
for i = 0, ragdoll:GetPhysicsObjectCount() - 1 do
local phys = ragdoll:GetPhysicsObjectNum(i)
local localPos = ragdoll:WorldToLocal( phys:GetPos() )
phys:SetPos(pos + localPos)
end
end
function ENT:Initialize()
if SERVER then
print("I AM SERVERSIDE")
@ -39,93 +32,116 @@ function ENT:Initialize()
if CLIENT then
print("I AM CLIENTSIDE")
self:SetNoDraw(true)
timer.Simple(0.2, function()
end)
end
end
function ENT:Think()
function ENT:Draw()
if CLIENT then
hook.Add("physics")
local old_ragdoll = self:GetNWEntity("parent")
local old_npc = self:GetNWEntity("parent_npc")
if IsValid(old_ragdoll) and IsValid(old_npc) then
local ragdoll = self.ragdoll
if !self.ready_to_unfake then
self:SetModel(old_ragdoll:GetModel())
if !self.ready_to_animate then
self.ragdoll = ClientsideRagdoll(old_ragdoll:GetModel())
self.bone_list = {}
self.bone_list_end = {}
self:SetupBones()
ragdoll = self.ragdoll
local i = 0
while i < self:GetBoneCount() do
local name = self:GetBoneName(i)
if name == "__INVALIDBONE__" then
i = i + 1
continue
end
self.ragdoll:SetNoDraw(false)
self.ragdoll:DrawShadow(true)
table.insert(bones_to_animate, name)
SetRagdollPos(self.ragdoll, old_ragdoll:GetPos())
local matrix = Matrix()
local position, angle = old_ragdoll:GetBonePosition(i)
if position == old_ragdoll:GetPos() then
get_matrix = old_ragdoll:GetBoneMatrix(i)
if get_matrix then
position = get_matrix:GetTranslation()
end
end
matrix:Translate(position)
matrix:Rotate(angle)
print(self.ragdoll)
print("SHOW UP YOU PIECE OF SHIT")
self.bone_list[name] = matrix
for i, name in pairs(bones_to_animate) do
local object = ragdoll:GetPhysicsObjectNum(ragdoll:TranslateBoneToPhysBone(ragdoll:LookupBone(name)))
local parent_object = old_ragdoll:GetBoneMatrix(old_ragdoll:LookupBone(name))
print(parent_object)
local matrix = Matrix()
if parent_object == nil then continue end
local position, angle = old_npc:GetBonePosition(i)
if position == old_npc:GetPos() then
get_matrix = old_npc:GetBoneMatrix(i)
if get_matrix then
position = get_matrix:GetTranslation()
end
end
matrix:Translate(position)
matrix:Rotate(angle)
object:SetPos(parent_object:GetTranslation())
object:SetAngles(parent_object:GetAngles())
self.bone_list_end[name] = matrix
i = i + 1
end
self.ready_to_animate = true
PrintTable(bones_to_animate)
--[[for i,name in pairs(bones_to_animate) do
local bone_index = old_ragdoll:LookupBone(name)
if bone_index == nil then continue end
self.bone_list[name] = Matrix(old_ragdoll:GetBoneMatrix(bone_index))
end]]
print("BONES:"..self:GetBoneCount())
self.ready_to_unfake = true
end
local fake_start = self:GetNWFloat("fake_start")
local fake_end = self:GetNWFloat("fake_end")
local progress = (CurTime() - fake_start) / (fake_end - fake_start)
self:SetPos(old_ragdoll:GetPos())
for i, name in pairs(bones_to_animate) do
local object = ragdoll:GetPhysicsObjectNum(ragdoll:TranslateBoneToPhysBone(ragdoll:LookupBone(name)))
local parent_bone = old_npc:LookupBone(name)
self:SetupBones()
if parent_bone == -1 then continue end
for name,matrix in pairs(self.bone_list) do
local bone_index = self:LookupBone(name)
if bone_index == -1 then
print(name.." does not exist, skipping...")
continue
end
local parent_bone_matrix = old_npc:GetBoneMatrix(parent_bone)
if !parent_bone_matrix then continue end
local parent_bone_pos, parent_bone_angle = parent_bone_matrix:GetTranslation(), parent_bone_matrix:GetAngles()
local old_bone_pos, old_bone_angle = object:GetPos(), object:GetAngles()
--parent_bone_angle.y = parent_bone_angle.y + 90
--print(name)
--local bone_matrix = old_ragdoll:GetBoneMatrix(bone_index)
local shadow_data = {
secondstoarrive = 0.01,
pos = LerpVector(progress, old_bone_pos, parent_bone_pos),
angle = LerpAngle(progress, old_bone_angle, parent_bone_angle),
maxspeed = 400 * 4,
maxangular = 2000 * 4,
maxspeeddamp = 120 * 4,
maxangularspeeddamp = 600 * 4,
}
if matrix:GetTranslation():DistToSqr(self:GetPos()) == 1 then continue end
-- Can't set position inside physics tick, crashes the game instantly.
-- Instead, send a shadow for it to follow (I think? I don't know GMod is kinda funky)
object:ComputeShadowControl(shadow_data)
object:Wake()
--i = i + 1
self:SetBoneMatrix(bone_index, matrix)
end
end
end
self:NextThink(CurTime() + 0.1)
self:DrawModel()
end
function ENT:Think()
return true
end
function ENT:OnRemove()
if CLIENT then
if self.ragdoll then
self.ragdoll:Remove()
end
--[[if self.anim_ragdoll then
self.anim_ragdoll:Remove()
end]]
end
end
end

View file

@ -1,191 +0,0 @@
AddCSLuaFile()
ENT.Type = "anim"
ENT.Base = "base_anim"
ENT.AutomaticFrameAdvance = true
local bones_to_animate = {
"ValveBiped.Bip01_Pelvis",
"ValveBiped.Bip01_Spine2",
"ValveBiped.Bip01_Head1",
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_L_Thigh",
"ValveBiped.Bip01_L_Calf",
"ValveBiped.Bip01_R_Calf",
"ValveBiped.Bip01_R_Foot",
"ValveBiped.Bip01_L_Foot",
"ValveBiped.Bip01_R_Upperarm",
"ValveBiped.Bip01_L_Upperarm",
"ValveBiped.Bip01_R_Forearm",
"ValveBiped.Bip01_L_Forearm",
"ValveBiped.Bip01_R_Hand",
"ValveBiped.Bip01_L_Hand",
}
local function GetBoneLocalOffsetMatrix(ent, bone_index)
local parent = ent:GetBoneParent(bone_index)
local parent_matrix = Matrix(ent:GetBoneMatrix(parent))
parent_matrix:Invert()
parent_matrix:Mul(ent:GetBoneMatrix(bone_index))
return parent_matrix
end
local function GetGlobalMatrixFromLocal(ent, bone_index, local_matrix)
local parent = ent:GetBoneParent(bone_index)
local parent_matrix = Matrix(ent:GetBoneMatrix(parent))
parent_matrix:Mul(local_matrix)
return parent_matrix
end
function ENT:Initialize()
if SERVER then
print("I AM SERVERSIDE")
self:SetModel("models/dav0r/hoverball.mdl")
end
if CLIENT then
print("I AM CLIENTSIDE")
timer.Simple(0.2, function()
end)
end
end
function ENT:Draw()
if CLIENT then
local old_ragdoll = self:GetNWEntity("parent")
local old_npc = self:GetNWEntity("parent_npc")
if IsValid(old_ragdoll) and IsValid(old_npc) then
if !self.ready_to_unfake then
self:SetModel(old_ragdoll:GetModel())
self.bone_list = {}
self.bone_list_end = {}
self.bone_list_end_offset = {}
self:SetupBones()
local i = 0
while i < self:GetBoneCount() do
local name = self:GetBoneName(i)
if name == "__INVALIDBONE__" then
i = i + 1
continue
end
table.insert(bones_to_animate, name)
local matrix = Matrix()
local position, angle = old_ragdoll:GetBonePosition(i)
if position == old_ragdoll:GetPos() then
get_matrix = old_ragdoll:GetBoneMatrix(i)
if get_matrix then
position = get_matrix:GetTranslation()
end
end
matrix:Translate(position)
matrix:Rotate(angle)
self.bone_list[name] = matrix
local matrix = Matrix()
local npc_bone_index = old_npc:LookupBone(name)
local position, angle = old_npc:GetBonePosition(npc_bone_index)
if position == old_npc:GetPos() then
print("DINGUS!!!! "..name)
self.bone_list_end_offset[name] = GetBoneLocalOffsetMatrix(self, npc_bone_index)
get_matrix = old_npc:GetBoneMatrix(npc_bone_index)
if get_matrix then
position = get_matrix:GetTranslation()
end
else
matrix:Translate(position)
matrix:Rotate(angle)
self.bone_list_end[name] = matrix
end
i = i + 1
end
PrintTable(bones_to_animate)
--[[for i,name in pairs(bones_to_animate) do
local bone_index = old_ragdoll:LookupBone(name)
if bone_index == nil then continue end
self.bone_list[name] = Matrix(old_ragdoll:GetBoneMatrix(bone_index))
end]]
print("BONES:"..self:GetBoneCount())
self.ready_to_unfake = true
end
self:SetPos(old_ragdoll:GetPos())
self:SetupBones()
local fake_start = self:GetNWFloat("fake_start")
local fake_end = self:GetNWFloat("fake_end")
local progress = (CurTime() - fake_start) / (fake_end - fake_start)
for name,matrix in pairs(self.bone_list) do
local bone_index = self:LookupBone(name)
if bone_index == -1 then
print(name.." does not exist, skipping...")
continue
end
local matrix_end = self.bone_list_end[name]
if matrix_end == nil then
if (self.bone_list_end_offset[name] == nil) then
print("OH FUCKKKKKKKK! No matrix end for "..name)
continue
end
matrix_end = GetGlobalMatrixFromLocal(self, bone_index, self.bone_list_end_offset[name])
end
local final_matrix = Matrix()
final_matrix:Translate(LerpVector(progress, matrix:GetTranslation(), matrix_end:GetTranslation()))
final_matrix:Rotate(LerpAngle(progress, matrix:GetAngles(), matrix_end:GetAngles()))
--print(name)
--local bone_matrix = old_ragdoll:GetBoneMatrix(bone_index)
if matrix:GetTranslation():DistToSqr(self:GetPos()) == 1 then continue end
self:SetBoneMatrix(bone_index, final_matrix)
end
end
end
self:DrawModel()
end
function ENT:Think()
return true
end
function ENT:OnRemove()
if CLIENT then
--[[if self.anim_ragdoll then
self.anim_ragdoll:Remove()
end]]
end
end

View file

@ -57,45 +57,16 @@ local corpses = {}
local last_corpse_tick = CurTime()
local last_tick = CurTime()
local hook_table = hook.GetTable()
local homigrad_damage_hook = hook_table.EntityTakeDamage["homigrad-damage"]
local homigrad_player_spawn_hook = hook_table.player_spawn["homigrad-spawn3"]
-- 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)
-- This disables player bullseye spawning, which fixes issues with combine targeting with ai_ignoreplayers
hook.Add("player_spawn", "homigrad-spawn3", function(data)
local ply = Player(data.userid)
if not IsValid(ply) then return end
ply.bull = {
IsValid = function() return true end,
Remove = function() return end
}
homigrad_player_spawn_hook(data)
end)
-- This is called right after the NPC bullseye is created for player ragdolls, which allows us to replace it with a dummy.
-- Fixes similar issues mentioned in previous comment
hook.Add("Ragdoll_Create", "zcnpci-override", function(ply, ragdoll)
if IsValid(ragdoll.bull) then
ragdoll.bull:Remove()
end
ragdoll.bull = {
IsValid = function() return true end,
SetPos = function() return true end,
Remove = function() return end
}
end)
hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll)
if !ent.organism then return end
@ -173,13 +144,6 @@ hook.Add("CreateEntityRagdoll", "zcnpci", function(ent, ragdoll)
-- 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()
}
ragdoll:AddCallback("PhysicsCollide", function(outEnt, data) hook.Run("Ragdoll Collide", ragdoll, data) end)
timer.Simple(0, function()
if !IsValid(ragdoll) then return end
@ -212,13 +176,17 @@ hook.Add("HomigradDamage", "zcnpci", function(ent, dmginfo)
if dmginfo:IsDamageType(DMG_BULLET + DMG_BUCKSHOT + DMG_BLAST) then
if dmginfo:GetDamage() > 3 then
table.insert(npcs_to_fake, ent)
--local attacker_angle = dmginfo:GetAttacker():EyeAngles()
--local normal = attacker_angle:Forward(normal)
--ent.npcfakeknockback = normal * 7 * 3
end
elseif dmginfo:IsDamageType(DMG_CLUB + DMG_SLASH) then
local attacker = dmginfo:GetAttacker()
if !IsValid(attacker) then return end
if attacker.HasWeapon == nil then return end
local fists = attacker:HasWeapon("weapon_hands_sh")
if !fists then fists = attacker:HasWeapon("weapon_hg_coolhands") end

View file

@ -99,8 +99,6 @@ local minimum_down_time = CreateConVar("zcnpci_down_time", "5", {FCVAR_ARCHIVE,
-- Unfaking
local can_unfake = CreateConVar("zcnpci_unfake_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether an NPC can unfake")
local unfake_time = CreateConVar("zcnpci_unfake_time", "1.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Time it takes for an NPC to unfake")
local movement_sensitivity = CreateConVar("zcnpci_movement_sensitivity", "40", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local death_timer = CreateConVar("zcnpci_death_timer", "-1", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-- Targeting
local npc_targeting_enabled = CreateConVar("zcnpci_npc_targeting_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Should NPCs target downed NPCs")
@ -201,7 +199,6 @@ function MODULE:Init()
self.LastThink = CurTime()
self.LastFakeUpCheck = CurTime()
self.LastPosCheck = target:GetPos()
self.SpawnTimestamp = CurTime()
self.bullseye = ents.Create("npc_ragdoll_target")
self.bullseye:SetPos(target:GetPos())
@ -397,26 +394,16 @@ function MODULE:PhysicsSimulate(phys, dt)
return false -- Cut the bullshit
end
if (death_timer:GetFloat() != -1) and ((CurTime() - self.SpawnTimestamp) > death_timer:GetFloat()) then
target.organism.alive = false
end
if (!target.organism.alive) then
-- If the NPC is dead, they probably aren't coming back; don't bother bringing them back to life
self:Remove()
return false -- Cut the bullshit
elseif (
(target.organism.consciousness <= 0.4) or
((target.organism.lleg >= 0.85) and (target.organism.rleg >= 0.85)) or
(target.organism.spine1 == 1) or
(target.organism.spine2 == 1) or
(target.organism.spine3 == 1)
) then
elseif (target.organism.consciousness <= 0.5) or ((target.organism.lleg >= 0.85) and (target.organism.rleg >= 0.85)) then
target.StartDie = cur_time
return false
end
if (self.LastPosCheck:DistToSqr(target:GetPos()) > (movement_sensitivity:GetFloat() ^ 2)) then
if (self.LastPosCheck:DistToSqr(target:GetPos()) > (32 ^ 2)) then
self.LastPosCheck = target:GetPos()
target.StartDie = cur_time
end
@ -452,9 +439,6 @@ function MODULE:PhysicsSimulate(phys, dt)
ent:SetSkin(target:GetSkin())
ent:AddFlags(target.respawn_data.flags)
ent:AddSpawnFlags(target.respawn_data.spawn_flags)
if target.citizentype then
ent:SetKeyValue("citizentype", target.citizentype)
end
@ -510,8 +494,7 @@ function MODULE:PhysicsSimulate(phys, dt)
self.unfaker:SetPos(target:GetPos())
self.unfaker:SetNWEntity("parent", target)
self.unfaker:SetNWEntity("parent_npc", ent)
self.unfaker:SetNWFloat("fake_start", self.FakeUpStart)
self.unfaker:SetNWFloat("fake_end", self.FakeUpEnd)
target:SetRenderMode(RENDERMODE_NONE)
ent:SetRenderMode(RENDERMODE_NONE)

View file

@ -208,11 +208,7 @@ function MODULE:PhysicsSimulate(phys, dt)
-- If the NPC is dead, they probably aren't coming back; don't bother bringing them back to life
self:Remove()
return false -- Cut the bullshit
elseif (
(target.organism.consciousness <= 0.4) or
(target.organism.spine2 == 1) or
(target.organism.spine3 == 1)
) then
elseif (target.organism.consciousness <= 0.3) then
return false
end