zcnpci/lua/zcnpci/modules/falling_legs.lua

400 lines
15 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[-------------------------------------------------------------------------
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 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", "3", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Down time!!!!!!!!!")
--[[-------------------------------------------------------------------------
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
--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
self.StopProcessing = false
-- 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 !IsValid(target) then return end
print("target is valid")
local phys = target:GetPhysicsObject()
if !IsValid(phys) or !phys:IsAsleep() then return end
print("phys is valid")
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)
phys:Wake()
if self.StopProcessing then return false end
local cur_time = CurTime()
local target = self:GetTarget()
if not IsValid(target) then self:Remove(); return false end
target.is_npc_corpse = true
if !self.StartDie then self.StartDie = cur_time end
-- Check for active animation
if cur_time < self.AnimationRollEndTime then
--self.StartDie = nil -- Resetting the "death" timer
return true -- We use standard physics.
end
-- Logic for the disappearance timer
local f = 1 -- Force multiplier (default: 1)
local minimum_down_timer = 0
if self.StartDie then
minimum_down_timer = math_Clamp((cur_time - self.StartDie) / minimum_down_time:GetFloat(), 0, 1)
end
if !target.organism then
self:Remove()
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.3) or ((target.organism.lleg >= 0.85) and (target.organism.rleg >= 0.85)) then
self.StartDie = cur_time
return false
end
-- Getting up
-- Don't need to check for consciousness and the like because we've done it already above
if (
(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:IsOnFire()
) then
local ent = ents.Create(target.class_in_previous_life)
ent:SetPos(target:GetPos())
ent:SetModel(target:GetModel())
if target:GetNumBodyGroups() > 0 then
local i = 1
while (i <= target:GetNumBodyGroups()) do
ent:SetBodygroup(i, target:GetBodygroup(i))
i = i + 1
end
end
ent:Spawn()
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
ent:CallOnRemove("organism", hg.organism.Remove, ent)
hg.send_bareinfo(ent.organism)
target:Remove()
end)
self.StopProcessing = true
self:Remove()
return false
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)
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) and not (ragmod and ragmod:IsRagmodRagdoll(target))) then
self.StartDie = self.StartDie or cur_time
else
--self.StartDie = nil
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)
end
--[[-------------------------------------------------------------------------
Module registration (if it is part of a module system)
---------------------------------------------------------------------------]]
-- Registration Example:
-- falling_legs_manager:RegisterModule("FedhoriaFallingLegs", MODULE)