/** * One run of a game: the world of the scene it is in and the systems that play it. Shared by the * editor's Play and a released game (#266), so both build, swap and end a scene the same way. */ import type { SaveArea } from '../core/graph/savedValues'; import type { Project } from '../core/model/Project'; import type { AudioSink } from './systems/Audio'; import { addGameSystems, type BehaviorSystem, type TimeScaled } from './systems/BehaviorSystem'; import type { Input } from './systems/Input'; import type { Rapier } from './physics/engine'; import { World } from './World'; export interface RunServices { audio: AudioSink; physics: Rapier; clock: TimeScaled; saves: SaveArea; } export class GameRun { private constructor( private readonly project: Project, private readonly input: Input, private readonly services: RunServices, public world: World, public behaviors: BehaviorSystem, ) {} static start(project: Project, sceneId: string, input: Input, services: RunServices): GameRun { const world = World.instantiate(project, sceneId); return new GameRun(project, input, services, world, addGameSystems(world, project, input, services)); } /** * One frame. `awake` is the step the clock would have moved without the game's own pause, which * stops the clock but not the behaviors that run while paused. A `Load scene` is carried out here; * stopping and restarting are the host's business, so they are only reported. */ tick(step: number, awake: number): 'stop' | 'restart' | 'scene' | null { const world = this.world; world.tick(world.paused ? awake : step); this.input.endFrame(); if (world.stopRequested) return 'stop'; if (world.restartRequested) return 'restart'; if (!world.loadSceneRequested) return null; this.loadScene(world.loadSceneRequested); return 'scene'; } /** * Swaps the running scene for another one, inside the same run: the world is torn down and built * again, while what belongs to the run – the clock, the shared variables, the global machines – * carries on. The sounds of the old scene end with it, because the behaviors playing them are * destroyed; a voice that was started detached is nobody's and plays on. */ loadScene(sceneId: string): void { const carried = this.behaviors.shared; this.world.destroy(); this.world = World.instantiate(this.project, sceneId); this.behaviors = addGameSystems(this.world, this.project, this.input, { ...this.services, carried }); } }