330 lines
15 KiB
Lua
330 lines
15 KiB
Lua
--[[-------------------------------------------------------------------------
|
||
Скрипт для управления поведением падающих частей тела 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})
|
||
|
||
--[[-------------------------------------------------------------------------
|
||
Функция дергания
|
||
Применяет случайный импульс к одной из костей ног.
|
||
---------------------------------------------------------------------------]]
|
||
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)
|
||
|