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

+5 -1
View File
@@ -96,7 +96,7 @@ export class GraphInterpreter {
const inputs = this.collectInputs(node, ctx);
const result = def.run(ctx, inputs, node.params, def);
const returned = result.call ? this.call(result.call, ctx, depth) : {};
this.frame.outputCache.set(node.id, { ...result.outputs, ...returned });
if (!def.read) this.frame.outputCache.set(node.id, { ...result.outputs, ...returned });
this.trace?.onNode(node.id);
if (result.repeat) this.loop(node, result.repeat, ctx, depth);
const next = result.next === undefined ? [] : Array.isArray(result.next) ? result.next : [result.next];
@@ -167,6 +167,10 @@ export class GraphInterpreter {
if (!source) return null;
const def = this.def(source);
if (!def) return null;
if (def.read) {
this.trace?.onNode(nodeId);
return def.read(ctx, source.params)[port] ?? null;
}
// Exec nodes that have not run in this pulse have no value yet.
if (isEventNode(def) || hasExecInput(def) || !def.run) return null;
if (++this.steps > MAX_STEPS_PER_FIRE)
+5
View File
@@ -81,6 +81,11 @@ export interface NodeDef {
* offers, which values the behavior a call node names takes. `resolveDef` applies it.
*/
derivePorts?(source: PortSource): DerivedShape;
/**
* Outputs readable at any moment, even while the node's exec input has not fired in this pulse.
* Never cached, so a read after a write sees the new value.
*/
read?(ctx: NodeContext, params: PropertyBag): Record<string, Value>;
/**
* Event nodes have no `run` – the interpreter seeds their outputs. `def` is resolved for the node at
* hand, the only way a node with derived ports knows how many it has.
+2
View File
@@ -85,6 +85,7 @@ import { GetVariable } from './variable/GetVariable';
import { GetVariables } from './variable/GetVariables';
import { SetVariable } from './variable/SetVariable';
import { SetVariables } from './variable/SetVariables';
import { Variable } from './variable/Variable';
/**
* Importing this module registers the built-in nodes. The palette sorts by category and label, so
@@ -110,6 +111,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
RunBehavior,
BehaviorEnd,
// variables
Variable,
GetVariable,
GetVariables,
SetVariable,
+29 -3
View File
@@ -1,7 +1,8 @@
import type { PropertyValue } from '../../model/Properties';
import { type PropertyBag, type PropertyValue, textOf } from '../../model/Properties';
import type { PropertyDef } from '../../properties/PropertySchema';
import type { NodeDef, PortDef } from '../NodeRegistry';
import { asBoolean, asNumber } from '../values';
import type { NodeContext } from '../NodeContext';
import type { DerivedShape, NodeDef, PortDef, PortSource, RunResult } from '../NodeRegistry';
import { asBoolean, asNumber, type Value, writeTyped } from '../values';
export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' });
export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' });
@@ -33,6 +34,31 @@ export const variableParam: PropertyDef = {
options: [],
};
const variableKey = (params: PropertyBag) => textOf(params.variable ?? '');
export const readVariable = (ctx: NodeContext, params: PropertyBag): Record<string, Value> => ({
value: ctx.variables[variableKey(params)] ?? null,
});
export function writeVariable(
ctx: NodeContext,
inputs: Record<string, Value>,
params: PropertyBag,
): RunResult {
const key = variableKey(params);
if (key) writeTyped(ctx.variables, key, inputs.value);
return { next: 'exec' };
}
/** Once a variable is picked, the title names it: "Get speed", or just "speed". */
export const variableTitle =
(verb: string) =>
({ node }: PortSource): DerivedShape => {
const name = variableKey(node.params);
if (!name) return {};
return { label: verb ? `${verb} ${name}` : name };
};
/** No `assetTypes`: any placeable asset is a valid answer here. */
export const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' };
+3 -5
View File
@@ -1,7 +1,6 @@
import type { NodeDef } from '../../NodeRegistry';
import { any, variableParam } from '../shared';
import { any, readVariable, variableParam, variableTitle } from '../shared';
import { textOf } from '../../../model/Properties';
export const GetVariable: NodeDef = {
type: 'GetVariable',
label: 'Get variable',
@@ -10,7 +9,6 @@ export const GetVariable: NodeDef = {
inputs: [],
outputs: [any('value')],
params: [variableParam],
run: (ctx, _inputs, params) => ({
outputs: { value: ctx.variables[textOf(params.variable ?? '')] ?? null },
}),
derivePorts: variableTitle('Get'),
run: (ctx, _inputs, params) => ({ outputs: readVariable(ctx, params) }),
};
+3 -8
View File
@@ -1,8 +1,6 @@
import type { NodeDef } from '../../NodeRegistry';
import { writeTyped } from '../../values';
import { any, exec, variableParam } from '../shared';
import { any, exec, variableParam, variableTitle, writeVariable } from '../shared';
import { textOf } from '../../../model/Properties';
export const SetVariable: NodeDef = {
type: 'SetVariable',
label: 'Set variable',
@@ -11,9 +9,6 @@ export const SetVariable: NodeDef = {
inputs: [exec(), any('value')],
outputs: [exec()],
params: [variableParam],
run: (ctx, inputs, params) => {
const key = textOf(params.variable ?? '');
if (key) writeTyped(ctx.variables, key, inputs.value);
return { next: 'exec' };
},
derivePorts: variableTitle('Set'),
run: writeVariable,
};
+18
View File
@@ -0,0 +1,18 @@
import type { NodeDef } from '../../NodeRegistry';
import { any, exec, readVariable, variableParam, variableTitle, writeVariable } from '../shared';
/** Get and Set in one node: the output can be read at any time, an arriving flow writes the input. */
export const Variable: NodeDef = {
type: 'Variable',
label: 'Variable',
category: 'variable',
description:
"Reads one of this behavior's variables at any time, and writes it when the flow arrives. " +
'A read after the write sees the new value.',
inputs: [exec(), any('value')],
outputs: [exec(), any('value')],
params: [variableParam],
derivePorts: variableTitle(''),
read: readVariable,
run: writeVariable,
};