From f03c018204196711557a6d64179e106e8784c7c0 Mon Sep 17 00:00:00 2001 From: AlexCube Date: Tue, 13 Oct 2026 16:26:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D1=83=D0=BA=D0=B8=20=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B8=D0=BC=D0=BE=D0=B3=D0=BE:=20=D0=BA=D1=83?= =?UTF-8?q?=D0=BB=D0=B0=D0=BA=20=D0=B2=20=D0=BC=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D1=83=D1=80=D0=BE=D0=BD=D0=B0=20=D0=B7=D0=BE=D0=BC?= =?UTF-8?q?=D0=B1=D0=B8,=20=D1=81=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D1=83=D0=B3=D0=BE=D0=BB=20=D1=80=D1=83=D0=BA=20?= =?UTF-8?q?45=C2=B0,=20=D1=81=D0=BA=D1=80=D1=8B=D1=82=D1=8B=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=8F=D1=87=D0=B5=D0=B9=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=8F=D1=81=D0=B0=20=D0=BF=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0=D0=B6=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удар рук откладывается на замеренную задержку урона зомби (ItemActionMelee.OnHoldingUpdate) минус ход кулака. - Руки сжимаются по экранным осям как при FOV 45 (как у персонажа), сдвиг - родной -1.7. - От первого лица прячется только windowToolbelt: полоски персонажа (та же группа окон) остаются. Co-Authored-By: Claude Opus 5.5 --- HarmonySrc/FpArms.cs | 91 +++++++++++++++++++++++++++++++++++++- HarmonySrc/Possession.cs | 20 +++++++-- HarmonySrc/SnakeControl.cs | 6 +-- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/HarmonySrc/FpArms.cs b/HarmonySrc/FpArms.cs index e3c2418..2e9aca9 100644 --- a/HarmonySrc/FpArms.cs +++ b/HarmonySrc/FpArms.cs @@ -49,7 +49,15 @@ namespace NecromancerTome }; /// Сдвиг рук от камеры (подбор necroarms pos). У персонажа игра опускает руки на 1.7 м: начало их /// скелета - у ног (vp_FPWeapon PositionOffset 0,-1.7,0.02 для SDCS). - public static Vector3 Offset = new Vector3(0f, -1.55f, 0.05f); // подобрано в игре 13.10 (у персонажа -1.7, но руки рисуются с FOV 45) + public static Vector3 Offset = new Vector3(0f, -1.7f, 0.02f); + /// Угол обзора, с которым игра рисует руки персонажа (vp_FPWeapon RenderingFieldOfView 45 у SDCS). Наши руки + /// рисует общая камера с широким углом - удар уходил выше и правее прицела (тест 13.10). Тот же вид даёт сжатие + /// рук по экранным осям: масштаб (k, k, 1) от камеры, k = tg(FOV камеры / 2) / tg(ArmsFov / 2) - каждый кадр + /// (FOV меняется при приближении). + public static float ArmsFov = 45f; + /// Через сколько после сигнала удара кулак рук доходит до цели (подбор necroarms impact). + public static float FpImpact = 0.15f; + static float _punchAt, _attackAt, _hitDelay = 0.45f; /// Родитель: true - сама камера, false - её UFPSRoot (как у персонажа). public static bool ParentCamera = true; @@ -266,6 +274,55 @@ namespace NecromancerTome } } + /// Удар зомби начат: урон пройдёт, когда его замах дойдёт до точки удара (ItemActionMelee.OnHoldingUpdate, + /// у разных зомби по-разному). Кулак рук запускаем с таким опозданием, чтобы пришёл в тот же миг (тест 13.10: + /// «сначала анимация, потом пауза и удар»); _hitDelay - замер прошлых ударов этого зомби. + public static void QueuePunch() + { + if (_rig == null) + { + return; + } + _attackAt = Time.time; + _punchAt = Time.time + Mathf.Max(0f, _hitDelay - FpImpact); + } + + /// Урон зомби прошёл - замер задержки для следующих ударов. + public static void OnZombieHit() + { + if (_attackAt <= 0f) + { + return; + } + float d = Time.time - _attackAt; + _attackAt = 0f; + if (d > 0.02f && d < 2f) + { + _hitDelay = d; + } + } + + /// Каждый кадр вида от первого лица (Possession.PlaceCamera): отложенный удар и сжатие под угол рук. + public static void Tick(Camera _cam) + { + if (_rig == null) + { + return; + } + if (_punchAt > 0f && Time.time >= _punchAt) + { + _punchAt = 0f; + Punch(); + } + float k = 1f; + if (_cam != null && ArmsFov > 1f) + { + k = Mathf.Tan(_cam.fieldOfView * 0.5f * Mathf.Deg2Rad) / Mathf.Tan(ArmsFov * 0.5f * Mathf.Deg2Rad); + } + _rig.transform.localScale = new Vector3(k, k, 1f); + _rig.transform.localPosition = new Vector3(Offset.x * k, Offset.y * k, Offset.z); + } + public static string Trigger(string _name) { Animator a = Anim; @@ -382,7 +439,7 @@ namespace NecromancerTome public class ConsoleCmdNecroArms : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necroarms" }; - public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set | trigger | tint r g b | albedo on|off | punch ]"; + public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set | trigger | tint r g b | albedo on|off | punch | fov <угол> | impact <с>]"; void Out(string _s) { @@ -418,6 +475,16 @@ namespace NecromancerTome Out("удар - " + FpArms.PunchTrigger); return; } + else if (cmd == "fov" && _params.Count >= 2 && float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float fov)) + { + FpArms.ArmsFov = fov; + } + else if (cmd == "impact" && _params.Count >= 2 && float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float imp)) + { + FpArms.FpImpact = imp; + Out("кулак доходит за " + imp.ToString(CultureInfo.InvariantCulture) + " с"); + return; + } else if (cmd == "anim") { Out(FpArms.DumpAnim()); @@ -468,4 +535,24 @@ namespace NecromancerTome [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateExit))] public static bool Exit(Animator animator) => !Ours(animator); } + + /// Миг урона одержимого зомби (ItemActionMelee.OnHoldingUpdate снимает bAttackStarted, когда замах дошёл до + /// точки удара) - FpArms мерит по нему задержку кулака. + [HarmonyPatch(typeof(ItemActionMelee), nameof(ItemActionMelee.OnHoldingUpdate))] + public static class Patch_ItemActionMelee_OnHoldingUpdate_FpArms + { + public static void Prefix(ItemActionData _actionData, out bool __state) + { + __state = _actionData is ItemActionMelee.InventoryDataMelee d && d.bAttackStarted && + Possession.IsPossessed(_actionData.invData?.holdingEntity); + } + + public static void Postfix(ItemActionData _actionData, bool __state) + { + if (__state && _actionData is ItemActionMelee.InventoryDataMelee d && !d.bAttackStarted) + { + FpArms.OnZombieHit(); + } + } + } } diff --git a/HarmonySrc/Possession.cs b/HarmonySrc/Possession.cs index fa358a4..18f032a 100644 --- a/HarmonySrc/Possession.cs +++ b/HarmonySrc/Possession.cs @@ -368,6 +368,7 @@ namespace NecromancerTome FirstPerson = false; HideBody(null); FpArms.Hide(_player); + ShowPlayerBelt(true); RatLoot.CloseIfViewing(); WolfBag.CloseWindow(); RatPhase.Stop(GameManager.Instance?.World, Zombie, "выход из одержимости"); @@ -596,13 +597,12 @@ namespace NecromancerTome if (FirstPerson) { FpArms.Show(_player, z); - // Пояс персонажа в виде от первого лица скрыт (13.10); обратно игра откроет его сама (OnHUD). - _player.PlayerUI?.windowManager.Close(SnakeControl.ToolbeltWindow); } else { HideBody(null); FpArms.Hide(_player); + ShowPlayerBelt(true); } Debug.Log("[NecromancerTome] Possession: вид от " + (FirstPerson ? "первого" : "третьего") + " лица"); } @@ -845,7 +845,7 @@ namespace NecromancerTome if (z.Attack(false)) { z.Attack(true); - FpArms.Punch(); // руки от первого лица бьют вместе с зомби (13.10) + FpArms.QueuePunch(); // руки от первого лица бьют вместе с зомби (13.10), кулак - в момент урона if (IsPet && BearControl.Is(z)) { BearControl.Smash(world, _player, z); // удар лапой и по блоку (решено 09.10) @@ -2110,6 +2110,18 @@ namespace NecromancerTome _flyZoomHeld = false; } + /// Ячейки пояса персонажа (окно windowToolbelt) - скрыты в виде от первого лица (указание 13.10: «пояс игрока + /// нужно прятать», у зомби свой пояс). Только это окно: группа toolbelt держит и полоски здоровья и сытости персонажа + /// (тест 13.10 - пропали вместе с поясом). Ставится каждый кадр - игра может показать окно снова. + static void ShowPlayerBelt(bool _show) + { + XUiV_Window w = _player?.PlayerUI?.xui?.GetWindow("windowToolbelt"); + if (w != null && w.IsVisible != _show) + { + w.IsVisible = _show; + } + } + /// Тело одержимого в виде от первого лица - только тень (тест 13.10: изнутри видно тело, у паука на /// бегу лапы и череп в кадре; сжатие одной головы не спасало). Видимость - дело своего экрана: другие игроки /// рисуют этого зомби сами и видят его как обычно; так же игра прячет тело персонажа от первого лица. @@ -2171,6 +2183,8 @@ namespace NecromancerTome : Vector3.Lerp(_fpEye, want, 1f - Mathf.Exp(-FpSmooth * Time.deltaTime)); HideBody(z); _camera.SetPositionAndRotation(_fpEye, rot); + FpArms.Tick(_player?.playerCamera); + ShowPlayerBelt(false); return; } Vector3 eye = anchor + Vector3.up * CamUp - rot * Vector3.forward * CamBack; diff --git a/HarmonySrc/SnakeControl.cs b/HarmonySrc/SnakeControl.cs index f7d245b..dd93c02 100644 --- a/HarmonySrc/SnakeControl.cs +++ b/HarmonySrc/SnakeControl.cs @@ -1320,8 +1320,8 @@ namespace NecromancerTome } /// Пояс в режиме змеи скрыт (указание 28.09: «прятать оверлей с поясом»), так же в - /// чертогах, у одержимого питомца (06.10) и у одержимого от первого лица (13.10: «пояс игрока нужно прятать - /// в одержимости от первого лица»; потом у зомби будут свои пояса). Игра каждый + /// чертогах и у одержимого питомца (06.10); от первого лица - только ячейки пояса (Possession.ShowPlayerBelt), не + /// вся группа: в ней же полоски здоровья и сытости персонажа. Игра каждый /// кадр в EntityPlayerLocal.OnHUD открывает окно "toolbelt", если оно закрыто, - поэтому не /// закрываем его раз за разом, а не даём открыться: все Open сходятся в openInternal. [HarmonyPatch(typeof(GUIWindowManager), "openInternal")] @@ -1329,7 +1329,7 @@ namespace NecromancerTome { public static bool Prefix(GUIWindow _w) { - return !((SnakeControl.Controlling || MindControl.Controlling || (Possession.Active && (Possession.IsPet || Possession.FirstPerson))) && _w != null && _w.Id == SnakeControl.ToolbeltWindow); + return !((SnakeControl.Controlling || MindControl.Controlling || (Possession.Active && Possession.IsPet)) && _w != null && _w.Id == SnakeControl.ToolbeltWindow); } }