game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays.
246 lines
9.6 KiB
JavaScript
246 lines
9.6 KiB
JavaScript
import { edge, node, port, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* More than one scene (#39): the project names the scene a run begins with – which is not the same
|
||
* question as which scene is open in the editor – and `Load scene` swaps the running one for
|
||
* another from inside a graph.
|
||
*
|
||
* What the swap keeps is the point: the scene is built from scratch, while what belongs to the
|
||
* *run* carries on. A Variables asset is the witness, because a fresh run would put it back to
|
||
* what it declares. The sounds of the old scene end with it – not by a rule of their own, but
|
||
* because the behaviors playing them are destroyed like everything else in that scene.
|
||
*/
|
||
|
||
const SOURCE = { variablesAsset: 'ls-vars' };
|
||
|
||
/** `On start → Variables{set score = score + 1}`, plus `On key down g → Load scene`. */
|
||
const graphFor = (sceneId, soundId) => ({
|
||
nodes: [
|
||
node('ls-start', 'OnStart', 0, 0),
|
||
node('ls-read', 'Variables', 0, 200, SOURCE, [port('score', 'number')]),
|
||
node('ls-add', 'Add', 260, 200, { b: 1 }),
|
||
node('ls-write', 'Variables', 560, 0, SOURCE, [written('score', 'number')]),
|
||
node('ls-play', 'PlaySound', 820, 0, { sound: soundId, volume: 1, loop: 'true' }),
|
||
node('ls-key', 'OnKeyDown', 0, 420, { key: 'g' }),
|
||
node('ls-load', 'LoadScene', 260, 420, { scene: sceneId }),
|
||
],
|
||
edges: [
|
||
edge('ls-e1', 'ls-start', 'exec', 'ls-write', 'exec'),
|
||
edge('ls-e2', 'ls-read', 'score', 'ls-add', 'a'),
|
||
edge('ls-e3', 'ls-add', 'result', 'ls-write', 'score'),
|
||
edge('ls-e4', 'ls-write', 'exec', 'ls-play', 'exec'),
|
||
edge('ls-e5', 'ls-key', 'exec', 'ls-load', 'exec'),
|
||
],
|
||
});
|
||
|
||
export async function loadScene(t) {
|
||
const { check, sleep, json, evaluate, mutate, click, clickText, locate, press, play, stop, shot, has } = t;
|
||
const playInfo = () => json('__gm.playInfo()');
|
||
const playFrom = () => json('__gm.playFrom()');
|
||
/** The switch is opened by position: its caption is elided at the width it has. */
|
||
const openPlayFrom = async () => {
|
||
const at = await json('__gm.playFromAt()');
|
||
await click(at.x, at.y);
|
||
await sleep(250);
|
||
};
|
||
const stats = () => json('__gm.sound.stats()');
|
||
const waitFor = async (read, done, tries = 25) => {
|
||
let value = await read();
|
||
for (let i = 0; i < tries && !done(value); i++) {
|
||
await sleep(150);
|
||
value = await read();
|
||
}
|
||
return value;
|
||
};
|
||
|
||
await press('Escape');
|
||
await evaluate('__gm.state.select(null)');
|
||
await evaluate("__gm.showTab('scene')");
|
||
await sleep(200);
|
||
|
||
const firstId = await json('__gm.state.project.activeSceneId');
|
||
const firstName = await json('__gm.state.activeScene.name');
|
||
|
||
// A sound to have something playing across the swap; the seed put the files on the server.
|
||
const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`);
|
||
const file = stored[0]?.file ?? '';
|
||
|
||
// 35a. A second scene with a box of its own, a Variables asset, and the behavior that counts and
|
||
// loads. Everything lives in the new scene, so nothing later in the suite sees any of it.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
${MAKE_OBJECT}
|
||
const second = structuredClone(scene);
|
||
second.id = 'ls-scene';
|
||
second.name = 'Level 2';
|
||
second.objects = [
|
||
make('ls-box', 'crate', '', 0, 'Second box'),
|
||
make('ls-brain', 'ls-logic', 'ls-box', 0, 'Loader'),
|
||
];
|
||
const logic = structuredClone(p.assets.find((a) => a.id === 'mover'));
|
||
Object.assign(logic, {
|
||
id: 'ls-logic',
|
||
name: 'Loader',
|
||
props: { enabled: true, runInEditor: false },
|
||
variables: [],
|
||
graph: ${JSON.stringify(graphFor(firstId, 'ls-sound'))},
|
||
});
|
||
p.assets.push(logic, second, {
|
||
id: 'ls-vars',
|
||
type: 'variables',
|
||
name: 'Run',
|
||
props: { score: 0 },
|
||
variables: ${JSON.stringify([variable('score', 'number', 0)])},
|
||
}, {
|
||
id: 'ls-sound',
|
||
type: 'sound',
|
||
name: 'Hum',
|
||
props: { volume: 1, loop: true },
|
||
file: ${JSON.stringify(file)},
|
||
});`);
|
||
await sleep(400);
|
||
|
||
// 35b. Without a start scene the switch says the active scene – the two questions have the same
|
||
// answer until someone answers them differently.
|
||
check(
|
||
'with no start scene named, Play starts from the active one',
|
||
(await json('__gm.state.project.startSceneId')) === '' &&
|
||
(await playFrom()).mode === 'start' &&
|
||
(await playFrom()).scene === firstName &&
|
||
// The caption is the mode, worded like the menu entry it came from.
|
||
(await playFrom()).label === 'Start scene',
|
||
JSON.stringify(await playFrom()),
|
||
);
|
||
|
||
// 35c. Naming one: right-click the scene in the assets panel.
|
||
await clickText('Level 2', 'right');
|
||
await sleep(250);
|
||
await clickText('Set as start scene');
|
||
await sleep(300);
|
||
check(
|
||
'the scene’s context menu names it the start scene, and the switch follows',
|
||
(await json('__gm.state.project.startSceneId')) === 'ls-scene' && (await playFrom()).scene === 'Level 2',
|
||
JSON.stringify(await playFrom()),
|
||
);
|
||
check(
|
||
'the assets panel marks the start scene apart from the active one',
|
||
await has('scene ▶'),
|
||
JSON.stringify(
|
||
await json('__gm.state.project.assets.filter((a) => a.type === "scene").map((a) => a.name)'),
|
||
),
|
||
);
|
||
await shot('35-start-scene');
|
||
|
||
// 35d. Play starts there rather than in the scene the editor is showing.
|
||
await play();
|
||
const started = await playInfo();
|
||
check(
|
||
'Play runs the start scene while the editor still shows the active one',
|
||
started.playing === true &&
|
||
started.sceneId === 'ls-scene' &&
|
||
(await json('__gm.state.project.activeSceneId')) === firstId,
|
||
JSON.stringify(started),
|
||
);
|
||
|
||
// 35d². The scene selector keeps working while the run is on. It only moves the editor's view,
|
||
// which a double-click on a scene in the Assets panel has always done – disabling the one
|
||
// and not the other was the inconsistency. The run itself is untouched by it.
|
||
const select = await json('__gm.sceneSelect()');
|
||
await click(select.x, select.y);
|
||
await sleep(250);
|
||
const rows = await json('__gm.menuRows()');
|
||
// The topmost hit, because the scene's name is on its row in the Assets panel as well.
|
||
const entry = await locate('Level 2', true);
|
||
await click(entry.x, entry.y);
|
||
await sleep(300);
|
||
const playing = await playInfo();
|
||
check(
|
||
'the scene selector stays usable in play mode, and the run runs on',
|
||
select.enabled === true &&
|
||
rows.some((row) => (row ?? '').includes('Level 2')) &&
|
||
(await json('__gm.state.project.activeSceneId')) === 'ls-scene' &&
|
||
playing.playing === true &&
|
||
playing.sceneId === 'ls-scene',
|
||
JSON.stringify({ select, rows, playing }),
|
||
);
|
||
|
||
// 35e. The swap itself. The score was counted in the old scene; a fresh run would show 0.
|
||
const before = await waitFor(
|
||
() => json('__gm.world()'),
|
||
(w) => (w?.shared?.variables?.['ls-vars']?.score ?? 0) === 1,
|
||
);
|
||
const humming = await waitFor(stats, (s) => s.playing > 0);
|
||
await press('g');
|
||
const after = await waitFor(
|
||
() => json('__gm.playInfo()'),
|
||
(info) => info.sceneId === firstId,
|
||
);
|
||
const world = await json('__gm.world()');
|
||
check(
|
||
'Load scene swaps the running scene without ending the run',
|
||
after.playing === true && after.sceneId === firstId,
|
||
JSON.stringify({ before: before.shared?.variables, after }),
|
||
);
|
||
check(
|
||
'the objects of the old scene are gone and the new scene is built',
|
||
!world.entities.some((e) => e.objectId === 'ls-box') && world.entities.length > 0,
|
||
JSON.stringify(world.entities.map((e) => e.name)),
|
||
);
|
||
check(
|
||
'what belongs to the run carries over: the shared score survives the swap',
|
||
world.shared?.variables?.['ls-vars']?.score === 1,
|
||
JSON.stringify(world.shared?.variables),
|
||
);
|
||
check(
|
||
'the sounds of the old scene end with the behaviors that were playing them',
|
||
humming.playing >= 1 && (await stats()).playing === 0,
|
||
JSON.stringify({ humming, now: await stats() }),
|
||
);
|
||
await stop();
|
||
await sleep(300);
|
||
|
||
// 35f. The switch is the way to run what you are working on instead – and it is remembered
|
||
// between runs, because it says how you work rather than what the game does.
|
||
await openPlayFrom();
|
||
await clickText('Active scene');
|
||
await sleep(300);
|
||
await play();
|
||
const fromActive = await playInfo();
|
||
check(
|
||
'switched to "active scene", Play runs the scene the editor shows',
|
||
fromActive.sceneId === firstId &&
|
||
(await playFrom()).mode === 'active' &&
|
||
(await playFrom()).label === 'Active scene' &&
|
||
(await json('__gm.state.project.startSceneId')) === 'ls-scene',
|
||
JSON.stringify({ fromActive, switch: await playFrom() }),
|
||
);
|
||
await stop();
|
||
await sleep(300);
|
||
await openPlayFrom();
|
||
await clickText('Start scene');
|
||
await sleep(300);
|
||
check(
|
||
'the switch goes back to the start scene',
|
||
(await playFrom()).mode === 'start' && (await playFrom()).scene === 'Level 2',
|
||
JSON.stringify(await playFrom()),
|
||
);
|
||
|
||
// 35g. Everything this step made goes again, the start scene included.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.startSceneId = '';
|
||
const gone = new Set(['ls-logic', 'ls-scene', 'ls-vars', 'ls-sound']);
|
||
p.assets = p.assets.filter((a) => !gone.has(a.id));
|
||
__gm.state.select(null);`);
|
||
await sleep(300);
|
||
check(
|
||
'the step leaves nothing of its own behind',
|
||
(await json('__gm.state.project.assets.filter((a) => a.id.startsWith("ls-")).length')) === 0 &&
|
||
(await json('__gm.state.project.startSceneId')) === '' &&
|
||
(await stats()).playing === 0,
|
||
JSON.stringify(await json('__gm.state.project.assets.map((a) => a.id)')),
|
||
);
|
||
}
|