Enemy hit-reacts: React/Stagger anim tiers off Health-drop edges (the Rukhanka-safe flinch)
SwordCombat React (1.4x, 0.3s pulse) + Stagger (1.2x, 0.55s) as Any-State states on AC_EnemyTopDown via EnemyRigTools.WireEnemyHitReacts; driven by EnemyAnimationDriveSystem's cache (now Pos/Hp/ReactUntil/Heavy) off replicated Health drops. Windup-honesty gate: light reacts require !IsAttacking (a sub-poise hit never visually cancels a live windup); the heavy tier tracks the server's B2 poise break (stagger threshold 50 = finisher staggers, light flinches at the locked tuning). Knobs: HitReactSeconds (0=off) / HitStaggerSeconds / HitReactStaggerDamage. Replaces the review-cut positional vibrate (Rukhanka inverse-fold no-op). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,8 @@ namespace ProjectM.EditorTools
|
||||
const string PlayerController = "Assets/_Project/Animation/AC_PlayerTopDown.controller";
|
||||
const string EnemyController = "Assets/_Project/Animation/AC_EnemyTopDown.controller";
|
||||
const string AttackClip = "Assets/_Project/Animation/EnemyAttackWindup.anim";
|
||||
const string HitReactFbx = "Assets/Synty/AnimationSwordCombat/Animations/Polygon/Hit/HitReact/A_Hit_F_React_Sword.fbx";
|
||||
const string HitStaggerFbx = "Assets/Synty/AnimationSwordCombat/Animations/Polygon/Hit/HitStagger/A_Hit_F_Stagger_Sword.fbx";
|
||||
|
||||
const string WerewolfAtlas = "Assets/Synty/PolygonWerewolf/Textures/PolygonWerewolf_01_A.png";
|
||||
const string KaijuAtlas = "Assets/Synty/PolygonKaiju/Textures/PolygonKaiju_01.png";
|
||||
@@ -364,5 +366,83 @@ namespace ProjectM.EditorTools
|
||||
}
|
||||
Debug.LogWarning($"[EnemyRigTools] Field/prop '{name}' not found on {o.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>07-21 hit-react pass (guidelines G5; the Rukhanka-safe flinch replacing the cut vibrate):
|
||||
/// adds IsHit/IsHitHeavy params + HitReact (light flinch) and HitStagger (heavy) Any-State states to
|
||||
/// AC_EnemyTopDown, pulsed by EnemyAnimationDriveSystem off replicated Health-drop edges. HONESTY GATE:
|
||||
/// the light react requires IsAttacking == false so a sub-poise hit never visually cancels a windup the
|
||||
/// server is still running (the telegraph must not lie); a HEAVY hit staggers unconditionally, which
|
||||
/// aligns with the server's B2 poise break (finisher knockback >= the stagger threshold zeroes the
|
||||
/// windup). Idempotent / re-runnable.</summary>
|
||||
[MenuItem("ProjectM/Animation/Enemy - Wire Hit Reacts (LANTERN)")]
|
||||
public static void WireEnemyHitReacts()
|
||||
{
|
||||
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(EnemyController);
|
||||
if (ac == null) { Debug.LogError($"[EnemyRigTools] Controller missing: {EnemyController}"); return; }
|
||||
|
||||
AnimationClip Clip(string fbxPath, string clipName)
|
||||
{
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath(fbxPath))
|
||||
if (o is AnimationClip c && c.name == clipName) return c;
|
||||
Debug.LogError($"[EnemyRigTools] Clip missing: {fbxPath} -> {clipName}");
|
||||
return null;
|
||||
}
|
||||
var react = Clip(HitReactFbx, "A_Hit_F_React_Sword");
|
||||
var stagger = Clip(HitStaggerFbx, "A_Hit_F_Stagger_Sword");
|
||||
if (react == null || stagger == null) return;
|
||||
|
||||
void EnsureParam(string n)
|
||||
{
|
||||
foreach (var p in ac.parameters) if (p.name == n) return;
|
||||
ac.AddParameter(n, AnimatorControllerParameterType.Bool);
|
||||
}
|
||||
EnsureParam("IsHit");
|
||||
EnsureParam("IsHitHeavy");
|
||||
|
||||
var sm = ac.layers[0].stateMachine;
|
||||
AnimatorState Find(string n)
|
||||
{
|
||||
foreach (var cs in sm.states) if (cs.state.name == n) return cs.state;
|
||||
return null;
|
||||
}
|
||||
|
||||
var hitReact = Find("HitReact");
|
||||
if (hitReact == null)
|
||||
{
|
||||
hitReact = sm.AddState("HitReact");
|
||||
var to = sm.AddAnyStateTransition(hitReact);
|
||||
to.hasExitTime = false; to.duration = 0.05f; to.canTransitionToSelf = false;
|
||||
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHit");
|
||||
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHitHeavy");
|
||||
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking"); // honesty: never cancel a live windup visual
|
||||
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsDead");
|
||||
var ex = hitReact.AddExitTransition();
|
||||
ex.hasExitTime = false; ex.duration = 0.10f;
|
||||
ex.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHit");
|
||||
}
|
||||
hitReact.motion = react;
|
||||
hitReact.speed = 1.4f; // 0.83s clip -> the flinch peak inside the ~0.3s pulse
|
||||
|
||||
var hitStagger = Find("HitStagger");
|
||||
if (hitStagger == null)
|
||||
{
|
||||
hitStagger = sm.AddState("HitStagger");
|
||||
var to = sm.AddAnyStateTransition(hitStagger);
|
||||
to.hasExitTime = false; to.duration = 0.05f; to.canTransitionToSelf = false;
|
||||
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHit");
|
||||
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHitHeavy"); // heavy overrides even a windup (server poise broke it)
|
||||
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsDead");
|
||||
var ex = hitStagger.AddExitTransition();
|
||||
ex.hasExitTime = false; ex.duration = 0.12f;
|
||||
ex.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHit");
|
||||
}
|
||||
hitStagger.motion = stagger;
|
||||
hitStagger.speed = 1.2f; // 1.07s clip -> the lurch inside the ~0.55s pulse
|
||||
|
||||
EditorUtility.SetDirty(ac);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[EnemyRigTools] Hit reacts wired: HitReact (light, windup-honest) + HitStagger (heavy) on AC_EnemyTopDown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user