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 1/3] 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); +}); From 5d2f6a3ce5bffce34466de1d70a5f06a89366f0c Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:24:25 +0530 Subject: [PATCH 2/3] docs(changelog): add #1444 fragment --- changelog.d/1444-replay-speed-countdown.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1444-replay-speed-countdown.md diff --git a/changelog.d/1444-replay-speed-countdown.md b/changelog.d/1444-replay-speed-countdown.md new file mode 100644 index 000000000..fb35d008b --- /dev/null +++ b/changelog.d/1444-replay-speed-countdown.md @@ -0,0 +1,3 @@ +- Keep Space Missions' replay countdown in real time when the replay speed + changes before liftoff; 0.25× had stretched T−8 to T−32 (Aditya, + @Rohilalala, #1444). From 98173afd6bbd8979fe780c496b987e0d07dd654c Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 22:51:37 -0800 Subject: [PATCH 3/3] test(launches): stub the replay panel the speed control now publishes to On main, setReplaySpeed publishes the speed control through parts.panel.publishPanel, so the replay rig needs a panel stub. --- src/layers/launches/replay.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layers/launches/replay.test.mjs b/src/layers/launches/replay.test.mjs index 4ed8e2ede..b0700d0f3 100644 --- a/src/layers/launches/replay.test.mjs +++ b/src/layers/launches/replay.test.mjs @@ -16,7 +16,7 @@ function replayRig() { }, }, }); - const parts = {}; + const parts = { panel: { publishPanel: noop } }; parts.paths = createPaths({ state, parts }); return { state, replay: createReplay({ state, parts }) }; }