Until now only the editor toolbar could pause a running game. A pause menu inside the game had no way to stop the world while its own graph kept running, and there was no slow motion. - Five new nodes: Pause game, Resume game, Is game paused, Set time scale and Time scale. Pause and resume take effect on the next frame. - A game pause stops world time, physics, animations, particles and shader time. Only behaviors with the new "Run while paused" switch keep running. - Set time scale 0 freezes the world but graphs keep running, as agreed in the issue. - Play and Stop reset the time scale to 1, Load scene keeps it. The pause belongs to the scene, so Load scene and Restart unpause. - The toolbar pause stays separate and also freezes the menu. When the game pauses itself the status line reads "The game paused itself". - While paused, collision events are not reported, sounds keep playing, and the editor run ignores both nodes with a log line. Findings: - The first status line was longer than any before and got cut with an ellipsis. It is shorter now. - The e2e harness did not know the key "p". It does now. Tests: - ./check.sh: 133 files, 1968 tests, exit 0 (before 132 / 1952) - npm run build: exit 0 - GM_STEPS=18 fresh: ALL PASSED, 33 checks (before 26) - 16 new unit tests: 8 catalog cases, game-pause.test.ts, editor run - 7 new e2e checks in block 18i, driven by real keys in the game - Six counter-checks all bite: behavior filter, collision lock, world time, editor run, clock reset and the game clock in the browser - Check total 1217 in README is computed (1210 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
/**
|
||
* The one clock everything time-based reads: the world's delta, particle age and shader time. It
|
||
* accumulates scaled frames instead of reading the wall clock, which is what makes a global time
|
||
* scale a single multiplication – a paused game is scale 0, and nothing underneath has to be
|
||
* rebased because the clock itself stands still.
|
||
*/
|
||
|
||
/** A frame longer than this (a breakpoint, a hidden tab) is treated as this long. */
|
||
export const MAX_FRAME = 0.1;
|
||
|
||
/**
|
||
* The scale that actually applies: a pause is a scale of 0, and so is anything that is not a
|
||
* positive number – time never runs backwards.
|
||
*/
|
||
export function effectiveScale(scale: number, paused: boolean): number {
|
||
if (paused || !Number.isFinite(scale) || scale <= 0) return 0;
|
||
return scale;
|
||
}
|
||
|
||
/** How far the clock moves in one frame. */
|
||
export function timeStep(dt: number, scale: number, paused: boolean): number {
|
||
return Math.min(Math.max(dt, 0), MAX_FRAME) * effectiveScale(scale, paused);
|
||
}
|
||
|
||
export class GameClock {
|
||
private seconds = 0;
|
||
/** The global time scale: 1 is real time, 2 twice as fast, 0 frozen. */
|
||
scale = 1;
|
||
|
||
/** Seconds the clock has run, in its own scaled time. */
|
||
get now(): number {
|
||
return this.seconds;
|
||
}
|
||
|
||
/** Moves the clock on and returns the step, which is the delta every ticked world gets. */
|
||
advance(dt: number, paused = false): number {
|
||
const step = timeStep(dt, this.scale, paused);
|
||
this.seconds += step;
|
||
return step;
|
||
}
|
||
|
||
/**
|
||
* Back to second zero and real time – a run starts there and leaves nothing behind (#167).
|
||
* Anything holding an absolute second of this clock has to start over.
|
||
*/
|
||
reset(): void {
|
||
this.seconds = 0;
|
||
this.scale = 1;
|
||
}
|
||
}
|
||
|
||
export const gameClock = new GameClock();
|