Files
feuerwerk/tests/core/load-scene.test.ts
T

126 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});