fix(anim): Phase K live-test fixes pt5 — backward + strafe animation cycle scales with Run

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 14:59:35 +02:00
parent 7d2bc8cb15
commit 0ecd4f34ae
2 changed files with 36 additions and 11 deletions

View file

@ -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);