Replace the five variable nodes with one Variables node

A single node now lists any number of variables. Every entry is an output
that reads the current value whenever it is pulled; an entry switched to
Set is also an input and is written when the flow arrives. Exec pins only
exist while at least one entry writes, and all inputs are read before any
write, so one node can swap two variables.

Get/Set variable, Get/Set variables and Variable are gone. Old project
files are converted on load: each old node becomes a Variables node with
the same variables, edges and typed values.

A written port without a typed value now shows the zero of its kind on
the node body, matching what the inspector shows and what the run writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 16:56:24 +02:00
1 parent 854ae2e756
commit 7c63c884ef
40 files changed
+895 -590

No files matched your search

+21 -20
View File
@@ -99,16 +99,19 @@ shown on the node and editable in the inspector.
- **Variables** are the behavior's own property schema (`BehaviorAsset.variables`): they
appear in the inspector, can be overridden per placed instance, and each running instance
gets its own mutable copy. `Get variable` / `Set variable` read and write them, and are titled
by the variable they name (*Get speed*, *Set speed*). `Variable` is both in one node, titled by the
variable alone: its output can be read at any time – no flow has to reach it – and is read at the
moment it is asked, so a read after a write sees the new value; an arriving flow writes its input.
`count = count + 1` is one `Variable` node feeding its own output through an `Add` back into its
input. Their plurals `Get variables` / `Set variables` do the same for several at once: their settings are a *list* of
variables, one port per entry, so reading a handful is one node instead of a column of five. Those
ports are picked from the behavior's variables rather than named, they take their type from the
declaration, and renaming a variable carries them (and their edges) along. `Get properties` /
`Set properties` do the same for the object they are handed: a built-in property (`width`,
gets its own mutable copy. One node reads and writes them, `Variables`: its settings are a *list*
of variables, and each one is an output that can be read at any time – no flow has to reach it –
and is read at the moment it is asked, so a read after a write sees the new value. A variable
switched to **Set** in the inspector is an input too, written when the flow arrives; the node has
its flow pins only while at least one is. Unconnected and with nothing typed, it writes the zero
of its kind (0, false, empty), and the node shows that value beside the name. All of them are written together, their inputs read
first, so one node swaps two variables. `count = count + 1` is one `Variables` node feeding its
own output through an `Add` back into its input. With a single variable the node is titled by it
(*speed*). The ports are picked from the behavior's variables rather than named, they take their
type from the declaration, and renaming a variable carries them (and their edges) along. Files
from before this node still load: their `Get/Set variable(s)` and `Variable` nodes become
`Variables` nodes that read or write the same variables. `Get properties` /
`Set properties` pick their ports the same way, for the object they are handed: a built-in property (`width`,
`color`, `align`, …) is picked from one list covering every object type, each entry named with
its type (*Sprite width*, *Camera zoom* – a key several types share is there once per type), and
keeps its own field and default – an unconnected `color` is a colour field starting white, shown
@@ -160,8 +163,8 @@ shown on the node and editable in the inspector.
rows stay off the node body while the delay is 0, so a plain key node keeps its single line.
- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On update`, `On key down/up/held`,
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner – anywhere when it has none), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
`Is asset` (is this object an instance of asset X – how you tell what you collided with), `Get/Set
variable`, `Variable`, `Get/Set variables`, `Get/Set property`, `Get/Set properties`, `Get position` (local and world), `Is enabled`, literals, `Add/Subtract/Multiply/Divide`,
`Is asset` (is this object an instance of asset X – how you tell what you collided with),
`Variables`, `Get/Set property`, `Get/Set properties`, `Get position` (local and world), `Is enabled`, literals, `Add/Subtract/Multiply/Divide`,
`Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`,
`Substring`, `Find text`, `Equals`,
lists `List` (a constant), `Fill` (one value repeated – how an empty board is made), `Count`,
@@ -244,7 +247,7 @@ shown on the node and editable in the inspector.
shape that could be hit, so it takes every click inside the camera frame instead (the
`world x`/`world y` outputs say where) – that is the "click anywhere" listener.
- **Idiomatic movement** (the demo's "Mover", five nodes): `On update → Move(Owner, dx = Axis
input.x, dy = Axis input.y, scale = Get variable speed, dt = On update.dt)`.
input.x, dy = Axis input.y, scale = speed, dt = On update.dt)`, `speed` being a `Variables` node.
- **Idiomatic platformer** (the example game's "Player"): `On update` adds `gravity · dt` to a
`vy` variable and moves the owner by `(axis.x · speed, vy)` with *Collide with solids* on;
`blocked y` while falling means standing, so `vy` goes back to 0 and `onGround` becomes true;
@@ -659,7 +662,7 @@ npm run check # typecheck + lint + format:check + tests, what CI would ru
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
export GM_REMOTE=user@host # optional: run the browser elsewhere; see below
node tools/seed-e2e.mjs && node tools/e2e.mjs # 569 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 572 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 – a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
@@ -764,15 +767,13 @@ parallel variables copied back and forth, and each strand packs only the field i
(`mergeStruct` keeps the rest). Reading a cell is `((shape · 4 + rot) · 4 + corner)` into that
list instead of the same index doubled and read twice.
Where a step needs several variables at once, one `Get variables` / `Set variables` stands for the
Where a step needs several variables at once, one `Variables` node stands for the
lot: the call to `Next move` has **one node at each side** of it rather than five, and the master
graph is **44 nodes / 64 edges** where the same game in single nodes was 69 / 76. Inside `Next
move` each strand writes its whole answer – the candidate, what kind of move it was and the wish
it used up – in one node rather than three to five, which is **30 nodes** against 40. Two things
bound how far such a node may reach: a data node answers once per pulse and keeps that answer, so
a `Get variables` must not straddle a write of one of its variables (which is why the board is
read a second time after the lines were cleared), and a `Set variables` reads all its inputs
before it writes any, so none of them may come from a variable it writes.
it used up – in one node rather than three to five, which is **30 nodes** against 40. A writing
node reads all its inputs before it writes any, so an input fed from a variable the same node
writes sees the value from before.
It is **ten behaviors**, not one graph. `Tetris` owns the state and does none of the work: each
step of a frame is a behavior of its own, called as a node – `Spawn piece`, `Next move`, `Fits`,