Compare commits

..

No commits in common. "legacy" and "master" have entirely different histories.

17 changed files with 1620 additions and 673 deletions

View file

@ -1,19 +1,53 @@
# Z-City NPC Integration # Z-City NPC Integration
I made this in an angry fugue after desperately trying to improve my frames by creating a more optimized euphoria ragdoll mod. Now that I'm done with that I figured out how tio actually fix my frames, and it was not this addon. However, I have another issue: NPCs with Z-City suck. Like they tank hits like crazy. You could unload a mag on someone's leg and there would be a non-zero chance they would still be perfectly fine. I'd like to turn my optimized Fedhoria mod into a base for a Z-City NPC improvement mod. Adds better NPC integration for the Garry's Mod addon Z-City.
Some ideas: 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.
- If an enemy breaks a leg, slow them down. - NPCs can be ragdolled like players can, and will wiggle around on the ground. They can get back up, too!
- If an enemy breaks an arm, make their aim worse. - It takes all elements of Z-City's health system into account, such as unconsciousness and pain.
- Break both legs? Hope you like writhing on the ground for the rest of your life! - NPCs are responsive to damage from many sources -- bullets, fire, melee (especially kicks!) and more.
- Generally, enemies need to be closer to players in terms of fleshiness. Like a shot to the arm should knock them over, that shit hurts yknow. - NPCs can also see and target downed NPCs, and will prioritize standing NPCs over them when needed.
- Enemies should target downed enemies of hostile factions. - Everything is configurable, so you can modify and disable features as you like to create your preferred experience.
Since theoretically the NPCs use the same health system that players use, these shouldn't be too hard to implement. This mod is built to perform reasonably well, thus it includes optimizations, configurable performance settings, and an automatic corpse cleanup tool.
More optimistic ideas: 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.
- NPCs will back out of combat when injured. ## Known issues
- Friendly NPCs can heal each other.
- NPCs can heal themselves to a limited degree. - Bandages, tourniquets, etc. that are on the ragdoll or NPC body will not be there when they get back up. Don't know how to fix this right now, though if anyone experienced with Z-City's codebase would know how to I would appreciate knowing.
- The NPC getting up animation is a little ugly. However, creating that animation was an adventure on levels that I would personally dislike re-experiencing.
## Compatibility
This should be compatible with:
- The majority of NPC AI improvement mods
- Competently-programmed Z-City addons
It is, however, not compatbile with:
- Most mods that modify the behavior of death ragdolls (Reagdoll, Artagdoll, especially Fedhoria.)
- Mods that automatically remove death ragdolls (it will delete all ragdolled NPCs, even if they're still alive! Try the built-in automatic corpse removal instead.)
## Todo list
MUST BE DONE BEFORE RELEASE:
- Fire / falling / pain reaction (more rapid flailing)
- NPCs should not unfake if being grabbed
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?
Post release ideas:
- Localization support for other languages
- ReaSFX integration would probably be nice. I mean there's so many SFX packs to choose from, so I don't even have to pack it in
- To be entirely honest Fedhoria isn't the best addon out there for euphoria physics. Maybe rebase to another addon (or alternatively build a new one?)
- NPC targeting works, but works really weirdly (sometimes it just. keeps targeting corpses when it shouldn't.) It actually isn't THAT much of an issue because it doesn't feel totally outlandish but I would still love to fix it.

11
addon.json Normal file
View file

@ -0,0 +1,11 @@
{
"title" : "Z-City NPC Integration",
"type" : "Effects",
"tags" : [ "realism", "fun", "scenic" ],
"ignore" :
[
"*.psd",
"*.vcproj",
"*.svn*"
]
}

View file

@ -1,48 +0,0 @@
local function PopulateSBXToolMenu(pnl)
pnl:CheckBox("Enabled", "fedhoria_enabled")
pnl:ControlHelp("Enable or disable the addon.")
pnl:CheckBox("Players", "fedhoria_players")
pnl:ControlHelp("Enable or disable effect for players.")
pnl:CheckBox("NPCs", "fedhoria_npcs")
pnl:ControlHelp("Enable or disable effect for NPCs.")
pnl:Help(" ")
pnl:NumSlider("Stumble time", "fedhoria_stumble_time", 0, 10, 3)
pnl:ControlHelp("How long the ragdoll should stumble for.")
pnl:NumSlider("Die time", "fedhoria_dietime", 0, 10, 3)
pnl:ControlHelp("How long before the ragdoll dies after drowning/being still for too long.")
pnl:NumSlider("Die time variation", "fedhoria_dietime_variation", 0, 10, 2)
pnl:ControlHelp("A random number between 0 and the value you choose here will be added to the die timer.")
pnl:NumSlider("Wound grab chance", "fedhoria_woundgrab_chance", 0, 1, 3)
pnl:ControlHelp("The chance the ragdoll will grab it's wound when shot.")
pnl:NumSlider("Wound grab time", "fedhoria_woundgrab_time", 0, 10, 3)
pnl:ControlHelp("How long the ragdoll should hold its wound.")
pnl:Help(" ")
pnl:NumSlider("Activation range", "fedhoria_active_range", 0, 5000, 0)
pnl:ControlHelp("How close the ragdoll has to be to a player to have euphoria physics")
pnl:CheckBox("Always activate on player kill", "fedhoria_always_active_on_player_kill")
pnl:ControlHelp("If on, all player-killed ragdolls will be activated regardless of other factors.")
end
if engine.ActiveGamemode() == "sandbox" then
hook.Add("AddToolMenuCategories", "FedhoriaCategory", function()
spawnmenu.AddToolCategory("Utilities", "Fedhoria", "Fedhoria")
end)
hook.Add("PopulateToolMenu", "FedhoriaMenuSettings", function()
spawnmenu.AddToolMenuOption("Utilities", "Fedhoria", "FedhoriaSettings", "Settings", "", "", function(pnl)
pnl:ClearControls()
PopulateSBXToolMenu(pnl)
end)
end)
end

View file

@ -1 +0,0 @@
include("fedhoria.lua")

View file

@ -1,219 +0,0 @@
include("fedhoria/modules.lua")
local enabled = CreateConVar("fedhoria_enabled", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local players = CreateConVar("fedhoria_players", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local npcs = CreateConVar("fedhoria_npcs", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local active_range = CreateConVar("fedhoria_active_range", 300, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local always_active_on_player_kill = CreateConVar("fedhoria_always_active_on_player_kill", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local last_dmgpos = {}
local last_hitgroup = {}
local last_attacker = {}
hook.Add("CreateEntityRagdoll", "Fedhoria", function(ent, ragdoll)
if (!enabled:GetBool() or !npcs:GetBool()) then return end
local dmgpos = last_dmgpos[ent]
if last_hitgroup[ent] == HITGROUP_HEAD then return end
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
print(dmgpos)
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
timer.Simple(0, function()
if !IsValid(ragdoll) then return end
fedhoria.StartModule(ragdoll, "stumble_legs", phys_bone, lpos)
last_dmgpos[ent] = nil
end)
end)
hook.Add("EntityTakeDamage", "Fedhoria", function(ent, dmginfo)
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()
last_attacker[ent] = dmginfo:GetAttacker()
end)
hook.Add("ScaleNPCDamage", "Fedhoria", function(npc, hitgroup, dmginfo)
if not IsValid(npc) then return end
last_hitgroup[npc] = hitgroup
print(last_hitgroup[npc] == HITGROUP_HEAD)
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)]]
local once = true
--RagMod/TTT support
hook.Add("OnEntityCreated", "Fedhoria", function(ent)
--If RagMod isn't installed remove this hook
if once then
once = nil
if (!RMA_Ragdolize and !CORPSE) then
hook.Remove("OnEntityCreated", "Fedhoria")
return
end
--these hooks fucks shit up
if RMA_Ragdolize then
hook.Remove( "PlayerDeath", "RM_PlayerDies")
hook.Add( "PostPlayerDeath", "RemoveRagdoll", function(ply)
if IsValid(ply.RM_Ragdoll) then
SafeRemoveEntity(ply:GetRagdollEntity())
ply:SpectateEntity(ply.RM_Ragdoll)
end
end)
end
end
if (!enabled:GetBool() or !players:GetBool() or !ent:IsRagdoll()) then return end
timer.Simple(0, function()
if !IsValid(ent) then return end
if CORPSE then
local ply = ent:GetDTEntity(CORPSE.dti.ENT_PLAYER)
if (IsValid(ply) and ply:IsPlayer()) then
fedhoria.StartModule(ent, "stumble_legs")
return
end
end
for _, ply in ipairs(player.GetAll()) do
if (ply.RM_IsRagdoll and ply.RM_Ragdoll == ent) then
fedhoria.StartModule(ent, "stumble_legs")
return
end
end
end)
end)
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
if enabled:GetBool() then
PLAYER.CreateRagdoll = CreateRagdoll
PLAYER.GetRagdollEntity = GetRagdollEntity
end
cvars.AddChangeCallback("fedhoria_enabled", function(name, old, new)
if (new == "1") then
if players:GetBool() then
PLAYER.CreateRagdoll = CreateRagdoll
PLAYER.GetRagdollEntity = GetRagdollEntity
end
else
PLAYER.CreateRagdoll = oldCreateRagdoll
PLAYER.GetRagdollEntity = oldGetRagdollEntity
end
end)
cvars.AddChangeCallback("fedhoria_players", function(name, old, new)
if (new == "1") then
if enabled:GetBool() then
if (debug.getinfo(PLAYER.CreateRagdoll).short_src == "[C]") then
PLAYER.CreateRagdoll = CreateRagdoll
PLAYER.GetRagdollEntity = GetRagdollEntity
end
end
else
PLAYER.CreateRagdoll = oldCreateRagdoll
PLAYER.GetRagdollEntity = oldGetRagdollEntity
end
end)
hook.Add("PostPlayerDeath", "Fedhoria", function(ply)
if (!enabled:GetBool() or !players:GetBool()) then return end
timer.Simple(0, function()
if !IsValid(ply) then return end
local ragdoll = ply:GetRagdollEntity()
if (IsValid(ragdoll) and ragdoll:IsRagdoll()) then
fedhoria.StartModule(ragdoll, "stumble_legs")
end
end)
end)
--RagMod Reworked support
hook.Add("RM_RagdollReady", "Fedhoria", function(ragdoll)
if IsValid(ragdoll) then
fedhoria.StartModule(ragdoll, "stumble_legs")
end
end)

View file

@ -1,331 +0,0 @@
--[[-------------------------------------------------------------------------
Скрипт для управления поведением падающих частей тела NPC (например, ног)
Включает эффекты дергания и анимацию переката при ударе.
Версия 2: Исправлены ошибки GetGameData и HitNormal.
---------------------------------------------------------------------------]]
MODULE = {}
MODULE.Model = "models/police.mdl" -- Модель, к которой применяется скрипт (вероятно, не используется напрямую в этом коде)
-- Список костей, которые МОГУТ быть затронуты физикой или анимацией.
-- Для дергания будут использоваться только кости ног из этого списка.
MODULE.BoneList =
{
"ValveBiped.Bip01_Pelvis",
-- "ValveBiped.Bip01_Spine2",
-- "ValveBiped.Bip01_Head1",
-- "ValveBiped.Bip01_R_Upperarm",
-- "ValveBiped.Bip01_R_Forearm",
-- "ValveBiped.Bip01_R_Hand",
-- "ValveBiped.Bip01_L_Upperarm",
-- "ValveBiped.Bip01_L_Forearm",
-- "ValveBiped.Bip01_L_Hand",
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_R_Calf",
"ValveBiped.Bip01_R_Foot",
"ValveBiped.Bip01_L_Thigh",
"ValveBiped.Bip01_L_Calf",
"ValveBiped.Bip01_L_Foot"
}
-- Список костей, к которым БУДЕТ применяться эффект дергания.
-- Отфильтровано из MODULE.BoneList, чтобы исключить таз и другие нежелательные кости.
local twitchable_bone_names = {
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_R_Calf",
"ValveBiped.Bip01_R_Foot",
"ValveBiped.Bip01_L_Thigh",
"ValveBiped.Bip01_L_Calf",
"ValveBiped.Bip01_L_Foot"
}
-- Локальные копии функций для оптимизации
local math_Clamp = math.Clamp
local math_Rand = math.Rand
local math_random = math.random
local table_Random = table.Random -- Используем table.Random для выбора случайного элемента
local IsValid = IsValid -- Локальная копия IsValid
local CurTime = CurTime -- Локальная копия CurTime
local timer_Create = timer.Create
local timer_Remove = timer.Remove
local VectorRand = VectorRand
--[[-------------------------------------------------------------------------
Конвары (Настройки)
---------------------------------------------------------------------------]]
-- Подергивание
local cv_twitch_enabled = CreateConVar("fedhoria_falling_twitch_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить дергание для падающих ног")
local cv_twitch_interval_min = CreateConVar("fedhoria_falling_twitch_interval_min", "3", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Минимальный интервал между дерганиями (сек)")
local cv_twitch_interval_max = CreateConVar("fedhoria_falling_twitch_interval_max", "6", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Максимальный интервал между дерганиями (сек)")
local cv_twitch_force_min = CreateConVar("fedhoria_falling_twitch_min", "100", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. угловая скорость для дергания")
local cv_twitch_force_max = CreateConVar("fedhoria_falling_twitch_max", "250", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Макс. угловая скорость для дергания")
-- Перекат
local cv_anim_roll_enabled = CreateConVar("fedhoria_falling_anim_roll_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить анимацию при ударе о землю")
local cv_anim_roll_min_delay = CreateConVar("fedhoria_falling_anim_roll_min_delay", "0.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. задержка перед возможной анимацией после удара (сек)")
local cv_anim_roll_duration = CreateConVar("fedhoria_falling_anim_roll_duration", "3.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Продолжительность принудительной анимации (сек)")
local cv_anim_roll_impact_threshold = CreateConVar("fedhoria_falling_anim_roll_impact_threshold", "300", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. скорость удара для запуска анимации")
local cv_anim_roll_playback_rate = CreateConVar("fedhoria_falling_anim_roll_playback_rate", "3.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Скорость воспроизведения анимации")
-- Время жизни объекта после остановки
local die_time = CreateConVar("fedhoria_dietime", 5, {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local die_time_variation = CreateConVar("fedhoria_dietime_variation", 3, {FCVAR_ARCHIVE, FCVAR_REPLICATED})
--[[-------------------------------------------------------------------------
Функция дергания
Применяет случайный импульс к одной из костей ног.
---------------------------------------------------------------------------]]
function MODULE:DoTwitch()
if not IsValid(self) or not IsValid(self:GetTarget()) then return end
if not cv_twitch_enabled:GetBool() then return end
local phys = self:GetPhysicsObject()
if not IsValid(phys) then return end
if #twitchable_bone_names == 0 then return end
local random_bone_name = table_Random(twitchable_bone_names)
if not random_bone_name then return end
-- Получаем индекс кости по имени
local bone_index = self:LookupBone(random_bone_name)
if not bone_index then
-- print("[Fedhoria Twitch] Bone index not found for name:", random_bone_name)
return
end
-- Получаем физический объект кости по ее индексу
local phys_bone = self:GetPhysicsObjectNum(bone_index) -- Используем GetPhysicsObjectNum для получения физ. объекта конкретной кости
if IsValid(phys_bone) then
local force = math_Rand(cv_twitch_force_min:GetFloat(), cv_twitch_force_max:GetFloat())
local torque = VectorRand():GetNormal() * force
phys_bone:AddAngleVelocity(torque)
-- print("[Fedhoria Twitch] Applied torque", torque, "to bone", random_bone_name, "(index:", bone_index, ")")
-- else
-- print("[Fedhoria Twitch] Could not find a valid physics bone for index:", bone_index, "name:", random_bone_name)
end
self:ScheduleNextTwitch()
end
--[[-------------------------------------------------------------------------
Планирование следующего вызова DoTwitch
---------------------------------------------------------------------------]]
function MODULE:ScheduleNextTwitch()
if not IsValid(self) then return end
if not cv_twitch_enabled:GetBool() then return end
local timer_name = "Fedhoria_FallingLegs_Twitch_" .. self:EntIndex()
timer_Remove(timer_name)
local delay = math_Rand(cv_twitch_interval_min:GetFloat(), cv_twitch_interval_max:GetFloat())
timer_Create(timer_name, delay, 1, function()
if not IsValid(self) then return end
self:DoTwitch()
end)
end
--[[-------------------------------------------------------------------------
Запуск анимации переката
---------------------------------------------------------------------------]]
function MODULE:StartAnimationRoll()
if not cv_anim_roll_enabled:GetBool() then return end
local target = self:GetTarget()
if not IsValid(target) then return end
local cur_time = CurTime()
self.AnimationRollEndTime = cur_time + cv_anim_roll_duration:GetFloat()
local seq = self:LookupSequence("Choked_Barnacle")
if seq and seq > 0 then
self:ResetSequence(seq)
self:SetPlaybackRate(cv_anim_roll_playback_rate:GetFloat())
else
self.AnimationRollEndTime = 0
return
end
--self.StartDie = nil
end
function MODULE:Init()
local seq = self:LookupSequence("Choked_Barnacle")
if seq then self:ResetSequence(seq) end
self:SetPlaybackRate(1)
self.LastCollideTime = 0
self.LastGroundCollideTime = 0
self.StartDie = nil
self.AnimationRollEndTime = 0
-- Добавляем демпфирование для всех костей
local phys = self:GetPhysicsObject()
if IsValid(phys) then
local bone_count = phys:GetBoneCount()
for bone_index = 0, bone_count - 1 do
local phys_bone = phys:GetBone(bone_index)
if IsValid(phys_bone) then
phys_bone:SetDamping(0.8, 0.9) -- Линейное и угловое демпфирование
end
end
end
-- запускаем систему дергания
self:ScheduleNextTwitch()
end
--[[-------------------------------------------------------------------------
Think-хук
---------------------------------------------------------------------------]]
function MODULE:Think()
-- Место для дополнительной логики
end
--[[-------------------------------------------------------------------------
Хук столкновения физики (ИСПРАВЛЕН)
---------------------------------------------------------------------------]]
function MODULE:PhysicsCollide(data, phys)
-- Проверяем валидность данных столкновения и наличие HitNormal
if not data or not data.HitNormal then
-- print("[Fedhoria Collide] Invalid collision data received.")
return
end
if data.HitEntity == self then return end
local cur_time = CurTime()
self.LastCollideTime = cur_time
-- Проверка на столкновение с землей (нормаль смотрит вверх)
if data.HitNormal.z > 0.7 then
local impact_speed = data.Speed
-- print("[Fedhoria Collide] Ground collision detected. Speed:", impact_speed)
if cv_anim_roll_enabled:GetBool() and
impact_speed > cv_anim_roll_impact_threshold:GetFloat() and
cur_time > self.AnimationRollEndTime and
cur_time > self.LastGroundCollideTime + cv_anim_roll_min_delay:GetFloat() then
self:StartAnimationRoll()
end
self.LastGroundCollideTime = cur_time
end
end
--[[-------------------------------------------------------------------------
Хук симуляции физики (ИСПРАВЛЕН)
---------------------------------------------------------------------------]]
function MODULE:PhysicsSimulate(phys, dt)
local cur_time = CurTime()
local target = self:GetTarget()
if not IsValid(target) then self:Remove(); return false end
-- проверка на активную анимацию
if cur_time < self.AnimationRollEndTime then
--self.StartDie = nil -- сбрасываем таймер "смерти"
return true -- используем стандартную физику
end
-- логика для таймера исчезновения
local f = 1 -- множитель силы (по умолчанию 1)
if self.StartDie then
f = math_Clamp(1 - (cur_time - self.StartDie) / die_time:GetFloat(), 0, 1)
end
-- поддержка для RagMod
if ragmod and ragmod:IsRagmodRagdoll(target) then
local owner = target:GetOwningPlayer()
f = (IsValid(owner) and owner:Alive()) and 1 or 0
--self.StartDie = nil
if f <= 0 then self.AnimationRollEndTime = 0 end
end
-- удаление, если истекло время жизни
if (f <= 0) then
self:Remove()
return false
end
local phys_bone_id = phys:GetID()
-- основная логика для корневой кости
if (phys_bone_id == 0) then
-- логика для воды
if target:WaterLevel() > 0 then
self.AnimationRollEndTime = 0
local seq_choked = self:LookupSequence("Choked_Barnacle")
if seq_choked then self:ResetSequence(seq_choked) end
return false
end
-- логика для анимации падения
local vel = phys:GetVelocity()
local pbr = math_Clamp(vel.z / -600, 0.5, 1.5)
if self:GetSequence() ~= self:LookupSequence("Choked_Barnacle") or self:GetPlaybackRate() ~= pbr then
local seq_idle = self:LookupSequence("Choked_Barnacle")
if seq_idle then
self:ResetSequence(seq_idle)
self:SetPlaybackRate(pbr)
end
end
-- фикс: безопасная работа с векторами
local pos = phys:GetPos()
if not pos then return true, f end -- если pos невалиден, ничего не делаем
self.last_pos = self.last_pos or pos
if not self.last_pos then self.last_pos = pos end
local offset_sqr = (pos - self.last_pos):LengthSqr()
self.last_pos = pos
if (offset_sqr < (10*10 * dt*dt) and not (ragmod and ragmod:IsRagmodRagdoll(target))) then
self.StartDie = self.StartDie or cur_time
else
--self.StartDie = nil
end
-- коррекция физики после столкновения
local delta_collide = cur_time - self.LastCollideTime
if (delta_collide < 0.2) then
return true, 1 * f
elseif (delta_collide < 1.2) then
return true, (1 - (delta_collide - 0.2)) * f
end
return true, f
end
-- логика для других костей
local delta_collide = cur_time - self.LastCollideTime
if (delta_collide < 0.2) then
return true, 1 * f
elseif (delta_collide < 1.2) then
return true, (1 - (delta_collide - 0.2)) * f
end
return true, f
end
--[[-------------------------------------------------------------------------
Хук удаления сущности
---------------------------------------------------------------------------]]
function MODULE:OnRemove()
local timer_name = "Fedhoria_FallingLegs_Twitch_" .. self:EntIndex()
timer_Remove(timer_name)
end
--[[-------------------------------------------------------------------------
Регистрация модуля (если это часть системы модулей)
---------------------------------------------------------------------------]]
-- Пример регистрации:
-- falling_legs_manager:RegisterModule("FedhoriaFallingLegs", MODULE)

View file

@ -0,0 +1,165 @@
local function PopulateAISBXToolMenu(pnl)
pnl:CheckBox("Allow NPCs to target downed NPCs", "zcnpci_npc_targeting_enabled")
pnl:ControlHelp("If enabled, NPCs target downed hostile NPCs.")
pnl:CheckBox("NPCs should target head", "zcnpci_target_should_follow_head")
pnl:ControlHelp("Should NPCs target the player's head? This can result in more effective targeting.")
pnl:CheckBox("NPCs shouldn't target unconscious NPCs", "zcnpci_no_unconscious_targeting")
end
local function PopulateCustomNPCSBXToolMenu(pnl)
local modded_npc_whitelist = CreateConVar("zcnpci_modded_npc_whitelist", "NPC-classes-here!", bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
pnl:Help("ZCNPCi has support for both modded NPCs, as well as base-game humanoid NPCs that Z-City never added. There is no guarantee that modded NPCs will work, but ones with similar skeletal structure to other base-game humanoid NPCs will generally work better.")
pnl:Help("As long as the NPC has a compatible skeleton, support will be provided for modded NPCs on a best-effort basis.")
pnl:CheckBox("Allow base-game extended NPCs", "zcnpci_allow_extended_base_npcs")
pnl:ControlHelp("Enable or disables applying custom health system to most other base-game humanoid NPCs, such as Kleiner.")
pnl:CheckBox("Allow modded NPCs", "zcnpci_allow_modded_npcs")
pnl:ControlHelp("Enable or disables applying custom health system to modded NPCs.")
pnl:CheckBox("Auto enable for all humanoid NPCs", "zcnpci_auto_enable_humanoid_npcs")
pnl:ControlHelp("If enabled, NPCs will automatically have the health system applied to them if they have a standard humanoid skeleton.")
pnl:Help("List of enabled modded NPC classes")
local text = vgui.Create("DTextEntry")
text:SetMultiline(true)
text:SetTall(200)
text:SetValue(modded_npc_whitelist:GetString())
text.OnChange = function (self)
RunConsoleCommand("zcnpci_set_modded_npc_whitelist", text:GetValue())
end
pnl:AddItem(text)
pnl:Help("A list of NPC classes, seperated by spaces or line breaks. Can only be set by a superadmin or server operator.")
end
local function PopulateRagdollSBXToolMenu(pnl)
pnl:NumSlider("Stumble time", "zcnpci_stumble_time", 0, 10, 3)
pnl:ControlHelp("How long the ragdoll should try to stumble for.")
pnl:NumSlider("Wound grab chance", "zcnpci_woundgrab_chance", 0, 1, 3)
pnl:ControlHelp("The chance the ragdoll will grab it's wound when shot.")
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.")
pnl:NumSlider("Flailing speed multiplier", "zcnpci_flailing_playback_rate", 0, 12, 3)
pnl:ControlHelp("How fast or slow should the NPC flail by default.")
pnl:NumSlider("Flailing speed multiplier when panicking", "zcnpci_flailing_playback_rate", 0, 12, 3)
pnl:ControlHelp("How fast or slow should the NPC flail when panicking (on fire, falling, etc.).")
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.")
pnl:NumSlider("Get up time", "zcnpci_unfake_time", 0, 5, 3)
pnl:ControlHelp("How long the getting up animation lasts. Setting to 0 will disable the getting up animation.")
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)
pnl:NumSlider("NPC ticks per second", "zcnpci_tps", 1, 66, 0)
pnl:ControlHelp("How many times NPCs should be processed per second. Will make them more responsive at the cost of performance.")
pnl:Help(" ")
pnl:Help(" ")
pnl:NumSlider("Activation range", "zcnpci_active_range", 0, 32768, 0)
pnl:ControlHelp("How close the ragdoll has to be to a player to live when downed by direct damage. Enemies downed by other causes are unaffected.")
pnl:CheckBox("Always activate on player kill", "zcnpci_always_active_on_player_kill")
pnl:ControlHelp("If on, all player-damaged ragdolls will be activated regardless of other factors.")
pnl:Help(" ")
pnl:Help(" ")
pnl:CheckBox("Enable automatic corpse removal", "zcnpci_enable_corpse_removal")
pnl:ControlHelp("Enable or disable the removal of dead corpses.")
pnl:NumSlider("Corpse time between loops", "zcnpci_corpse_loop_time", 1, 66, 0)
pnl:ControlHelp("How long should we wait in between checks?")
pnl:NumSlider("Max corpses", "zcnpci_max_corpses", -1, 64, 0)
pnl:ControlHelp("The maximum amount of dead corpses that are allowed before one is removed. Set to -1 to disable")
pnl:NumSlider("Max time", "zcnpci_max_corpse_time", -1, 600, 2)
pnl:ControlHelp("The maximum amount of time before a corpse is removed. Set to -1 to disable")
pnl:NumSlider("Max distance between player", "zcnpci_max_corpse_distance", -1, 32768, 2)
pnl:ControlHelp("If the corpse is further than this distance, it will be removed. Set to -1 to disable")
pnl:NumSlider("Min distance between player", "zcnpci_min_corpse_distance", 0, 32768, 2)
pnl:ControlHelp("If the corpse is closer than this distance, it will never be cleared.")
pnl:CheckBox("Treat crippled NPCs as dead", "zcnpci_treat_crippled_as_dead")
pnl:ControlHelp("If on, bodies with amputations or both legs broken will be clearable.")
pnl:CheckBox("Treat unconscious NPCs as dead", "zcnpci_treat_unconscious_as_dead")
pnl:ControlHelp("If on, bodies that are unconscious will be clearable.")
pnl:CheckBox("Treat near-death NPCs as dead", "zcnpci_treat_near_death_as_dead")
pnl:ControlHelp("If on, bodies near death (severe blood loss, comas, etc.) will be clearable.")
end
local function PopulateMainSBXToolMenu(pnl)
pnl:CheckBox("Enabled", "zcnpci_enabled")
pnl:ControlHelp("Enable or disable the addon.")
pnl:Help("This addon was developed by ToasterPanic.")
pnl:Help("It is based on Kazarei's Euphoria, which in turn is a fork of Fedhoria by Rama. Their work powers the ragdoll movement.")
end
if engine.ActiveGamemode() == "sandbox" then
hook.Add("AddToolMenuCategories", "ZCNPCICategory", function()
spawnmenu.AddToolCategory("Utilities", "zcnpci", "Z-City NPCi")
end)
hook.Add("PopulateToolMenu", "ZCNPCIMenuSettings", function()
spawnmenu.AddToolMenuOption("Utilities", "zcnpci", "ModdedNPCSettings", "Custom NPCs", "", "", function(pnl)
pnl:ClearControls()
PopulateCustomNPCSBXToolMenu(pnl)
end)
spawnmenu.AddToolMenuOption("Utilities", "zcnpci", "PerformanceSettings", "Performance", "", "", function(pnl)
pnl:ClearControls()
PopulatePerformanceSBXToolMenu(pnl)
end)
spawnmenu.AddToolMenuOption("Utilities", "zcnpci", "RagdollSettings", "Ragdoll", "", "", function(pnl)
pnl:ClearControls()
PopulateRagdollSBXToolMenu(pnl)
end)
spawnmenu.AddToolMenuOption("Utilities", "zcnpci", "AISettings", "AI", "", "", function(pnl)
pnl:ClearControls()
PopulateAISBXToolMenu(pnl)
end)
spawnmenu.AddToolMenuOption("Utilities", "zcnpci", "MainSettings", "Main", "", "", function(pnl)
pnl:ClearControls()
PopulateMainSBXToolMenu(pnl)
end)
end)
end

View file

@ -0,0 +1 @@
include("zcnpci.lua")

View file

@ -97,7 +97,7 @@ end
function ENT:Initialize() function ENT:Initialize()
if !self.Module then return end if !self.Module then return end
self:SetModel(self.Module.Model) if self.Module.Model then self:SetModel(self.Module.Model) end
for key, value in pairs(self.Module) do for key, value in pairs(self.Module) do
if !self[key] then if !self[key] then

View file

@ -0,0 +1,120 @@
AddCSLuaFile()
ENT.Type = "ai"
ENT.Base = "base_ai"
ENT.AutomaticFrameAdvance = true
local debug_show_ragdoll_targets = CreateConVar("zcnpci_debug_show_ragdoll_targets", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local should_follow_head = CreateConVar("zcnpci_target_should_follow_head", 0, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local stop_targeting_when_unconscious = CreateConVar("zcnpci_no_unconscious_targeting", 1, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local vector_offset_to_shoot_at = Vector(0, 0, 8)
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())
ent:AddEntityRelationship(me, D_HT, -1)
else
ent:AddEntityRelationship(me, D_NU, -1)
end
end
local function RemoveRelationship(ent, me)
if !IsValid(ent) or !ent:IsNPC() or (ent:GetClass() == "npc_ragdoll_target") then return end
ent:AddEntityRelationship(me, D_NU, -1)
ent:ClearEnemyMemory(me)
if ent:GetEnemy() == me then
ent:SetEnemy(nil)
ent:ClearSchedule()
end
end
function ENT:Classify()
return CLASS_NONE
end
function ENT:Initialize()
if SERVER then
self:SetModel("models/maxofs2d/hover_basic.mdl")
self:SetCollisionGroup(COLLISION_GROUP_NONE)
self:SetHullType(HULL_TINY_CENTERED)
self:SetNoDraw(!debug_show_ragdoll_targets:GetBool())
self.dummy_entity = ents.Create(self.ragdoll_to_follow.class_in_previous_life)
self.last_stop_targeting = nil
-- Have to use this collision group otherwise ragdoll will obstruct visibility of target
self.ragdoll_to_follow:SetCollisionGroup(COLLISION_GROUP_DEBRIS)
hook.Add("OnEntityCreated", "zcnpci-"..self:GetCreationID(), function(new_entity)
if !IsValid(new_entity) or !new_entity:IsNPC() then return end
if !self.last_stop_targeting then
UpdateRelationship(new_entity, self)
end
end)
end
end
function ENT:Think()
if SERVER then
local ragdoll = self.ragdoll_to_follow
if !IsValid(ragdoll) then self:Remove(); return end
if should_follow_head:GetBool() then
local head = ragdoll:GetPhysicsObjectNum(ragdoll:TranslateBoneToPhysBone(ragdoll:LookupBone("ValveBiped.Bip01_Head1")))
self:SetPos(head:GetPos() + vector_offset_to_shoot_at)
else
self:SetPos(ragdoll:GetPos())
end
self:SetNoDraw(!debug_show_ragdoll_targets:GetBool())
if !ragdoll.organism then return true end
if !ragdoll.organism.alive then self:Remove(); return end
local should_stop_targeting = !stop_targeting_when_unconscious:GetBool() or (
(ragdoll.organism.consciousness <= 0.4) or
(ragdoll.organism.critical)
)
if (should_stop_targeting != self.last_stop_targeting) then
self.last_stop_targeting = should_stop_targeting
if should_stop_targeting then
self:SetColor(Color(255, 0, 0))
for i,ent in ipairs(ents.GetAll()) do
RemoveRelationship(ent, self)
end
else
self:SetColor(Color(0, 255, 0))
for i,ent in ipairs(ents.GetAll()) do
UpdateRelationship(ent, self)
end
end
end
self:NextThink(CurTime())
end
return true
end
function ENT:OnRemove()
if SERVER then
for i,ent in ipairs(ents.GetAll()) do
RemoveRelationship(ent, self)
end
self.dummy_entity:Remove()
hook.Remove("OnEntityCreated", "zcnpci-"..self:GetCreationID())
end
end

View file

@ -0,0 +1,131 @@
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",
}
-- 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")
self:SetModel("models/dav0r/hoverball.mdl")
end
if CLIENT then
print("I AM CLIENTSIDE")
self:SetNoDraw(true)
end
end
function ENT:Think()
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_animate then
self.ragdoll = ClientsideRagdoll(old_ragdoll:GetModel())
ragdoll = self.ragdoll
self.ragdoll:SetNoDraw(false)
self.ragdoll:DrawShadow(true)
SetRagdollPos(self.ragdoll, old_ragdoll:GetPos())
print(self.ragdoll)
print("SHOW UP YOU PIECE OF SHIT")
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)
if parent_object == nil then continue end
object:SetPos(parent_object:GetTranslation())
object:SetAngles(parent_object:GetAngles())
end
self.ready_to_animate = 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)
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)
if parent_bone == -1 then 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
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,
}
-- 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
end
end
end
self:NextThink(CurTime() + 0.1)
return true
end
function ENT:OnRemove()
if CLIENT then
if self.ragdoll then
self.ragdoll:Remove()
end
end
end

430
lua/zcnpci.lua Normal file
View file

@ -0,0 +1,430 @@
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()
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
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
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
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
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()
}
ragdoll:AddCallback("PhysicsCollide", function(outEnt, data) hook.Run("Ragdoll Collide", ragdoll, data) end)
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()
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
if attacker.HasWeapon == nil 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
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
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 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)
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

View file

@ -1,14 +1,14 @@
fedhoria = {} zcnpci = {}
local modules = {} local modules = {}
function fedhoria.GetModule(name) function zcnpci.GetModule(name)
if modules[name] then if modules[name] then
return modules[name] return modules[name]
end end
local path = "fedhoria/modules/"..name..".lua" local path = "zcnpci/modules/"..name..".lua"
if !file.Exists(path, "LUA") then if !file.Exists(path, "LUA") then
print("fedhoria.GetModule failed, couldn't find module '"..name.."'") print("zcnpci.GetModule failed, couldn't find module '"..name.."'")
return return
end end
local MODULE_old = MODULE local MODULE_old = MODULE
@ -20,12 +20,12 @@ function fedhoria.GetModule(name)
return modules[name] return modules[name]
end end
function fedhoria.GetModuleList() function zcnpci.GetModuleList()
return modules return modules
end end
function fedhoria.StartModule(ent, name, ...) function zcnpci.StartModule(ent, name, ...)
if (!modules[name] and !fedhoria.GetModule(name)) then if (!modules[name] and !zcnpci.GetModule(name)) then
return false return false
end end
local contr = ents.Create("active_ragdoll_controller") local contr = ents.Create("active_ragdoll_controller")
@ -38,8 +38,8 @@ function fedhoria.StartModule(ent, name, ...)
end end
--preload modules --preload modules
for _, file_name in pairs(file.Find("fedhoria/modules/*.lua", "LUA")) do for _, file_name in pairs(file.Find("zcnpci/modules/*.lua", "LUA")) do
fedhoria.GetModule(file_name:sub(1, -5)) zcnpci.GetModule(file_name:sub(1, -5))
end end
local ENTITY = FindMetaTable("Entity") local ENTITY = FindMetaTable("Entity")
@ -75,6 +75,7 @@ function ENTITY:GetClosestPhysBone(pos)
end end
if !collides then return end if !collides then return end
if type(pos) != "Vector" then return end
local closest_bone local closest_bone
local dist = math.huge local dist = math.huge

View file

@ -0,0 +1,639 @@
--[[-------------------------------------------------------------------------
A script for controlling the behavior of falling NPC body parts (e.g., legs).
Includes flinching effects and roll animation upon impact.
Version 2: Fixed GetGameData and HitNormal errors.
---------------------------------------------------------------------------]]
MODULE = {}
MODULE.Model = "models/police.mdl" -- The model to which the script is applied (likely not used directly in this code)
-- A list of bones that MAY be affected by physics or animation.
-- Only the leg bones from this list will be used for pulling.
MODULE.BoneList =
{
"ValveBiped.Bip01_Pelvis",
-- "ValveBiped.Bip01_Spine2",
-- "ValveBiped.Bip01_Head1",
-- "ValveBiped.Bip01_R_Upperarm",
-- "ValveBiped.Bip01_R_Forearm",
-- "ValveBiped.Bip01_R_Hand",
-- "ValveBiped.Bip01_L_Upperarm",
-- "ValveBiped.Bip01_L_Forearm",
-- "ValveBiped.Bip01_L_Hand",
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_R_Calf",
"ValveBiped.Bip01_R_Foot",
"ValveBiped.Bip01_L_Thigh",
"ValveBiped.Bip01_L_Calf",
"ValveBiped.Bip01_L_Foot"
}
-- A list of bones to which the jiggle effect WILL be applied.
-- Filtered from MODULE.BoneList to exclude the pelvis and other unwanted bones.
local twitchable_bone_names = {
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_R_Calf",
"ValveBiped.Bip01_R_Foot",
"ValveBiped.Bip01_L_Thigh",
"ValveBiped.Bip01_L_Calf",
"ValveBiped.Bip01_L_Foot"
}
local fakeup_bone_names = {
"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 fakeup_bone_down_names = {
"ValveBiped.Bip01_R_Forearm",
"ValveBiped.Bip01_L_Forearm",
"ValveBiped.Bip01_R_Thigh",
"ValveBiped.Bip01_L_Thigh",
}
-- Local Copies of Functions for Optimization
local math_Clamp = math.Clamp
local math_Rand = math.Rand
local math_random = math.random
local table_Random = table.Random -- We use `table.Random` to select a random element.
local IsValid = IsValid -- Local copy of IsValid
local CurTime = CurTime -- Local copy of CurTime
local timer_Create = timer.Create
local timer_Remove = timer.Remove
local VectorRand = VectorRand
--[[-------------------------------------------------------------------------
Convars (Settings)
---------------------------------------------------------------------------]]
-- Twitching
local cv_twitch_enabled = CreateConVar("zcnpci_falling_twitch_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить дергание для падающих ног")
local cv_twitch_interval_min = CreateConVar("zcnpci_falling_twitch_interval_min", "3", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Минимальный интервал между дерганиями (сек)")
local cv_twitch_interval_max = CreateConVar("zcnpci_falling_twitch_interval_max", "6", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Максимальный интервал между дерганиями (сек)")
local cv_twitch_force_min = CreateConVar("zcnpci_falling_twitch_min", "100", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. угловая скорость для дергания")
local cv_twitch_force_max = CreateConVar("zcnpci_falling_twitch_max", "250", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Макс. угловая скорость для дергания")
-- Roll
local cv_anim_roll_enabled = CreateConVar("zcnpci_falling_anim_roll_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить анимацию при ударе о землю")
local cv_anim_roll_min_delay = CreateConVar("zcnpci_falling_anim_roll_min_delay", "0.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. задержка перед возможной анимацией после удара (сек)")
local cv_anim_roll_duration = CreateConVar("zcnpci_falling_anim_roll_duration", "3.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Продолжительность принудительной анимации (сек)")
local cv_anim_roll_impact_threshold = CreateConVar("zcnpci_falling_anim_roll_impact_threshold", "300", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. скорость удара для запуска анимации")
local cv_anim_roll_playback_rate = CreateConVar("zcnpci_falling_anim_roll_playback_rate", "3.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Скорость воспроизведения анимации")
-- Down time
local minimum_down_time = CreateConVar("zcnpci_down_time", "5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Down time!!!!!!!!!")
-- 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")
-- Flailing
local flailing_playback_rate = CreateConVar("zcnpci_flailing_playback_rate", "1.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local flailing_panic_multiplier = CreateConVar("zcnpci_flailing_panic_multiplier", "2.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local flailing_panic_airbone_check_distance = CreateConVar("zcnpci_flailing_panic_airbone_check_distance", "128", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-- Writhing
local writhing_strength = CreateConVar("zcnpci_writhing_strength", "1.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
--[[-------------------------------------------------------------------------
Tug function
Applies a random impulse to one of the leg bones.
---------------------------------------------------------------------------]]
function MODULE:DoTwitch()
if not IsValid(self) or not IsValid(self:GetTarget()) then return end
if not cv_twitch_enabled:GetBool() then return end
local phys = self:GetPhysicsObject()
if not IsValid(phys) then return end
if #twitchable_bone_names == 0 then return end
local random_bone_name = table_Random(twitchable_bone_names)
if not random_bone_name then return end
-- Get bone index by name
local bone_index = self:LookupBone(random_bone_name)
if not bone_index then
-- print("[Fedhoria Twitch] Bone index not found for name:", random_bone_name)
return
end
-- We retrieve the physical bone object by its index.
local phys_bone = self:GetPhysicsObjectNum(bone_index) -- We use GetPhysicsObjectNum to retrieve the physics object for a specific bone.
if IsValid(phys_bone) then
local force = math_Rand(cv_twitch_force_min:GetFloat(), cv_twitch_force_max:GetFloat())
local torque = VectorRand():GetNormal() * force
phys_bone:AddAngleVelocity(torque)
-- print("[Fedhoria Twitch] Applied torque", torque, "to bone", random_bone_name, "(index:", bone_index, ")")
-- else
-- print("[Fedhoria Twitch] Could not find a valid physics bone for index:", bone_index, "name:", random_bone_name)
end
self:ScheduleNextTwitch()
end
--[[-------------------------------------------------------------------------
Planning the next DoTwitch call
---------------------------------------------------------------------------]]
function MODULE:ScheduleNextTwitch()
if not IsValid(self) then return end
if not cv_twitch_enabled:GetBool() then return end
local timer_name = "Fedhoria_FallingLegs_Twitch_" .. self:EntIndex()
timer_Remove(timer_name)
local delay = math_Rand(cv_twitch_interval_min:GetFloat(), cv_twitch_interval_max:GetFloat())
timer_Create(timer_name, delay, 1, function()
if not IsValid(self) then return end
self:DoTwitch()
end)
end
--[[-------------------------------------------------------------------------
Starting the roll animation
---------------------------------------------------------------------------]]
function MODULE:StartAnimationRoll()
if not cv_anim_roll_enabled:GetBool() then return end
local target = self:GetTarget()
if not IsValid(target) then return end
local cur_time = CurTime()
self.AnimationRollEndTime = cur_time + cv_anim_roll_duration:GetFloat()
local seq = self:LookupSequence("Choked_Barnacle")
if seq and seq > 0 then
self:ResetSequence(seq)
self:SetPlaybackRate(cv_anim_roll_playback_rate:GetFloat())
else
self.AnimationRollEndTime = 0
return
end
--target.StartDie = nil
end
function MODULE:Init()
local seq = self:LookupSequence("Choked_Barnacle")
if seq then self:ResetSequence(seq) end
local target = self:GetTarget()
self:SetPlaybackRate(1)
self.LastCollideTime = 0
self.LastGroundCollideTime = 0
target.StartDie = nil
self.AnimationRollEndTime = 0
self.StopProcessing = false
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())
self.bullseye.ragdoll_to_follow = target
self.bullseye:Spawn()
self.bullseye:Activate()
-- Add damping to all bones.
local phys = self:GetPhysicsObject()
if IsValid(phys) then
local bone_count = phys:GetBoneCount()
for bone_index = 0, bone_count - 1 do
local phys_bone = phys:GetBone(bone_index)
if IsValid(phys_bone) then
phys_bone:SetDamping(0.8, 0.9) -- Linear and Angular Damping
end
end
end
-- запускаем систему дергания
self:ScheduleNextTwitch()
end
--[[-------------------------------------------------------------------------
Think-hook
---------------------------------------------------------------------------]]
function MODULE:Think()
--if (CurTime() - self.LastThink) < 1 then return end
local target = self:GetTarget()
if !IsValid(target) then return end
self.LastThink = CurTime()
local phys = target:GetPhysicsObject()
if target.FakeUp then
local parent = self.FakeParent
if !IsValid(parent) then return end
if !self.ModelBoneList then return end
--[[for i,v in pairs(self.ModelBoneList) do
local object = target:GetPhysicsObjectNum(target:TranslateBoneToPhysBone(target:LookupBone(v)))
if !IsValid(object) then continue end
object:EnableCollisions(false)
end]]
end
if !IsValid(phys) or !phys:IsAsleep() then return end
phys:Wake()
end
--[[-------------------------------------------------------------------------
Physics Collision Hook (FIXED)
---------------------------------------------------------------------------]]
function MODULE:PhysicsCollide(data, phys)
-- Checking the validity of collision data and the presence of HitNormal.
if not data or not data.HitNormal then
-- print("[Fedhoria Collide] Invalid collision data received.")
return
end
if data.HitEntity == self then return end
local cur_time = CurTime()
self.LastCollideTime = cur_time
-- Ground collision check (normal points upward)
if data.HitNormal.z > 0.7 then
local impact_speed = data.Speed
-- print("[Fedhoria Collide] Ground collision detected. Speed:", impact_speed)
if cv_anim_roll_enabled:GetBool() and
impact_speed > cv_anim_roll_impact_threshold:GetFloat() and
cur_time > self.AnimationRollEndTime and
cur_time > self.LastGroundCollideTime + cv_anim_roll_min_delay:GetFloat() then
self:StartAnimationRoll()
end
self.LastGroundCollideTime = cur_time
end
end
function MODULE:EntityTakeDamage(ent, dmginfo)
print("I TOOK DAMAGE")
end
--[[-------------------------------------------------------------------------
Physics Simulation Hook (FIXED)
---------------------------------------------------------------------------]]
function MODULE:PhysicsSimulate(phys, dt)
if !SERVER then return end
local cur_time = CurTime()
local target = self:GetTarget()
if not IsValid(target) then self:Remove(); self.bullseye:Remove(); return false end
if target.FakeUp then
local parent = self.FakeParent
if !IsValid(parent) then
self:Remove()
return false
end
if !self.ModelBoneList then
self.ModelBoneList = {}
local i = 0
while i < target:GetBoneCount() do
local name = target:GetBoneName(i)
local object = target:LookupBone(name)
if object == -1 then i = i + 1; continue end
self.ModelBoneList[name] = target:GetBoneMatrix(object)
i = i + 1
end
end
local animation_progress = (CurTime() - self.FakeUpStart) / (self.FakeUpEnd - self.FakeUpStart)
for i, name in pairs(fakeup_bone_names) do
local object = target:GetPhysicsObjectNum(target:TranslateBoneToPhysBone(target:LookupBone(name)))
local parent_bone = parent:LookupBone(name)
if parent_bone == -1 then continue end
local parent_bone_matrix = parent: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
local shadow_data = {
secondstoarrive = 0.01,
pos = LerpVector(animation_progress, old_bone_pos, parent_bone_pos),
angle = LerpAngle(animation_progress, old_bone_angle, parent_bone_angle),
maxspeed = 400,
maxangular = 2000,
maxspeeddamp = 60,
maxangularspeeddamp = 600,
}
-- 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
end
return false
end
--print((CurTime() - self.LastPhysProcess) < 0.05)
--if (CurTime() - self.LastPhysProcess) < (1 /) then return false end())
phys:Wake()
--self.bullseye:SetAngles(target:EyeAngles())
target.is_npc_corpse = true
if !target.StartDie then target.StartDie = cur_time end
-- Check for active animation
if cur_time < self.AnimationRollEndTime then
--target.StartDie = nil -- Resetting the "death" timer
return true -- We use standard physics.
end
-- Force multiplier
local f = target.organism.consciousness * writhing_strength:GetFloat()
local minimum_down_timer = 0
if target.StartDie then
minimum_down_timer = math_Clamp((cur_time - target.StartDie) / minimum_down_time:GetFloat(), 0, 1)
end
if !target.organism then
self:Remove()
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
target.StartDie = cur_time
return false
end
if (self.LastPosCheck:DistToSqr(target:GetPos()) > (movement_sensitivity:GetFloat() ^ 2)) then
self.LastPosCheck = target:GetPos()
target.StartDie = cur_time
end
local distance_check = util.TraceLine({
start = target:GetPos(),
endpos = target:GetPos() + Vector(0, 0, -128),
filter = {target},
mask = MASK_SOLID
}).Hit
if !distance_check then
target.StartDie = cur_time
end
if ((CurTime() - self.LastFakeUpCheck) >= 1.0) and (can_unfake:GetBool()) then
self.LastFakeUpCheck = CurTime()
-- This basically checks if the NPC will be in collision with anything when it spawns.
-- Side note: the mins and maxs values are slightly larger than the default npc_citizen
-- hitboxes, mostly because I assume that most humanoid NPCs will be around that size.
local tracehull = util.TraceHull({
start = target:GetPos(),
endpos = target:GetPos(),
mins = Vector(-20, -20, 0),
maxs = Vector(20, 20, 84),
mask = MASK_NPCSOLID,
filter = { target }
})
if (
(!tracehull.Hit) and
(minimum_down_timer >= 1.0) 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.pain <= 80) and
(target.organism.consciousness > 0.65) and
!target:IsOnFire()
) then
local ent = ents.Create(target.class_in_previous_life)
ent:SetPos(target:GetPos())
ent:SetModel(target:GetModel())
ent:SetMaterial(target:GetMaterial())
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
for i = 0, target:GetNumBodyGroups() - 1 do
ent:SetBodygroup(i, target:GetBodygroup(i))
end
ent:Spawn()
ent:SetNotSolid(true)
ent:SetNPCState(NPC_STATE_NONE)
timer.Simple(0, function()
hg.organism.Add(ent)
table.Merge(ent.organism, target.organism)
ent.tourniquets = table.Copy(target.tourniquets)
ent.bandaged_limbs = table.Copy(target.bandaged_limbs)
ent.organism.alive = true
ent.organism.owner = ent
target.organism = nil
ent:CallOnRemove("organism", hg.organism.Remove, ent)
hg.send_bareinfo(ent.organism)
ent:SetSkin(target:GetSkin())
for i = 0, target:GetNumBodyGroups() - 1 do
ent:SetBodygroup(i, target:GetBodygroup(i))
end
self.unfaker:Spawn()
self.unfaker:Activate()
end)
target.FakeUp = true
self.FakeParent = ent
self.FakeUpTime = unfake_time:GetFloat()
self.FakeUpEnd = CurTime() + self.FakeUpTime
self.FakeUpStart = CurTime()
ent:SetAngles(Angle(0, math.random(-180, 180), 0))
local phys = target:GetPhysicsObject()
phys:EnableGravity(false)
target:SetNotSolid(true)
target:SetMoveType(MOVETYPE_NONE)
self.unfaker = ents.Create("npc_ragdoll_unfaker")
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)
timer.Simple(self.FakeUpTime, function()
if !IsValid(ent) then return end
ent:SetNotSolid(false)
ent:SetNPCState(NPC_STATE_IDLE)
ent:SetRenderMode(RENDERMODE_NORMAL)
target:Remove()
self:Remove()
end)
return false
end
end
local phys_bone_id = phys:GetID()
-- Main logic for the root bone
if (phys_bone_id == 0) then
-- Water Logic
if target:WaterLevel() > 0 then
self.AnimationRollEndTime = 0
local seq_choked = self:LookupSequence("Choked_Barnacle")
if seq_choked then self:ResetSequence(seq_choked) end
return false
end
-- Logic for fall animation
local vel = phys:GetVelocity()
local pbr = math_Clamp(vel.z / -600, 0.5, 1.5) * flailing_playback_rate:GetFloat()
target.panicking = (
target:IsOnFire() or
(!distance_check) or
(target.organism.pain > 80)
)
if target.panicking then
pbr = pbr * flailing_panic_multiplier:GetFloat()
end
if self:GetSequence() ~= self:LookupSequence("Choked_Barnacle") or self:GetPlaybackRate() ~= pbr then
local seq_idle = self:LookupSequence("Choked_Barnacle")
if seq_idle then
self:ResetSequence(seq_idle)
self:SetPlaybackRate(pbr)
end
end
-- Fix: Safe vector handling
local pos = phys:GetPos()
if not pos then return true, f end -- If `pos` is invalid, do nothing.
self.last_pos = self.last_pos or pos
if not self.last_pos then self.last_pos = pos end
local offset_sqr = (pos - self.last_pos):LengthSqr()
self.last_pos = pos
if offset_sqr < (10*10 * dt*dt) then
target.StartDie = target.StartDie or cur_time
end
-- Physics correction after collision
local delta_collide = cur_time - self.LastCollideTime
if (delta_collide < 0.2) then
return true, 1 * f
elseif (delta_collide < 1.2) then
return true, (1 - (delta_collide - 0.2)) * f
end
return true, f
end
-- Logic for other bones
local delta_collide = cur_time - self.LastCollideTime
if (delta_collide < 0.2) then
return true, 1 * f
elseif (delta_collide < 1.2) then
return true, (1 - (delta_collide - 0.2)) * f
end
return true, f
end
--[[-------------------------------------------------------------------------
Entity Deletion Hook
---------------------------------------------------------------------------]]
function MODULE:OnRemove()
local timer_name = "Fedhoria_FallingLegs_Twitch_" .. self:EntIndex()
timer_Remove(timer_name)
if IsValid(self.unfaker) then self.unfaker:Remove() end
end
--[[-------------------------------------------------------------------------
Module registration (if it is part of a module system)
---------------------------------------------------------------------------]]
-- Registration Example:
-- falling_legs_manager:RegisterModule("FedhoriaFallingLegs", MODULE)

View file

@ -21,26 +21,27 @@ local table_HasValue = table.HasValue
local hand_offset = Vector(2, 0, 0) local hand_offset = Vector(2, 0, 0)
--twtiching -- Twitching
local cv_twitch_enabled = CreateConVar("fedhoria_falling_twitch_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить дергание для падающих торсов") local cv_twitch_enabled = CreateConVar("zcnpci_falling_twitch_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить дергание для падающих ног")
local cv_twitch_interval_min = CreateConVar("fedhoria_falling_twitch_interval_min", "3", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Минимальный интервал между дерганиями (сек)") local cv_twitch_interval_min = CreateConVar("zcnpci_falling_twitch_interval_min", "3", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Минимальный интервал между дерганиями (сек)")
local cv_twitch_interval_max = CreateConVar("fedhoria_falling_twitch_interval_max", "6", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Максимальный интервал между дерганиями (сек)") local cv_twitch_interval_max = CreateConVar("zcnpci_falling_twitch_interval_max", "6", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Максимальный интервал между дерганиями (сек)")
local cv_twitch_force_min = CreateConVar("fedhoria_falling_twitch_min", "100", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. угловая скорость для дергания") local cv_twitch_force_min = CreateConVar("zcnpci_falling_twitch_min", "100", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. угловая скорость для дергания")
local cv_twitch_force_max = CreateConVar("fedhoria_falling_twitch_max", "250", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Макс. угловая скорость для дергания") local cv_twitch_force_max = CreateConVar("zcnpci_falling_twitch_max", "250", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Макс. угловая скорость для дергания")
local ignore_bone_indices_twitch = {0} -- игнор нахуй
--перекат ебаный -- Roll
local cv_anim_roll_enabled = CreateConVar("fedhoria_falling_anim_roll_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить анимацию 'idleonfire' при ударе о землю") local cv_anim_roll_enabled = CreateConVar("zcnpci_falling_anim_roll_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить анимацию при ударе о землю")
local cv_anim_roll_min_delay = CreateConVar("fedhoria_falling_anim_roll_min_delay", "0.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. задержка перед возможной анимацией после удара (сек)") local cv_anim_roll_min_delay = CreateConVar("zcnpci_falling_anim_roll_min_delay", "0.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. задержка перед возможной анимацией после удара (сек)")
local cv_anim_roll_duration = CreateConVar("fedhoria_falling_anim_roll_duration", "3.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Продолжительность принудительной анимации 'idleonfire' (сек)") local cv_anim_roll_duration = CreateConVar("zcnpci_falling_anim_roll_duration", "3.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Продолжительность принудительной анимации (сек)")
local cv_anim_roll_impact_threshold = CreateConVar("fedhoria_falling_anim_roll_impact_threshold", "300", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. скорость удара для запуска анимации") local cv_anim_roll_impact_threshold = CreateConVar("zcnpci_falling_anim_roll_impact_threshold", "300", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Мин. скорость удара для запуска анимации")
local cv_anim_roll_playback_rate = CreateConVar("fedhoria_falling_anim_roll_playback_rate", "3.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Скорость воспроизведения анимации 'idleonfire'") local cv_anim_roll_playback_rate = CreateConVar("zcnpci_falling_anim_roll_playback_rate", "3.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Скорость воспроизведения анимации")
local die_time = CreateConVar("fedhoria_dietime", 5, {FCVAR_ARCHIVE, FCVAR_REPLICATED}) local flailing_playback_rate = CreateConVar("zcnpci_flailing_playback_rate", "1.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local die_time_variation = CreateConVar("fedhoria_dietime_variation", 3, {FCVAR_ARCHIVE, FCVAR_REPLICATED}) local flailing_panic_multiplier = CreateConVar("zcnpci_flailing_panic_multiplier", "2.5", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-- Writhing
local writhing_strength = CreateConVar("zcnpci_writhing_strength", "1.0", {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-- адно дергание -- Do it!
function MODULE:DoTwitch() function MODULE:DoTwitch()
if not IsValid(self) or not IsValid(self:GetTarget()) then return end if not IsValid(self) or not IsValid(self:GetTarget()) then return end
@ -119,17 +120,20 @@ function MODULE:StartAnimationRoll()
return return
end end
self.StartDie = nil -- блять я заебался с этой хуйней target.StartDie = nil -- блять я заебался с этой хуйней
-- print("[Fedhoria AnimRoll] StartAnimationRoll initiated. Speed:", cv_anim_roll_playback_rate:GetFloat(), "Duration:", cv_anim_roll_duration:GetFloat(), "End Time:", self.AnimationRollEndTime) -- Debug -- print("[Fedhoria AnimRoll] StartAnimationRoll initiated. Speed:", cv_anim_roll_playback_rate:GetFloat(), "Duration:", cv_anim_roll_duration:GetFloat(), "End Time:", self.AnimationRollEndTime) -- Debug
end end
function MODULE:Init() function MODULE:Init()
local seq = self:LookupSequence("idleonfire") local seq = self:LookupSequence("idleonfire")
if seq then self:ResetSequence(seq) end if seq then self:ResetSequence(seq) end
local target = self:GetTarget()
self:SetPlaybackRate(1) self:SetPlaybackRate(1)
self.LastCollideTime = 0 self.LastCollideTime = 0
self.LastGroundCollideTime = 0 self.LastGroundCollideTime = 0
self.StartDie = nil target.StartDie = nil
self.AnimationRollEndTime = 0 self.AnimationRollEndTime = 0
-- идте нахуй с этой хуйней -- идте нахуй с этой хуйней
@ -172,12 +176,15 @@ end
function MODULE:PhysicsSimulate(phys, dt) function MODULE:PhysicsSimulate(phys, dt)
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 !IsValid(target) then self:Remove(); return false end -- если цели нет, то идем нахуй
if !target.organism then self:Remove(); return false end
if target.FakeUp then self.AnimationRollEndTime = 0; return false end
-- проверка на физику -- проверка на физику
if cur_time < self.AnimationRollEndTime then if cur_time < self.AnimationRollEndTime then
-- print("[Fedhoria Sim] In Animation Roll. Time left:", self.AnimationRollEndTime - cur_time) -- Debug -- print("[Fedhoria Sim] In Animation Roll. Time left:", self.AnimationRollEndTime - cur_time) -- Debug
-- self.StartDie = nil -- Сбрасываем таймер смерти -- target.StartDie = nil -- Сбрасываем таймер смерти
return true -- стандартная физика пошла нахуй, у меня по ней 2 return true -- стандартная физика пошла нахуй, у меня по ней 2
-- return false -- если физика нахуй идет -- return false -- если физика нахуй идет
@ -185,29 +192,34 @@ function MODULE:PhysicsSimulate(phys, dt)
-- --- логика идет нахуй -- --- логика идет нахуй
-- логика для таймера -- Force multiplier
local f = 1 -- сила есть ума не надо (по умолчанию 1) local f = target.organism.consciousness * writhing_strength:GetFloat()
if self.StartDie then
f = math_Clamp(1 - (cur_time - self.StartDie) / die_time:GetFloat(), 0, 1)
end
print(f)
-- ебаная логика для regmod -- ебаная логика для regmod
if ragmod and ragmod:IsRagmodRagdoll(target) then if ragmod and ragmod:IsRagmodRagdoll(target) then
local owner = target:GetOwningPlayer() local owner = target:GetOwningPlayer()
-- владелец идот нахуй -- владелец идот нахуй
f = (IsValid(owner) and owner:Alive()) and 1 or 0 f = (IsValid(owner) and owner:Alive()) and 1 or 0
self.StartDie = nil target.StartDie = nil
if f <= 0 then self.AnimationRollEndTime = 0 end if f <= 0 then self.AnimationRollEndTime = 0 end
end end
-- if !target or !target.organism then
if (f <= 0) then
-- print("[Fedhoria Sim] Removing entity (f <= 0)") -- Debug
self:Remove() self:Remove()
return false -- Прекращаем хуйню return false -- Cut the bullshit
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.spine2 == 1) or
(target.organism.spine3 == 1)
) then
return false
end end
local phys_bone_id = phys:GetID() -- айди кости local phys_bone_id = phys:GetID() -- айди кости
@ -227,7 +239,12 @@ function MODULE:PhysicsSimulate(phys, dt)
-- логика для анимации переката -- логика для анимации переката
local vel = phys:GetVelocity() local vel = phys:GetVelocity()
local pbr = math_Clamp(vel.z / -600, 0.5, 1.5) local pbr = math_Clamp(vel.z / -600, 0.5, 1.5) * flailing_playback_rate:GetFloat()
if target.panicking then
pbr = pbr * flailing_panic_multiplier:GetFloat()
end
-- падения и ебаная скорость -- падения и ебаная скорость
-- если мы не в.. а впрочем иди нахуй -- если мы не в.. а впрочем иди нахуй
if self:GetSequence() ~= self:LookupSequence("idleonfire") or self:GetPlaybackRate() ~= pbr then if self:GetSequence() ~= self:LookupSequence("idleonfire") or self:GetPlaybackRate() ~= pbr then
@ -245,10 +262,10 @@ 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 -- uменьшил порог неподвижности if (offset_sqr < (10*10 * dt*dt) and not (ragmod and ragmod:IsRagmodRagdoll(target))) then -- uменьшил порог неподвижности
self.StartDie = self.StartDie or cur_time -- идите нахуй target.StartDie = target.StartDie or cur_time -- идите нахуй
-- print("[Fedhoria Sim] Torso seems stationary. StartDie:", self.StartDie) -- Debug -- print("[Fedhoria Sim] Torso seems stationary. StartDie:", target.StartDie) -- Debug
else else
-- self.StartDie = nil -- нахуй таймер -- target.StartDie = nil -- нахуй таймер
-- print("[Fedhoria Sim] Torso moved. Resetting StartDie.") -- Debug -- print("[Fedhoria Sim] Torso moved. Resetting StartDie.") -- Debug
end end

View file

@ -18,9 +18,9 @@ MODULE.BoneList =
"ValveBiped.Bip01_L_Foot" "ValveBiped.Bip01_L_Foot"
} }
local stumble_time = CreateConVar("fedhoria_stumble_time", 2, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) local stumble_time = CreateConVar("zcnpci_stumble_time", 2, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local grab_chance = CreateConVar("fedhoria_woundgrab_chance", 0.9, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) local grab_chance = CreateConVar("zcnpci_woundgrab_chance", 0.9, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local grab_time = CreateConVar("fedhoria_woundgrab_time", 5, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) local grab_time = CreateConVar("zcnpci_woundgrab_time", 5, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED))
local math_atan2 = math.atan2 local math_atan2 = math.atan2
local math_pi = math.pi local math_pi = math.pi
@ -223,8 +223,8 @@ function MODULE:Init(phys_bone, lpos)
end end
local function DoFallingAnim(ent) local function DoFallingAnim(ent)
local mod1 = fedhoria.StartModule(ent, "falling_legs") local mod1 = zcnpci.StartModule(ent, "falling_legs")
local mod2 = fedhoria.StartModule(ent, "falling_torso") local mod2 = zcnpci.StartModule(ent, "falling_torso")
end end
function MODULE:OnRemove() function MODULE:OnRemove()
@ -279,6 +279,8 @@ local trace = {output={}}
local tr = trace.output local tr = trace.output
function MODULE:PhysicsSimulate(phys, dt) function MODULE:PhysicsSimulate(phys, dt)
if self.FakeUp then return end
local phys_bone = phys:GetID() local phys_bone = phys:GetID()
local target = self:GetTarget() local target = self:GetTarget()
@ -291,6 +293,11 @@ function MODULE:PhysicsSimulate(phys, dt)
local st = stumble_time:GetFloat() local st = stumble_time:GetFloat()
-- End stumbling if player is dead
if !target.organism or !target.organism.alive then
st = 0
end
if (st <= 0) then if (st <= 0) then
self:Remove() self:Remove()
return false return false
@ -298,16 +305,6 @@ function MODULE:PhysicsSimulate(phys, dt)
local f = 1 - (CurTime() - self.Created) / st local f = 1 - (CurTime() - self.Created) / st
--RagMod Reworked support
if ragmod and ragmod:IsRagmodRagdoll(target) then
local owner = target:GetOwningPlayer()
if !IsValid(owner) or !owner:Alive() then
f = 0
else
f = 1
end
end
if (f <= 0) then if (f <= 0) then
self:Remove() self:Remove()
return false return false