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:
parent
3308cddda7
commit
08ea2c0af8
3 changed files with 128 additions and 0 deletions
|
|
@ -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).
|
||||
//
|
||||
|
|
|
|||
|
|
@ -413,6 +413,88 @@ public sealed class AnimationSequencer
|
|||
return BuildBlendedFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail semantics: actions and modifiers live in
|
||||
/// <see cref="MotionTable.Modifiers"/> (a separate dict from
|
||||
/// <see cref="MotionTable.Cycles"/>) keyed by
|
||||
/// <c>(style << 16) | (motion & 0xFFFFFF)</c>. A motion like
|
||||
/// <c>Jump = 0x2500003b</c> is a Modifier (class byte 0x25) not a
|
||||
/// SubState — feeding it to <see cref="SetCycle"/> silently fails the
|
||||
/// cycle lookup. Routing through <c>PlayAction</c> instead resolves
|
||||
/// from the Modifiers table and interleaves the action frames with
|
||||
/// the ongoing cyclic motion.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// If no entry is found in the Modifiers table for the requested
|
||||
/// motion, this is a no-op.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="motionCommand">Raw MotionCommand (e.g. 0x2500003b for Jump).</param>
|
||||
/// <param name="speedMod">Speed multiplier for the action's framerate.</param>
|
||||
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<AnimNode>(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<AnimNode>? 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the sequencer to an unplaying state without clearing the
|
||||
/// motion table reference.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,17 @@ public static class MotionCommand
|
|||
public const uint SideStepLeft = 0x65000010u;
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public const uint Jump = 0x2500003Bu;
|
||||
/// <summary>
|
||||
/// 0x10000050 — FallDown (Action; the landing animation played after
|
||||
/// a jump. Enqueued via <see cref="AnimationSequencer.PlayAction"/>).
|
||||
/// </summary>
|
||||
public const uint FallDown = 0x10000050u;
|
||||
/// <summary>0x10000057 — Dead.</summary>
|
||||
public const uint Dead = 0x10000057u;
|
||||
/// <summary>0x41000011 — Crouch lower bound for blocked-jump check.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue