diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b304d37..26e3ab6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2427,12 +2427,14 @@ public sealed class GameWindow : IDisposable // K-fix10 (2026-04-26): force the Falling animation cycle on // the remote so the legs match the arc. Mirrors the local // player's UpdatePlayerAnimation path which sets - // animCommand = Falling whenever !IsOnGround. Without this, - // the remote's existing locomotion cycle (RunForward, - // Ready, etc.) keeps playing through the jump — body goes - // up but legs stay running. Style is the sequencer's - // current style (NonCombat 0x8000003D for humanoids); cycle - // pace = 1.0 (Falling animation has its own baked rate). + // animCommand = Falling whenever !IsOnGround. + // + // K-fix18 (2026-04-26): pass skipTransitionLink:true so the + // RunForward→Falling transition frames don't play first. + // Without that flag the remote stood still for ~100 ms at + // the start of the jump while the link drained, then + // folded into Falling. Skipping the link makes the pose + // engage immediately on jump start. if (_entitiesByServerGuid.TryGetValue(update.Guid, out var ent) && _animatedEntities.TryGetValue(ent.Id, out var ae) && ae.Sequencer is not null) @@ -2440,7 +2442,8 @@ public sealed class GameWindow : IDisposable uint style = ae.Sequencer.CurrentStyle != 0 ? ae.Sequencer.CurrentStyle : 0x8000003Du; // NonCombat default - ae.Sequencer.SetCycle(style, AcDream.Core.Physics.MotionCommand.Falling, 1.0f); + ae.Sequencer.SetCycle(style, AcDream.Core.Physics.MotionCommand.Falling, 1.0f, + skipTransitionLink: true); } } @@ -5067,7 +5070,16 @@ public sealed class GameWindow : IDisposable { animScale = s; } - ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale); + // K-fix18 (2026-04-26): when transitioning into Falling + // (jump start), skip the link so the legs engage Falling + // immediately. Without this the local player visibly + // stood still for ~100 ms at the start of every jump + // while the RunForward→Falling transition link drained. + // For everything else (Walk → Run, Run → Ready, etc.) we + // keep the link so transitions stay smooth. + bool skipLink = animCommand == AcDream.Core.Physics.MotionCommand.Falling; + ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale, + skipTransitionLink: skipLink); } // Legacy path: update the manual slerp fields (for entities without sequencer) diff --git a/src/AcDream.Core/Physics/AnimationSequencer.cs b/src/AcDream.Core/Physics/AnimationSequencer.cs index 9655ed1..9afe076 100644 --- a/src/AcDream.Core/Physics/AnimationSequencer.cs +++ b/src/AcDream.Core/Physics/AnimationSequencer.cs @@ -322,7 +322,15 @@ public sealed class AnimationSequencer /// MotionCommand style / stance (e.g. NonCombat 0x003D0000). /// Target motion command (e.g. WalkForward 0x45000005). /// Speed multiplier applied to framerates (1.0 = normal). - public void SetCycle(uint style, uint motion, float speedMod = 1f) + /// K-fix18 (2026-04-26): when true, do + /// NOT enqueue the transition-link frames between the previous and + /// new cycle. Used when the caller wants the new cycle to engage + /// instantly — e.g. swapping to Falling on a jump start, where the + /// RunForward→Falling link is a short "stop running" pose that + /// makes the jump look delayed (legs stand still for ~100 ms while + /// the link drains, then fold into Falling). Defaults to false to + /// preserve normal smooth transitions for everything else. + public void SetCycle(uint style, uint motion, float speedMod = 1f, bool skipTransitionLink = false) { // ── adjust_motion: remap left→right / backward→forward variants ─── // ACE MotionInterp.cs:394-428. The MotionTable never stores TurnLeft, @@ -372,9 +380,12 @@ public sealed class AnimationSequencer // 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, CurrentSpeedMod, adjustedMotion, adjustedSpeed) - : null; + // K-fix18: when the caller asked to skip the transition link + // (instant-engage cases like Falling on jump start), force + // linkData to null so only the cycle gets enqueued. + MotionData? linkData = (skipTransitionLink || CurrentMotion == 0) + ? null + : GetLink(style, CurrentMotion, CurrentSpeedMod, adjustedMotion, adjustedSpeed); // Resolve target cycle using the ADJUSTED motion (TurnRight not TurnLeft). int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu)); @@ -384,6 +395,19 @@ public sealed class AnimationSequencer // been played yet (ACE behaviour: non-cyclic anims drain naturally). ClearCyclicTail(); + // K-fix18: when the caller asked for instant-engage, ALSO drain + // any in-flight non-cyclic transition frames from the previous + // cycle. Without this, the old RunForward → ??? link would + // continue draining for ~100 ms before the new Falling cycle + // starts, defeating the "skip the link" intent. + if (skipTransitionLink) + { + _queue.Clear(); + _currNode = null; + _firstCyclic = null; + _framePosition = 0.0; + } + // Clear sequence-wide physics before the rebuild. Retail's // GetObjectSequence calls sequence.clear_physics() before each // add_motion chain (MotionTable.cs L100-L101, L152-L153).