Fix action drift during freeze states by parking ship and combat bodies

The level-up marquee, jump clip, and ack comm freezes gate world
drivers but not physics: the Arcade world still steps, so coasting
bodies kept gliding through the beat. Add _parkForFreeze() to zero
ship velocity/acceleration and all enemy/ally body motion every
frozen frame, killing any mid-freeze injection on the next tick.
This commit is contained in:
Brian Fertig 2026-09-26 12:08:29 -06:00
parent 098b41b2fc
commit bb0005b5a5
1 changed files with 50 additions and 1 deletions

View File

@ -2627,7 +2627,26 @@ export class GameScene extends Phaser.Scene {
// The level-up marquee's freeze (js/ui/LevelUpMarquee.js): the beat
// owns the screen, so the action holds while it plays — the same
// rule as the ack pause, one frame of the world standing still.
const _frozen = _commsPaused || this.levelMarquee?.active === true;
// The JUMP CLIP's freeze (GameScene._playJumpClip): the transport
// owns the screen (opaque backdrop + the gate video) for the clip's
// length, so the action holds the same way — the ship parks, the
// fight waits, the ticks stand down — and restart() lands the
// destination the moment it ends (the deadlines are time-based, so
// a project that finished mid-clip completes on the first tick
// after, exactly like the ack pause's completion path).
const _frozen =
_commsPaused || this.levelMarquee?.active === true || !!this._jumping;
// The freeze's PARKING (the ack pause's onPause is the same contract
// for its own freeze): while ANY freeze holds, the action is parked
// dead EVERY frame — the ship (target + throttle cleared, velocity
// and acceleration zeroed) and the combat actors' bodies. The Arcade
// world keeps stepping through a freeze (only the drivers —
// ship.update / combat.update — stand down), so anything that still
// injects motion (a coasting body, a collision response, a body that
// was set after the freeze began) is killed the very next frame.
// Resuming a freeze needs no work (the ship stays parked; the next
// input steers it again — the ack pause's same rule).
if (_frozen) this._parkForFreeze();
// The deck goes to STANDBY while an 'ack' comm holds (the pause
// contract's face): the slots dim and stop hovering/pressing, so the
// pulsing ACKNOWLEDGE button is the only live control on screen.
@ -2771,6 +2790,36 @@ export class GameScene extends Phaser.Scene {
if (!_frozen) this.updateDiscovery(_time, delta); // last: sees this frame's final camera view
}
/**
* Park the action for a frozen frame (the level-up beat, the jump
* clip, the ack comm's pause — all three gate on _frozen in update
* above, and all three need this): the ship dead (stop() clears its
* target + throttle and zeros velocity/acceleration), and the combat
* actors' bodies zeroed too. The Arcade world still steps while the
* drivers (ship.update / combat.update) stand down, so un-parked
* bodies would keep gliding through the beat/clip — the visible
* "the action isn't actually paused" drift. Cheap + idempotent, so it
* runs every frozen frame (a velocity set mid-freeze dies next frame);
* resuming a freeze needs no work (the next input steers again).
*/
_parkForFreeze() {
try {
this.ship?.stop?.();
} catch {
/* the ship is optional (a scene mid-setup) — the park stands or falls with it */
}
const c = this.combat;
if (!c) return;
for (const list of [c.enemies, c.allies]) {
for (const a of list ?? []) {
const b = a?.body;
if (!b) continue;
if (typeof b.setVelocity === 'function') b.setVelocity(0, 0);
if (typeof b.setAcceleration === 'function') b.setAcceleration(0, 0);
}
}
}
/**
* The ship met the tether boundary: a small camera kick (impact), and a
* console warning — throttled so a held push against the line doesn't