feat(anim): motion-action-queue infrastructure + retail jump-is-physics-only note

Adds AnimationSequencer.PlayAction as the proper path for Action and
Modifier-class motions (the MotionTable.Modifiers dict, distinct from
Cycles). Action nodes are inserted before the looping cyclic tail so
they drain once and the cycle resumes naturally — leveraging the
sequencer's existing "non-looping head drains, cyclic tail wraps"
queue semantics.

What this does:
- New AnimationSequencer.PlayAction(motionCommand, speedMod=1f):
  - Resolves (style<<16) | (motion&0xFFFFFF) from MotionTable.Modifiers
  - Falls back to (motion&0xFFFFFF) plain key
  - Silent no-op when not found (some motion tables lack these)
  - Inserts AnimNodes before _firstCyclic; re-points the cursor when on
    the cyclic tail so the action plays immediately
- New MotionCommand.Jump (0x2500003B) + MotionCommand.FallDown (0x10000050)
  constants.
- GameWindow.UpdatePlayerAnimation fires PlayAction(Jump) on
  result.JumpExtent.HasValue and PlayAction(FallDown) on JustLanded.

Key research finding: retail does NOT animate jumps.
- ACE Player.HandleActionJump explicitly clears PendingMotions and sets
  IsAnimating=false during a jump (Player.cs:914-915).
- Empirical verification: the player humanoid's MotionTable only has 8
  Modifier entries — all TurnRight/SideStepRight stance variants. No
  Jump (0x2500003B) or FallDown (0x10000050) entries.
- Jump is a physics-only action: the character keeps whatever cycle
  was active (walk/run/idle) while the physics body arcs through the
  air. There is no "raise arms to jump" pose in retail.

PlayAction is still called on jump/land as a safety hatch for creature
Setups that DO carry leap animations in their Modifiers dict (drudge
jumps, monster pounces, etc.). For player humanoids it's a no-op. The
infrastructure is also ready for future emote/combat actions that
legitimately use the Modifiers dict.

470 tests pass, build clean.
This commit is contained in:
Erik 2026-04-18 15:12:12 +02:00
parent 3308cddda7
commit 08ea2c0af8
3 changed files with 128 additions and 0 deletions

View file

@ -3024,11 +3024,46 @@ public sealed class GameWindow : IDisposable
/// to match the current motion command. Only re-resolves when the command
/// actually changes (forward → run, idle → walk, etc.) to avoid re-building
/// the animation entry every frame.
///
/// <para>
/// Action motions (Jump, FallDown, emotes, attacks) are routed through
/// <see cref="AcDream.Core.Physics.AnimationSequencer.PlayAction"/> — they
/// live in the motion table's Modifiers dict, not the Cycles dict, and
/// are inserted into the queue on top of the current cycle instead of
/// replacing it.
/// </para>
/// </summary>
private void UpdatePlayerAnimation(AcDream.App.Input.MovementResult result)
{
if (_dats is null) return;
// ── Action-motion events (jump / land) ─────────────────────────────
//
// Retail does NOT animate jumps — confirmed via ACE's HandleActionJump
// (Player.cs:914-915) which explicitly clears PendingMotions and
// sets IsAnimating=false during the jump. The character keeps
// whatever cycle it was on and the physics body arcs through the air.
// Humanoid Setup MotionTables have NO entry for Jump (0x2500003B)
// or FallDown (0x10000050) in the Modifiers dict — verified empirically
// (only 8 TurnRight stance-variants + SideStepRight).
//
// We still call PlayAction here as a no-op safety hatch: if a future
// Setup / creature DOES carry a jump/fall modifier in its MotionTable
// (e.g. a leaping-monster) the sequencer will pick it up for free.
// For player humanoids, the lookup silently misses and nothing changes.
if (result.JumpExtent.HasValue || result.JustLanded)
{
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var actionPe)
&& _animatedEntities.TryGetValue(actionPe.Id, out var actionAe)
&& actionAe.Sequencer is not null)
{
if (result.JumpExtent.HasValue)
actionAe.Sequencer.PlayAction(AcDream.Core.Physics.MotionCommand.Jump);
if (result.JustLanded)
actionAe.Sequencer.PlayAction(AcDream.Core.Physics.MotionCommand.FallDown);
}
}
// Determine the animation command: forward takes priority, then sidestep,
// then turn, then idle (Ready 0x41000003).
//