diff --git a/src/editor/Editor.ts b/src/editor/Editor.ts index 0285d7c6..b145dfa9 100644 --- a/src/editor/Editor.ts +++ b/src/editor/Editor.ts @@ -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()); } diff --git a/src/editor/EditorRun.ts b/src/editor/EditorRun.ts index fadd0d91..42e85873 100644 --- a/src/editor/EditorRun.ts +++ b/src/editor/EditorRun.ts @@ -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') { diff --git a/tests/editor/editor-run.test.ts b/tests/editor/editor-run.test.ts index 62944c3c..76e9e0ad 100644 --- a/tests/editor/editor-run.test.ts +++ b/tests/editor/editor-run.test.ts @@ -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)); diff --git a/tools/e2e/64-pattern.mjs b/tools/e2e/64-pattern.mjs index 58429565..a943181a 100644 --- a/tools/e2e/64-pattern.mjs +++ b/tools/e2e/64-pattern.mjs @@ -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 }), ); }