From 0ecd4f34ae852d6270ffe4e57a94e091a47d89b2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Apr 2026 14:59:35 +0200 Subject: [PATCH] =?UTF-8?q?fix(anim):=20Phase=20K=20live-test=20fixes=20pt?= =?UTF-8?q?5=20=E2=80=94=20backward=20+=20strafe=20animation=20cycle=20sca?= =?UTF-8?q?les=20with=20Run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: when running backward (X) or strafing (Z/C) at run speed, the visual moves faster but the animation cycle continues playing at walk pace, looking disjointed. Root cause: GameWindow's player-anim driver fed the sequencer's SetCycle speed from result.ForwardSpeed, but PlayerMovementController intentionally pins ForwardSpeed = 1.0 for WalkBackward (ACE expects this for the auto-upgrade) and SidestepSpeed isn't used by the anim path at all. So Forward+Run played the RunForward cycle at runRate × (correct), but Backward+Run + Strafe+Run used speedMod = 1.0 even though the body was moving at runRate × velocity. Fix: split the visual-pacing field from the wire-correctness field. Added MovementResult.LocalAnimationSpeed — runRate when any directional input is held with Run, else 1.0. GameWindow's SetCycle path now uses this instead of ForwardSpeed. The wire output stays unchanged; only the local animation cycle pace shifts. Effect: - Forward+Run: runRate × cycle pace (unchanged behavior). - Backward+Run: runRate × cycle pace (was 1×; now matches velocity). - Strafe+Run: runRate × cycle pace (was 1×; now matches velocity). - Anything not in Run: 1× (unchanged). Tests stay 1222 green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Input/PlayerMovementController.cs | 20 ++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 27 +++++++++++-------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index e2d0c55..13bcde7 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -52,6 +52,13 @@ public readonly record struct MovementResult( float? TurnSpeed, bool IsRunning = false, uint? LocalAnimationCommand = null, // which cycle to play on the local player (RunForward when running) + // K-fix5 (2026-04-26): cycle-pace multiplier for the LOCAL animation + // sequencer. Decoupled from ForwardSpeed so the wire can keep sending + // 1.0 for WalkBackward (ACE-compatible) while the animation plays at + // runRate × so the cycle visually matches the run-speed velocity. + // Forward+Run = runRate (same as ForwardSpeed); Backward+Run, Strafe+Run + // = runRate (where ForwardSpeed is 1.0 / null); everything else = 1.0. + float LocalAnimationSpeed = 1f, bool JustLanded = false, // true on the single frame we transitioned airborne → grounded float? JumpExtent = null, // non-null when a jump was triggered this frame Vector3? JumpVelocity = null); // world-space launch velocity (sent in jump packet) @@ -556,6 +563,18 @@ public sealed class PlayerMovementController HeartbeatDue = false; } + // K-fix5 (2026-04-26): local-animation-cycle pacing. Visual rate + // should match the actual movement speed. For Forward+Run this is + // already runRate (it equals ForwardSpeed). For Backward+Run and + // Strafe+Run it must be runRate too even though the wire keeps + // those at 1.0. Picking runMul (already computed above) keeps the + // math in one place. + bool anyDirectional = input.Forward || input.Backward + || input.StrafeLeft || input.StrafeRight; + float localAnimSpeed = (input.Run && anyDirectional) + ? (_weenie.InqRunRate(out float vrrAnim) ? vrrAnim : 1f) + : 1f; + return new MovementResult( Position: Position, CellId: CellId, @@ -569,6 +588,7 @@ public sealed class PlayerMovementController TurnSpeed: outTurnSpeed, IsRunning: input.Run && input.Forward, LocalAnimationCommand: localAnimCmd, + LocalAnimationSpeed: localAnimSpeed, JustLanded: justLanded, JumpExtent: outJumpExtent, JumpVelocity: outJumpVelocity); diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ddbfb34..48c0326 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -4726,7 +4726,12 @@ public sealed class GameWindow : IDisposable // command is unchanged but speed changed, we must still propagate // so the sequencer can MultiplyCyclicFramerate — keeping the run // loop smooth without restart. - float newSpeed = result.ForwardSpeed ?? 1f; + // K-fix5 (2026-04-26): use LocalAnimationSpeed (cycle pace) NOT + // ForwardSpeed (wire field) — backward+run + strafe+run keep + // ForwardSpeed/SidestepSpeed at 1.0 for ACE compatibility but + // need the local cycle to play at runRate × so the animation + // matches the actual movement velocity. + float newSpeed = result.LocalAnimationSpeed; bool sameCmd = animCommand == _playerCurrentAnimCommand; bool sameSpeed = MathF.Abs(newSpeed - _playerCurrentAnimSpeed) < 1e-3f; if (sameCmd && sameSpeed) return; @@ -4781,19 +4786,19 @@ public sealed class GameWindow : IDisposable // Sequencer path: SetCycle handles adjust_motion internally // (TurnLeft→TurnRight with negative speed, etc.) // - // Speed scaling: use the MovementResult's ForwardSpeed for - // locomotion cycles. This mirrors what the server broadcasts for - // remote observers, and keeps our own character's animation rate - // in sync with movement velocity (a 1.5× RunRate player's anim - // plays 1.5× as fast — matching retail). + // Speed scaling: K-fix5 (2026-04-26) — use LocalAnimationSpeed + // (the PlayerMovementController-computed cycle pace) instead of + // the wire ForwardSpeed. Forward+Run = runRate; Backward+Run = + // runRate (where ForwardSpeed is the ACE-compatible 1.0); + // Strafe+Run = runRate (where SidestepSpeed is 1.0). Anything + // not in run = 1.0. The animation cycle now visually matches + // the movement velocity in every direction. if (ae.Sequencer is not null) { uint fullStyle = 0x80000000u | (uint)NonCombatStance; - float animSpeed = 1f; - if (result.ForwardSpeed is { } fs && fs > 0f) - { - animSpeed = fs; - } + float animSpeed = result.LocalAnimationSpeed > 0f + ? result.LocalAnimationSpeed + : 1f; // ACDREAM_ANIM_SPEED_SCALE: optional visual-pacing knob. Retail's // animation framerate scales linearly with speedMod (r03 §8.3), // and our speedMod = runRate. If the visual feel doesn't match