/** * A game is more than one scene. `Load scene` swaps the running one for another inside the same * run: the scene is rebuilt from scratch, while what belongs to the run – the shared variables and * the global machines – carries on. Where a run *begins* is a second question, and the project * answers it separately from whatever scene is open in the editor. */ import { describe, expect, it } from 'vitest'; import { CreateAssetCommand, SetActiveSceneCommand, SetStartSceneCommand, } from '../../src/core/commands/assetCommands'; import { CommandError } from '../../src/core/commands/Command'; import { startScene } from '../../src/core/model/Project'; import { addGameSystems } from '../../src/runtime/systems/BehaviorSystem'; import { Input } from '../../src/runtime/systems/Input'; import { World } from '../../src/runtime/World'; import { graphEditor, makeBehavior, makeSpriteScene, makeVariables, place, roundTrip, startWorld, type SceneFixture, } from '../helpers'; /** The sprite scene plus a second scene, and the id of that second one. */ function twoScenes(): SceneFixture & { otherId: string } { const f = makeSpriteScene('Box'); const second = new CreateAssetCommand('scene', 'Level 2'); f.stack.execute(second); return { ...f, otherId: second.asset.id }; } describe('the start scene', () => { it('is what a run begins with, and falls back to the active scene while none is named', () => { const f = twoScenes(); expect(f.project.startSceneId).toBe(''); expect(startScene(f.project)?.id).toBe(f.sceneId); f.stack.execute(new SetStartSceneCommand(f.otherId)); f.stack.execute(new SetActiveSceneCommand(f.sceneId)); expect(startScene(f.project)?.id).toBe(f.otherId); }); it('can be taken back, and reads as none once the scene it names is gone', () => { const f = twoScenes(); f.stack.execute(new SetStartSceneCommand(f.otherId)); f.stack.execute(new SetStartSceneCommand('')); expect(f.project.startSceneId).toBe(''); f.stack.undo(); expect(f.project.startSceneId).toBe(f.otherId); f.project.startSceneId = 'no-such-scene'; expect(startScene(f.project)?.id).toBe(f.project.activeSceneId); }); it('refuses an asset that is not a scene', () => { const f = twoScenes(); expect(() => f.stack.execute(new SetStartSceneCommand(f.spriteId))).toThrow(CommandError); }); it('survives a round trip and drops an id no scene answers to', () => { const f = twoScenes(); f.stack.execute(new SetStartSceneCommand(f.otherId)); expect(roundTrip(f.project).startSceneId).toBe(f.otherId); f.project.startSceneId = 'gone'; expect(roundTrip(f.project).startSceneId).toBe(''); }); }); describe('Load scene', () => { /** A behavior asking for `sceneId` on start, placed below the box. */ function asking(f: SceneFixture, sceneId: string) { const behavior = makeBehavior(f, 'Logic'); const box = place(f, f.spriteId, { x: 0, y: 0 }); place(f, behavior.id, { x: 0, y: 0 }, box); const edit = graphEditor(f, behavior); const start = edit.addNode('OnStart'); const load = edit.addNode('LoadScene', { scene: sceneId }); edit.connect(start, 'exec', load, 'exec'); return { behavior, load }; } it('asks the world for the scene, once the frame is over rather than mid-flow', () => { const f = twoScenes(); asking(f, f.otherId); const { world, frame } = startWorld(f, { behaviors: true }); frame(); // Still standing in the old world: the swap is the caller's to make, once the tick is over. expect(world.loadSceneRequested).toBe(f.otherId); expect(world.entities.some((e) => e.name === 'Box')).toBe(true); }); it('asks for nothing when the id names no scene, and says so in the log', () => { const f = twoScenes(); asking(f, 'no-such-scene'); const { world, frame } = startWorld(f, { behaviors: true }); frame(); expect(world.loadSceneRequested).toBe(''); expect(world.log.some((line) => line.startsWith('Load scene: no scene'))).toBe(true); }); /** The point of the whole exercise: a score that survives the next level. */ it('carries the shared variables into the world of the next scene', () => { const f = twoScenes(); const vars = makeVariables(f, 'Game', ['score']); const input = new Input(); const first = World.instantiate(f.project, f.sceneId); const behaviors = addGameSystems(first, f.project, input); behaviors.liveVariables(vars.id)!.score = 7; const second = World.instantiate(f.project, f.otherId); const carried = addGameSystems(second, f.project, input, undefined, undefined, behaviors.shared); expect(carried.liveVariables(vars.id)?.score).toBe(7); // A fresh run, by contrast, starts at what the asset declares. const third = World.instantiate(f.project, f.otherId); expect(addGameSystems(third, f.project, input).liveVariables(vars.id)?.score).toBe(0); }); });