From a2c813280e66f9aec36ea1fa64a8cbc33658a4ea Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:16:54 +0530 Subject: [PATCH] fix(missions): keep the replay countdown real-time when the speed changes Moving the replay-speed slider during the pre-launch hold or countdown rescaled the time left to liftoff, so 0.25x turned T-8 into T-32 and 4x into T-2. The speed change now skips a replay that has not lifted off; mission time already flown is still preserved as before. --- src/layers/launches/replay.js | 3 +- src/layers/launches/replay.test.mjs | 47 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/layers/launches/replay.test.mjs diff --git a/src/layers/launches/replay.js b/src/layers/launches/replay.js index 5354996bd..affcba339 100644 --- a/src/layers/launches/replay.js +++ b/src/layers/launches/replay.js @@ -281,7 +281,8 @@ export function createReplay({ state: layerState, services, parts, source }) { ? layerState._replayPausedAtMs : Date.now(); for (const [launchId, startedAt] of layerState._animationStarts) { - if (!Number.isFinite(startedAt)) continue; + // A countdown runs in real time; rescaling it turned T-8 into T-32 at 0.25x. + if (!Number.isFinite(startedAt) || startedAt > now) continue; const elapsedMissionMs = (now - startedAt) * previousSpeed; layerState._animationStarts.set( launchId, diff --git a/src/layers/launches/replay.test.mjs b/src/layers/launches/replay.test.mjs new file mode 100644 index 000000000..4ed8e2ede --- /dev/null +++ b/src/layers/launches/replay.test.mjs @@ -0,0 +1,47 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createState } from './state.js'; +import { createReplay } from './replay.js'; +import { createPaths } from './paths.js'; + +const noop = () => {}; + +function replayRig() { + const state = createState({ + services: { + overlays: { + setOverlayEntries: noop, + setOverlaySourceVisible: noop, + clearOverlaySource: noop, + }, + }, + }); + const parts = {}; + parts.paths = createPaths({ state, parts }); + return { state, replay: createReplay({ state, parts }) }; +} + +test('changing replay speed during the countdown keeps its real-time liftoff', (t) => { + t.mock.timers.enable({ apis: ['Date'], now: 1_000_000 }); + const { state, replay } = replayRig(); + const launch = { launchTime: '2026-07-20T10:00:00Z', timeline: [] }; + state._animationStarts.set('countdown', 1_008_000); // T-8 + state._animationStarts.set('flying', 990_000); // 10 s into the mission + + replay.setReplaySpeed(0.25); + + const startedAt = state._animationStarts.get('countdown'); + assert.equal(startedAt, 1_008_000); + const countdown = replay.replayState( + launch, + startedAt, + 12, + 28, + 5400, + state._replaySpeed, + Date.now(), + ); + assert.equal(countdown.countdownSeconds, 8); + // Mission time already flown is still preserved across the change. + assert.equal(state._animationStarts.get('flying'), 960_000); +});