diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 81d27ae..0bae242 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7244,6 +7244,27 @@ public sealed class GameWindow : IDisposable // For everything else (Walk → Run, Run → Ready, etc.) we // keep the link so transitions stay smooth. bool skipLink = animCommand == AcDream.Core.Physics.MotionCommand.Falling; + + // #45 (2026-05-06): scale sidestep speedMod to match ACE's + // wire formula. PlayerMovementController hands us a raw + // localAnimSpeed (1.0 slow / runRate fast), but ACE's + // BroadcastMovement converts SidestepSpeed via + // `speed × 3.12 / 1.25 × 0.5` + // (Network/Motion/MovementData.cs:124-131). Without the + // matching multiplier here, the local sidestep cycle plays + // at speedMod = 1.0 while the observer-side cycle plays at + // ~1.248 — local strafe visibly slower than retail (user + // report at #45 close-out of #39). + // Factor = WalkAnimSpeed / SidestepAnimSpeed × 0.5 + // = 3.12 / 1.25 × 0.5 = 1.248. + uint cmdLow = animCommand & 0xFFu; + if (cmdLow == 0x0Fu /* SideStepRight */ || cmdLow == 0x10u /* SideStepLeft */) + { + animSpeed *= AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed + / AcDream.Core.Physics.MotionInterpreter.SidestepAnimSpeed + * 0.5f; + } + ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale, skipTransitionLink: skipLink); }