diff --git a/README.md b/README.md index e69de29..7d52480 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,19 @@ +# 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. + +Some ideas: + +- If an enemy breaks a leg, slow them down. +- If an enemy breaks an arm, make their aim worse. +- Break both legs? Hope you like writhing on the ground for the rest of your life! +- 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. +- Enemies should target downed enemies of hostile factions. + +Since theoretically the NPCs use the same health system that players use, these shouldn't be too hard to implement. + +More optimistic ideas: + +- NPCs will back out of combat when injured. +- Friendly NPCs can heal each other. +- NPCs can heal themselves to a limited degree. \ No newline at end of file diff --git a/addon.txt b/addon.txt new file mode 100644 index 0000000..99f75ce --- /dev/null +++ b/addon.txt @@ -0,0 +1,11 @@ +"AddonInfo" +{ + "name" "Z-City NPC Integration" + "version" "1.0" + "up_date" "24th August 2026" + "author_name" "owouw.us" + "author_email" "me@owouw.us" + "author_url" "owouw.us" + "info" "owouw.us" + "override" "0" +} diff --git a/lua/README.md b/lua/README.md new file mode 100644 index 0000000..e69de29 diff --git a/lua/autorun/client/fedh_menu.lua b/lua/autorun/client/fedh_menu.lua new file mode 100644 index 0000000..785109f --- /dev/null +++ b/lua/autorun/client/fedh_menu.lua @@ -0,0 +1,48 @@ +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 \ No newline at end of file diff --git a/lua/autorun/server/fedh_init.lua b/lua/autorun/server/fedh_init.lua new file mode 100644 index 0000000..a493209 --- /dev/null +++ b/lua/autorun/server/fedh_init.lua @@ -0,0 +1 @@ +include("fedhoria.lua") \ No newline at end of file diff --git a/lua/entities/active_ragdoll_controller.lua b/lua/entities/active_ragdoll_controller.lua new file mode 100644 index 0000000..67cb16e --- /dev/null +++ b/lua/entities/active_ragdoll_controller.lua @@ -0,0 +1,325 @@ +AddCSLuaFile() + +ENT.Type = "anim" +ENT.Base = "base_anim" + +ENT.AutomaticFrameAdvance = true + +function ENT:SetupDataTables() + self:NetworkVar("Entity", 0, "Target") +end + +function ENT:SetModule(mod) + self.Module = mod +end + +function ENT:SetInitParams(...) + self.InitParams = {...} +end + +local FORCE_SCALE = 3 + +local math_atan2 = math.atan2 +local math_min = math.min +local math_max = math.max +local math_abs = math.abs + +local phys_settings = +{ + ["ValveBiped.Bip01_Pelvis"] = {mass = 12.741364, inertia = Vector(0.80, 0.97, 0.96)}, + + ["ValveBiped.Bip01_Spine2"] = {mass = 24.297474, inertia = Vector(2.15, 3.33, 3.12)}, + + ["ValveBiped.Bip01_R_UpperArm"] = {mass = 3.529606, inertia = Vector(0.06, 0.28, 0.28)}, + + ["ValveBiped.Bip01_L_UpperArm"] = {mass = 3.466939, inertia = Vector(0.06, 0.27, 0.27)}, + + ["ValveBiped.Bip01_L_Forearm"] = {mass = 1.801132, inertia = Vector(0.02, 0.10, 0.10)}, + + ["ValveBiped.Bip01_L_Hand"] = {mass = 1.075074, inertia = Vector(0.02, 0.02, 0.02)}, + + ["ValveBiped.Bip01_R_Forearm"] = {mass = 1.781718, inertia = Vector(0.02, 0.10, 0.10)}, + + ["ValveBiped.Bip01_R_Hand"] = {mass = 1.018670, inertia = Vector(0.02, 0.02, 0.02)}, + + ["ValveBiped.Bip01_R_Thigh"] = {mass = 10.187500, inertia = Vector(0.35, 1.74, 1.76)}, + + ["ValveBiped.Bip01_R_Calf"] = {mass = 4.996145, inertia = Vector(0.10, 0.63, 0.64)}, + + ["ValveBiped.Bip01_Head1"] = {mass = 5.163157, inertia = Vector(0.19, 0.21, 0.27)}, + + ["ValveBiped.Bip01_L_Thigh"] = {mass = 10.188610, inertia = Vector(0.35, 1.74, 1.76)}, + + ["ValveBiped.Bip01_L_Calf"] = {mass = 4.995875, inertia = Vector(0.10, 0.63, 0.64)}, + + ["ValveBiped.Bip01_L_Foot"] = {mass = 2.378366, inertia = Vector(0.05, 0.13, 0.13)}, + + ["ValveBiped.Bip01_R_Foot"] = {mass = 2.378366, inertia = Vector(0.05, 0.13, 0.13)} +} + +function ENT:PhysAlignAngles(phys, ang) + local avel = Vector(0, 0, 0) + + local ang1 = phys:GetAngles() + + local forward1 = ang1:Forward() + local forward2 = ang:Forward() + local fd = forward1:Dot(forward2) + + local right1 = ang1:Right() + local right2 = ang:Right() + local rd = right1:Dot(right2) + + local up1 = ang1:Up() + local up2 = ang:Up() + local ud = up1:Dot(up2) + + local pitchvel = math.asin(forward1:Dot(up2)) * 180 / math.pi + local yawvel = math.asin(forward1:Dot(right2)) * 180 / math.pi + local rollvel = math.asin(right1:Dot(up2)) * 180 / math.pi + + avel.y = avel.y + pitchvel + avel.z = avel.z + yawvel + avel.x = avel.x + rollvel + + return avel +end + +function ENT:SetMaxAngVel(val) + self.max_ang_vel = val + self.max_ang_vel_sqr = val * val +end + +function ENT:SetBoneList(list) + self.bone_list = list +end + +function ENT:Initialize() + if !self.Module then return end + + self:SetModel(self.Module.Model) + + for key, value in pairs(self.Module) do + if !self[key] then + self[key] = value + end + end + + self:StartMotionController() + + local target = self:GetTarget() + + if !target._FixedSettings then + target._FixedSettings = true + for bone_name, info in pairs(phys_settings) do + local bone = target:LookupBone(bone_name) + if bone then + local phys_bone = target:TranslateBoneToPhysBone(bone) + local phys = target:GetPhysicsObjectNum(phys_bone) + if IsValid(phys) then + phys:SetInertia(info.inertia) + phys:SetMass(info.mass) + end + end + end + end + + self.bone_translate = {} + self.bone_parent = {} + + local is_match = false + + for _, bone_name in pairs(self.Module.BoneList) do + local bone = target:LookupBone(bone_name) + if bone then + is_match = true + local phys_bone = target:TranslateBoneToPhysBone(bone) + local phys = target:GetPhysicsObjectNum(phys_bone) + if IsValid(phys) then + bone = target:TranslatePhysBoneToBone(phys_bone) + + self.root_bone = self.root_bone or bone + self.root_phys_bone = self.root_phys_bone or phys_bone + + self:AddToMotionController(phys) + self.bone_translate[bone] = self:LookupBone(bone_name) + + local bone_parent = target:GetBoneParent(bone) + bone_parent = target:TranslateBoneToPhysBone(bone_parent) + bone_parent = target:TranslatePhysBoneToBone(bone_parent) + local bone_name_parent = target:GetBoneName(bone_parent) + + self.bone_parent[bone] = bone_parent + + self.bone_translate[bone_parent] = self:LookupBone(bone_name_parent) + end + end + end + + if !is_match then + self:Remove() + return + end + + self.bone_head = self:LookupBone("ValveBiped.Bip01_Head1") + + if self.bone_head then + self.phys_bone_head = self:TranslatePhysBoneToBone(self.bone_head) + end + + self.Created = CurTime() + + target:DeleteOnRemove(self) + + self:SetMaxAngVel(400) --default + + if self.Module.Init then + if self.InitParams then + self.Module.Init(self, unpack(self.InitParams)) + else + self.Module.Init(self) + end + end + + if self.Module.PhysicsCollide then + self.PCCB = target:AddCallback("PhysicsCollide", function(ent, data) + self.Module.PhysicsCollide(self, ent, data) + end) + end +end + +function ENT:Think() + if !self.Module then return end + if self.Module.Think then + self.Module.Think(self) + end +end + +function ENT:OnRemove() + if !self.Module then return end + if self.Module.OnRemove then + self.Module.OnRemove(self) + end + local target = self:GetTarget() + if (self.PCCB and IsValid(target)) then + target:RemoveCallback("PhysicsCollide", self.PCCB) + end +end + +function ENT:UpdateTransmitState() + return TRANSMIT_NEVER + --return TRANSMIT_PVS +end + +function ENT:PhysicsSimulate(phys, dt) + local factor = 1 + + if self.Module.PhysicsSimulate then + local b, f = self.Module.PhysicsSimulate(self, phys, dt) + if b == false then + return + end + factor = f or factor + end + + local target = self:GetTarget() + + local phys_bone = phys:GetID() + + if target.GS2IsDismembered then + --decapitatied? + if (self.phys_bone_head and target:GS2IsDismembered(self.phys_bone_head)) then + self:Remove() + return + end + local phys_bone = phys_bone + local dis = false + local bone = target:TranslatePhysBoneToBone(phys_bone) + repeat + phys_bone = target:TranslateBoneToPhysBone(bone) + if target:GS2IsDismembered(phys_bone) then + dis = true + break + end + bone = target:GetBoneParent(bone) + until (phys_bone == 1 or phys_bone == 0) + + if (phys_bone == 0) then + dis = target:GS2IsDismembered(1) + if dis then + self.StartDie = self.StartDie or CurTime() + end + elseif (phys_bone != 1) then + dis = true + end + + if dis then + self:RemoveFromMotionController(phys) + return + end + end + + self:FrameAdvance() + + local bone = target:TranslatePhysBoneToBone(phys_bone) + local bone_parent = self.bone_parent[bone] + + local self_bone = self.bone_translate[bone] + local self_bone_parent = self.bone_translate[bone_parent] + + if (self_bone and self_bone_parent) then + local _, bone_ang = self:GetBonePosition(self_bone) + + local _, bone_ang_parent = self:GetBonePosition(self_bone_parent) + + local _, lang = WorldToLocal(vector_origin, bone_ang, vector_origin, bone_ang_parent) + + local _, target_ang = LocalToWorld(vector_origin, lang, target:GetBonePosition(bone_parent)) + + --TODO: clamp to ragdoll joint limits to avoid spazz + + local ang_vel = self:PhysAlignAngles(phys, target_ang) + + ang_vel:Mul(6 * factor) + + ang_vel:Sub(phys:GetAngleVelocity()) + + if self.max_ang_vel then + local len_sqr = ang_vel:LengthSqr() + + if (len_sqr > self.max_ang_vel_sqr) then + ang_vel:Normalize() + ang_vel:Mul(self.max_ang_vel) + end + end + + phys:AddAngleVelocity(ang_vel) + + --maybe this can work some day but that day is not today + --[[local phys_bone_parent = target:TranslateBoneToPhysBone(self_bone_parent) + local phys_parent = target:GetPhysicsObjectNum(phys_bone_parent) + + local _, lang = WorldToLocal(vector_origin, bone_ang_parent, vector_origin, bone_ang) + + local _, target_ang = LocalToWorld(vector_origin, lang, target:GetBonePosition(bone)) + + --TODO: clamp to ragdoll joint limits to avoid spazz + + local ang_vel = self:PhysAlignAngles(phys_parent, target_ang) + + ang_vel:Mul(20 * factor) + + ang_vel:Sub(phys_parent:GetAngleVelocity()) + + if self.max_ang_vel then + local len_sqr = ang_vel:LengthSqr() + + if (len_sqr > self.max_ang_vel_sqr) then + ang_vel:Normalize() + ang_vel:Mul(self.max_ang_vel) + end + end + + phys_parent:AddAngleVelocity(ang_vel)]] + end +end \ No newline at end of file diff --git a/lua/fedhoria.lua b/lua/fedhoria.lua new file mode 100644 index 0000000..b026f5f --- /dev/null +++ b/lua/fedhoria.lua @@ -0,0 +1,219 @@ +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) \ No newline at end of file diff --git a/lua/fedhoria/modules.lua b/lua/fedhoria/modules.lua new file mode 100644 index 0000000..7137589 --- /dev/null +++ b/lua/fedhoria/modules.lua @@ -0,0 +1,117 @@ +fedhoria = {} + +local modules = {} + +function fedhoria.GetModule(name) + if modules[name] then + return modules[name] + end + local path = "fedhoria/modules/"..name..".lua" + if !file.Exists(path, "LUA") then + print("fedhoria.GetModule failed, couldn't find module '"..name.."'") + return + end + local MODULE_old = MODULE + MODULE = {} + include(path) + modules[name] = MODULE + MODULE = MODULE_old + + return modules[name] +end + +function fedhoria.GetModuleList() + return modules +end + +function fedhoria.StartModule(ent, name, ...) + if (!modules[name] and !fedhoria.GetModule(name)) then + return false + end + local contr = ents.Create("active_ragdoll_controller") + contr:SetTarget(ent) + contr:SetModule(modules[name]) + contr:SetInitParams(...) + contr:Spawn() + + return contr +end + +--preload modules +for _, file_name in pairs(file.Find("fedhoria/modules/*.lua", "LUA")) do + fedhoria.GetModule(file_name:sub(1, -5)) +end + +local ENTITY = FindMetaTable("Entity") + +--TODO: make this actually take mass into account +function ENTITY:GetMassCenter() + local center = Vector(0, 0, 0) + local mass = 0 + local count = self:GetPhysicsObjectCount() + for phys_bone = 0, count - 1 do + local phys = self:GetPhysicsObjectNum(phys_bone) + local m = phys:GetMass() + mass = mass * m + --center:Add(phys:LocalToWorld(phys:GetMassCenter()) * m) + center:Add(phys:LocalToWorld(phys:GetMassCenter())) + end + --center:Div(mass) + center:Div(count) + return center +end + +local PCOLLIDE_CACHE = {} + +local vec_max = Vector(1, 1, 1) * 4 +local vec_min = -vec_max + +function ENTITY:GetClosestPhysBone(pos) + local mdl = self:GetModel() + local collides = PCOLLIDE_CACHE[mdl] + if !collides then + PCOLLIDE_CACHE[mdl] = CreatePhysCollidesFromModel(mdl) + collides = PCOLLIDE_CACHE[mdl] + end + + if !collides then return end + + local closest_bone + local dist = math.huge + + for phys_bone = 0, self:GetPhysicsObjectCount() - 1 do + local phys = self:GetPhysicsObjectNum(phys_bone) + local collide = collides[phys_bone + 1] + if IsValid(collide) then + local phys_pos = phys:GetPos() + local phys_ang = phys:GetAngles() + local lpos = phys:WorldToLocal(pos) + local hitpos, _, d = collide:TraceBox(phys_pos, phys_ang, pos, pos, vec_min, vec_max) + if hitpos then + if (d < dist) then + dist = d + closest_bone = phys_bone + end + end + end + end + + return closest_bone +end + +local PHYS = FindMetaTable("PhysObj") + +function PHYS:GetID() + local ent = self:GetEntity() + for phys_bone = 0, ent:GetPhysicsObjectCount() - 1 do + if (ent:GetPhysicsObjectNum(phys_bone) == self) then + return phys_bone + end + end + return -1 +end + +function PHYS:GetAABBCenter() + local min, max = self:GetAABB() + return (min + max) / 2 +end \ No newline at end of file diff --git a/lua/fedhoria/modules/example.lua b/lua/fedhoria/modules/example.lua new file mode 100644 index 0000000..461a6f9 --- /dev/null +++ b/lua/fedhoria/modules/example.lua @@ -0,0 +1,35 @@ +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" +} + +function MODULE:Init() + +end + +function MODULE:Think() + +end + +function MODULE:PhysicsCollide(ent, data) + +end + +function MODULE:PhysicsSimulate(phys, dt) + +end \ No newline at end of file diff --git a/lua/fedhoria/modules/falling_legs.lua b/lua/fedhoria/modules/falling_legs.lua new file mode 100644 index 0000000..8c525cd --- /dev/null +++ b/lua/fedhoria/modules/falling_legs.lua @@ -0,0 +1,331 @@ +--[[------------------------------------------------------------------------- + Скрипт для управления поведением падающих частей тела 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) + diff --git a/lua/fedhoria/modules/falling_torso.lua b/lua/fedhoria/modules/falling_torso.lua new file mode 100644 index 0000000..dd002ff --- /dev/null +++ b/lua/fedhoria/modules/falling_torso.lua @@ -0,0 +1,283 @@ +--здесь был BRAT. + +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_L_Upperarm", + "ValveBiped.Bip01_L_Forearm", +} + +local math_Clamp = math.Clamp +local math_Rand = math.Rand +local math_random = math.random +local table_HasValue = table.HasValue + +local hand_offset = Vector(2, 0, 0) + +--twtiching +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 ignore_bone_indices_twitch = {0} -- игнор нахуй + +--перекат ебаный +local cv_anim_roll_enabled = CreateConVar("fedhoria_falling_anim_roll_enabled", "1", {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Включить анимацию 'idleonfire' при ударе о землю") +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}, "Продолжительность принудительной анимации 'idleonfire' (сек)") +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}, "Скорость воспроизведения анимации 'idleonfire'") + +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 + + local bone_count = phys:GetBoneCount() + if bone_count <= 1 then return end -- нет костей для дергания (кроме, возможно, корневой) + + local phys_bone = nil + local attempts = 0 + local bone_index = -1 -- есл нету кости то иди нахуй + + -- нахуй игнорируем кости, которые не нужны для дергания + repeat + bone_index = math_random(0, bone_count - 1) + if not table_HasValue(ignore_bone_indices_twitch, bone_index) then + phys_bone = phys:GetBone(bone_index) + end + attempts = attempts + 1 + until IsValid(phys_bone) or attempts > 20 -- мало попытов но если не нашли то идем нахуй + + + 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", bone_index) + -- else + -- print("[Fedhoria Twitch] Could not find a valid bone to twitch after", attempts, "attempts.") + end + + -- следующее дергание + self:ScheduleNextTwitch() +end + +-- следующая дрочка +function MODULE:ScheduleNextTwitch() + if not IsValid(self) then return end + if not cv_twitch_enabled:GetBool() then return end + + local timer_name = "Fedhoria_FallingTorso_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) + -- print("[Fedhoria Twitch] Scheduled next twitch in", delay, "seconds.") -- ебаная отладка +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("idleonfire") + if seq then + self:ResetSequence(seq) + self:SetPlaybackRate(cv_anim_roll_playback_rate:GetFloat()) + else + print("[Fedhoria AnimRoll ERROR] Sequence 'idleonfire' not found!") + self.AnimationRollEndTime = 0 + return + end + + self.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 +end + +function MODULE:Init() + local seq = self:LookupSequence("idleonfire") + if seq then self:ResetSequence(seq) end + self:SetPlaybackRate(1) + self.LastCollideTime = 0 + self.LastGroundCollideTime = 0 + self.StartDie = nil + self.AnimationRollEndTime = 0 + + -- идте нахуй с этой хуйней + self:ScheduleNextTwitch() +end + +function MODULE:Think() + +end + +function MODULE:PhysicsCollide(ent, data) + + if data.HitEntity == self then return end + + -- if data.HitEntity == self:GetTarget() 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 impact detected. Speed:", impact_speed, "Threshold:", cv_anim_roll_impact_threshold:GetFloat()) -- Debug + -- перекат ебаный + 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 -- минимальная задержка перед идте нахуй + + -- print("[Fedhoria AnimRoll] Ground Impact Threshold met. Speed:", impact_speed) -- Debug + self:StartAnimationRoll() + end + self.LastGroundCollideTime = cur_time + -- else -- Debug для других столкновений + -- print("[Fedhoria Collide] Non-ground collision. Normal Z:", data.HitNormal.z) + 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 + -- print("[Fedhoria Sim] In Animation Roll. Time left:", self.AnimationRollEndTime - cur_time) -- Debug + -- self.StartDie = nil -- Сбрасываем таймер смерти + + return true -- стандартная физика пошла нахуй, у меня по ней 2 + -- return false -- если физика нахуй идет + end + + -- --- логика идет нахуй + + -- логика для таймера + local f = 1 -- сила есть ума не надо (по умолчанию 1) + if self.StartDie then + f = math_Clamp(1 - (cur_time - self.StartDie) / die_time:GetFloat(), 0, 1) + end + + print(f) + + -- ебаная логика для regmod + 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 + -- print("[Fedhoria Sim] Removing entity (f <= 0)") -- Debug + self:Remove() + return false -- Прекращаем хуйню + end + + local phys_bone_id = phys:GetID() -- айди кости + + -- ыыыуыыыыыыыы + if (phys_bone_id == 0) then + -- логика для блядской воды (если торс попал в воду) + if target:WaterLevel() > 0 then + -- print("[Fedhoria Sim] Target in water.") -- Debug + self.AnimationRollEndTime = 0 -- нахуй анимация пошла + local seq_choked = self:LookupSequence("Choked_Barnacle") + if seq_choked then self:ResetSequence(seq_choked) end + -- возможн нужно добавить таймер на удаление или другую логику для воды + -- timer.Simple(0.5, function() if IsValid(self) then self:Remove() end end) -- пример опиздюливания через 5 сек + return false + end + + -- логика для анимации переката + local vel = phys:GetVelocity() + local pbr = math_Clamp(vel.z / -600, 0.5, 1.5) + -- падения и ебаная скорость + -- если мы не в.. а впрочем иди нахуй + if self:GetSequence() ~= self:LookupSequence("idleonfire") or self:GetPlaybackRate() ~= pbr then + local seq_idle = self:LookupSequence("idleonfire") + if seq_idle then + -- print("[Fedhoria Sim] Resetting sequence to idleonfire, pbr:", pbr) -- Debug + self:ResetSequence(seq_idle) + self:SetPlaybackRate(pbr) + end + end + + -- логика таймера ебаного умирания + local pos = phys:GetPos() + self.last_pos = self.last_pos or pos + 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 -- uменьшил порог неподвижности + self.StartDie = self.StartDie or cur_time -- идите нахуй + -- print("[Fedhoria Sim] Torso seems stationary. StartDie:", self.StartDie) -- Debug + else + -- self.StartDie = nil -- нахуй таймер + -- print("[Fedhoria Sim] Torso moved. Resetting StartDie.") -- Debug + 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_FallingTorso_Twitch_" .. self:EntIndex() + timer.Remove(timer_name) + -- print("[Fedhoria] OnRemove called for", self:EntIndex(), "Timer removed:", timer_name) -- Debug +end \ No newline at end of file diff --git a/lua/fedhoria/modules/stumble_legs.lua b/lua/fedhoria/modules/stumble_legs.lua new file mode 100644 index 0000000..dd5f7d1 --- /dev/null +++ b/lua/fedhoria/modules/stumble_legs.lua @@ -0,0 +1,554 @@ +MODULE.Model = "models/barney.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" +} + +local stumble_time = CreateConVar("fedhoria_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_time = CreateConVar("fedhoria_woundgrab_time", 5, bit.bor(FCVAR_ARCHIVE, FCVAR_REPLICATED)) + +local math_atan2 = math.atan2 +local math_pi = math.pi +local math_Clamp = math.Clamp +local math_max = math.max +local math_min = math.min + +local constant = 10000 + +local function CreateSpring(phys1, phys2) + if (!IsValid(phys1) or !IsValid(phys2)) then return NULL end + local str_axis = tostring(phys2:LocalToWorld(phys2:GetAABBCenter())) + + local const = ents.Create("phys_spring") + const:SetPos(phys1:LocalToWorld(phys1:GetAABBCenter())) + const:SetKeyValue("springaxis", str_axis) + const:SetKeyValue("constant", constant) + const:SetKeyValue("damping", 0.1) + const:SetKeyValue("relativedamping", 0.1) + const:SetPhysConstraintObjects(phys1, phys2) + const:Spawn() + const:Activate() + --const:Fire("SetSpringLength", 0, 0) + + return const +end + +local allowed_touch = +{ + "ValveBiped.Bip01_R_Calf", + "ValveBiped.Bip01_R_Foot", + "ValveBiped.Bip01_L_Calf", + "ValveBiped.Bip01_L_Foot" +} + +local hand_offset = Vector(2, 0, 0) + +function MODULE:Init(phys_bone, lpos) + self.seq_walk = self:LookupSequence("walk_all") + self.seq_walk_speed = self:GetSequenceMoveDist(self.seq_walk) * self:SequenceDuration(self.seq_walk) + + self.seq_run = self:LookupSequence("run_all") + self.seq_run_speed = self:GetSequenceMoveDist(self.seq_run) * self:SequenceDuration(self.seq_run) + + self.seq_sprint = self:LookupSequence("sprint_all") + self.seq_sprint_speed = self:GetSequenceMoveDist(self.seq_sprint) * self:SequenceDuration(self.seq_sprint) + + self:ResetSequence(self:LookupSequence("idle01")) + + local target = self:GetTarget() + + for _, bone_name in pairs(self.BoneList) do + local bone = self:LookupBone(bone_name) + if bone then + local phys_bone = target:TranslateBoneToPhysBone(bone) + local phys = target:GetPhysicsObjectNum(phys_bone) + --phys:AddGameFlag(FVPHYSICS_NO_SELF_COLLISIONS) + end + end + + self.bone_lthigh = self:LookupBone("ValveBiped.Bip01_L_Thigh") + self.bone_rthigh = self:LookupBone("ValveBiped.Bip01_R_Thigh") + + if (self.bone_lthigh and self.bone_rthigh) then + self.phys_bone_lthigh = self:TranslateBoneToPhysBone(self.bone_lthigh) + self.phys_bone_rthigh = self:TranslateBoneToPhysBone(self.bone_rthigh) + end + + self.bone_lcalf = self:LookupBone("ValveBiped.Bip01_L_Calf") + self.bone_rcalf = self:LookupBone("ValveBiped.Bip01_R_Calf") + + if (self.bone_lcalf and self.bone_rcalf) then + self.phys_bone_lcalf = self:TranslateBoneToPhysBone(self.bone_lcalf) + self.phys_bone_rcalf = self:TranslateBoneToPhysBone(self.bone_rcalf) + end + + self.bone_lfoot = self:LookupBone("ValveBiped.Bip01_L_Foot") + self.bone_rfoot = self:LookupBone("ValveBiped.Bip01_R_Foot") + + if (self.bone_lfoot and self.bone_rfoot) then + self.phys_bone_lfoot = self:TranslateBoneToPhysBone(self.bone_lfoot) + self.phys_bone_rfoot = self:TranslateBoneToPhysBone(self.bone_rfoot) + end + + self.bone_lhand = self:LookupBone("ValveBiped.Bip01_L_Hand") + self.bone_rhand = self:LookupBone("ValveBiped.Bip01_R_Hand") + + if (self.bone_lhand and self.bone_rhand) then + self.phys_bone_lhand = self:TranslateBoneToPhysBone(self.bone_lhand) + self.phys_bone_rhand = self:TranslateBoneToPhysBone(self.bone_rhand) + end + + self.phys_allowed_touch = {} + + for _, bone_name in pairs(allowed_touch) do + local bone = self:LookupBone(bone_name) + if bone then + local phys_bone = self:TranslateBoneToPhysBone(bone) + self.phys_allowed_touch[phys_bone] = true + end + end + + self.bone_pelvis = self:LookupBone("ValveBiped.Bip01_Pelvis") + self.bone_torso = self:LookupBone("ValveBiped.Bip01_Spine2") + + if (self.bone_pelvis and self.bone_torso) then + self.phys_bone_pelvis = target:TranslateBoneToPhysBone(self.bone_pelvis) + self.phys_bone_torso = target:TranslateBoneToPhysBone(self.bone_torso) + + --constraint.Weld(target, target, phys_bone_pelvis, phys_bone_torso) + end + + local vel = target:GetVelocity():Length2D() + + if phys_bone then + self.Springs = {} + --Grab wound + local phys = target:GetPhysicsObjectNum(phys_bone) + local dmgpos = phys:LocalToWorld(lpos) + if (IsValid(phys) and phys_bone != self.phys_bone_lhand and phys_bone != self.phys_bone_rhand) then + local phys_lhand = target:GetPhysicsObjectNum(self.phys_bone_lhand) + local phys_rhand = target:GetPhysicsObjectNum(self.phys_bone_rhand) + + local str_axis = tostring(dmgpos) + + if (IsValid(phys_lhand) and math.random() < grab_chance:GetFloat()) then + local const = ents.Create("phys_spring") + const:SetPos(phys_lhand:LocalToWorld(hand_offset)) + const:SetKeyValue("springaxis", str_axis) + const:SetKeyValue("constant", 300) + const:SetKeyValue("damping", 0.1) + const:SetKeyValue("relativedamping", 0.1) + const:SetPhysConstraintObjects(phys_lhand, phys) + const:Spawn() + const:Activate() + const:Fire("SetSpringLength", 0, 0) + + SafeRemoveEntityDelayed(const, grab_time:GetFloat()) + + table.insert(self.Springs, const) + end + + if (IsValid(phys_rhand) and math.random() < grab_chance:GetFloat()) then + local const = ents.Create("phys_spring") + const:SetPos(phys_rhand:LocalToWorld(hand_offset)) + const:SetKeyValue("springaxis", str_axis) + const:SetKeyValue("constant", 300) + const:SetKeyValue("damping", 0.1) + const:SetKeyValue("relativedamping", 0.1) + const:SetPhysConstraintObjects(phys_rhand, phys) + const:Spawn() + const:Activate() + const:Fire("SetSpringLength", 0, 0) + + SafeRemoveEntityDelayed(const, grab_time:GetFloat()) + + table.insert(self.Springs, const) + end + end + end + + --[[if (!target.GS2IsDismembered and vel < self.seq_walk_speed) then + --self:Remove() + self.Springs = {} + if (self.phys_bone_lthigh and self.phys_bone_lcalf) then + local phys_lthigh = target:GetPhysicsObjectNum(self.phys_bone_lthigh) + local phys_lcalf = target:GetPhysicsObjectNum(self.phys_bone_lcalf) + + table.insert(self.Springs, CreateSpring(phys_lthigh, phys_lcalf)) + end + + if (self.phys_bone_rthigh and self.phys_bone_rcalf) then + local phys_rthigh = target:GetPhysicsObjectNum(self.phys_bone_rthigh) + local phys_rcalf = target:GetPhysicsObjectNum(self.phys_bone_rcalf) + + table.insert(self.Springs, CreateSpring(phys_rthigh, phys_rcalf)) + end + + if (self.phys_bone_pelvis and self.phys_bone_torso) then + local phys_pelvis = target:GetPhysicsObjectNum(self.phys_bone_pelvis) + local phys_torso = target:GetPhysicsObjectNum(self.phys_bone_torso) + + --CreateSpring(phys_pelvis, phys_torso) + end + + if (self.phys_bone_pelvis and self.phys_bone_lthigh) then + local phys_pelvis = target:GetPhysicsObjectNum(self.phys_bone_pelvis) + local phys_lthigh = target:GetPhysicsObjectNum(self.phys_bone_lthigh) + + --CreateSpring(phys_pelvis, phys_lthigh) + end + + if (self.phys_bone_pelvis and self.phys_bone_rthigh) then + local phys_pelvis = target:GetPhysicsObjectNum(self.phys_bone_pelvis) + local phys_rthigh = target:GetPhysicsObjectNum(self.phys_bone_rthigh) + + --CreateSpring(phys_pelvis, phys_rthigh) + end + end]] +end + +local function DoFallingAnim(ent) + local mod1 = fedhoria.StartModule(ent, "falling_legs") + local mod2 = fedhoria.StartModule(ent, "falling_torso") +end + +function MODULE:OnRemove() + local target = self:GetTarget() + if !IsValid(target) then return end + + for _, bone_name in pairs(self.BoneList) do + local bone = target:LookupBone(bone_name) + if bone then + local phys_bone = target:TranslateBoneToPhysBone(bone) + local phys = target:GetPhysicsObjectNum(phys_bone) + if IsValid(phys) then + phys:ClearGameFlag(FVPHYSICS_NO_SELF_COLLISIONS) + end + end + end + + --[[if self.Springs then + for _, spring in pairs(self.Springs) do + SafeRemoveEntityDelayed(spring, 1) + end + end]] + + DoFallingAnim(target) +end + +function MODULE:Think() + +end + +function MODULE:PhysicsCollide(ent, data) + --if (data.HitEntity == ent) then return end + if (data.HitEntity != game.GetWorld()) then return end + if (CurTime() - self.Created < 0.1) then return end + + local phys = data.PhysObject + local phys_bone = phys:GetID() + + if !self.phys_allowed_touch[phys_bone] then + SafeRemoveEntityDelayed(self, 0) + end +end + +local constant = 1 + +local tilt_ang = Angle(0, 0, 0) + +local pelvis_offset = Vector(0, 0, 35) +local torso_offset = Vector(0, 0, 50) + +local trace = {output={}} +local tr = trace.output + +function MODULE:PhysicsSimulate(phys, dt) + local phys_bone = phys:GetID() + + local target = self:GetTarget() + + if (self.Springs and target:GetNWInt("GS2DisMask", 0) != 0) then + for _, spring in pairs(self.Springs) do + SafeRemoveEntity(spring) + end + end + + local st = stumble_time:GetFloat() + + if (st <= 0) then + self:Remove() + return false + end + + 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 + self:Remove() + return false + end + + --helps reduce excessive twitching + if (phys:GetStress() > 100) then + return false + end + + if (phys_bone == self.phys_bone_torso) then + local phys_pelvis = target:GetPhysicsObjectNum(self.phys_bone_pelvis or 0) + local vel = phys:GetVelocity() + vel.z = 0 + vel:Normalize() + + local ang = phys_pelvis:GetAngles() + ang.p = 0 + ang.r = 0 + + local x = ang:Right():Dot(vel) + local y = ang:Forward():Dot(vel) + + local yaw = 180 * math_atan2(y, x) / math_pi + + if (yaw < 90 and yaw > -90) then + return false + end + end + + if (phys_bone == (self.phys_bone_pelvis or 0)) then + local phys_torso = target:GetPhysicsObjectNum(self.phys_bone_torso or 1) + --calculate animation settings here so its only done once per frame + local pos = phys:GetPos() + local ang = phys:GetAngles() + --local vel = phys:GetVelocity() + local vel = phys_torso:GetVelocity() + + local forward = ang:Forward() + local right = ang:Right() + local up = ang:Up() + + --try keeping balance a little bit better + tilt_ang.y = math_Clamp(90 + 180 * math_atan2(right.z, up.z) / math_pi, -50, 20) * f + + --too much balance? + tilt_ang.p = -ang.p * f + + self:ManipulateBoneAngles(self.bone_lthigh, tilt_ang) + self:ManipulateBoneAngles(self.bone_rthigh, tilt_ang) + + local speed = vel:Length2D() + + local slow = false + + if (speed > self.seq_sprint_speed) then + self:ResetSequence(self.seq_sprint) + elseif (speed > self.seq_run_speed) then + self:ResetSequence(self.seq_run) + elseif (speed > self.seq_walk_speed) then + self:ResetSequence(self.seq_walk) + else + slow = true + end + + if slow then + return false + --[[elseif (self.Springs) then + for _, spring in pairs(self.Springs) do + SafeRemoveEntity(spring) + end + self.Springs = nil]] + end + + --self:ResetSequence(self.seq_sprint) + + --local mass_center = target:GetMassCenter() + + self.last_pos = self.last_pos or pos + + local delta = (pos - self.last_pos) / dt + + self.last_pos = pos + + local pbr = vel:Length2D() / self:GetSequenceMoveDist(self:GetSequence()) + --local pbr = vel:Dot(ang:Right()) / self:GetSequenceMoveDist(self:GetSequence()) + + --pbr = pbr / self:SequenceDuration() + + vel.z = 0 + vel:Normalize() + + local ang = phys:GetAngles() + ang.p = 0 + ang.r = 0 + + local x = ang:Right():Dot(vel) + local y = ang:Forward():Dot(vel) + + self.pbr = math.min(3, Lerp(dt * constant, self.pbr or pbr, pbr)) + + local yaw = 180 * math_atan2(y, x) / math_pi + + local m = 1 + + if (yaw > 90) then + m = -1.5 + yaw = -90 + yaw - 90 + self:ResetSequence(self.seq_sprint) + elseif (yaw < -90) then + m = -1.5 + yaw = 90 - (yaw + 90) + self:ResetSequence(self.seq_sprint) + end + + self.move_yaw = Lerp(dt * constant, self.move_yaw or yaw, yaw) + + self:SetPoseParameter("move_yaw", self.move_yaw) + + self:SetPlaybackRate(m * self.pbr * self:SequenceDuration()) + + trace.filter = target + trace.start = pos + trace.endpos = pos - torso_offset + + util.TraceLine(trace) + + if tr.Hit then + if (x < 0) then + ang = phys:GetAngles() + local a = math.max(0, -ang:Right().z)^2 + + if (a < 0.2) then + --self:Remove() + --return false + end + + pos = phys_torso:GetPos() + + local d = (pos - tr.HitPos):Length2D() + + local div = self:GetSequenceMoveDist(self:GetSequence()) * self:SequenceDuration() + + local m = math_max(0, 1 - (d / div)) + + vel = phys:GetVelocity() + vel.x = 0 + vel.y = 0 + + m = m * a + + vel.z = vel.z * m + + phys:ApplyForceCenter(-vel * phys:GetMass() * f) + + vel = phys_torso:GetVelocity() + vel.x = 0 + vel.y = 0 + + vel.z = vel.z * m + + phys_torso:ApplyForceCenter(-vel * phys_torso:GetMass() * f) + return false + else + local l2d = delta:Length2D() + + local f = f * math_Clamp(l2d / self:GetSequenceMoveDist(self:GetSequence()), 0, 1) + + local targetZ = tr.HitPos.z + torso_offset.z * f + + local offsetZ = targetZ - phys_torso:GetPos().z + + if (offsetZ > 0) then + local force = offsetZ^2 - phys_torso:GetVelocity().z + force = force * 0.5 + force = math_min(force, 40) + phys_torso:ApplyForceCenter(Vector(0, 0, force) * phys_torso:GetMass() * f) + end + + local targetZ = tr.HitPos.z + pelvis_offset.z * f + + local offsetZ = targetZ - phys:GetPos().z + + if (offsetZ > 0) then + local force = offsetZ^2 - phys:GetVelocity().z + force = force * 0.5 + force = math_min(force, 40) + phys:ApplyForceCenter(Vector(0, 0, force) * phys:GetMass() * f) + end + end + else + self:Remove() + return false + end + + if (self.phys_bone_lhand) then + --[[local phys_lhand = target:GetPhysicsObjectNum(self.phys_bone_lhand) + + pos = phys_lhand:GetPos() + + local pos2 = target:GetBonePosition(target:LookupBone("ValveBiped.Bip01_L_Upperarm")) + + local offset = pos2 - pos + + offset:Add(phys:GetVelocity() * dt) + + offset.z = 0 + + offset = offset:GetNormal() * offset:Length()^2 + + offset:Sub(phys_lhand:GetVelocity()) + + phys_lhand:ApplyForceCenter(offset * phys_lhand:GetMass() * f)]] + end + + if (self.phys_bone_rhand) then + --[[local phys_rhand = target:GetPhysicsObjectNum(self.phys_bone_rhand) + + pos = phys_rhand:GetPos() + + local pos2 = target:GetBonePosition(target:LookupBone("ValveBiped.Bip01_R_Upperarm")) + + local offset = pos2 - pos + + offset:Add(phys:GetVelocity() * dt) + + offset.z = 0 + + offset = offset:GetNormal() * offset:Length()^2 + + offset:Sub(phys_rhand:GetVelocity()) + + phys_rhand:ApplyForceCenter(offset * phys_rhand:GetMass() * f)]] + end + + return false + end + + --[[if self.Springs then + return false + end]] + + return true, f +end \ No newline at end of file