fix(anim): implement adjust_motion — TurnLeft/SideStepLeft play backward

ROOT CAUSE FIX for missing left-side animations.

The AC client's MotionTable has NO cycles for TurnLeft (0x000E),
SideStepLeft (0x0010), or WalkBackward (0x0006). The real client
calls adjust_motion() which remaps these to their right-side
equivalents with NEGATIVE speed before looking up the cycle. Then
multiply_framerate() swaps LowFrame↔HighFrame so the animation
plays backward.

Source: ACE MotionInterp.cs:394-428, decompiled FUN_005267E0.

Changes:
- AnimationSequencer.SetCycle: adds adjust_motion block that remaps
  left→right with speed *= -1 (TurnLeft, SideStepLeft) or
  speed *= -0.65 (WalkBackward = BackwardsFactor)
- LoadAnimNode: when framerate < 0, swaps Low↔High (matching the
  decompiled multiply_framerate)
- GameWindow.UpdatePlayerAnimation: passes original animCommand to
  SetCycle (sequencer handles remapping internally), keeps legacy
  fallback for non-sequencer entities

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-13 12:17:26 +02:00
parent 0e66078e57
commit 67b51a3e6f
3 changed files with 339 additions and 35 deletions

View file

@ -2054,47 +2054,35 @@ public sealed class GameWindow : IDisposable
stanceOverride: NonCombatStance,
commandOverride: cmdOverride);
// AC reuses right-side animations for left-side motions (played in
// reverse). If the left-side command has no cycle, fall back to the
// right-side equivalent so the player isn't stuck in idle.
uint resolvedCommand = animCommand; // track which command actually resolved
// The sequencer handles left→right remapping internally via
// adjust_motion (TurnLeft→TurnRight with negative speed, etc.).
// Pass the ORIGINAL animCommand — SetCycle does the remapping.
if (ae.Sequencer is not null)
{
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
ae.Sequencer.SetCycle(fullStyle, animCommand);
}
// Legacy path fallback: for the non-sequencer slerp path, do the
// left→right remapping here since that path doesn't have adjust_motion.
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame)
{
ushort fallback = cmdOverride switch
{
0x000E => 0x000D, // TurnLeft → TurnRight
0x0010 => 0x000F, // SideStepLeft → SideStepRight
0x0006 => 0x0005, // WalkBackward → WalkForward
0x000E => 0x000D,
0x0010 => 0x000F,
0x0006 => 0x0005,
_ => (ushort)0,
};
if (fallback != 0)
{
cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
ae.Setup, _dats,
motionTableIdOverride: _playerMotionTableId,
stanceOverride: NonCombatStance,
commandOverride: fallback);
// Update resolvedCommand so the sequencer looks up the right cycle
resolvedCommand = (animCommand & 0xFF000000u) | fallback;
}
}
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame)
{
Console.WriteLine($"[ANIM-DIAG] FAILED: animCmd=0x{animCommand:X8} resolved=0x{resolvedCommand:X8} cmdOverride=0x{cmdOverride:X4} cycle={cycle is not null} fr={cycle?.Framerate} lo={cycle?.LowFrame} hi={cycle?.HighFrame}");
return;
}
Console.WriteLine($"[ANIM-DIAG] OK: animCmd=0x{animCommand:X8} resolved=0x{resolvedCommand:X8} hasSeq={ae.Sequencer is not null} fr={cycle.Framerate} lo={cycle.LowFrame} hi={cycle.HighFrame}");
// If the entity has a sequencer, use SetCycle for transition-link-aware
// motion switching. Pass the RESOLVED command (after left→right fallback)
// so the sequencer's internal cycle lookup finds the same animation.
if (ae.Sequencer is not null)
{
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
ae.Sequencer.SetCycle(fullStyle, resolvedCommand);
}
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
ae.Animation = cycle.Animation;
ae.LowFrame = Math.Max(0, cycle.LowFrame);