Spawn node: place assets at runtime into the play copy, Spawner demo behavior

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 16:57:19 +02:00
1 parent c3a533c4e5
commit 91ee2feadd
13 files changed
+219 -37

No files matched your search

+21 -6
View File
@@ -237,7 +237,7 @@ try {
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);
check('panels stay visible while playing', Boolean(await evaluate(`__gm.locate('INSPECTOR')`)) && (await evaluate('__gm.hierarchyOrder().length')) === 7);
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);
@@ -247,15 +247,30 @@ try {
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));
check('Delete while playing removes the entity and its collider child from the world', !liveIds.includes('o2') && !liveIds.includes('oc2') && liveIds.length === 5, 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));
// 7a-ii. Spawning: the "Spawner" behavior places a Crate at (0, 150) on Space. The new object
// shows up in the runtime scene (hierarchy), in the world and in the Game view at once;
// Stop discards it with the rest of the runtime copy.
const visibleBeforeSpawn = JSON.parse(await evaluate('JSON.stringify(__gm.gameInfo())')).visibleItems;
await keyPress(' ', 'Space', { windowsVirtualKeyCode: 32 });
await sleep(200);
const spawnedObjects = JSON.parse(await evaluate('JSON.stringify(__gm.state.activeScene.objects)'));
const spawned = spawnedObjects.find((o) => !['o1', 'o2', 'o3', 'o4', 'o5', 'oc1', 'oc2'].includes(o.id));
check('Spawn node adds a crate object to the runtime scene at (0, 150)', spawned && spawned.assetId === 'crate' && spawned.parentId === '' && spawned.position.x === 0 && spawned.position.y === 150 && spawnedObjects.length === 8, JSON.stringify(spawned));
const spawnWorld = JSON.parse(await evaluate('JSON.stringify(__gm.world())'));
check('the spawned entity lives in the world and its name reached the Log node', spawnWorld.entities.length === 8 && spawnWorld.log.includes('Crate'), JSON.stringify({ n: spawnWorld.entities.length, log: spawnWorld.log }));
check('hierarchy lists the spawned object', (await evaluate('__gm.hierarchyOrder().length')) === 8);
info = await evaluate('JSON.stringify(__gm.gameInfo())');
check('the Game view renders the spawned crate', JSON.parse(info).visibleItems === visibleBeforeSpawn + 1, `${info} before=${visibleBeforeSpawn}`);
await shot('08c-spawned');
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 restores the saved model untouched by play-mode edits (spawned object gone)', o2AfterEdit === xBeforePlay && (await evaluate('__gm.state.activeScene.objects.length')) === 7, `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.
@@ -272,7 +287,7 @@ try {
await evaluate(`__gm.state.select({ kind: 'object', sceneId: 'scene1', objectId: 'o3' })`);
await keyPress('Delete', 'Delete', { windowsVirtualKeyCode: 46 });
await sleep(200);
check('camera object deleted', (await evaluate('__gm.state.activeScene.objects.length')) === 5);
check('camera object deleted', (await evaluate('__gm.state.activeScene.objects.length')) === 6);
await clickText('▶ Play');
await sleep(300);
info = await evaluate('JSON.stringify(__gm.gameInfo())');
@@ -284,7 +299,7 @@ try {
await sleep(300);
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 });
await sleep(200);
check('undo restored the camera object', (await evaluate('__gm.state.activeScene.objects.length')) === 6);
check('undo restored the camera object', (await evaluate('__gm.state.activeScene.objects.length')) === 7);
// 8. Context menu on the Crate asset → menu shows "Place in scene".
await clickText('Crate', 'right');
@@ -295,7 +310,7 @@ try {
await clickText('Place in scene');
await sleep(200);
const count = await evaluate('__gm.state.activeScene.objects.length');
check('placing from the menu adds an object', count === 7, `objects=${count}`);
check('placing from the menu adds an object', count === 8, `objects=${count}`);
// 9. Create a new asset through the + menu.
await clickText('+');
+17
View File
@@ -34,6 +34,20 @@ const moverGraph = {
],
};
/** "Spawner": pressing Space places a new Crate at world (0, 150) and logs its name. */
const spawnerGraph = {
nodes: [
{ id: 's-key', type: 'OnKeyDown', x: 0, y: 0, params: { key: ' ' } },
{ id: 's-spawn', type: 'Spawn', x: 260, y: 0, params: { asset: 'crate', y: 150 } },
{ id: 's-log', type: 'Log', x: 520, y: 0, params: {} },
],
edges: [
{ id: 's-e1', from: { node: 's-key', port: 'exec' }, to: { node: 's-spawn', port: 'exec' } },
{ id: 's-e2', from: { node: 's-spawn', port: 'exec' }, to: { node: 's-log', port: 'exec' } },
{ id: 's-e3', from: { node: 's-spawn', port: 'object' }, to: { node: 's-log', port: 'message' } },
],
};
const project = {
formatVersion: 1,
id: projectId,
@@ -55,6 +69,8 @@ const project = {
// instance is widened to match its 140-wide sprite override.
{ id: 'oc1', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: 'o1' },
{ id: 'oc2', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 140 }, parentId: 'o2' },
// Top-level spawner (no owner) left of the red crate: Space spawns a crate.
{ id: 'o5', assetId: 'spawner', name: '', position: { x: -300, y: 0, z: 0 }, overrides: {}, parentId: '' },
],
},
{ id: 'crate', type: 'sprite', name: 'Crate', props: { width: 100, height: 60, color: 0xe0553f } },
@@ -68,6 +84,7 @@ const project = {
variables: [{ key: 'speed', label: 'speed', kind: 'number', default: 0 }],
graph: moverGraph,
},
{ id: 'spawner', type: 'behavior', name: 'Spawner', props: {}, variables: [], graph: spawnerGraph },
],
};