Add the Variable node and show the variable name in node titles

Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).

The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 13:34:09 +02:00
1 parent 5f8a308d62
commit e9fa592078
19 files changed
+354 -38

No files matched your search

+13 -2
View File
@@ -780,13 +780,24 @@ export async function graph(t) {
await click(chevron.x, chevron.y);
await sleep(200);
const shut = await json(`__gm.graph.nodeRect('n-move')`);
const zoom = openRect.w / 176;
check(
'the chevron collapses the node to its title bar, and leaves the selection alone',
(await nodeOf('n-move')).collapsed === true &&
Math.abs(shut.h / shut.w - 24 / 176) < 0.01 &&
Math.abs(shut.h - 24 * zoom) < 1 &&
(await json('__gm.graph.selected()')).length === 0,
JSON.stringify({ openRect, shut, collapsed: (await nodeOf('n-move')).collapsed }),
);
// Title starts 20 units in and ends 11 before the edge; `locate` matches exactly, so a cut title is null.
const titleText = await json(`__gm.locate('Move')`);
check(
'a collapsed node narrows to its whole title, keeping its left edge under the chevron',
titleText !== null &&
Math.abs(shut.x - openRect.x) < 1 &&
shut.w < openRect.w &&
Math.abs(shut.w - (2 * (titleText.x - shut.x) - 9 * zoom)) < 2,
JSON.stringify({ openRect, shut, titleText }),
);
const title = await json(`__gm.graph.nodeAt('n-move')`);
const anchorOf = (port, out) => json(`__gm.graph.portAt('n-move', '${port}', ${out})`);
const inputs = [
@@ -894,7 +905,7 @@ export async function graph(t) {
const reloaded = await json(`__gm.graph.nodeRect('n-move')`);
check(
'and a reloaded project draws it collapsed again',
(await nodeOf('n-move')).collapsed === true && Math.abs(reloaded.h / reloaded.w - 24 / 176) < 0.01,
(await nodeOf('n-move')).collapsed === true && Math.abs(reloaded.h / reloaded.w - shut.h / shut.w) < 0.01,
JSON.stringify(reloaded),
);
const reopen = await json(`__gm.graph.chevronAt('n-move')`);
+86 -1
View File
@@ -35,7 +35,22 @@ const logicGraph = {
};
export async function variableNodes(t) {
const { check, sleep, evaluate, json, shot, click, locate, undo, entity, asset, mutate, stop } = t;
const {
check,
sleep,
evaluate,
json,
shot,
click,
locate,
undo,
entity,
asset,
mutate,
stop,
typeText,
press,
} = t;
const portAt = (nodeId, key, isOutput) =>
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
const portsOf = async (nodeId) =>
@@ -216,6 +231,76 @@ export async function variableNodes(t) {
JSON.stringify(helper),
);
// 23i. The Variable node: picked from the palette, titled by the variable picked in the inspector,
// and in the run `vz = vz + 1` once a frame – its own output feeding its input via Add.
const nodeIds = async () => (await asset('vp-logic')).graph.nodes.map((n) => n.id);
const idsBefore = await nodeIds();
const centre = await json('__gm.graph.center()');
// Below the Owner node; centre + (250, 250) would land on the Pack.
await click(centre.x - 250, centre.y + 250, 'right');
await sleep(150);
// Its description is the one that says "any time"; "variable" would put Get variable first.
await typeText('any time');
await sleep(200);
await press('Enter');
await sleep(300);
const added = (await asset('vp-logic')).graph.nodes.find((n) => !idsBefore.includes(n.id));
check('the palette offers the Variable node', added?.type === 'Variable', JSON.stringify(added));
const variableId = added?.id ?? '';
const titleOf = (id) => json(`__gm.graph.nodeLabel(${JSON.stringify(id)})`);
const untitled = await titleOf(variableId);
// The node's own title says "Variable" too, and the canvas is drawn first.
const picker = await json(`__gm.inspectorField('Variable', true)`);
await click(picker.x, picker.y);
await sleep(200);
await clickLast('vz');
await sleep(300);
check(
'picking its variable makes that the title, and drops the row that would repeat it',
untitled === 'Variable' &&
(await titleOf(variableId)) === 'vz' &&
(await json(`__gm.graph.nodeSettings(${JSON.stringify(variableId)})`)).length === 0,
JSON.stringify({ untitled, titled: await titleOf(variableId), picker }),
);
await shot('05-variable-node');
await mutate(`
const g = __gm.state.project.assets.find((x) => x.id === 'vp-logic').graph;
g.nodes.push(
${JSON.stringify(node('vp-add', 'Add', 800, 200, { b: 1 }))},
${JSON.stringify(node('vp-set-vx', 'SetVariable', 800, 400, { variable: 'vx' }))},
${JSON.stringify(node('vp-set-gone', 'SetVariable', 800, 500, { variable: 'gone' }))},
);
g.edges.push(
${JSON.stringify(edge('vp-e7', 'vp-move', 'exec', variableId, 'exec'))},
${JSON.stringify(edge('vp-e8', variableId, 'value', 'vp-add', 'a'))},
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'value'))},
);
`);
await sleep(300);
const settingsOf = (id) => json(`__gm.graph.nodeSettings(${JSON.stringify(id)})`);
const named = { title: await titleOf('vp-set-vx'), rows: await settingsOf('vp-set-vx') };
const gone = { title: await titleOf('vp-set-gone'), rows: await settingsOf('vp-set-gone') };
check(
'Set variable names its variable too, and a deleted one keeps the row that marks it',
named.title === 'Set vx' &&
named.rows.length === 0 &&
gone.title === 'Set gone' &&
JSON.stringify(gone.rows) === '["Variable: gone?"]',
JSON.stringify({ named, gone }),
);
await t.play();
await sleep(1000);
const counted = (await entity('vp-brain'))?.props.vz;
await stop();
await sleep(300);
check(
'in the run it reads through its own output and writes the sum back, so vz climbs',
typeof counted === 'number' && counted > 20,
`vz = ${counted} after ~1 s`,
);
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'vp-logic');