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:
parent
0ecd4f34ae
commit
32583cdfe4
2 changed files with 88 additions and 23 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,9 +367,13 @@ public sealed class AnimationSequencer
|
|||
return;
|
||||
}
|
||||
|
||||
// Resolve transition link (currentSubstate → adjustedMotion).
|
||||
// Resolve transition link (currentSubstate → adjustedMotion). Pass
|
||||
// both speeds — GetLink switches lookup branches based on sign.
|
||||
// CurrentSpeedMod defaults to 1.0 (positive) on a fresh sequencer,
|
||||
// so a Ready → WalkBackward transition correctly enters GetLink's
|
||||
// negative-speed (reversed-key) branch.
|
||||
MotionData? linkData = CurrentMotion != 0
|
||||
? GetLink(style, CurrentMotion, adjustedMotion)
|
||||
? GetLink(style, CurrentMotion, CurrentSpeedMod, adjustedMotion, adjustedSpeed)
|
||||
: null;
|
||||
|
||||
// Resolve target cycle using the ADJUSTED motion (TurnRight not TurnLeft).
|
||||
|
|
@ -766,7 +770,10 @@ public sealed class AnimationSequencer
|
|||
if ((motionCommand & ActionMask) != 0 && CurrentMotion != 0)
|
||||
{
|
||||
// Action: look up the transition link from current substate → action.
|
||||
data = GetLink(CurrentStyle, CurrentMotion, motionCommand);
|
||||
// Action overlays always play forward (positive speeds) — the
|
||||
// action speed mod is the caller-supplied modifier, not part of
|
||||
// the substate cycle's direction.
|
||||
data = GetLink(CurrentStyle, CurrentMotion, /*substateSpeed:*/ 1f, motionCommand, /*speed:*/ 1f);
|
||||
}
|
||||
if (data is null && (motionCommand & ModifierMask) != 0)
|
||||
{
|
||||
|
|
@ -855,31 +862,74 @@ public sealed class AnimationSequencer
|
|||
new() { Direction = AnimationHookDir.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Look up the transition MotionData for going from <paramref name="fromMotion"/>
|
||||
/// to <paramref name="toMotion"/> within <paramref name="style"/>.
|
||||
/// Look up the transition MotionData for going from <paramref name="substate"/>
|
||||
/// (current state, played at <paramref name="substateSpeed"/>) to
|
||||
/// <paramref name="motion"/> (new state, played at <paramref name="speed"/>).
|
||||
///
|
||||
/// Port of ACE's MotionTable.get_link:
|
||||
/// 1. Try Links[(style<<16)|(fromMotion&0xFFFFFF)][toMotion]
|
||||
/// 2. Fallback: try Links[style<<16][toMotion]
|
||||
/// <para>
|
||||
/// Port of ACE's MotionTable.get_link (MotionTable.cs:395-426). The lookup
|
||||
/// path differs by sign of the speeds — the retail/ACE mechanism is two
|
||||
/// distinct branches:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Both speeds positive</b> (forward → forward, normal case):
|
||||
/// Look up Links[(style<<16) | substate][motion] — the link FROM
|
||||
/// substate TO motion. Played forward.</item>
|
||||
/// <item><b>Either speed negative</b> (any direction reversal —
|
||||
/// WalkBackward, SideStepLeft, TurnLeft): Look up the REVERSED key
|
||||
/// Links[(style<<16) | motion][substate] — the link FROM motion TO
|
||||
/// substate. Played in reverse, this anim visually transitions
|
||||
/// substate → motion's pose, then the cycle continues from where it
|
||||
/// left off. Without this branch, Ready→WalkBackward would queue the
|
||||
/// "start walking forward" link played in reverse, which strands the
|
||||
/// cursor at the wrong cycle frame and causes the user-visible
|
||||
/// "left leg twitches forward two times" glitch on the X key.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// DatReaderWriter encodes Links as Dictionary<int, MotionCommandData>
|
||||
/// where MotionCommandData.MotionData is Dictionary<int, MotionData>.
|
||||
/// </summary>
|
||||
private MotionData? GetLink(uint style, uint fromMotion, uint toMotion)
|
||||
private MotionData? GetLink(uint style, uint substate, float substateSpeed, uint motion, float speed)
|
||||
{
|
||||
int outerKey1 = (int)((style << 16) | (fromMotion & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(outerKey1, out var cmd1))
|
||||
if (speed < 0f || substateSpeed < 0f)
|
||||
{
|
||||
if (cmd1.MotionData.TryGetValue((int)toMotion, out var result1))
|
||||
return result1;
|
||||
// Reversed-direction path: link FROM motion TO substate.
|
||||
int reversedKey = (int)((style << 16) | (motion & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(reversedKey, out var revLink)
|
||||
&& revLink.MotionData.TryGetValue((int)substate, out var revResult))
|
||||
{
|
||||
return revResult;
|
||||
}
|
||||
|
||||
// Style-defaults fallback per ACE MotionTable.cs:405-409.
|
||||
if (_mtable.StyleDefaults.TryGetValue(
|
||||
(DatReaderWriter.Enums.MotionCommand)style, out var defaultMotion))
|
||||
{
|
||||
int subKey = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(subKey, out var subLink)
|
||||
&& subLink.MotionData.TryGetValue((int)defaultMotion, out var subResult))
|
||||
{
|
||||
return subResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback: style-level catch-all.
|
||||
int outerKey2 = (int)(style << 16);
|
||||
if (_mtable.Links.TryGetValue(outerKey2, out var cmd2))
|
||||
// Forward-direction path: link FROM substate TO motion (the original
|
||||
// implementation pre-K-fix6).
|
||||
int outerKey1 = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(outerKey1, out var cmd1)
|
||||
&& cmd1.MotionData.TryGetValue((int)motion, out var result1))
|
||||
{
|
||||
if (cmd2.MotionData.TryGetValue((int)toMotion, out var result2))
|
||||
return result2;
|
||||
return result1;
|
||||
}
|
||||
|
||||
// Fallback: style-level catch-all (ACE line 419-422).
|
||||
int outerKey2 = (int)(style << 16);
|
||||
if (_mtable.Links.TryGetValue(outerKey2, out var cmd2)
|
||||
&& cmd2.MotionData.TryGetValue((int)motion, out var result2))
|
||||
{
|
||||
return result2;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue