Keep the game's music when Play ends a run in the editor
A behavior with "Run in editor" kept its world until the frame after Play. By then the game's On start had already started its pattern or song, and ending the editor run called stopAll() on the sound library. The game came up silent. This is what turned the game blocks of e2e 64 and 65 red in suites 542 and 550. - EditorRun.modeChanged() ends the run the moment the mode leaves edit. enterPlay() announces the mode before the game is built, so the editor run's sounds stop first and the game's start after. - The Editor wires the mode event to it instead of invalidate(). Tests: - e2e 64 starts its game with an idle "Run in editor" behavior in the scene. Without the fix it fails like CI did, "game": [] and 2 FAILED. With it: ALL PASSED, 30 checks (stein2). - Unit editor-run: 21 green, 1 new. - Typecheck, ESLint, Prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1f4a57589
commit
af9b202100
4 files changed
+50
-4
No files matched your search
@@ -364,7 +364,7 @@ export class Editor implements PlayHost {
|
||||
const playing = this.play.machineStates(assetId);
|
||||
return playing.length > 0 ? playing : this.editorRun.machineStates(assetId);
|
||||
};
|
||||
// Only one of the two ever runs a given asset: playing stops the editor run (see `EditorRun.tick`).
|
||||
// Only one of the two ever runs a given asset: playing stops the editor run (see `EditorRun.modeChanged`).
|
||||
this.inspector.liveVariables = (assetId) =>
|
||||
this.play.liveVariables(assetId) ?? this.editorRun.liveVariables(assetId);
|
||||
this.variablesEditor.liveVariables = this.inspector.liveVariables;
|
||||
@@ -560,7 +560,7 @@ export class Editor implements PlayHost {
|
||||
this.valueTargets.clear();
|
||||
this.showTab('scene');
|
||||
});
|
||||
state.on('mode', () => this.editorRun.invalidate());
|
||||
state.on('mode', () => this.editorRun.modeChanged());
|
||||
state.on('pick', () => this.followPick());
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,15 @@ export class EditorRun {
|
||||
if (!this.writing) this.stale = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playing starts: the run ends at once, not on the next frame. The game's `On start` runs in the
|
||||
* same click, and stopping the run's sounds a frame later would take its music along.
|
||||
*/
|
||||
modeChanged(): void {
|
||||
if (this.state.mode !== 'edit') this.stop();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/** `step` is what the global clock moved this frame – already scaled and capped. */
|
||||
tick(step: number): void {
|
||||
if (this.state.mode !== 'edit') {
|
||||
|
||||
@@ -178,6 +178,24 @@ describe('the run-in-editor switch', () => {
|
||||
expect(f.boxX()).toBe(0);
|
||||
});
|
||||
|
||||
it('ends the moment playing starts, before the game’s On start, not a frame later', () => {
|
||||
const f = scene();
|
||||
f.state.on('mode', () => f.run.modeChanged());
|
||||
f.state.execute(new SetOverrideCommand(f.sceneId, f.logic, 'runInEditor', true));
|
||||
f.frame(0.1);
|
||||
expect(f.run.debugInfo().running).toBe(true);
|
||||
|
||||
// The game is built right after `enterPlay` in the same click; a run ended only by the next
|
||||
// frame stopped every sound, the music its On start had just begun with them.
|
||||
f.state.enterPlay();
|
||||
expect(f.run.debugInfo().running).toBe(false);
|
||||
expect(f.keyboard.world).toBeNull();
|
||||
|
||||
f.state.exitPlay();
|
||||
f.frame(0.1);
|
||||
expect(f.run.debugInfo().running).toBe(true);
|
||||
});
|
||||
|
||||
it('does not run a behavior whose scene is not the loaded one', () => {
|
||||
const f = scene();
|
||||
f.state.execute(new SetOverrideCommand(f.sceneId, f.logic, 'runInEditor', true));
|
||||
|
||||
@@ -510,6 +510,21 @@ export async function pattern(t) {
|
||||
edges: [edge('pt64-e3', 'pt64-start', 'exec', 'pt64-play', 'exec')],
|
||||
},
|
||||
});
|
||||
// A behavior running in the editor ends when the game starts, and its sounds with it – but only
|
||||
// its own: the pattern the game's `On start` plays in the same click has to live on.
|
||||
const idle = await scaffoldBehavior(t, {
|
||||
assetId: 'pt64-idle',
|
||||
name: 'Idle 64',
|
||||
boxId: 'pt64-idlebox',
|
||||
logicId: 'pt64-idler',
|
||||
y: LANES.sound + 200,
|
||||
props: { enabled: true, runInEditor: true },
|
||||
graph: { nodes: [node('pt64-idle-start', 'OnStart', 0, 0)], edges: [] },
|
||||
});
|
||||
await waitFor(
|
||||
() => json('__gm.editorRun().running'),
|
||||
(running) => running,
|
||||
);
|
||||
const patterns = () => json('__gm.sound.playbacks()');
|
||||
await evaluate('__gm.sound.stopAll()');
|
||||
await play();
|
||||
@@ -556,6 +571,7 @@ export async function pattern(t) {
|
||||
JSON.stringify({ once: once.map((g) => g.planned.length), gone, onceStats }),
|
||||
);
|
||||
const left = await rig.remove();
|
||||
const idleLeft = await idle.remove();
|
||||
|
||||
// 64m. The step takes back everything it made.
|
||||
await evaluate('__gm.sound.stopAll()');
|
||||
@@ -571,7 +587,10 @@ export async function pattern(t) {
|
||||
`__gm.state.project.assets.filter((a) => a.id.startsWith('pt64') || a.type === 'pattern').length`,
|
||||
)) === 0 &&
|
||||
left.objects === 0 &&
|
||||
left.assets === 0,
|
||||
JSON.stringify(left),
|
||||
left.assets === 0 &&
|
||||
idleLeft.objects === 0 &&
|
||||
idleLeft.assets === 0 &&
|
||||
!(await json('__gm.editorRun().running')),
|
||||
JSON.stringify({ left, idleLeft }),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user