diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 3182b07..b9dd13f 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -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.
+ ///
+ ///
+ /// Action motions (Jump, FallDown, emotes, attacks) are routed through
+ /// — 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.
+ ///
///
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).
//
diff --git a/src/AcDream.Core/Physics/AnimationSequencer.cs b/src/AcDream.Core/Physics/AnimationSequencer.cs
index 90c8542..4c39583 100644
--- a/src/AcDream.Core/Physics/AnimationSequencer.cs
+++ b/src/AcDream.Core/Physics/AnimationSequencer.cs
@@ -413,6 +413,88 @@ public sealed class AnimationSequencer
return BuildBlendedFrame();
}
+ ///
+ /// Play a one-shot action/modifier motion (Jump, emote, attack, etc.)
+ /// on top of the current cycle. The action frames are inserted in the
+ /// queue immediately before the looping cyclic tail; they drain once
+ /// and then the cycle resumes naturally.
+ ///
+ ///
+ /// Retail semantics: actions and modifiers live in
+ /// (a separate dict from
+ /// ) keyed by
+ /// (style << 16) | (motion & 0xFFFFFF). A motion like
+ /// Jump = 0x2500003b is a Modifier (class byte 0x25) not a
+ /// SubState — feeding it to silently fails the
+ /// cycle lookup. Routing through PlayAction instead resolves
+ /// from the Modifiers table and interleaves the action frames with
+ /// the ongoing cyclic motion.
+ ///
+ ///
+ ///
+ /// If no entry is found in the Modifiers table for the requested
+ /// motion, this is a no-op.
+ ///
+ ///
+ /// Raw MotionCommand (e.g. 0x2500003b for Jump).
+ /// Speed multiplier for the action's framerate.
+ public void PlayAction(uint motionCommand, float speedMod = 1f)
+ {
+ // Resolve motion data. Modifiers use (style << 16) | (motion & 0xFFFFFF)
+ // as the styled key, or just (motion & 0xFFFFFF) for style-independent
+ // entries (matches ACE MotionTable.GetObjectSequence lookup order).
+ uint styleKey = CurrentStyle << 16;
+ int keyStyled = (int)(styleKey | (motionCommand & 0xFFFFFFu));
+ int keyPlain = (int)(motionCommand & 0xFFFFFFu);
+
+ if (!_mtable.Modifiers.TryGetValue(keyStyled, out var data))
+ _mtable.Modifiers.TryGetValue(keyPlain, out data);
+
+ if (data is null || data.Anims.Count == 0)
+ return;
+
+ // Build AnimNodes from the action's AnimData list. All non-looping —
+ // they drain once, then the queue falls through to _firstCyclic.
+ var newNodes = new List(data.Anims.Count);
+ for (int i = 0; i < data.Anims.Count; i++)
+ {
+ var node = LoadAnimNode(data.Anims[i], speedMod, isLooping: false);
+ if (node != null) newNodes.Add(node);
+ }
+ if (newNodes.Count == 0) return;
+
+ // Insert before the cyclic tail (so the action plays, then cycle resumes).
+ // If there's no cyclic tail yet, append at the end.
+ LinkedListNode? firstInserted = null;
+ if (_firstCyclic != null)
+ {
+ foreach (var n in newNodes)
+ {
+ var inserted = _queue.AddBefore(_firstCyclic, n);
+ firstInserted ??= inserted;
+ }
+ }
+ else
+ {
+ foreach (var n in newNodes)
+ {
+ var inserted = _queue.AddLast(n);
+ firstInserted ??= inserted;
+ }
+ }
+
+ // If we're currently on the cyclic tail (or past where we inserted),
+ // jump the cursor back to the first newly-inserted action node so the
+ // action plays immediately instead of after the next cycle wrap.
+ bool cursorOnCyclic = _currNode != null && _currNode.Value.IsLooping;
+ if (cursorOnCyclic || _currNode == null)
+ {
+ _currNode = firstInserted;
+ if (_currNode != null)
+ _framePosition = _currNode.Value.GetStartFramePosition();
+ }
+ }
+
///
/// Reset the sequencer to an unplaying state without clearing the
/// motion table reference.
diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs
index a6193b6..d55c84b 100644
--- a/src/AcDream.Core/Physics/MotionInterpreter.cs
+++ b/src/AcDream.Core/Physics/MotionInterpreter.cs
@@ -47,6 +47,17 @@ public static class MotionCommand
public const uint SideStepLeft = 0x65000010u;
/// 0x40000008 — Fallen (lying on ground).
public const uint Fallen = 0x40000008u;
+ ///
+ /// 0x2500003B — Jump (Modifier action; played via
+ /// ). NOT a SubState — it
+ /// overlays the current cycle via the motion table's Modifiers dict.
+ ///
+ public const uint Jump = 0x2500003Bu;
+ ///
+ /// 0x10000050 — FallDown (Action; the landing animation played after
+ /// a jump. Enqueued via ).
+ ///
+ public const uint FallDown = 0x10000050u;
/// 0x10000057 — Dead.
public const uint Dead = 0x10000057u;
/// 0x41000011 — Crouch lower bound for blocked-jump check.