diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 4d7e278..7eb151c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3448,14 +3448,24 @@ public sealed class GameWindow : IDisposable uint currentMotion = ae.Sequencer!.CurrentMotion; uint lowByte = currentMotion & 0xFFu; + float currentSign = MathF.Sign(ae.Sequencer.CurrentSpeedMod); + if (currentSign == 0f) currentSign = 1f; - // Forward-only refinement scope. WalkForward = 0x05, RunForward = 0x07. - // Sidestep (0x0F/0x10), WalkBackward (0x06), turns and any other - // motion (emote, attack, etc.) are left to UM-driven SetCycle. - const uint LowWalkForward = 0x05u; - const uint LowRunForward = 0x07u; - bool isForward = lowByte == LowWalkForward || lowByte == LowRunForward; - if (!isForward) return; + // Recognised locomotion directions: + // 0x05 (WalkForward) — also encodes WalkBackward via negative speed + // (ACE convention: SidestepCommand= cancel, ForwardCommand= + // WalkForward, ForwardSpeed *= -0.65) + // 0x07 (RunForward) + // 0x0F (SideStepRight) + // 0x10 (SideStepLeft) + // Other motions (Ready, Turn, emotes, attacks) are left to UM-driven SetCycle. + const uint LowWalkForward = 0x05u; + const uint LowRunForward = 0x07u; + const uint LowSideStepRight = 0x0Fu; + const uint LowSideStepLeft = 0x10u; + bool isForwardClass = lowByte == LowWalkForward || lowByte == LowRunForward; + bool isSidestep = lowByte == LowSideStepRight || lowByte == LowSideStepLeft; + if (!isForwardClass && !isSidestep) return; float horizSpeed = MathF.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y); @@ -3468,7 +3478,54 @@ public sealed class GameWindow : IDisposable uint targetMotion; float speedMod; - if (lowByte == LowRunForward) + + if (isSidestep) + { + // Sidestep: motion ID stays the same (SideStepLeft / SideStepRight). + // Retail's wire encoding for sidestep speed buckets uses the same + // motion ID with different SidestepSpeed (slow ≈ 1.25 multiplier, + // fast ≈ 3.0 clamp per ACE MovementData.cs:124-131). On Shift + // toggle while a strafe key is held, retail does NOT broadcast a + // fresh MoveToState (same wire-silence rule as the forward case), + // so observer-side cycle refinement must come from UP-derived + // velocity here. Preserve the sign — SideStepLeft is sometimes + // emitted with negative speedMod by the adjust_motion path. + // + // Magnitude: horizSpeed / WalkAnimSpeed maps the observed speed + // back to a speedMod the sequencer can apply as a framerate + // multiplier. WalkAnimSpeed is the reasonable base because + // sidestep cycles use the WalkAnim equivalent (no separate + // RunSidestep cycle in the dat). + float sideMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed; + sideMag = MathF.Min(MathF.Max( + sideMag, + AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod), + AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod); + targetMotion = currentMotion; + speedMod = sideMag * currentSign; + } + else if (currentSign < 0f) + { + // BACKWARD walk: ACE encodes WalkBackward as `WalkForward` motion + // with NEGATIVE speedMod (MovementData.cs:115 `interpState.ForwardSpeed *= -0.65f`). + // No "RunBackward" motion exists — Shift toggle on backward + // changes only the magnitude of speedMod (slow back ≈ -0.65, + // fast back ≈ -1.91 = -runRate × 0.65). Keep WalkForward motion, + // refine magnitude, preserve negative sign. + // + // Without this branch (the original fix #1), backward refinement + // computed a positive speedMod from horizSpeed and overwrote the + // negative sign, making the legs play forward-walk while the body + // continued moving backward (the rubber-banding the user reported). + float backMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed; + backMag = MathF.Min(MathF.Max( + backMag, + AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod), + AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod); + targetMotion = AcDream.Core.Physics.MotionCommand.WalkForward; + speedMod = -backMag; + } + else if (lowByte == LowRunForward) { if (horizSpeed < PlayerRunDemoteSpeed) { @@ -3489,7 +3546,7 @@ public sealed class GameWindow : IDisposable } else { - // currently WalkForward (0x05) + // currently WalkForward (0x05) with positive speedMod = walking forward. if (horizSpeed > PlayerRunPromoteSpeed) { targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;