fix(anim): walk-backward leg-twitch + jump-too-low — port ACE negative-speed link path + faster charge

Two animation/movement issues from live verification:

1. Walk-backward leg twitches forward two times before the cycle
   reverses (X key glitch).

   Root cause: AnimationSequencer.GetLink only implemented the
   forward-direction lookup path. ACE's MotionTable.get_link
   (MotionTable.cs:395-426) takes BOTH the substate and the new
   motion's speeds, and switches lookup branches when EITHER speed
   is negative:
     * Forward path: Links[(style<<16) | substate][motion]
     * Reversed path (any negative speed): Links[(style<<16) |
                                            motion][substate]
   For Ready → WalkBackward we adjust_motion to WalkForward at
   speed -0.65 (negative). Our previous code looked up
   Links[Ready][WalkForward] — the "start walking forward"
   transition. Played in reverse, the cursor stranded at the
   wrong cycle frame and produced the user-visible "left leg
   twitches forward two times" before the cycle stabilized.
   With the reversed key Links[WalkForward][Ready] (the "stop
   walking → ready" anim) played at the cycle's negative speed,
   the link smoothly transitions Ready → start-of-cycle, then
   the cycle reverses cleanly.

   GetLink signature changed from (style, fromMotion, toMotion)
   to (style, substate, substateSpeed, motion, speed). Both
   call sites updated: SetCycle passes CurrentSpeedMod +
   adjustedSpeed; the Action-overlay path passes 1f, 1f
   (action overlays are always forward).

2. Jump too low.

   Two changes after deep investigation in named-retail decomp:

   a) Charge rate sped up from 1.0/s → 2.0/s. Retail's PowerBar
      charge constant is illegible in the named decomp (the
      divisor was clobbered in GetPowerBarLevel's FPU stack
      reordering at 0x0056ade0). 2.0/s (full charge in 0.5s)
      matches retail muscle memory better — a tap gives a
      noticeable hop, half-hold a meaningful jump, full-hold
      the maximum.

   b) Default jumpSkill bumped 200 → 300. Retail formula:
        height = (skill / (skill + 1300)) × 22.2 + 0.05
      At extent=1.0:
        skill=200 → 3.01m max (felt too low)
        skill=300 → 4.21m max (closer to retail mid-tier "I
                               can clear that fence" hop)
      Override via ACDREAM_JUMP_SKILL env var.

      Long-term fix is issue #7 — parsing PlayerDescription's
      skill block to apply the server's authoritative skill
      values. Until then, this default is the right baseline.

      (Velocity formula sqrt(height × 19.6) is unchanged and
      matches retail byte-for-byte; we only changed how much
      extent-feeding skill we default to.)

Tests stay 1222 green. The walk-backward fix has no new test
because GetLink is private; the cycle-transition behavior
will be exercised live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 15:46:10 +02:00
parent 0ecd4f34ae
commit 32583cdfe4
2 changed files with 88 additions and 23 deletions

View file

@ -130,7 +130,15 @@ public sealed class PlayerMovementController
// Jump charge state.
private bool _jumpCharging;
private float _jumpExtent;
private const float JumpChargeRate = 1.0f; // 0→1 over 1 second
// K-fix6 (2026-04-26): retail's PowerBar charge constant for jump is
// not legible in the named decomp (the divisor was clobbered in
// GetPowerBarLevel's FPU stack reordering at FUN_0056ade0). 2.0/s
// (full charge in 0.5s) feels matches retail muscle memory better
// than the previous 1.0/s — a tap gives a noticeable hop, half-hold
// a meaningful jump, full-hold the maximum extent. The vertical
// velocity formula itself (height × 19.6 → vz) is unchanged and
// matches retail byte-for-byte; only the time-to-fill is faster.
private const float JumpChargeRate = 2.0f;
// Airborne → grounded transition detection. Flipped on every frame where
// the body transitions from airborne to on-walkable; used by the GameWindow
@ -159,12 +167,19 @@ public sealed class PlayerMovementController
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
};
// Default skills — tuned toward mid-retail feel (jump ≈ 3m at full charge,
// run rate ≈ 2.4x). Real characters' skills come from PlayerDescription
// (0xF7B0/0x0013) which we don't parse yet; override via env vars:
// Default skills — tuned toward mid-retail feel. Real characters'
// skills come from PlayerDescription (0xF7B0/0x0013) which we don't
// parse yet; override via env vars:
// ACDREAM_RUN_SKILL, ACDREAM_JUMP_SKILL
// K-fix6 (2026-04-26): bumped default jump skill from 200 → 300.
// Retail formula: height = (skill/(skill+1300))*22.2 + 0.05 (extent=1):
// skill=200 → 3.01m max (felt too low — user complaint)
// skill=300 → 4.21m max (closer to a typical retail mid-tier
// character's "I can clear that fence" hop)
// Until #7 ships and PlayerDescription gives us the server's real
// skill, this default is the right "feels like retail" baseline.
int runSkill = int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_RUN_SKILL"), out var rs) ? rs : 200;
int jumpSkill = int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_JUMP_SKILL"), out var jsv) ? jsv : 200;
int jumpSkill = int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_JUMP_SKILL"), out var jsv) ? jsv : 300;
_weenie = new PlayerWeenie(runSkill: runSkill, jumpSkill: jumpSkill);
_motion = new MotionInterpreter(_body, _weenie);
}