Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The running game reads the graph by reference each frame, so a new node, edge, changed value, disconnect or undo takes effect from the next frame. Stop discards these edits like everything else changed in play. A new On start node does not fire for a behavior that has already started. During play every data output shows the value it last produced, in the highlight colour and refreshed ten times a second. Values are kept per graph, because node ids are only unique within one; with several instances the most recent value wins. The Variables node reads the live variable bag, so only its own outputs are kept, as a snapshot. Each time an exec edge fires, a bead runs along it in 0.3 s, at most one every 0.15 s per edge, following the same curve the edge is drawn with. Beads run on game time and freeze while paused; the game logic is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
495756b500
commit
656d61f187
17 files changed
+542
-37
No files matched your search
+101
-3
@@ -31,9 +31,24 @@ const pauseGraph = {
|
||||
};
|
||||
|
||||
export async function pause(t) {
|
||||
const { check, sleep, json, has, shot, clickText, press, entity, world, stop } = t;
|
||||
const {
|
||||
check,
|
||||
sleep,
|
||||
json,
|
||||
has,
|
||||
shot,
|
||||
click,
|
||||
clickText,
|
||||
press,
|
||||
entity,
|
||||
world,
|
||||
stop,
|
||||
select,
|
||||
evaluate,
|
||||
asset,
|
||||
} = t;
|
||||
const playInfo = () => json('__gm.playInfo()');
|
||||
const boxX = async () => (await entity('p-box')).x;
|
||||
const boxX = async () => (await entity('p-box'))?.x ?? NaN;
|
||||
const keyHits = async () => (await world()).log.filter((line) => line.includes(KEY_LOG)).length;
|
||||
|
||||
// 19a. A box with a drifting behavior below it, written into the model directly: what is under
|
||||
@@ -143,10 +158,93 @@ export async function pause(t) {
|
||||
(await boxX()) > restartedX,
|
||||
`after stop ${JSON.stringify(afterStop)}, x ${restartedX} → ${await boxX()}`,
|
||||
);
|
||||
|
||||
// 19f. The graph stays editable while the game runs, with the mouse as in edit mode: a click
|
||||
// selects the Move node, Delete removes it, and the running world stops drifting the box.
|
||||
await select({ kind: 'asset', assetId: 'pauser' });
|
||||
await evaluate(`__gm.showTab('graph')`);
|
||||
await sleep(150);
|
||||
const outputs = (id) => json(`__gm.graph.nodeOutputs(${JSON.stringify(id)})`);
|
||||
const live = {
|
||||
owner: await outputs('p-owner'),
|
||||
update: await outputs('p-update'),
|
||||
move: await outputs('p-move'),
|
||||
};
|
||||
check(
|
||||
'while playing, each output shows the value it last carried in front of its name',
|
||||
live.owner[0] === 'Pause box object' &&
|
||||
/^0\.\d{3} {2}dt$/.test(live.update[1]) &&
|
||||
live.move[1] === 'false blocked x',
|
||||
JSON.stringify(live),
|
||||
);
|
||||
await shot('36b-graph-live-values');
|
||||
const beadsOn = async (edgeId) =>
|
||||
(await json('__gm.graph.beads()')).filter((b) => b.edgeId === edgeId).map((b) => b.progress);
|
||||
const chain = await beadsOn('p-e1');
|
||||
const gaps = chain.slice(1).map((p, i) => chain[i] - p);
|
||||
check(
|
||||
'an exec edge firing every frame carries an evenly spaced chain of beads',
|
||||
chain.length >= 1 && chain.length <= 3 && gaps.every((g) => Math.abs(g - 0.5) < 0.1),
|
||||
JSON.stringify(chain),
|
||||
);
|
||||
check(
|
||||
'data edges carry no beads',
|
||||
(await beadsOn('p-e2')).length === 0 && (await beadsOn('p-e3')).length === 0,
|
||||
);
|
||||
const beforeKey = await beadsOn('p-e4');
|
||||
await press('ArrowRight');
|
||||
await sleep(80);
|
||||
const onKey = await beadsOn('p-e4');
|
||||
await sleep(500);
|
||||
const afterKey = await beadsOn('p-e4');
|
||||
check(
|
||||
'a single key press sends exactly one bead, and it arrives',
|
||||
beforeKey.length === 0 && onKey.length === 1 && afterKey.length === 0,
|
||||
JSON.stringify({ beforeKey, onKey, afterKey }),
|
||||
);
|
||||
const movePos = await json(`__gm.graph.nodeAt('p-move')`);
|
||||
await click(movePos.x, movePos.y);
|
||||
await sleep(100);
|
||||
check(
|
||||
'a click selects a node while playing',
|
||||
JSON.stringify(await json('__gm.graph.selected()')) === '["p-move"]',
|
||||
JSON.stringify(await json('__gm.graph.selected()')),
|
||||
);
|
||||
const area = await json('__gm.graph.canvasRect()');
|
||||
await click(area.x + area.w - 15, area.y + area.h - 15, 'right');
|
||||
await sleep(100);
|
||||
check('right-clicking the canvas opens the node palette while playing', await has('Search nodes…'));
|
||||
await press('Escape');
|
||||
await sleep(100);
|
||||
check(
|
||||
'Escape closes the palette and leaves the game running',
|
||||
!(await has('Search nodes…')) && (await playInfo()).playing === true,
|
||||
);
|
||||
await click(movePos.x, movePos.y);
|
||||
await press('Delete');
|
||||
await sleep(150);
|
||||
const stoppedAt = await boxX();
|
||||
await sleep(500);
|
||||
check(
|
||||
'deleting the Move node while playing stops the drift on the next frames',
|
||||
!(await asset('pauser')).graph.nodes.some((n) => n.id === 'p-move') &&
|
||||
Math.abs((await boxX()) - stoppedAt) < 0.001,
|
||||
`x ${stoppedAt} → ${await boxX()}`,
|
||||
);
|
||||
await stop();
|
||||
await sleep(200);
|
||||
check(
|
||||
'stopping takes the values off the nodes',
|
||||
JSON.stringify(await outputs('p-owner')) === '["object"]',
|
||||
JSON.stringify(await outputs('p-owner')),
|
||||
);
|
||||
check(
|
||||
'stopping discards the graph edit with the rest of the run',
|
||||
(await asset('pauser')).graph.nodes.some((n) => n.id === 'p-move'),
|
||||
JSON.stringify((await asset('pauser')).graph.nodes.map((n) => n.id)),
|
||||
);
|
||||
|
||||
// 19f. Play discarded its copy, so the box is back at the position the model holds.
|
||||
// 19g. Play discarded its copy, so the box is back at the position the model holds.
|
||||
const left = await rig.remove();
|
||||
check(
|
||||
'the step leaves nothing of its own behind',
|
||||
|
||||
@@ -276,7 +276,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
||||
const storedObjects = () => json('__gm.state.activeScene.objects');
|
||||
const asset = (id) => json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)})`);
|
||||
const world = () => json('__gm.world()');
|
||||
const entity = async (objectId) => (await world()).entities.find((e) => e.objectId === objectId);
|
||||
const entity = async (objectId) => (await world())?.entities.find((e) => e.objectId === objectId);
|
||||
const select = (selection) => evaluate(`__gm.state.select(${JSON.stringify(selection)})`);
|
||||
/**
|
||||
* Runs `body` (JavaScript source) against the page's model and previews the result, the way a
|
||||
|
||||
Reference in new issue
Block a user