Руки одержимого: урон по анимации удара рук, как у персонажа; в покое руки выше, на удар - ниже

- ЛКМ от первого лица - удар рук; через RaycastTime их состояния удара (как impactStart у персонажа) зомби бьёт сразу (ForceImpact в IsAttackImpact, задержка луча снята). Замер задержки урона убран.
- Положение: в покое 0,-1.55,0.05 без сжатия; на удар - плавно к -1.7 со сжатием под FOV 45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-13 16:41:57 +03:00
co-authored by Claude Opus 5.5
parent 885c344b3c
commit 2cae2377e2
2 changed files with 97 additions and 40 deletions
+59 -38
View File
@@ -55,9 +55,14 @@ namespace NecromancerTome
/// рук по экранным осям: масштаб (k, k, 1) от камеры, k = tg(FOV камеры / 2) / tg(ArmsFov / 2) - каждый кадр /// рук по экранным осям: масштаб (k, k, 1) от камеры, k = tg(FOV камеры / 2) / tg(ArmsFov / 2) - каждый кадр
/// (FOV меняется при приближении).</summary> /// (FOV меняется при приближении).</summary>
public static float ArmsFov = 45f; public static float ArmsFov = 45f;
/// <summary>Через сколько после сигнала удара кулак рук доходит до цели (подбор necroarms impact).</summary> /// <summary>Положение рук в покое (подобрано 13.10, без сжатия): «руки подними как были»; на удар - Offset со
public static float FpImpact = 0.15f; /// сжатием под ArmsFov, «анимацию удара занизь как сейчас». Переход - за BlendTime.</summary>
static float _punchAt, _attackAt, _hitDelay = 0.45f; public static Vector3 IdleOffset = new Vector3(0f, -1.55f, 0.05f);
public const float BlendTime = 0.12f;
/// <summary>Минимальный промежуток между ударами рук.</summary>
public const float SwingTime = 0.6f;
static bool _inAttack;
static float _impactAt, _blend;
/// <summary>Родитель: true - сама камера, false - её UFPSRoot (как у персонажа).</summary> /// <summary>Родитель: true - сама камера, false - её UFPSRoot (как у персонажа).</summary>
public static bool ParentCamera = true; public static bool ParentCamera = true;
@@ -274,32 +279,23 @@ namespace NecromancerTome
} }
} }
/// <summary>Удар зомби начат: урон пройдёт, когда его замах дойдёт до точки удара (ItemActionMelee.OnHoldingUpdate, /// <summary>Анимация удара рук вошла в состояние удара (AnimatorMeleeAttackState у наших рук): через RaycastTime -
/// у разных зомби по-разному). Кулак рук запускаем с таким опозданием, чтобы пришёл в тот же миг (тест 13.10: /// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел).</summary>
/// «сначала анимация, потом пауза и удар»); _hitDelay - замер прошлых ударов этого зомби.</summary> public static void OnSwingEnter(float _raycastTime)
public static void QueuePunch()
{ {
if (_rig == null) _inAttack = true;
{ _impactAt = Time.time + Mathf.Max(0.01f, _raycastTime);
return;
}
_attackAt = Time.time;
_punchAt = Time.time + Mathf.Max(0f, _hitDelay - FpImpact);
} }
/// <summary>Урон зомби прошёл - замер задержки для следующих ударов.</summary> public static void OnSwingExit()
{
_inAttack = false;
}
/// <summary>Урон зомби прошёл - вынужденный миг удара снят.</summary>
public static void OnZombieHit() public static void OnZombieHit()
{ {
if (_attackAt <= 0f) Possession.ForceImpact = false;
{
return;
}
float d = Time.time - _attackAt;
_attackAt = 0f;
if (d > 0.02f && d < 2f)
{
_hitDelay = d;
}
} }
/// <summary>Каждый кадр вида от первого лица (Possession.PlaceCamera): отложенный удар и сжатие под угол рук.</summary> /// <summary>Каждый кадр вида от первого лица (Possession.PlaceCamera): отложенный удар и сжатие под угол рук.</summary>
@@ -309,18 +305,20 @@ namespace NecromancerTome
{ {
return; return;
} }
if (_punchAt > 0f && Time.time >= _punchAt) if (_impactAt > 0f && Time.time >= _impactAt)
{ {
_punchAt = 0f; _impactAt = 0f;
Punch(); Possession.FpImpact();
} }
float k = 1f; float k = 1f;
if (_cam != null && ArmsFov > 1f) if (_cam != null && ArmsFov > 1f)
{ {
k = Mathf.Tan(_cam.fieldOfView * 0.5f * Mathf.Deg2Rad) / Mathf.Tan(ArmsFov * 0.5f * Mathf.Deg2Rad); k = Mathf.Tan(_cam.fieldOfView * 0.5f * Mathf.Deg2Rad) / Mathf.Tan(ArmsFov * 0.5f * Mathf.Deg2Rad);
} }
_rig.transform.localScale = new Vector3(k, k, 1f); _blend = Mathf.MoveTowards(_blend, _inAttack ? 1f : 0f, Time.deltaTime / BlendTime);
_rig.transform.localPosition = new Vector3(Offset.x * k, Offset.y * k, Offset.z); 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) public static string Trigger(string _name)
@@ -439,7 +437,7 @@ namespace NecromancerTome
public class ConsoleCmdNecroArms : ConsoleCmdAbstract public class ConsoleCmdNecroArms : ConsoleCmdAbstract
{ {
public override string[] getCommands() => new[] { "necroarms" }; public override string[] getCommands() => new[] { "necroarms" };
public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set <param> <value> | trigger <name> | tint r g b | albedo on|off | punch <trigger> | fov <угол> | impact <с>]"; public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set <param> <value> | trigger <name> | tint r g b | albedo on|off | punch <trigger> | fov <deg>]";
void Out(string _s) void Out(string _s)
{ {
@@ -479,12 +477,6 @@ namespace NecromancerTome
{ {
FpArms.ArmsFov = 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") else if (cmd == "anim")
{ {
Out(FpArms.DumpAnim()); Out(FpArms.DumpAnim());
@@ -527,13 +519,29 @@ namespace NecromancerTome
static bool Ours(Animator _a) => _a != null && _a.GetComponent<AnimationEventBridge>() == null; static bool Ours(Animator _a) => _a != null && _a.GetComponent<AnimationEventBridge>() == null;
[HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateEnter))] [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))] [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateUpdate))]
public static bool Update(Animator animator) => !Ours(animator); public static bool Update(Animator animator) => !Ours(animator);
[HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateExit))] [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;
}
} }
/// <summary>Миг урона одержимого зомби (ItemActionMelee.OnHoldingUpdate снимает bAttackStarted, когда замах дошёл до /// <summary>Миг урона одержимого зомби (ItemActionMelee.OnHoldingUpdate снимает bAttackStarted, когда замах дошёл до
@@ -555,4 +563,17 @@ namespace NecromancerTome
} }
} }
} }
/// <summary>Миг удара одержимого от первого лица задают руки (Possession.FpImpact): пока ForceImpact - «замах дошёл».</summary>
[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;
}
}
}
} }
+38 -2
View File
@@ -365,6 +365,7 @@ namespace NecromancerTome
return; return;
} }
Active = false; Active = false;
ForceImpact = false;
FirstPerson = false; FirstPerson = false;
HideBody(null); HideBody(null);
FpArms.Hide(_player); FpArms.Hide(_player);
@@ -838,14 +839,21 @@ namespace NecromancerTome
// Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте. // Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте.
DogSense.Tick(world, z, Time.deltaTime); 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, "удар"); PossessionPowers.BreakInvis(z, "удар");
z.SetLookPosition(MeleeAim(world, z)); z.SetLookPosition(MeleeAim(world, z));
if (z.Attack(false)) if (z.Attack(false))
{ {
z.Attack(true); z.Attack(true);
FpArms.QueuePunch(); // руки от первого лица бьют вместе с зомби (13.10), кулак - в момент урона
if (IsPet && BearControl.Is(z)) if (IsPet && BearControl.Is(z))
{ {
BearControl.Smash(world, _player, z); // удар лапой и по блоку (решено 09.10) BearControl.Smash(world, _player, z); // удар лапой и по блоку (решено 09.10)
@@ -2110,6 +2118,34 @@ namespace NecromancerTome
_flyZoomHeld = false; _flyZoomHeld = false;
} }
/// <summary>Миг удара рук от первого лица (FpArms): зомби бьёт сразу - его удар начат и сразу «дошёл до точки
/// удара» (ForceImpact в EntityAlive.IsAttackImpact, задержка луча ItemActionMelee снята). Урон, дальность,
/// цель - его, взгляд - на прицел (MeleeAim).</summary>
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;
}
}
/// <summary>Ячейки пояса персонажа (окно windowToolbelt) - скрыты в виде от первого лица (указание 13.10: «пояс игрока /// <summary>Ячейки пояса персонажа (окно windowToolbelt) - скрыты в виде от первого лица (указание 13.10: «пояс игрока
/// нужно прятать», у зомби свой пояс). Только это окно: группа toolbelt держит и полоски здоровья и сытости персонажа /// нужно прятать», у зомби свой пояс). Только это окно: группа toolbelt держит и полоски здоровья и сытости персонажа
/// (тест 13.10 - пропали вместе с поясом). Ставится каждый кадр - игра может показать окно снова.</summary> /// (тест 13.10 - пропали вместе с поясом). Ставится каждый кадр - игра может показать окно снова.</summary>