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

+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');
});
});