fix(anim): jump animation via Falling SubState + kept PlayAction infra for emotes

The actual retail behavior for jump animations is a plain SubState swap —
NOT an Action overlay as I'd initially guessed. MotionCommand.Falling
(0x40000015) is a SubState cycle whose motion-table entries handle the
whole jump lifecycle:

  - Links[(stance, RunForward)][Falling]     = leap-into-air link
  - Cycles[(stance, Falling)]                = airborne cycle (loops)
  - Links[(stance, Falling)][Ready/...]      = landing link back to normal

Empirical verification from the diagnostic dump:
    Links[0x003D0007] has 3 inner entries:
        inner key: 0x41000003 (Ready)
        inner key: 0x45000005 (WalkForward)
        inner key: 0x40000015 (Falling) ← jackpot

SetCycle() already handles SubState + Links + Cycles resolution correctly,
so the whole fix is three lines:

    if (!result.IsOnGround)
        animCommand = MotionCommand.Falling;

What's in this commit:
- Added MotionCommand.Falling (0x40000015) constant + comments explaining
  the retail jump-is-a-SubState flow
- GameWindow.UpdatePlayerAnimation swaps to Falling when airborne (the
  cleanest possible implementation — motion table does all the work)
- Kept AnimationSequencer.PlayAction infrastructure (ported via Links
  fallback + Modifiers fallback). Not needed for jump, but perfectly
  valid for emotes like /wave, /bow (found in the same Links dict as
  inner keys 0x13000080-0x13000083) and eventual combat attacks
- Kept MotionCommand.Jump / Jumpup / FallDown constants (unused for now
  but useful reference if non-humanoid motion tables use them)
- Removed all diagnostic logging

What was learned (for future motion work):
- Retail's MotionTable.Cycles dict holds SubState loops (Ready, Walk,
  Run, Falling, Crouch, etc.) by (style<<16) | (motion & 0xFFFFFF)
- MotionTable.Links dict holds TRANSITIONS between motions: the OUTER
  key is the (style, fromMotion) combo; the INNER key is the TARGET
  motion. The stored MotionData IS the link animation played during
  the transition. This is what ACE's get_link traverses.
- MotionTable.Modifiers dict holds overlay motions (mask 0x20) — rare
  for humanoids, only 8 TurnRight/SideStepRight stance variants
- Actions (mask 0x10) in retail ALSO go through Links — they're
  transition animations FROM current substate, not overlays. Use
  PlayAction (now correctly routed to Links dict) for them.

Jump animation now works retail-faithfully for running + jumping off.
Standing-jump behavior depends on whether the player's motion table
has a Ready→Falling link; SetCycle's fallback chain should handle it
via the style-level catch-all if the direct link is absent.

470 tests pass. Build clean.
This commit is contained in:
Erik 2026-04-18 15:32:52 +02:00
parent 08ea2c0af8
commit 6bce9b8019
3 changed files with 70 additions and 48 deletions

View file

@ -48,14 +48,28 @@ public static class MotionCommand
/// <summary>0x40000008 — Fallen (lying on ground).</summary>
public const uint Fallen = 0x40000008u;
/// <summary>
/// 0x2500003B — Jump (Modifier action; played via
/// <see cref="AnimationSequencer.PlayAction"/>). NOT a SubState — it
/// overlays the current cycle via the motion table's Modifiers dict.
/// 0x40000015 — Falling (SubState). The airborne cycle. Retail's
/// MotionTable has Links from RunForward/Ready/WalkForward → Falling,
/// and a Cycles entry for (style, Falling) that loops while the body
/// is in the air. Swap via <see cref="AnimationSequencer.SetCycle"/>
/// when airborne; swap back to Ready/WalkForward/RunForward on land.
/// </summary>
public const uint Falling = 0x40000015u;
/// <summary>
/// 0x2500003B — Jump (Modifier flag). NOT an animation trigger; retail
/// uses this as a state flag internally. Kept for future use.
/// </summary>
public const uint Jump = 0x2500003Bu;
/// <summary>
/// 0x10000050 — FallDown (Action; the landing animation played after
/// a jump. Enqueued via <see cref="AnimationSequencer.PlayAction"/>).
/// 0x1000004B — Jumpup (Action). Not present in the humanoid player
/// motion table's Links dict (empirically verified). Retail uses the
/// Falling SubState for airborne animation instead.
/// </summary>
public const uint Jumpup = 0x1000004Bu;
/// <summary>
/// 0x10000050 — FallDown (Action). Same story as Jumpup; not in the
/// humanoid motion table's Links. Landing returns to Ready via the
/// regular SetCycle transition.
/// </summary>
public const uint FallDown = 0x10000050u;
/// <summary>0x10000057 — Dead.</summary>