Commit Graph
49 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 8a4e4a6762 Play sounds as notes of an instrument asset #331
game-maker / suite (push) Failing after 2m40s
game-maker / gate (push) Successful in 3m14s
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.

- New asset type instrument, under the heading Music, not placeable.
  A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
  only, the lowest C-1. Notes has From and To, outputs any, note,
  frequency and velocity, and with Single notes a trigger per note,
  each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
  can take any number of edges now (PortDef.many): a trigger, and
  the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
  through the pitch worklet of #317, so it keeps its length. Gate
  fades it out at the end of the note, One-shot plays through. Loop
  runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
  default. Sound FX sends the sound through a sound FX asset, its
  parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
  only for the notes it stands for, a silent source silences what it
  alone feeds, a Mix with a silent side is the other side. velocity
  and the parameters stay open for the voice. Builds are cached per
  note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
  with its own file and volume. release fades the gated ways out,
  one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
  detached, and hands out voice for Set sound FX parameters and
  Stop sound. Velocity is always loudness. Instrument parameters
  picks values for the instrument's parameters like Sound FX
  parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
  128 keys below. Keys without sound are grey. A key plays while it
  is held. Focused, the computer keyboard plays as in a tracker by
  KeyboardEvent.code, the arrows move the octave.

Findings:
- A label of the keys set fill on the shared small text style and
  darkened every small label of the editor. Caught in a screenshot
  before the commit, the keys have a style of their own now.
- Suite task 523 on cf6799a failed two checks of e2e 34 that timed
  audio by the page's clock: a 2.95 s echo tail was gone after 1 s,
  and Quad in was further along than 0.7 s allow. Both time on the
  audio clock now through a new hook __gm.sound.clock().

Tests:
- instrument.test.ts (new, 22): notes, asset, palette, round trip,
  compile per note, drum kit, zones, Mix, velocity and parameters,
  Sound FX node with rename, problems, cache, Play note, the keys
- sound-library +7 (layers, release, duration, early release, loop
  region in buffer and worklet), pitch-player +2 (loop region),
  node-catalog +4, graph adjusted. 582 tests in the touched files.
- e2e 4 and 34 on melektaus-atx: ALL PASSED, 34 with 44 checks (+7)
- Gate on melektaus-atx: CHECK EXIT 0, 243 test files, 3750 tests
- Counter-checks: any always on, release ending one-shots, Sound FX
  inputs ignored, loop region ignored. 3, 2, 1 and 1 tests failed.
- Browser probe on stein2: keys, held key, Single notes names and
  the header looked at in screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:07:40 +02:00
adminandClaude Opus 5.5 297fca3e94 Show Direction and Curve of a node only where they matter
An animation key shows Direction only for one of the ten curves and
the Curve row only for Curve. Lerp and Set sound FX parameters hid
them on the node body, but the inspector still offered both rows for
every easing, so a linear Lerp had a Direction to pick that did
nothing.

- A node definition can name `idleParams`: settings that do nothing
  at the current values. The node body and the inspector both leave
  them out.
- `hiddenParams` stays as it was. It only trims the node body, the
  inspector keeps those rows (the Order of an event, the mode of
  Find), so it could not take this job.
- Lerp and Set sound FX parameters use `idleEasing` (formerly
  `hiddenEasing`) for it.
- The idle settings are part of the node signature of the inspector.
  Picking an easing that needs Direction or Curve rebuilds the
  section, and the row appears.

Tests:
- easing-curves: 14 tests (+1), node-catalog: 381, green
- e2e 06 (149 checks, green) checks that a new linear Lerp has no
  Direction and no Curve row in the inspector. Picking Cubic brings
  the Direction row, and Out is picked there as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:15:15 +02:00
adminandClaude Opus 5.5 183f356c99 Let a game hand values to the parameters of a sound FX #311
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 14m15s
A sound FX played the same every time: its numbers were settings of
the nodes. Now the Input declares parameters with a default, every
number of Gain, Pitch and EQ is an input they can feed, and a Play
sound hands in values for them.

- Input declares number parameters in the inspector, each with a
  Default (DeclaredPort.default, SetPortDefaultCommand). They are
  outputs of the Input. Input and Output count as terminals now, one
  of each per graph.
- Gain, Pitch and EQ take their numbers at inputs under the keys the
  settings had, so saved graphs sound as before without a migration.
  Each value is held to its range however it arrives.
- The compiler gives an FxPlan with the parameters still open.
  fxChainOf fills them in when a voice starts. The player still only
  sees an FxChain.
- New behavior node Sound FX parameters: names an FX and picks its
  parameters like a Variables node picks variables. Each picked one is
  a number input that starts at the FX's default. Its output has the
  new port type fxparams, which only fits Play sound's fx params.
- Play sound warns when the parameters name another FX than the one
  it plays. Names the FX does not know are left out.
- Renaming a parameter at the Input renames it in every Sound FX
  parameters node naming that FX, edges and values included.
- The Sound FX tab has a number field per parameter in its header.
  Play uses those values, they are never written into the asset.
- A value counts from the start of a voice. Changing a playing voice
  is #315, a Mix node is #316.

Tests:
- soundfx.test.ts 22 (+11): parameters in the plan, held values, a
  0-by-default stage, EQ band, save with defaults, wrong port types,
  default command with undo, Sound FX parameters into Play sound,
  defaults of picked ones, node problems, mismatch, rename with undo
- node-catalog 368 (+3): Play sound fx params, Sound FX parameters
- e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5,
  a Sound FX parameters node gives the game's voice depth 7)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test
  files, 3636 tests green
- Counter-check: with fxChainOf ignoring the values and the rename
  not followed, 5 of the new tests fail.
- Browser probe: header fields, inspector Default and the picked
  parameter looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:26:20 +02:00
adminandClaude Opus 5.5 de14f0f9e2 Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.

- New asset type soundfx ("Sound FX"), not placeable, with a graph
  and its own tab. The heading in the asset panel is "Sound effects",
  so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
  that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
  EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
  emitter. An input takes one edge, so the result is one line: a
  pitch and a list of gain and filter stages. A node without input
  or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
  sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
  An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
  order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
  through the graph as it stands. Stop there ends only its own voice.

Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
  node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
  2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
  test files green. The red one was sound-library (its fake
  AudioContext had no playbackRate), fixed afterwards, gate not
  re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
  test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:45:42 +02:00
adminandClaude Opus 5.5 a526e1b40f Use a shader graph as a node in other shaders (#303)
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Failing after 14m37s
A calculation needed in several shaders (a greyscale, a noise, an
outline) had to be built again in every one of them. A shader
function keeps it as an asset of its own, called like a behavior is
since #16.

- New asset type `shaderfunction` ("Shader function"), edited in the
  Shader tab. The panel beside the graph shows no picture, the
  readout counts its inputs and outputs.
- Two new nodes: `Function input` and `Function output`. Their
  declared ports, numbers or colours only, are the ports of every
  call. One of each per function, like Start and End.
- The call is node `ShaderCall`, named after the function. The
  palette lists every function, it can be dragged in from the Assets
  panel, and a double-click opens it.
- The compiler sets the function in where it is called
  (`inlineFunctions`). It renames the copied nodes `<call>/<node>`
  and turns an unconnected port into a constant. Passes, Time, the
  Edge cut and the corner walk then see a plain graph.
- A problem inside a function is reported at the call, in the
  function's words. The function's own tab checks it on its own
  (`checkShaderFunction`).
- `ShaderLibrary` keys a shader on its graph and on every function it
  calls, however deep, so a change reaches every caller.
- The port commands mirror a new, renamed or removed port into every
  call, as for a behavior's Start and End. `NodeDef.portKinds` holds
  the two ends to Number and Color.
- As agreed in the issue: no `Property`, `Color property`, Output or
  Vertex output in a function, and `Time` is the caller's clock.
- Own decision: a `Pass` may stand in a function. It becomes a pass
  of the calling shader, so a wide blur can be a function.
- A function calling itself, also through another, says so at the
  node and is not set in. The palette does not offer such a call.
- The Shader tab showed only the compiler's problems. A call nothing
  reaches was never compiled and looked healthy, so a call now also
  shows its own problem there, as in a behavior.

Tests:
- Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in
  tests/core/shader-functions.test.ts, 2 for the new icon)
- e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in
  place both #606060/#a2a2a2, halved in the function #303030/#515151.
- Counter-check: a fingerprint without the called functions fails 1
  unit test, and in the browser the change check (#606060 stays).
- The first e2e runs found two faults, both fixed: a double-click
  only selected the function, and an unreached call of itself
  showed no problem.
- e2e total 1728 is computed (1725 + 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:09:53 +02:00
adminandClaude Opus 5.5 f03ca0d382 Draw a shader in passes with a Pass node (#301)
game-maker / gate (push) Successful in 3m3s
game-maker / suite (push) Successful in 14m43s
A Blur stops at radius 8 in one pass, and bloom needs a softened copy
of the picture beside the original. A Pass node cuts the graph: what
feeds it is drawn into a picture of its own first, and the nodes that
read a picture read that one when it feeds them.

- Pass has color and alpha like the Output, a Resolution setting
  (Full, Half, Quarter) and a picture output. Texture color, Texture
  alpha, Sample texture, Blur and Texture size get a picture input.
  Unconnected it is the wearer's own picture, so the original stays
  readable beside any pass.
- picture is a port type of its own. It plugs only into a picture
  input, not even into an any port.
- The walk shared by the compilers can stop at a node (cut). The
  shader compiler draws each Pass it meets as a fragment of its own,
  earlier passes first. A ring through a picture says Feeds itself,
  and more than 8 passes say so at the node.
- A pass draws untinted. The tint comes on when the last part reads a
  picture, so a particle source draws its passes once for all of its
  particles (user decision).
- A pass's picture is the wearer's picture and its Padding, at the
  pixels of the picture times Resolution, at most 2048 per side. It is
  read with the picture's own filter and continues past its frame as
  the Output's Edge says.
- PassPictures keeps one render texture per pass and wearer. It draws
  again only when picture, size, Padding or a uniform changed. A
  field's picture is a render texture that changes under the same id,
  so a field draws its passes every time.
- Sprite, sliced sprite, text, particle source, field shader and the
  Shader tab's preview all draw passes. syncShader takes a fixed
  picture for a 9-slice, which holds its corners but keeps the margin.
- texelAt and blur take the sampler they read. Existing shaders
  compile to the same program apart from that argument.
- A field shader's Blur and Texture size count a pixel as a unit of
  the frame (uPixelScale), not of the screen. Before, a field's blur
  grew when zooming out and shrank when zooming in. This came with
  #170 and #300 and is fixed here because passes inherit it.
- tools/perf-shaders.mjs and tools/perf-field-shaders.mjs measure two
  passes with GM_PERF_PASSES=1 and GM_PERF_PASS_RESOLUTION. Below Full
  an open Pass shrinks the picture first, as the wiki's recipe does.

Found on the way:
- Destroying the pictures before the shader made Pixi warn that a
  texture was still bound. The shader goes first now, and a picture of
  a new size is resized rather than replaced.
- Two passes in a row could draw plain white the first time. Pixi
  builds the code that binds a program's resources once and finds them
  by position. A pass of one graph is often the same program as a pass
  of another with fewer slots, so it bound a placeholder. Every shader
  now declares all 8 pass slots and its uniform group.

Tests:
- Gate on melektaus-atx: 225 files, 3480 tests (3467 + 13), exit 0.
- e2e 28 and 31 on melektaus-atx: 97 checks (94 + 3) and 44 checks
  (43 + 1), all passed, no Pixi warning in the console.
- 28l-vii: a Blur across, a Pass and a Blur down draw the 5x5 spot of
  a Blur of Both (difference 0.000). On a grey sprite a pass's picture
  added to the original is 231/60/60, each tinted once. Two Passes in
  a row after a graph with one show red and green, not white.
- 31v-iii: a Blur of 8 fades a field's frame edge over 8.0 units at
  50 % and at 200 % zoom.
- Counter-checks: a tinted pass fails 1 unit test. Pictures never
  bound fail both e2e checks (spot 0.20 instead of 0.04, 243/158/158).
  Pass slots by the number of passes draw #ffffff, and a field without
  its pixel scale ramps over 16 and 4 units: exactly the two new
  checks fail.
- e2e sum 1716 is counted, not yet measured (1712 + 4).
- Measured on stein2 (HD 630, one run per row): 50 field shaders at
  radius 8 go from 11.4 to 41.3 fps in two passes, 44.4 at radius 16
  (three passes at Half). 800 sprites at radius 8 go from 13.9 to
  24.5 fps. At radius 4 one pass stays faster from 400 sprites on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:51:18 +02:00
adminandClaude Opus 5.5 46c620ae3b Give a behavior more ways in and out, and hear it as an event (#294)
game-maker / gate (push) Successful in 2m48s
game-maker / suite (push) Failing after 13m34s
A behavior run by another one had a single flow port at its Start
and at its End, so it could not branch for its caller. And it could
not be used as an event: a call needs a pulse at its flow input,
and the events of a called graph never fire. An input like
"Confirm" (Space or Enter) had to be built again in every graph.

- A declared port of a Start or an End node can be of the kind
  Flow. It sits above the values and shares their names, exec stays
  reserved. Only these two nodes declare one, a struct does not.
- Run behavior shows a flow port of the Start as a further flow
  input. A call arriving through it leaves the Start through the
  output of that name and not through exec.
- Run behavior shows a flow port of the End as a further flow
  output. The call goes on through the one the End was reached by
  last, and then through exec. exec fires always, as before.
- On behavior is a new event node without a flow input. The
  behavior it names runs at the node for as long as the behavior
  around it runs, with all of its own events and one set of
  variables per node.
- Its outputs are those of the End. It fires once the inner pulse
  is through, by the output the End was reached by last, exec only
  for End.exec. A pulse that never reaches the End is not seen.
- The value ports of the Start are its inputs, read anew for each
  inner pulse. The Start itself never fires there.
- It nests to any depth. Under a Run behavior it stays silent. A
  behavior hearing itself, however far round, is not started and
  the node says "A behavior cannot listen to itself".
- The palette offers every other behavior with an End once more as
  "On <name>". A node naming one without End says so.
- An On node removed while the game runs ends its behavior with On
  disable and On destroy. The sounds of the entity end only with
  the behavior placed on it.
- Renaming and deleting a declared port follows the edges at On
  behavior nodes as it does at Run behavior nodes.
- The flow outputs of the Start of a behavior placed on an object
  never fire, like its exec.
- The engine prompt of the benchmark names flow ports and On
  behavior.

Tests:
- Gate on melektaus-atx right before this commit: 215 files, 3319
  tests (3278 + 41), exit 0.
- e2e 19 on melektaus-atx: 39 checks (+4), all pass. e2e
  1-6,17-24,26,36,51: 628 checks, all pass.
- 19 unit tests in flow-ports.test.ts, 19 in on-behavior.test.ts
  on a real world, the rest in the node catalog.
- Four new e2e checks in module 19: a flow port declared by mouse
  at the End, On Confirm in the graph, the call leaving through the
  further output while playing, Enter and Space on a real keyboard.
- 19 unit counter-probes all fail their tests. One did not at first
  (a removed node kept its instance), a test was added. Two browser
  counter-probes fail their checks.
- One screenshot looked at: On Confirm with exec and pressed, the
  second flow pin at the calling node.
- The e2e sum of 1680 is counted (1676 + 4), the full suite and
  the smokes did not run.
- Not seen in the browser: flow inputs at the Start, nesting, the
  ring, values through On behavior, the palette entry. No test
  holds that the sounds of the entity go on when a heard behavior
  ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:33:34 +02:00
adminandClaude Opus 5.5 0f6e628616 Check the declared type when a graph writes a typed variable (#271)
A list variable with an item type only kept its shape in the editor.
A graph could write any list into it at runtime, and the List node had
no type at all. Now the declaration decides what a write may store.

- Writing a list into a typed list variable leaves out every item that
  does not have exactly the fields of the type. The rest is written and
  the console names each item that was left out, like Add item does.
- The same rule holds for typed list fields inside a struct variable
  and for lists inside typed items, at any depth.
- A typed struct variable refuses a struct with a field the type does
  not have, and a value that is no struct. A struct with missing fields
  still merges in as before, so a partial Pack keeps working.
- The check runs in the Variables node (own variables and Variables
  assets) and in the Properties node when it sets a behavior variable
  of another object.
- Ports learn their declaration through GraphLookup: what a variable
  declares about its type, and which behavior owns a graph. The rule
  itself lives in the new src/core/graph/typedValues.ts, fitsType moved
  there from SetItem.
- The List node gets a Type setting. With a type its items are shaped
  and marked, follow the struct asset, count when the type is deleted
  and offer Split into fields in For each. The mark never reaches the
  running game.
- A type on a List node holding something other than structs is
  refused with a console line.
- Setting the List node back to None removes the marks, unlike a list
  variable. A node list has no per-item type row, so marked items could
  never be freed there. Undo brings everything back.
- tests/core/kinds.test.ts looked for the value as the first parameter
  of a literal. Type now comes first on the List node, so the test
  finds the value by its key.

Tests:
- Gate: 198 files, 2869 tests (2856 + 13), CHECK EXIT 0
- e2e 55: 19 checks (+55j write at runtime, +2 for 55k List node)
- e2e 54: 16 checks, smokes 24 / 19 / 21
- Five mutations, each fails between 1 and 4 tests
- e2e total 1606 is computed (1603 + 3), no full run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:08:22 +02:00
adminandClaude Opus 5.5 4e6aeb5f72 Query objects in bulk and filter, count or test a list (#259)
game-maker / gate (push) Successful in 3m36s
game-maker / suite (push) Failing after 10m6s
The object queries only ever returned one object: Object by name,
Child by name and Find all stop at the first hit. Getting every coin,
every child or every item that passes a test needed a For each with
its own bookkeeping. These nodes return the whole set as a list of
Object, so For each and Find can use item straight away.

- Objects by name returns every scene object with that name in scene
  order.
- Children by name returns every match below object, nearer levels
  first, the same order as the singular node.
- Children returns the direct children, with recursive all of them
  level by level.
- Objects of asset returns every instance of an object asset or
  container by the same rule as Is asset, spawned ones included.
- All four take a snapshot. Destroyed objects are never in it,
  disabled ones are, as with the singular nodes.
- Find has a Mode setting: First, All, Any and Count. First is the
  default and hides the row, so existing graphs look the same.
- Match follows the mode of its paired Find: done with items and
  indices, done with any, or done with count. An unpaired Match shows
  First, since the pairing only comes from the wiring.
- First and Any stop at the first hit, All and Count walk the list.
- Changing the mode drops edges at outputs the Match no longer has,
  in one undo step. SetNodeParamCommand now cleans the whole graph,
  but only edges that were valid before the change.
- The palette entry "filter" adds a Find set to All with its Match.

Found on the way:

- After adding the filter entry, "find" + Enter picked Filter. The
  entry has node type Find, counted as an exact hit, and the tie went
  to the alphabet. The search now ranks by the entry id. e2e 6z-v
  caught it.

Tests:

- ./check.sh: 188 files, 2746 tests, CHECK EXIT 0.
- find-modes.test.ts with 11 tests, 11 catalog cases for the four
  nodes, the asset input guard now expects 3.
- e2e 6z-vii with 3 checks: filter from the palette, Count picked in
  the inspector drops the items edge, a run logs 2, 2, 2, 3, 4, "0, 2".
- GM_STEPS=1-6: all passed, 278 checks.
- Counter-checks fail as expected: every mode stopping early (2),
  Match ignoring the mode (6), cleanup on the own node only (1), no
  recursion (2), no inOrder (2), old ranking (1). In the browser a
  Match ignoring the mode fails both editor checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:43:03 +02:00
adminandClaude Opus 5.5 2aaca01929 Find the first matching item with a Find … Match block (#255)
Searching a list with For each needs the test result wired back into
break on the left of the loop node. The wires then run across the node
and the graph reads badly. Find … Match keeps the whole search flowing
left to right, while the test in between stays a free graph.

- Find takes exec and list and offers item and index. It sets them to
  each element in turn and has no exec output of its own.
- Match reads its bool input match once per element. The first true
  value continues at found with item and index of the hit. No hit or an
  empty list continues at not found with an empty item and index -1.
- The pair is derived from the wiring and stored nowhere. A Match
  belongs to the Find whose item or index its test is built from. Copy,
  paste and undo therefore need no extra code (findBlock.ts).
- An unpaired Find or Match, two Matches on one Find and a Match fed
  by two Finds show a warning on the node. At run time the flow ends at
  an unpaired Find and the console says so.
- Adding Find from the palette places its Match 480 px to the right in
  the same undo step.
- The graph draws a quiet frame behind Find, Match and the nodes of the
  test. Literals feeding the test from outside stay outside the frame.
- The test consists of data nodes only, like the break of For each.

Tests:
- ./check.sh: 184 files, 2694 tests, CHECK EXIT 0
- e2e GM_STEPS=1-6: ALL PASSED, 272 checks
- New find-match.test.ts with 12 tests, 2 catalog cases, e2e 6z-v
  with 3 checks using a real mouse.
- Counter-checks: ignoring the test fails 4 unit tests, no ambiguity
  check fails 2, framing everything fails 1. In the browser, no frame
  and no Match from the palette each fail their check.
- Check total in README: 1517.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:33:25 +02:00
adminandClaude Opus 5.5 cb8a2d029a Loop over a list and stop at the first match with For each (#249)
Finding the first item that matches took Count, Repeat, Item at and a
Branch, and the loop could not stop once it had found something. A graph
has no lambdas, so the predicate is an input that is read every round.

- For each runs body once per element, with item and index set, and
  goes on at done.
- Its break input is read anew before every round, after item is set.
  Once it is true the loop ends there and that round's body does not
  run.
- done sees the matching item and index with stopped true. Without a
  match it sees no item, index -1 and stopped false, the OrDefault.
- RepeatResult has optional items, stopWhen, stopped and exhausted.
  GraphInterpreter.loop reads them, Repeat runs as before.

Tests:
- ./check.sh: 180 files, 2623 tests, CHECK EXIT 0
- e2e GM_STEPS=1-6: ALL PASSED, 269 checks
- 5 interpreter tests in graph.test.ts, 2 catalog cases
- e2e 6z-iv: the palette creates the node, and a search for
  Name == "Gold" in Axe, Gold, Bow, Gold logs "1true"
- Counter-checks: without the break check 2 unit tests fail, and with
  break not read over its edge 6z-iv fails
- This commit alone: tsc and the three touched unit files pass
  (349 tests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:06:40 +02:00
adminandClaude Opus 5.5 7979b8ff19 Let structs and lists hold objects across behavior runs (#238)
game-maker / gate (push) Successful in 3m28s
game-maker / suite (push) Successful in 10m14s
An object could only live in a dedicated object variable, kept in slots
beside the variable bag. Struct fields and list items could not hold
one, so a spawned object packed into a struct or list was lost. This
replaces the slots with one mechanism for all three places.

- "No object" has one resting form everywhere: {"$object": ""}. A
  running game can tell an object slot from an empty text that way.
- During play the object handle lives directly in the value, also
  nested in structs and lists. objectSlots.ts and the special paths for
  Keep and Properties are gone.
- liveObjects.ts strips objects to (null) at every exit: overrides the
  editor run writes back to the scene, saved values of persistent
  Variables assets, live values in the inspector, and the world
  snapshot of the test hooks (objects as names there).
- A destroyed object reads as (null) through isAlive, alone, in a
  struct field or as a list item.
- "+ Field" and "+ Item" offer Object. The inspector and the list tab
  show a fixed (null) row without input.
- Lists from Raycast all stay objects when written to a variable.
  Before they turned into names.
- A struct that only changes by an object writes no override. A list
  that grows by an object writes [(null)], its length really changed.

Found on the way: CI at 98211a8 failed only in e2e 21d. #237 added
asset to the field kinds without updating that fixed expectation. 21d
now expects ...,animation,asset,object,struct.

No project on the server has object variables, so no old data is
affected.

Tests:
- ./check.sh: 169 files, 2475 tests, CHECK EXIT 0.
- e2e 6, 21, 23, 32: ALL PASSED, 213 checks.
- object-variable.test.ts rewritten: variable, struct field and list
  item over several frames, destroyed objects, nothing in scene, file
  or save, editor run reload, Keep.
- New e2e 21d-ii adds an Object item by click. 21d and 23k updated.
- 7 counter-probes, each fails 1 to 9 tests. Originals back via cmp.
- Not tested on its own: stripping of inspector live values and the
  names in the test snapshot, the "+ Field -> Object" menu by click.
- e2e check total 1407, only calculated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:12:44 +02:00
adminandClaude Opus 5.5 1d18862ed6 Give unconnected node inputs the default the node shows (#234)
A new node handed null to every unconnected input that had no port
default, even a Bool. The inspector showed false, 0, "" or an empty
list at the same time, so what the node read was not what the editor
said. 97 of 135 inputs of the built-in nodes were affected.

- inlineValue() falls back to the default the inspector shows. One
  place covers every node, also future ones and saved projects.
- A default list is handed out as a fresh copy per read, so a node
  that edits its input in place cannot change the shared default.
- The node shows the value it reads: an empty text or list as
  (empty), an object or struct input without a link as (null).
  Live values while playing use the same words.
- A key input without a key keeps showing a dash, like the key field
  in the inspector.
- Visible changes in old projects: To text without input gives ""
  instead of "0", Select and First not empty give "" instead of null.

Tests:
- 8 new unit tests in tests/core/inline-defaults.test.ts, among them
  one over all built-in nodes that no typed-in input reads null.
- The e2e check in module 6 reads the rows of an unlinked Name of
  node. The check sum stays the same.
- ./check.sh: 167 files, 2434 tests, exit 0. e2e 6: 117 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:35:43 +02:00
adminandClaude Opus 5.5 454d202109 Let each variable of a behavior choose whether calls keep it (#229)
game-maker / gate (push) Successful in 3m13s
game-maker / suite (push) Failing after 8m46s
The first version of #229 put a Keep state switch on the Run behavior
node. The user wanted it where the variable is declared instead. Whether
a value lives on belongs to the variable, not to every call site.

- The node switch keepState is gone again. RunBehavior, CallResult and
  nested.ts are back to what they were before #229.
- BehaviorAsset.keptVariables lists the kept names. It lives on the
  asset and not in PropertyDef, because that is the schema of every
  inspector row.
- A variable added from now on starts with Keep on. Variables in
  existing projects start with Keep off, so old projects run unchanged.
- SetVariableKeptCommand flips the switch as one undo step. Add, remove
  and rename carry the list along. Loading drops names that are not
  declared.
- The interpreter keeps only the listed variables, per Run behavior
  node and per object as before. The others start at their defaults.
- Local state machines in a called behavior start empty on every call
  again, as before #229. Global and scene machines are unchanged.
- The inspector shows a Keep button in each variable row of a behavior.
  Its state is part of the asset signature, so a click redraws the row.
  Without that the button kept its old look until the inspector was
  reopened.

Found: the project Garden of Sacrifices still has keepState = true on
the call Player -> Character Movement. It is ignored now. Direction
there predates the switch, so Keep is off until set by hand.

Tests:
- ./check.sh: 163 files, 2392 tests, CHECK EXIT 0 (before 2388)
- tests/core/keep-state.test.ts rewritten, 15 tests
- e2e 19: ALL PASSED, 35 checks. 19c-ii now clicks the Keep button
  of a new variable: on when added, off by a click, on again, off
  after undo.
- Counter-checks, each fails its own test: keep list ignored (2),
  states kept (1), new variable not kept (8), rename drops the list
  (1), no Keep button (e2e 19c-ii).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:50:59 +02:00
adminandClaude Opus 5.5 7c70245c79 Let a nested behavior keep its variables between calls (#229)
game-maker / gate (push) Successful in 3m3s
game-maker / suite (push) Failing after 8m39s
A Run behavior call started with fresh variables every time, so a
nested behavior could not count, remember or keep a local state
machine across calls. A new "Keep state" switch on the node lets the
callee resume where its last call left off. It is off by default, so
existing projects run unchanged.

- RunBehavior gets the boolean parameter keepState and its node
  description explains it.
- GraphInterpreter keeps the state in keptCalls, keyed by the call
  path, the chain of Run behavior nodes. Two nodes calling the same
  behavior each keep their own state.
- One interpreter exists per behavior instance, so every object gets
  its own state without further code.
- Local state machines of the callee are kept like its variables.
- resumedVariables() matches kept values against today's
  declarations. A new variable starts at its default, a removed one
  drops out, and one whose kind changed starts over.
- The kept state lives as long as the object in the run. It is not
  written to save games.

Limit: a callee input named keepState collides with the switch. The
same already holds for the name behavior, so it is not caught.

Tests:
- ./check.sh: 163 files, 2388 tests, CHECK EXIT 0 (before 2377)
- 10 new unit tests in tests/core/keep-state.test.ts and 1 new
  node catalog case.
- e2e 19: ALL PASSED, 35 checks, with the new check 19c-ii for the
  checkbox, its click and undo.
- Counter probes: ignoring keepState fails 5 tests, state per
  behavior instead of per node fails 1, dropping the outer path
  fails 1, skipping the kind check fails 1, and without the node
  parameter the e2e check fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:41:17 +02:00
adminandClaude Opus 5 d7a2521ca7 A port can take its type from what is plugged into it (#207)
game-maker / gate (push) Canceled after 1m8s
game-maker / suite (push) Canceled after 1m8s
Until now every port of "First not empty" was grey and typeless, so the
editor could not tell you what actually travels through the node. The
output is now typed as soon as both inputs agree, and every any-input is
painted in the colour of its own edge. Nothing changes at runtime.

- A NodeDef can declare which of its ports carry the same value
  (`sharedType: { inputs, outputs }`). The rule itself lives as a pure
  function `agreedType` in `src/core/graph/portTypes.ts`.
- Only the output inherits. It becomes the shared type when every input
  of the group is connected and they all carry the same type, otherwise
  it stays `any`.
- The inputs stay `any` on purpose. `canConnect` checks against the port
  type and a busy input is replaced by the new edge, so a typed `a`
  could never be re-plugged with a text without disconnecting first.
- Chains do not propagate. When looking up what feeds an input, a node
  of the same kind reports the `any` it is registered with, so two of
  them in a row stay untyped and a data cycle cannot ask itself in a
  circle.
- An edge that the new type makes invalid stays where it is and turns
  red. `EdgeLayer` already paints a mismatching edge with `danger`,
  nothing is disconnected on its own.
- New `PortDef.paintType`: colour only, never truth. It is set in
  `drawnDef` and read in `NodeView.redraw`, so `canConnect`, the
  interpreter and the compiler keep seeing `any`. Every connected
  any-input is painted, not just members of a `sharedType` group; ghost
  ports keep their hollow red look.
- The pin now matches the edge that ends in it, because both take the
  colour from the resolved output type of the neighbour. Mixed sources
  therefore show two colours, which explains why the output stays grey.
- New hooks `__gm.graph.portTypes(id)`, `__gm.graph.edgeColor(id)` and
  `__gm.themeColor(name)`. `portTypes` reports type and paint colour
  separately so the type checks do not silently become colour checks.

Findings:

- `resolveDef` only appends derived ports to the registered ones, so a
  fixed port cannot be retyped that way -- it would appear twice. The
  shared type is therefore resolved before the ports are built.
- Answer 5 of the issue (inline field follows the type) has no case left
  under variant A: an unconnected input stays `any` and keeps its text
  field.
- `Item at`, `Join` and `Equals` do not profit: `Item at` has no
  any-input, the other two have fixed outputs. The next candidates are
  the shader arithmetic nodes, which were left alone.

Tests:

- ./check.sh -> 143 files, 2127 tests, exit 0 (was 142 / 2113)
- GM_STEPS=1-6 fresh -> ALL PASSED, 245 checks (was 242), 9 of them new
- 14 new unit tests in tests/core/shared-port-type.test.ts
- 9 new e2e checks in block 6z, one of them a pixel check straight on
  the pin via pixelAt + themeColor
- Counter-probes all bite: filtering unconnected inputs out of
  `agreedType` fails 3 unit tests, ignoring `inherit` fails 1, killing
  the derivation in the bundle fails exactly the 2 expected e2e checks,
  and never setting `paintType` fails 4 unit tests plus the 3 pin
  checks (#bdbdbd instead of #e6a8ff / #6fcf97)
- Screenshot read: `a` pink, `b` green, output `value` grey, edges in
  the same colours
- Check total 1302 (1299 + 3), 1293 was measured by CI at 1a3babf

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:15:21 +02:00
adminandClaude Opus 5 ffdf0a8ddb The new node "Raycast all" answers with every collider along the ray instead of only the nearest one: it carries the same inputs and settings as Raycast (Ignore owner, Hit triggers, Layers from #158) plus "max hits" where 0 means all, and it hands back parallel lists sorted by distance - objects, colliders, points x/y, normals x/y, distances - next to count, so index i of every list speaks about the same hit; a collider counts once no matter how many convex parts its shape is built from, while a tilemap counts per cell it is hit in, and the filter sits in Solids.raycastAll like the one of #158, the one path the editor run and Play both take, so all 12 new unit tests run through describe.each against both systems; the node needed the value system to learn something it could not do - a graph list carried numbers, text, booleans and bundles but no object handles, asList turned an object into its name, so objects/colliders would have been lists of names, which loses the object when two carry the same name, and collider children usually do; Value now knows a travelling list that may hold handles and "Item at" gives such an item out as an object, ready for Destroy, Apply force or Name of, while the single door into a stored file - writeTyped/asTypeOf - runs the list through the new storedList() so every object becomes its name again and nothing living reaches a project file, which is its own unit test over JSON.stringify; three list nodes follow the handles: "Index of" compares objects by identity because a name is not the object of that name, "Set item" and "Fill" write an object as an object, and "Join text" prints the names instead of the innards; this decision was mine to make because the question ran into the MCP timeout - the alternative was a list of names plus "Object by name", which is the smaller change but lossy; 4 new e2e checks in the new block 43e (shield and floor in one go reading 2 and "145, 280", the second object answering to its name, which only a real object in a list can do, "max hits" 1 stopping at the nearest, and the node showing the row) and 18 new unit tests, 12 in raycast.test.ts, 4 on the value system and 2 in the node catalog; two counter-checks hold: not counting tilemap cells one by one drops 2 unit tests, and answering with the nearest hit only drops module 43 with 2 FAILs; one finding on the way - the node read "maxHits" while every other input carries a spoken label, because int() took the key as its label, so it has a third parameter now, and the e2e check caught that, no unit test did (#157)
game-maker / gate (push) Successful in 2m4s
game-maker / suite (push) Successful in 7m26s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 18:54:12 +02:00
adminandClaude Opus 5 7babf88636 A state change is something the graph can react to, so the machine announces it (#53, #54, #55, #57)
game-maker / suite (push) Successful in 10m58s
Four issues that all reach for the same line: how the current state is
read and written. Splitting them into four commits would mean four
states of the tree that do not compile, so they arrive together.

#55 start state — the machine asset carries an optional `start` holding
a State node's id. Right-click a state to set or remove it, a ring in
the node's title says which one it is. A dead id (the state was deleted)
reads as "no start", so nothing has to clean up after a deletion.

#53 entered/exited — `On state entered` and `On state exited`, one exec
output per state. Every change now goes through a single `setState`
that reports it; BehaviorSystem delivers it after the exec flow that
caused it, `exited` of the old before `entered` of the new, round after
round until the queue is empty, with a cap that stops a ping-pong graph
with an endless-loop error. A local machine tells the instance that
switched, a global one tells every instance holding a matching event
node. The start state fires `entered` too: per instance for a local
machine, once per run for a global one.

#57 reading — `Current state` with `name` for showing and `id` for
comparing, `Is in state` with one boolean per state. Neither has a
`run`, so `read` learned `def` and `lookup`, and the catalog test knows
this third kind of node.

#54 seeing it — during a run the state machine tab marks every state
some instance is holding faintly, and the one held by the selected
scene object's instance strongly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 06:18:20 +02:00
rootandClaude Opus 5 68c92daaa6 A transition carries a name, so a state needs no visible port (#52)
game-maker / suite (push) Successful in 10m22s
A state keeps its `in`/`out` exec pins in the model and the editor never draws
them. That is the whole trick: connecting, undo, deleting a state and saving all
keep working untouched, because `normalizeGraph` filters nodes and never edges,
and no edge ever checks its port key against a NodeDef. Only two places change --
`NodeView.redraw` stops drawing the row, and `portAt` refuses to hand one out, so
an invisible pin cannot be grabbed either.

Transitions are drawn by right-clicking a state, `Make transition`, then clicking
the target. The armed mode takes the same shape as a live drag, so the existing
preview draws it with no second code path, and it dies with Escape, a click on
empty canvas or a switch to another asset.

Removing the pins removed the only handle a transition had: edges have no
hit-test anywhere, and the pin right-click was the sole route to Disconnect.
The inspector's TRANSITIONS OUT / TRANSITIONS IN lists are therefore not a
convenience, they are the delete path.

`GraphEdge` gained an optional `label`, which `normalizeGraph` has to pass
through explicitly or a saved name is dropped on load. `State transition`
replaces `Next state`: it takes the machine on an input port and the name from a
picker, and resolves against the *current* state. Names are deliberately not
unique across a machine -- two states may both have an `alarm` -- so a node named
`alarm` moves whichever state has one, and an empty name follows the single
unnamed edge.

The geometry moved to a Pixi-free `transitions.ts`: a gap to the box border, and
a fan that puts a pair of opposite edges beside each other instead of on top of
each other. Keeping it as plain maths is what let a screenshot bug become a unit
test -- an earlier pin-to-pin line ran under the target node and took its
arrowhead with it, because `nodeLayer` sits above `edgeLayer`.

One limit stands and is not fixed here: `ConnectCommand` rejects a second edge
between the same two states ("Already connected"), because every transition
hangs off the same two ports. With names, `A --alarm--> B` beside
`A --seen--> B` is a sensible pair, so this is worth its own issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:05:18 +02:00
root a041322a0e An edge to a vanished port needs a handle, so the port stays as a ghost (#60)
game-maker / suite (push) Successful in 10m7s
Four ways erase a derived port and take no edge with them: deleting a state,
deleting the `State machine` literal, deleting `Behavior start`/`end`, deleting
the asset. The common cause is `RemoveNodeCommand` clearing only its own graph,
so the fix sits in `drawnDef` (graph/nested.ts) and touches no delete site.

A right-click on an edge goes through `view.portAt` only — there is no curve
hit-test. Without a port row such an edge is not merely invisible, it is
unreachable, so the ghost row is the handle Disconnect hangs on.

`resolveDef` stays untouched: it is the truth for interpreter, compiler,
ConnectCommand and dropTarget, so nothing runs or connects through a ghost
without a second guard.
2026-09-14 11:50:36 +02:00
root 0f9e1611d1 A particle is a function of its age, not a thing that is stepped (#73)
game-maker / suite (push) Failing after 4m22s
The asset type `particles` is placeable and carries a graph at the same time —
`behavior` was the blueprint. Because a continuous emitter gives birth to particle n
at time n/rate, the whole field is a function of the clock rather than of the frames:
no ring-buffer bookkeeping, exact time jumps, reproducibility as a measurable property.

compile.ts follows compileShader's frame but emits JS closures into a Float64Array
instead of GLSL, and without eval; a node with two consumers is evaluated once.
Drawing is one Pixi ParticleContainer per source, so one draw call.

Collision, wind and braking in flight are out for good — that is the price of the
formula model, not an omission.

The tab count goes from nine to ten, which is exactly the case #84 was built for one
day earlier: at 1280 px the row no longer holds every tab, so guard 14b now asks for
hidden.length > 0 instead of === 0.
2026-09-14 05:44:25 +02:00
rootandClaude Fable 5.1 3da0ef1be2 Shader graph: alpha input on Output, Texture alpha node (#70)
game-maker / suite (push) Successful in 7m55s
Output gains a number input `alpha`; unconnected it reads the texel's
alpha, like `color` reads the texel. The epilogue premultiplies with the
resolved alpha. New `Texture alpha` node exposes texel.a for maths.

PortDef.inline=false keeps the alpha port without an inline field: every
number port is inline by default, and a typed default of 0 would have made
every shaded sprite invisible. The compiler's texel fallback is typed:
colour ports get rgb, number ports get texel.a.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 11:51:59 +02:00
rootandClaude Fable 5.1 42cafa7543 Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:41:14 +02:00
rootandClaude Fable 5.1 e7d82bd1a9 State machines: asset, tab, State branch and Set state nodes (#51)
A state machine is an asset whose graph holds one `State` node per state
(no ports, name as title), edited in a new "State machine" tab that reuses
the graph canvas: right-click places "State n", snap toggle shared with the
Behavior tab via SnapToggle. `statemachine` is a value kind like texture
(port, literal node, variable, struct field, tagged list item).

Behaviors use it through `State branch` (one exec output per state) and
`Set state` (one exec input per state; the interpreter now hands `run` the
entry port). States live per behavior instance in NodeContext.states, with
no initial state. Ports derive from the inline picker or a wired
`State machine` literal; BehaviorLookup became GraphLookup with
behavior(id) and machine(id).

The Graph tab is labelled "Behavior" (key unchanged). Unit tests
(state-machine, kinds, catalog, ports), e2e step 26 and README added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 18:00:23 +02:00
rootandClaude Opus 5 3ed8b68ecf Describe every value kind in one table instead of spreading it over the code
A kind like colour or texture used to be spelled out in five parallel
lists, its port type, its empty value, its glyph, its label and its list
tag - 22 literal places for texture alone. core/properties/kinds.ts now
holds one row per kind and the rest is derived from it, so adding a kind
is one row. The five literal nodes collapse into literals.ts, the six
kind menus into one helper.

Number, Text, Boolean is now the one order everywhere; the variable and
port menus used to list Boolean before Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:47:17 +02:00
rootandClaude Fable 5.1 11c900c998 Drop the per-list item declaration; every item carries its own kind
A list no longer says what its items are. ListDef.items, ListDef.fields,
ListFieldDef.items and SetListItemsCommand are gone; the single seam is
src/core/properties/listItems.ts. JSON already tells number, text, boolean
and struct apart, so only two kinds need a tag: {"$color": n} and the new
{"$texture": id}. Field names cannot start with "$", so a tag never
collides with a struct field. On a data link a texture item arrives as
its bare id; the graph has no images.

List tab: the mark in front of each row is the kind picker, "+ Item"
asks for the kind, both are ordinary value edits (one undo step). A list
inside a struct field only offers the flat kinds because the depth rule
forbids a struct item there (ListAccess.kinds); the menu used to offer
it and silently do nothing.

Legacy lists that declared items: 'color' are tagged on load.

Actor demo: the four walk animations and four standing frames live in
two list variables of texture items indexed by facing (Item at), so the
graph can be rebuilt in the editor. The stand frames get their grid ids
back; 44 nodes instead of 46.

Also: the user's edits to on-start-during-play.md and the new note
reseed-because-of-list.md.

Verified: check.sh 794 tests, full e2e 631 checks, smokes 24/19/20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:35:02 +02:00
rootandClaude Opus 5 495756b500 Remove exports and methods nothing uses any more
A reference search over every export and method in src/ found code that
only tests still called, or that was never called at all:

- RemoveSceneObjectCommand: the product only removes through the
  multi-object command; the tests now use that one too.
- NodeRegistry.requireNodeDef: a second function of the same name in
  commands/require.ts raises a CommandError instead of throwing, and is
  the one the product uses. The twin invited the wrong import.
- isTextureAsset, listContainers, AssetTabEditor.show,
  FocusManager.destroy, KeyboardBridge.selectAll and the getters
  ColorField.value, ListView.selected, NumberField.number.

Also: Theme.controlActive now matches the resting control colour, and a
new note asks what editing the graph during play would take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 20:15:49 +02:00
rootandClaude Opus 5 58f19a3451 Replace the property nodes with one Properties node
Get/Set property, Get/Set properties, Set enabled and Set text are gone.
A single Properties node takes a fixed "object" input; every chosen
property is an output that reads the current value, and one switched to
Set is also an input written when the flow arrives. Exec pins only exist
while at least one entry writes.

The node carries a type (Sprite, Text, Collider, Camera, Container,
Behavior, Any) that decides what "+ Property" offers: the fields of that
type, the variables of the chosen behavior, or for Any a kind to pick
and a name to type. Each port takes the default and field of its type,
so a Camera width starts at 640. Switching the type keeps the ports and
marks those the new type lacks. At runtime the type is only an editor
aid; the node writes to whatever object it gets.

Old project files are converted on load. The type is inferred from the
keys: the first object type that has them all, else the one behavior
that declares them all, else Any. enabled belongs to every object, so a
converted Set enabled lands on Any.

The jump-run and tetris seeders use Properties throughout; draw-piece
switches its cell on in the same node that sets the colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:40:47 +02:00
rootandClaude Opus 5 7c63c884ef 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>
2026-09-11 16:56:24 +02:00
rootandClaude Opus 5 854ae2e756 Add Get/Set properties nodes and a breadcrumb for opened sub-behaviors
Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.

The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.

Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.

The "Add node..." header button is gone; nodes come from the context menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:07:07 +02:00
rootandClaude Opus 5 e9fa592078 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>
2026-09-11 13:34:09 +02:00
rootandClaude Opus 5 8f92a06c84 Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.

Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
  the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing

Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:00:30 +02:00
rootandClaude Opus 5 10d030376a Finish the struct and list leftovers: inline lists, list fields, item kinds
An unconnected list input is a button into the List tab instead of a number
field; a struct field can hold a list, so ListSlot carries an optional field
name and the three owner cases only say what sits at a key; and a list
variable picks its item kind in the inspector (marker doubles as the chooser),
so a list of structs no longer has to be born from a project file.

The declaration keeps the storable shapes finite: a struct field may be a list,
but that list holds primitives only, so list->struct->primitive and
struct->list->primitive are the two deepest forms.

check 674, e2e 483, jump-run 24, Tetris 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:36:15 +02:00
root 3c76106fd2 Let a Sequence grow an output as you connect the last one
The Outputs setting is gone. A Sequence now keeps one free output below the
last connected one: nothing plugged in is one output, connecting the spare
grows a new one, unplugging the bottom edge shrinks it back.

It counts the last *connected* port rather than the number of edges, so
unplugging a middle one leaves a gap instead of silently dropping an edge.

Removing the setting also removes a bug it caused. Typing in the Outputs field
did nothing and the arrow keys did nothing, while dragging the value worked:
onInput fires per keystroke, the preview writes into node.params, that emits
'project', and Inspector.refreshValues() sees the node's signature change --
the port list is part of it -- so it rebuilds and destroys the row being typed
in. A drag survives because it keeps receiving globalpointermove on the old
widget. refreshValues() has no focus guard where refreshLive() does; the trap
is still there for the next typed field that changes a node's shape.

Ports that depend on the edges around a node mean derivePorts needs the graph,
not just the params, so resolveDef and portSignature take it too. And a node
whose ports are derived cannot know how many it has from its type, so run is
handed the definition resolved for it.
2026-09-10 08:32:14 +02:00
root b6a609ae24 Let a node work out its own ports
Ports that vary per node already existed, but only for the three nested-behavior
types, and the rule for them sat as a switch on type names in nested.ts – the
one place in the project that knew node types by name. It dissolves into two
optional contracts on NodeDef, so a type carries its own rule:

  declaresPorts  the node holds an editable list, shown as a section in the
                 inspector – behavior start/end, and now Pack/Expand
  derivePorts    the node computes them from its settings – Sequence from a
                 count, Run behavior from the behavior it names

Sequence gains an Outputs setting (2 to 26). Its keys stay a, b, c and count on
with d, e, so the edges of the sequences already out there survive.

A param can now take a port away, and the edge on it would stay: it draws as
nothing yet survives every save, and comes back to life if the port returns.
SetNodeParamCommand takes those edges with it, which also closes the same leak
that switching a Run behavior node's target has always had.

The port editor addresses its node by id rather than by type, so two Pack nodes
can live in one graph, and it calls the rows Fields when they are not a
behavior's signature.
2026-09-10 08:14:18 +02:00
root 2cf788eedd Let a value be a bundle of named fields
PropertyValue was number | string | boolean | list. A struct joins it, which
costs two things the typechecker found rather than I did.

An object on a data edge used to mean a live entity, so asEntity would have
taken a struct for one and asString would have read .name off it. EntityHandle
now carries a symbol brand – a symbol so no field name can impersonate it, and
it never serializes, which suits a thing that exists only while the game runs.
It is built in exactly one place.

String(value) would have written a struct as [object Object] at 39 call sites.
They go through textOf(), which is identical for primitives.

normalizeGraph dropped everything but primitives from node params, so the List
node lost its value on every save ([3,1,4] round-tripped to undefined).
Variables were spared: they have a schema, params have none.
2026-09-10 08:14:18 +02:00
rootandClaude Opus 5 3ab8d88ad6 Let On key down repeat while the key stays held
A held key fired once and then nothing, so sliding a Tetris piece sideways or
walking a menu meant building a timer next to every key node. Two settings on
the node do it now: Repeat after and Repeat interval, both in milliseconds so
the usual values stay whole numbers (300, 50) instead of 0.3 and 0.05. A delay
of 0 keeps the node exactly as it was, an interval of 0 means every frame.

The browser's own auto-repeat is deliberately not used - Input.press has always
dropped it - because its delay and rate are an operating-system setting of the
player's, not a game's. The repeat is built from the measured hold time instead,
per node, so two nodes on one key keep their own clocks. The rule itself is a
pure function in keyRepeat.ts and is checked without a world.

Both rows stay off the node body while the delay is 0: two lines of "0" on every
key node in every graph would be noise, and the screenshot said so. NodeDef gains
hiddenParams for that; the inspector still shows them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:44:00 +02:00
root 6702ff8abe Let a behavior be a node in another graph
A behavior that has a Start and an End node can now be dropped into any
other graph as a node of its own. What is declared on the start node
becomes the inputs of that node, what hangs on the end node its outputs;
the port list is derived from the sub graph, not copied, so declaring a
port in one graph grows the node in the other without touching it. The
node carries the behavior's name and the palette lists behaviors by
name, so a call is one grab instead of a node plus a dropdown.

An embedded behavior has no object and therefore no place for
variables: every call starts on the asset defaults and communicates
only through its ports. That is the conservative direction - state can
still be added later, and shared state already has a home in a behavior
object in the scene. Node ids are unique per graph only, so each call
gets its own pulse cache; without that, two graphs would collide.

Renaming a port used to silently drop the value typed on the calling
node. It is carried along now, project wide, like the edges.
2026-09-09 20:01:49 +02:00
rootandClaude Opus 5 4f78c7518c Give the graph a loop, and build Tetris with it
A Tetris example project needs to draw 200 board cells and scan rows
for a full line. Neither is expressible without iteration, so the node
system gets a Repeat node plus the missing text side: Substring, Find
text and To number. The interpreter drops its data cache once per
round -- otherwise every round would recompute with the first round's
values.

Object by name takes its name as an input now, not just as a setting;
addressing "c" + index is impossible otherwise. An unconnected input
reads the stored setting, so saved projects are untouched.

The board itself is a 200-character text, "." for empty. That makes
both operations of Tetris a single expression instead of a loop, and
each frame attempts exactly one move -- rotate beats sideways beats
fall -- so the fit check exists once in the whole graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 19:10:41 +02:00
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00
rootandClaude Opus 5 d54a620ebd Add ESLint and Prettier, and close the node test gaps
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:02:22 +02:00
rootandClaude Fable 5.1 1eb28314cf Refactoring: one file per node, consistent comments, shared helpers, split large modules
- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with
  shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry
  separated, writeTyped() replaces duplicated typed writes
- Comments: one style (JSDoc on non-obvious exports, // for decisions and
  traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars
- Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts,
  commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code
  removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe,
  Input.axis, setMode, titleText, World.worldPosition, unused ctor params)
- Editor: Inspector split into inspector/ sections, App.ts into Editor.ts +
  debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared
  "Add collider" action, unified type labels/swatches
- UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers,
  Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes
- Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib
  (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections
- Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 08:37:22 +02:00
rootandClaude Opus 5 76aa441dce Enabled switch, text assets, mouse input and the jump-and-run example game
Engine additions that the example game needed:
- `enabled` as a shared property of every placeable asset (default on, an
  override on the scene object). A switched-off object is not drawn, does not
  collide and its behavior does not run - and neither does anything parented
  below it. Disabled objects are now hidden in the scene view as well, so the
  editor shows what the game shows; they stay in the hierarchy panel, which is
  where they are selected and switched back on.
- Asset type `text` (text, fontSize, color, align), rendered in scene and game.
- Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`,
  `Is hovered`, hit-tested against colliders, else sprite/text footprints.
- New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text,
  Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game.

Tooling: shared CDP harness (tools/harness.mjs), central project registry with
a guard that keeps destructive scripts off the playground (tools/projects.mjs),
plus smoke suites for the demo scene and the jump-and-run game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:52:10 +02:00
rootandClaude Fable 5.1 91ee2feadd Spawn node: place assets at runtime into the play copy, Spawner demo behavior
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:57:19 +02:00
rootandClaude Fable 5.1 6ecb20cf02 Trigger colliders and Move and slide against solid colliders
- Collider property isTrigger (no blocking, still reports enter/stay/exit)
- Node 'Move and slide' with blocked x/y outputs, axis-wise resolution
- axisSeparation/axisCorrection geometry for box/box, circle/circle, box/circle
- Amber outline for trigger colliders in the viewport
- Demo mover uses Move and slide; E2E step 15b, 7 new unit tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:38:14 +02:00
rootandClaude Fable 5.1 0a59513b74 Collider asset type, CollisionSystem and collision nodes
- Asset type 'collider' (box/circle) placed as child of a body object
- 'Add collider' shortcut (hierarchy context menu + inspector)
- CollisionSystem with enter/stay/exit pairs, sibling colliders excluded
- Nodes: On collision enter/stay/exit, Is touching
- Green outline in edit mode, hidden in play

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:25:04 +02:00
rootandClaude Fable 5.1 26d5edc496 Scene hierarchy: parentId with local positions, tree panel, reparenting and Parent node
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:04:56 +02:00
rootandClaude Fable 5.1 9060da615a Replace Move 'per second' setting with a dt data input
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:21:46 +02:00
rootandClaude Fable 5.1 922774778b Behaviors with Blueprint-style node graph, graph editor tab and BehaviorSystem
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:44:43 +02:00