zcnpci/lua/zcnpci/modules/falling_legs.lua

650 lines
24 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 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")
--[[-------------------------------------------------------------------------
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.bullseye = ents.Create("npc_bullseye")
self.bullseye:SetPos(target:GetPos())
self.bullseye:SetMoveType(MOVETYPE_OBSERVER)
self.bullseye:SetModel("models/editor/bullseye.mdl")
self.bullseye:SetHealth(99999)
self.bullseye:Spawn()
self.bullseye:Activate()
self.bullseye:SetNotSolid(true)
self.bullseye.npc_rag = target
self.dummy_entity = ents.Create(target.class_in_previous_life)
for i,ent in ipairs(ents.GetAll()) do
if !IsValid(ent) or !ent.IsNPC() then continue end
if ent:Disposition(self.dummy_entity) == D_HT then
ent:AddEntityRelationship(self.bullseye, D_HT, -1)
print("HATE. HATE. HATE")
end
end
-- 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
print("thinking")
local target = self:GetTarget()
if !IsValid(target) then return end
self.LastThink = CurTime()
local phys = target:GetPhysicsObject()
if target.FakeUp then
print("fucking it")
local parent = self.FakeParent
if !IsValid(parent) then return end
--parent:FrameAdvance(FrameTime())
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)
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
print("fucking it")
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
table.insert(self.ModelBoneList, target:GetBoneName(i))
i = i + 1
end
end
local animation_progress = (CurTime() - self.FakeUpStart) / (self.FakeUpEnd - self.FakeUpStart)
--[[util.TraceLine({
start = parent:GetPos() + Vector(0, 128, 0),
endpos = parent:GetPos(),
collisiongroup = COLLISION_GROUP_NPC
})]]
for i,v in pairs(fakeup_bone_down_names) do
local object = target:GetPhysicsObjectNum(target:TranslateBoneToPhysBone(target:LookupBone(v)))
object:SetMass(0.5)
end
for i,v in pairs(fakeup_bone_names) do
local object = target:GetPhysicsObjectNum(target:TranslateBoneToPhysBone(target:LookupBone(v)))
local parent_bone = parent:LookupBone(v)
if parent_bone == -1 then continue end
local parent_bone_matrix = parent:GetBoneMatrix(parent_bone)
local parent_bone_pos, parent_bone_angle = parent_bone_matrix:GetTranslation(), parent_bone_matrix:GetAngles()
parent_bone_angle.y = parent_bone_angle.y + 90
local shadow_data = {
secondstoarrive = 0.01,
pos = LerpVector(animation_progress, object:GetPos(), parent_bone_pos),
angle = LerpAngle(animation_progress, object:GetAngles(), 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()
object:EnableGravity(false)
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:SetPos(target:GetPos())
--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
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()
self.bullseye: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()
self.bullseye:Remove()
return false -- Cut the bullshit
elseif (target.organism.consciousness <= 0.5) or ((target.organism.lleg >= 0.85) and (target.organism.rleg >= 0.85)) then
target.StartDie = cur_time
return false
end
if ((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())
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
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)
ent:SetRenderMode(RENDERMODE_NONE)
timer.Simple(self.FakeUpTime, function()
ent:SetNotSolid(false)
ent:SetNPCState(NPC_STATE_IDLE)
ent:SetRenderMode(RENDERMODE_NORMAL)
target:Remove()
self:Remove()
end)
--[[]
local parent = self.FakeParent
if !self.ModelBoneList then
self.ModelBoneList = {}
local i = 0
while i < target:GetBoneCount() do
table.insert(self.ModelBoneList, target:GetBoneName(i))
i = i + 1
end
end
for i,v in pairs(self.ModelBoneList) do
print(v)
print("I'm here")
local bone = target:TranslateBoneToPhysBone(target:LookupBone(v))
local object = target:GetPhysicsObjectNum(bone)
if !IsValid(object) then i = i + 1; continue end
print("I'm getting to object")
local parent_bone = parent:TranslateBoneToPhysBone(parent:LookupBone(v))
if parent_bone == nil then i = i + 1; continue end
print("I'm getting to parent object")
object:Wake()
local shadow_data = {
secondstoarrive = 0.01,
pos = LerpVector(0.1, object:GetPos(), parent:GetBonePosition(parent_bone)),
angle = LerpAngle(0.1, object:GetAngles(), parent:GetBoneMatrix(parent_bone):GetAngles()),
maxspeed = 5000,
maxangular = 5000,
maxspeeddamp = 2000,
maxangularspeeddamp = 2000,
}
object:ComputeShadowControl(shadow_data)
--object:SetPos(LerpVector(0.2, object:GetPos(), parent:GetBonePosition(parent_bone)))
--object:SetAngles(LerpAngle(0.2, object:GetAngles(), parent:GetBoneMatrix(parent_bone):GetAngles()))
--object:EnableMotion(false)
--object:SetVelocity(Vector())
object:EnableGravity(false)
i = i + 1
end]]
self.bullseye:Remove()
return false
end
end
-- Getting up
-- Don't need to check for consciousness and the like because we've done it already above
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) 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.bullseye) then self.bullseye:Remove() end
end
--[[-------------------------------------------------------------------------
Module registration (if it is part of a module system)
---------------------------------------------------------------------------]]
-- Registration Example:
-- falling_legs_manager:RegisterModule("FedhoriaFallingLegs", MODULE)