diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 13bcde7..fbc1425 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -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); } diff --git a/src/AcDream.Core/Physics/AnimationSequencer.cs b/src/AcDream.Core/Physics/AnimationSequencer.cs index 690d9ca..9655ed1 100644 --- a/src/AcDream.Core/Physics/AnimationSequencer.cs +++ b/src/AcDream.Core/Physics/AnimationSequencer.cs @@ -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 }; /// - /// Look up the transition MotionData for going from - /// to within . + /// Look up the transition MotionData for going from + /// (current state, played at ) to + /// (new state, played at ). /// - /// Port of ACE's MotionTable.get_link: - /// 1. Try Links[(style<<16)|(fromMotion&0xFFFFFF)][toMotion] - /// 2. Fallback: try Links[style<<16][toMotion] + /// + /// 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: + /// + /// Both speeds positive (forward → forward, normal case): + /// Look up Links[(style<<16) | substate][motion] — the link FROM + /// substate TO motion. Played forward. + /// Either speed negative (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. + /// + /// /// /// DatReaderWriter encodes Links as Dictionary<int, MotionCommandData> /// where MotionCommandData.MotionData is Dictionary<int, MotionData>. /// - 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;