fix(motion): #39 — handle backward sign + sidestep in ApplyPlayerLocomotionRefinement
User report from fix #3 visual verify (commit 2653b30):
- Forward Run↔Walk Shift toggle: WORKS now
- Strafe Shift toggle: no transition (was out of scope)
- "When I shift walk backwards, the retail char gets animated walking
slow forward but blipping backwards" — REGRESSION
Root cause of the backward regression: ACE encodes WalkBackward as
`WalkForward` motion with NEGATIVE speedMod (MovementData.cs:115
`interpState.ForwardSpeed *= -0.65f`). My fix #1's hysteresis branches
treated lowByte 0x05 / 0x07 as "forward" and computed positive speedMod
from horizSpeed, overwriting the negative sign. Result: animation
played forward-walk while body kept moving backward (the rubber-band).
Strafe gap: sidestep (low byte 0x0F / 0x10) wasn't in fix #1's scope,
so ApplyPlayerLocomotionRefinement returned early for sidestep cycles.
Retail does the same wire-silence on Shift toggle for sidestep, so
observer-side cycle refinement must also fire for it.
Fix:
- Probe `currentSign = sign(CurrentSpeedMod)` to detect backward direction
- For sidestep (lowByte 0x0F or 0x10): keep motion ID, refine
speedMod magnitude = horizSpeed / WalkAnimSpeed, preserve sign
- For backward (forward-class lowByte AND currentSign < 0): keep
WalkForward motion (per ACE encoding), refine magnitude, preserve
negative sign — no "RunBackward" motion exists, only |speedMod|
changes between Walk-back (~0.65) and Run-back (~1.91 = runRate × 0.65)
- Forward (currentSign >= 0): existing Walk↔Run hysteresis unchanged
Build clean. Diagnostics: [UPCYCLE_PLAYER] line still prints; the
new sidestep / backward branches use the same SetCycle call so
their decisions appear in [SCFULL] / [CURRNODE] for inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2653b307c7
commit
cc62e1cfde
1 changed files with 66 additions and 9 deletions
|
|
@ -3448,14 +3448,24 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
uint currentMotion = ae.Sequencer!.CurrentMotion;
|
uint currentMotion = ae.Sequencer!.CurrentMotion;
|
||||||
uint lowByte = currentMotion & 0xFFu;
|
uint lowByte = currentMotion & 0xFFu;
|
||||||
|
float currentSign = MathF.Sign(ae.Sequencer.CurrentSpeedMod);
|
||||||
|
if (currentSign == 0f) currentSign = 1f;
|
||||||
|
|
||||||
// Forward-only refinement scope. WalkForward = 0x05, RunForward = 0x07.
|
// Recognised locomotion directions:
|
||||||
// Sidestep (0x0F/0x10), WalkBackward (0x06), turns and any other
|
// 0x05 (WalkForward) — also encodes WalkBackward via negative speed
|
||||||
// motion (emote, attack, etc.) are left to UM-driven SetCycle.
|
// (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 LowWalkForward = 0x05u;
|
||||||
const uint LowRunForward = 0x07u;
|
const uint LowRunForward = 0x07u;
|
||||||
bool isForward = lowByte == LowWalkForward || lowByte == LowRunForward;
|
const uint LowSideStepRight = 0x0Fu;
|
||||||
if (!isForward) return;
|
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);
|
float horizSpeed = MathF.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y);
|
||||||
|
|
||||||
|
|
@ -3468,7 +3478,54 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
uint targetMotion;
|
uint targetMotion;
|
||||||
float speedMod;
|
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)
|
if (horizSpeed < PlayerRunDemoteSpeed)
|
||||||
{
|
{
|
||||||
|
|
@ -3489,7 +3546,7 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// currently WalkForward (0x05)
|
// currently WalkForward (0x05) with positive speedMod = walking forward.
|
||||||
if (horizSpeed > PlayerRunPromoteSpeed)
|
if (horizSpeed > PlayerRunPromoteSpeed)
|
||||||
{
|
{
|
||||||
targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;
|
targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue