Editing while playing: play runs on a runtime copy, new Game tab (Scene | Game | Graph)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 16:34:26 +02:00
1 parent cb8885359f
commit c3a533c4e5
14 files changed
+408 -118

No files matched your search

+34 -7
View File
@@ -208,7 +208,7 @@ try {
await sleep(300);
s = JSON.parse(await stateJson());
check('play mode entered', s.mode === 'play', JSON.stringify(s));
let info = await evaluate('JSON.stringify(__gm.viewportInfo())');
let info = await evaluate('JSON.stringify(__gm.gameInfo())');
check('play renders the camera frame with both crates', JSON.parse(info).rendering && JSON.parse(info).visibleItems === 2, info);
await shot('08-playing');
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 });
@@ -231,11 +231,38 @@ try {
const o1AfterPlay = await evaluate(`__gm.state.activeScene.objects.find(o => o.id === 'o1').position.x`);
check('play did not write back to the model', o2AfterPlay === xBeforePlay && o1AfterPlay === -150, `${o2AfterPlay} vs ${xBeforePlay}, o1=${o1AfterPlay}`);
// 7a. Editing while playing: Play opens the Game tab, the panels stay open and edit a runtime
// copy. Moving the blue crate (model hook + preview, as a scrub does) and deleting it with
// the Delete key change the world at once, undo brings it back, Esc restores the saved model.
await clickText('▶ Play');
await sleep(300);
check('play opens the Game tab', (await evaluate('__gm.tab()')) === 'game');
check('panels stay visible while playing', Boolean(await evaluate(`__gm.locate('INSPECTOR')`)) && (await evaluate('__gm.hierarchyOrder().length')) === 6);
await evaluate(`__gm.state.select({ kind: 'object', sceneId: 'scene1', objectId: 'o2' })`);
await evaluate(`__gm.state.activeScene.objects.find(o => o.id === 'o2').position.x = 400; __gm.state.preview()`);
await sleep(100);
let liveO2 = JSON.parse(await evaluate('JSON.stringify(__gm.world())')).entities.find((e) => e.objectId === 'o2');
check('an edit while playing moves the entity in the world', liveO2 && liveO2.x === 400, JSON.stringify(liveO2));
await shot('08b-editing-while-playing');
await keyPress('Delete', 'Delete', { windowsVirtualKeyCode: 46 });
await sleep(150);
const liveIds = JSON.parse(await evaluate('JSON.stringify(__gm.world())')).entities.map((e) => e.objectId);
check('Delete while playing removes the entity and its collider child from the world', !liveIds.includes('o2') && !liveIds.includes('oc2') && liveIds.length === 4, JSON.stringify(liveIds));
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 });
await sleep(150);
liveO2 = JSON.parse(await evaluate('JSON.stringify(__gm.world())')).entities.find((e) => e.objectId === 'o2');
check('undo while playing brings the entity back into the world', liveO2 && liveO2.x === 400, JSON.stringify(liveO2));
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(300);
const o2AfterEdit = await evaluate(`__gm.state.activeScene.objects.find(o => o.id === 'o2').position.x`);
check('stop restores the saved model untouched by play-mode edits', o2AfterEdit === xBeforePlay && (await evaluate('__gm.state.activeScene.objects.length')) === 6, `x=${o2AfterEdit}`);
check('stop returns to the Scene tab', (await evaluate('__gm.tab()')) === 'scene');
// 7b. Culling: move the blue crate far outside the camera frame → only one item visible.
await evaluate(`__gm.state.activeScene.objects.find(o => o.id === 'o2').position.x = 5000; __gm.state.preview()`);
await clickText('▶ Play');
await sleep(300);
info = await evaluate('JSON.stringify(__gm.viewportInfo())');
info = await evaluate('JSON.stringify(__gm.gameInfo())');
check('objects outside the camera frame are culled', JSON.parse(info).visibleItems === 1, info);
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(200);
@@ -248,7 +275,7 @@ try {
check('camera object deleted', (await evaluate('__gm.state.activeScene.objects.length')) === 5);
await clickText('▶ Play');
await sleep(300);
info = await evaluate('JSON.stringify(__gm.viewportInfo())');
info = await evaluate('JSON.stringify(__gm.gameInfo())');
s = JSON.parse(await stateJson());
check('play without camera renders nothing', s.mode === 'play' && !JSON.parse(info).rendering && JSON.parse(info).visibleItems === 0, info);
check('play without camera shows a notice', s.notice.includes('no camera'), s.notice);
@@ -424,7 +451,7 @@ try {
const restored = await graphCounts();
check('undo restores node and edge', restored.nodes === before13.nodes + 1 && restored.edges === before13.edges + 1, JSON.stringify(restored));
// 13e. Play always starts on the Scene tab; switching back to Graph while playing shows
// 13e. Play always starts on the Game tab; switching to Graph while playing shows
// the live highlight: hold ArrowRight → the exec edge is hot.
const fpsEdit = await evaluate('__gm.fps()');
check('fps readout is measured in edit mode', typeof fpsEdit === 'number' && fpsEdit > 0, `fps=${fpsEdit}`);
@@ -433,7 +460,7 @@ try {
check('fps label is visible in the toolbar', fpsLabel === true);
await clickText('▶ Play');
await sleep(200);
check('play switches to the scene tab', (await evaluate('__gm.tab()')) === 'scene');
check('play switches to the game tab', (await evaluate('__gm.tab()')) === 'game');
await sleep(600);
const fpsPlay = await evaluate('__gm.fps()');
check('fps readout keeps running in play mode', typeof fpsPlay === 'number' && fpsPlay > 0, `fps=${fpsPlay}`);
@@ -584,8 +611,8 @@ try {
const collisionWorld = JSON.parse(await evaluate('JSON.stringify(__gm.world())'));
check('On collision enter fired once with the other body', collisionWorld && collisionWorld.log.filter((l) => l === 'Blue crate').length === 1, JSON.stringify(collisionWorld?.log));
const spriteCount = await evaluate(`__gm.state.activeScene.objects.filter(o => __gm.state.project.assets.find(a => a.id === o.assetId)?.type === 'sprite').length`);
info = await evaluate('JSON.stringify(__gm.viewportInfo())');
check('colliders are not rendered in play mode', JSON.parse(info).visibleItems === spriteCount, `${info} sprites=${spriteCount}`);
info = await evaluate('JSON.stringify(__gm.gameInfo())');
check('colliders are not rendered in the game view', JSON.parse(info).visibleItems === spriteCount, `${info} sprites=${spriteCount}`);
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(200);