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:
parent
7d2bc8cb15
commit
0ecd4f34ae
2 changed files with 36 additions and 11 deletions
|
|
@ -52,6 +52,13 @@ public readonly record struct MovementResult(
|
||||||
float? TurnSpeed,
|
float? TurnSpeed,
|
||||||
bool IsRunning = false,
|
bool IsRunning = false,
|
||||||
uint? LocalAnimationCommand = null, // which cycle to play on the local player (RunForward when running)
|
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
|
bool JustLanded = false, // true on the single frame we transitioned airborne → grounded
|
||||||
float? JumpExtent = null, // non-null when a jump was triggered this frame
|
float? JumpExtent = null, // non-null when a jump was triggered this frame
|
||||||
Vector3? JumpVelocity = null); // world-space launch velocity (sent in jump packet)
|
Vector3? JumpVelocity = null); // world-space launch velocity (sent in jump packet)
|
||||||
|
|
@ -556,6 +563,18 @@ public sealed class PlayerMovementController
|
||||||
HeartbeatDue = false;
|
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(
|
return new MovementResult(
|
||||||
Position: Position,
|
Position: Position,
|
||||||
CellId: CellId,
|
CellId: CellId,
|
||||||
|
|
@ -569,6 +588,7 @@ public sealed class PlayerMovementController
|
||||||
TurnSpeed: outTurnSpeed,
|
TurnSpeed: outTurnSpeed,
|
||||||
IsRunning: input.Run && input.Forward,
|
IsRunning: input.Run && input.Forward,
|
||||||
LocalAnimationCommand: localAnimCmd,
|
LocalAnimationCommand: localAnimCmd,
|
||||||
|
LocalAnimationSpeed: localAnimSpeed,
|
||||||
JustLanded: justLanded,
|
JustLanded: justLanded,
|
||||||
JumpExtent: outJumpExtent,
|
JumpExtent: outJumpExtent,
|
||||||
JumpVelocity: outJumpVelocity);
|
JumpVelocity: outJumpVelocity);
|
||||||
|
|
|
||||||
|
|
@ -4726,7 +4726,12 @@ public sealed class GameWindow : IDisposable
|
||||||
// command is unchanged but speed changed, we must still propagate
|
// command is unchanged but speed changed, we must still propagate
|
||||||
// so the sequencer can MultiplyCyclicFramerate — keeping the run
|
// so the sequencer can MultiplyCyclicFramerate — keeping the run
|
||||||
// loop smooth without restart.
|
// 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 sameCmd = animCommand == _playerCurrentAnimCommand;
|
||||||
bool sameSpeed = MathF.Abs(newSpeed - _playerCurrentAnimSpeed) < 1e-3f;
|
bool sameSpeed = MathF.Abs(newSpeed - _playerCurrentAnimSpeed) < 1e-3f;
|
||||||
if (sameCmd && sameSpeed) return;
|
if (sameCmd && sameSpeed) return;
|
||||||
|
|
@ -4781,19 +4786,19 @@ public sealed class GameWindow : IDisposable
|
||||||
// Sequencer path: SetCycle handles adjust_motion internally
|
// Sequencer path: SetCycle handles adjust_motion internally
|
||||||
// (TurnLeft→TurnRight with negative speed, etc.)
|
// (TurnLeft→TurnRight with negative speed, etc.)
|
||||||
//
|
//
|
||||||
// Speed scaling: use the MovementResult's ForwardSpeed for
|
// Speed scaling: K-fix5 (2026-04-26) — use LocalAnimationSpeed
|
||||||
// locomotion cycles. This mirrors what the server broadcasts for
|
// (the PlayerMovementController-computed cycle pace) instead of
|
||||||
// remote observers, and keeps our own character's animation rate
|
// the wire ForwardSpeed. Forward+Run = runRate; Backward+Run =
|
||||||
// in sync with movement velocity (a 1.5× RunRate player's anim
|
// runRate (where ForwardSpeed is the ACE-compatible 1.0);
|
||||||
// plays 1.5× as fast — matching retail).
|
// 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)
|
if (ae.Sequencer is not null)
|
||||||
{
|
{
|
||||||
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
||||||
float animSpeed = 1f;
|
float animSpeed = result.LocalAnimationSpeed > 0f
|
||||||
if (result.ForwardSpeed is { } fs && fs > 0f)
|
? result.LocalAnimationSpeed
|
||||||
{
|
: 1f;
|
||||||
animSpeed = fs;
|
|
||||||
}
|
|
||||||
// ACDREAM_ANIM_SPEED_SCALE: optional visual-pacing knob. Retail's
|
// ACDREAM_ANIM_SPEED_SCALE: optional visual-pacing knob. Retail's
|
||||||
// animation framerate scales linearly with speedMod (r03 §8.3),
|
// animation framerate scales linearly with speedMod (r03 §8.3),
|
||||||
// and our speedMod = runRate. If the visual feel doesn't match
|
// and our speedMod = runRate. If the visual feel doesn't match
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue