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

+11 -1
View File
@@ -4,7 +4,7 @@ import { CollapseNodesCommand } from '../../src/core/commands/graphCommands';
import type { BehaviorAsset } from '../../src/core/model/Asset';
import { findAsset } from '../../src/core/model/Project';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { foldMarkKey } from '../../src/editor/graph/folding';
import { collapsedNodeWidth, foldMarkKey } from '../../src/editor/graph/folding';
import { graphEditor, makeBehavior, makeProject, roundTrip } from '../helpers';
function threeNodes() {
@@ -61,6 +61,16 @@ describe('collapsing nodes', () => {
});
});
describe('the width of a collapsed node', () => {
it('fits the title plus its chrome, rounded up so the title is not cut', () => {
expect(collapsedNodeWidth(40.2, 31, 176)).toBe(72);
});
it('never grows past an open node – a long title is cut instead', () => {
expect(collapsedNodeWidth(300, 31, 176)).toBe(176);
});
});
describe('hidden links at a collapsed node', () => {
it('share one mark per side, because every port sits on the same point', () => {
const a = foldMarkKey({ node: 'n', port: 'x' }, false, true);
+16
View File
@@ -150,6 +150,22 @@ const CASES: Record<string, Case[]> = {
after: (ctx) => expect(ctx.variables).toEqual({ speed: 100 }),
},
],
// Reading without a flow needs the interpreter: see variable-node.test.ts.
Variable: [
{
name: 'writes its variable when the flow arrives, keeping the declared type',
inputs: { value: '250' },
params: { variable: 'speed' },
next: 'exec',
after: (ctx) => expect(ctx.variables.speed).toBe(250),
},
{
name: 'no variable chosen writes nothing',
inputs: { value: 5 },
next: 'exec',
after: (ctx) => expect(ctx.variables).toEqual({ speed: 100 }),
},
],
// references and properties
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest';
import { RenameVariableCommand } from '../../src/core/commands/variableCommands';
import { GraphInterpreter, resolveDef } from '../../src/core/graph';
import type { BehaviorAsset } from '../../src/core/model/Asset';
import { behaviorLookup } from '../../src/core/model/behaviorPrograms';
import { fakeContext, graphEditor, makeBehavior, makeProject } from '../helpers';
function setup() {
const fixture = makeProject();
const behavior = makeBehavior(fixture, 'B', ['count', 'copy']);
return { fixture, behavior, edit: graphEditor(fixture, behavior) };
}
function run(behavior: BehaviorAsset, fixture: ReturnType<typeof makeProject>, count = 5) {
const ctx = fakeContext({ variables: { count, copy: 0 } });
new GraphInterpreter(behavior.graph, behaviorLookup(fixture.project)).fire('OnStart', ctx);
return ctx.variables;
}
const titleOf = (behavior: BehaviorAsset, index: number) =>
resolveDef(behavior.graph.nodes[index]!, behavior.graph, () => null)!.label;
describe('the Variable node', () => {
it('can be read without a flow ever reaching it', () => {
const { fixture, behavior, edit } = setup();
const start = edit.addNode('OnStart');
const variable = edit.addNode('Variable', { variable: 'count' });
const copy = edit.addNode('SetVariable', { variable: 'copy' });
edit.wire([
[start, 'exec', copy, 'exec'],
[variable, 'value', copy, 'value'],
]);
expect(run(behavior, fixture)).toEqual({ count: 5, copy: 5 });
});
it('writes its input when the flow arrives, unconnected as well as through an edge', () => {
const { fixture, behavior, edit } = setup();
const start = edit.addNode('OnStart');
const inline = edit.addNode('Variable', { variable: 'count', value: 7 });
const number = edit.addNode('Number', { value: 3 });
const wired = edit.addNode('Variable', { variable: 'copy' });
edit.wire([
[start, 'exec', inline, 'exec'],
[inline, 'exec', wired, 'exec'],
[number, 'value', wired, 'value'],
]);
expect(run(behavior, fixture)).toEqual({ count: 7, copy: 3 });
});
it('counts up through its own output: count = count + 1', () => {
const { fixture, behavior, edit } = setup();
const start = edit.addNode('OnStart');
const variable = edit.addNode('Variable', { variable: 'count' });
const add = edit.addNode('Add', { b: 1 });
edit.wire([
[start, 'exec', variable, 'exec'],
[variable, 'value', add, 'a'],
[add, 'result', variable, 'value'],
]);
expect(run(behavior, fixture).count).toBe(6);
});
it('is read at the moment it is asked, so it sees a later write by another node', () => {
const { fixture, behavior, edit } = setup();
const start = edit.addNode('OnStart');
const variable = edit.addNode('Variable', { variable: 'count', value: 7 });
const overwrite = edit.addNode('SetVariable', { variable: 'count', value: 9 });
const copy = edit.addNode('SetVariable', { variable: 'copy' });
edit.wire([
[start, 'exec', variable, 'exec'],
[variable, 'exec', overwrite, 'exec'],
[overwrite, 'exec', copy, 'exec'],
[variable, 'value', copy, 'value'],
]);
expect(run(behavior, fixture)).toEqual({ count: 9, copy: 9 });
});
});
describe('variable node titles', () => {
it('name the picked variable, and keep the plain title until one is picked', () => {
const { behavior, edit } = setup();
edit.addNode('GetVariable', { variable: 'count' });
edit.addNode('SetVariable', { variable: 'count' });
edit.addNode('Variable', { variable: 'count' });
edit.addNode('GetVariable');
edit.addNode('Variable');
expect([0, 1, 2, 3, 4].map((i) => titleOf(behavior, i))).toEqual([
'Get count',
'Set count',
'count',
'Get variable',
'Variable',
]);
});
it('follow a renamed variable', () => {
const { fixture, behavior, edit } = setup();
edit.addNode('Variable', { variable: 'count' });
fixture.stack.execute(new RenameVariableCommand(behavior.id, 'count', 'score'));
expect(titleOf(behavior, 0)).toBe('score');
});
});