126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
/**
|
||
* 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, { carried: 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);
|
||
});
|
||
});
|