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

@ -173,32 +173,55 @@ public sealed class AnimationSequencer
/// <param name="speedMod">Speed multiplier applied to framerates (1.0 = normal).</param>
public void SetCycle(uint style, uint motion, float speedMod = 1f)
{
// ── adjust_motion: remap left→right variants with negative speed ───
// The AC client's MotionTable has NO cycles for TurnLeft, SideStepLeft,
// or WalkBackward. These are played as their right-side / forward
// equivalents with negative framerate (animation runs backward).
// ACE: MotionInterp.cs:394-428
uint adjustedMotion = motion;
float adjustedSpeed = speedMod;
switch (motion & 0xFFFFu)
{
case 0x000E: // TurnLeft → TurnRight
adjustedMotion = (motion & 0xFFFF0000u) | 0x000Du;
adjustedSpeed *= -1f;
break;
case 0x0010: // SideStepLeft → SideStepRight
adjustedMotion = (motion & 0xFFFF0000u) | 0x000Fu;
adjustedSpeed *= -1f;
break;
case 0x0006: // WalkBackward → WalkForward
adjustedMotion = (motion & 0xFFFF0000u) | 0x0005u;
adjustedSpeed *= -0.65f; // BackwardsFactor from ACE
break;
}
// Fast-path: already playing this exact motion at the same speed.
if (CurrentStyle == style && CurrentMotion == motion
&& _firstCyclic != null && _queue.Count > 0)
return;
// Resolve transition link (currentSubstate → newMotion).
// Resolve transition link (currentSubstate → adjustedMotion).
MotionData? linkData = CurrentMotion != 0
? GetLink(style, CurrentMotion, motion)
? GetLink(style, CurrentMotion, adjustedMotion)
: null;
// Resolve target cycle.
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (motion & 0xFFFFFFu));
// Resolve target cycle using the ADJUSTED motion (TurnRight, not TurnLeft).
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
_mtable.Cycles.TryGetValue(cycleKey, out var cycleData);
// Clear the old cyclic tail; keep any non-cyclic head that hasn't
// been played yet (ACE behaviour: non-cyclic anims drain naturally).
ClearCyclicTail();
// Enqueue link frames.
// Enqueue link frames (with adjusted speed for left→right remapping).
if (linkData is { Anims.Count: > 0 })
EnqueueMotionData(linkData, speedMod, isLooping: false);
EnqueueMotionData(linkData, adjustedSpeed, isLooping: false);
// Enqueue new cycle.
if (cycleData is { Anims.Count: > 0 })
{
EnqueueMotionData(cycleData, speedMod, isLooping: true);
EnqueueMotionData(cycleData, adjustedSpeed, isLooping: true);
}
else if (_queue.Count == 0)
{
@ -366,9 +389,23 @@ public sealed class AnimationSequencer
if (low >= numFrames) low = numFrames - 1;
if (high >= numFrames) high = numFrames - 1;
if (low < 0) low = 0;
if (low > high) high = low;
float fr = ad.Framerate * speedMod;
// multiply_framerate: when speed is negative (TurnLeft, SideStepLeft),
// swap Low↔High so the animation plays backward. This is exactly what
// the decompiled FUN_005267E0 does. ACE: AnimData.GetFramerate(speed).
// After swap, LowFrame > HighFrame — the Advance loop handles this
// by checking negative frametime against LowFrame (the higher value).
if (fr < 0f)
{
(low, high) = (high, low);
}
else
{
if (low > high) high = low; // only clamp for positive-speed case
}
return new AnimNode(anim, fr, low, high, isLooping);
}