diff --git a/HarmonySrc/FpArms.cs b/HarmonySrc/FpArms.cs index 2e9aca9..19aa5d7 100644 --- a/HarmonySrc/FpArms.cs +++ b/HarmonySrc/FpArms.cs @@ -55,9 +55,14 @@ namespace NecromancerTome /// рук по экранным осям: масштаб (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; + /// Положение рук в покое (подобрано 13.10, без сжатия): «руки подними как были»; на удар - Offset со + /// сжатием под ArmsFov, «анимацию удара занизь как сейчас». Переход - за BlendTime. + public static Vector3 IdleOffset = new Vector3(0f, -1.55f, 0.05f); + public const float BlendTime = 0.12f; + /// Минимальный промежуток между ударами рук. + public const float SwingTime = 0.6f; + static bool _inAttack; + static float _impactAt, _blend; /// Родитель: true - сама камера, false - её UFPSRoot (как у персонажа). public static bool ParentCamera = true; @@ -274,32 +279,23 @@ namespace NecromancerTome } } - /// Удар зомби начат: урон пройдёт, когда его замах дойдёт до точки удара (ItemActionMelee.OnHoldingUpdate, - /// у разных зомби по-разному). Кулак рук запускаем с таким опозданием, чтобы пришёл в тот же миг (тест 13.10: - /// «сначала анимация, потом пауза и удар»); _hitDelay - замер прошлых ударов этого зомби. - public static void QueuePunch() + /// Анимация удара рук вошла в состояние удара (AnimatorMeleeAttackState у наших рук): через RaycastTime - + /// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел). + public static void OnSwingEnter(float _raycastTime) { - if (_rig == null) - { - return; - } - _attackAt = Time.time; - _punchAt = Time.time + Mathf.Max(0f, _hitDelay - FpImpact); + _inAttack = true; + _impactAt = Time.time + Mathf.Max(0.01f, _raycastTime); } - /// Урон зомби прошёл - замер задержки для следующих ударов. + public static void OnSwingExit() + { + _inAttack = false; + } + + /// Урон зомби прошёл - вынужденный миг удара снят. public static void OnZombieHit() { - if (_attackAt <= 0f) - { - return; - } - float d = Time.time - _attackAt; - _attackAt = 0f; - if (d > 0.02f && d < 2f) - { - _hitDelay = d; - } + Possession.ForceImpact = false; } /// Каждый кадр вида от первого лица (Possession.PlaceCamera): отложенный удар и сжатие под угол рук. @@ -309,18 +305,20 @@ namespace NecromancerTome { return; } - if (_punchAt > 0f && Time.time >= _punchAt) + if (_impactAt > 0f && Time.time >= _impactAt) { - _punchAt = 0f; - Punch(); + _impactAt = 0f; + Possession.FpImpact(); } 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); + _blend = Mathf.MoveTowards(_blend, _inAttack ? 1f : 0f, Time.deltaTime / BlendTime); + float kk = Mathf.Lerp(1f, k, _blend); + _rig.transform.localScale = new Vector3(kk, kk, 1f); + _rig.transform.localPosition = Vector3.Lerp(IdleOffset, new Vector3(Offset.x * k, Offset.y * k, Offset.z), _blend); } public static string Trigger(string _name) @@ -439,7 +437,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 | fov <угол> | impact <с>]"; + 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 ]"; void Out(string _s) { @@ -479,12 +477,6 @@ namespace NecromancerTome { 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()); @@ -527,13 +519,29 @@ namespace NecromancerTome static bool Ours(Animator _a) => _a != null && _a.GetComponent() == null; [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateEnter))] - public static bool Enter(Animator animator) => !Ours(animator); + public static bool Enter(AnimatorMeleeAttackState __instance, Animator animator) + { + if (!Ours(animator)) + { + return true; + } + FpArms.OnSwingEnter(__instance.RaycastTime); // миг удара - как у персонажа (13.10) + return false; + } [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateUpdate))] public static bool Update(Animator animator) => !Ours(animator); [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateExit))] - public static bool Exit(Animator animator) => !Ours(animator); + public static bool Exit(Animator animator) + { + if (!Ours(animator)) + { + return true; + } + FpArms.OnSwingExit(); + return false; + } } /// Миг урона одержимого зомби (ItemActionMelee.OnHoldingUpdate снимает bAttackStarted, когда замах дошёл до @@ -555,4 +563,17 @@ namespace NecromancerTome } } } + + /// Миг удара одержимого от первого лица задают руки (Possession.FpImpact): пока ForceImpact - «замах дошёл». + [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.IsAttackImpact))] + public static class Patch_EntityAlive_IsAttackImpact_FpArms + { + public static void Postfix(EntityAlive __instance, ref bool __result) + { + if (Possession.ForceImpact && Possession.IsPossessed(__instance)) + { + __result = true; + } + } + } } diff --git a/HarmonySrc/Possession.cs b/HarmonySrc/Possession.cs index 18f032a..66fc587 100644 --- a/HarmonySrc/Possession.cs +++ b/HarmonySrc/Possession.cs @@ -365,6 +365,7 @@ namespace NecromancerTome return; } Active = false; + ForceImpact = false; FirstPerson = false; HideBody(null); FpArms.Hide(_player); @@ -838,14 +839,21 @@ namespace NecromancerTome // Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте. DogSense.Tick(world, z, Time.deltaTime); } - if (attack && Time.time >= _nextAttack) + if (attack && Time.time >= _nextAttack && FpArms.Active) + { + // От первого лица - как у персонажа (указание 13.10): ЛКМ - удар рук, урон зомби - когда их анимация дошла + // до точки удара (FpArms -> FpImpact), а не по его собственному замаху. + PossessionPowers.BreakInvis(z, "удар"); + FpArms.Punch(); + _nextAttack = Time.time + Mathf.Max(FpArms.SwingTime, z.GetAttackTimeoutTicks() / 20f); + } + else if (attack && Time.time >= _nextAttack) { PossessionPowers.BreakInvis(z, "удар"); z.SetLookPosition(MeleeAim(world, z)); if (z.Attack(false)) { z.Attack(true); - FpArms.QueuePunch(); // руки от первого лица бьют вместе с зомби (13.10), кулак - в момент урона if (IsPet && BearControl.Is(z)) { BearControl.Smash(world, _player, z); // удар лапой и по блоку (решено 09.10) @@ -2110,6 +2118,34 @@ namespace NecromancerTome _flyZoomHeld = false; } + /// Миг удара рук от первого лица (FpArms): зомби бьёт сразу - его удар начат и сразу «дошёл до точки + /// удара» (ForceImpact в EntityAlive.IsAttackImpact, задержка луча ItemActionMelee снята). Урон, дальность, + /// цель - его, взгляд - на прицел (MeleeAim). + public static bool ForceImpact; + + public static void FpImpact() + { + EntityAlive z = Zombie; + if (!Active || z == null || z.IsDead()) + { + return; + } + z.SetLookPosition(MeleeAim(z.world, z)); + _meleeLookUntil = Time.time + 0.6f; + if (!z.Attack(false)) + { + Debug.Log("[NecromancerTome] Possession: удар рук - зомби не может бить сейчас"); + return; + } + z.Attack(true); + ForceImpact = true; + if (z.inventory?.holdingItemData?.actionData != null && z.inventory.holdingItemData.actionData.Count > 0 && + z.inventory.holdingItemData.actionData[0] is ItemActionMelee.InventoryDataMelee d) + { + d.lastUseTime -= 10f; + } + } + /// Ячейки пояса персонажа (окно windowToolbelt) - скрыты в виде от первого лица (указание 13.10: «пояс игрока /// нужно прятать», у зомби свой пояс). Только это окно: группа toolbelt держит и полоски здоровья и сытости персонажа /// (тест 13.10 - пропали вместе с поясом). Ставится каждый кадр - игра может показать окно снова.