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.