diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index f629da8a..d46ccf37 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9990,6 +9990,35 @@ public sealed class GameWindow : IDisposable // per-tick shape ordinary locomotion uses. TickRemoteMoveTo(rm); rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false); + + // #170 glide fix (2026-07-04): refresh the body's + // translation velocity from the current interpreted + // state, matching retail. apply_current_movement's + // SINK path (all remotes have a DefaultSink) only + // dispatches the ANIMATION — the + // get_state_velocity → set_local_velocity write + // that retail's apply_current_movement performs + // lives ONLY in acdream's no-sink fallback + // (MotionInterpreter.ApplyCurrentMovementInterpreted, + // ~1702), so a remote's body.Velocity is never + // recomputed and goes STALE. get_state_velocity + // (0x00527d50, already a verbatim port) returns + // WalkForward→3.12×spd, RunForward→4.0×spd, and 0 + // for every other forward command — so when a + // chasing creature's ForwardCommand switches to an + // attack (0x1000006x, action-class), the body + // stops instead of coasting at the last run + // velocity while playing an idle+attack pose (the + // user-reported "glides after me"). Grounded only + // (this branch already asserted Contact+OnWalkable); + // airborne velocity is owned by the jump arc. + // Confirmed vs a live ACDREAM_DUMP_MOTION capture of + // Mite Scamp 0x80000244 (66 mt-0 attack UMs carrying + // ForwardCommand=0x62/63/64 vs 2 mt-6 chase UMs). + if (rm.Body.OnWalkable) + rm.Body.set_local_velocity( + rm.Motion.get_state_velocity(), + rm.Body.LastMoveWasAutonomous); } } else diff --git a/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs b/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs index 45c71889..7df939fc 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs @@ -184,4 +184,34 @@ public sealed class MotionVelocityPipelineTests Assert.Equal(0f, v.X, 5); Assert.Equal(0f, v.Y, 5); } + + // ── attack / non-locomotion forward command (#170) ─────────────────────── + + [Theory] + [InlineData(0x10000062u)] // AttackHigh1 + [InlineData(0x10000063u)] // AttackMed1 + [InlineData(0x10000064u)] // AttackLow1 + [InlineData(0x10000186u)] // AttackHigh4 (shifted late block) + public void AttackForwardCommand_ZeroVelocity(uint attackCommand) + { + // #170: an ATTACK forward command (action-class 0x1000006x/0x100001xx) + // is neither WalkForward (0x45000005) nor RunForward (0x44000007), so + // retail's get_state_velocity (0x00527d50) hits its `else → 0` branch — + // the creature plants its feet instead of coasting at the last run + // velocity. Cross-checked against holtburger grounded_local_velocity + // (`_ => Vector3::zero()`) and the retail decomp. This is the invariant + // the #170 glide fix relies on: GameWindow's remote dead-reckon now + // refreshes the body velocity from get_state_velocity each tick, so a + // creature that switches from a run-chase (ForwardCommand=RunForward) + // to an attack (ForwardCommand=0x1000006x) resolves to zero velocity + // and stops gliding. + var interp = MakeInterp(RunRate); + interp.InterpretedState.ForwardCommand = attackCommand; + interp.InterpretedState.ForwardSpeed = 0.97f; + + var v = interp.get_state_velocity(); + Assert.Equal(0f, v.X, 5); + Assert.Equal(0f, v.Y, 5); + Assert.Equal(0f, v.Z, 5); + } }