Commit Graph
32 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 ed7872c893 Let a called behavior share the caller's local state machines
A local state machine started over in every Run behavior call, so a
state set in a sub-behavior was gone by the next call. A character
controller split into sub-behaviors could therefore never remember
that it was attacking. Scene or global scope would keep the state, but
then every character shares it.

- A call now runs on the states of the instance that runs it, however
  deep. A state one call sets is still set in the next one, and the
  call begins in the start state like the instance does.
- A move made inside a call is reported like one the caller made. The
  caller's On state entered and On state exited hear it, the event
  nodes of the called graph stay silent as before.
- Two objects running the same behavior still hold a state each.
- This reverses the rule from #229 that local machines of a called
  behavior are not kept. Keep stays a switch for variables only.

Tests:
- Two unit tests that pinned the old rule now expect the shared state.
- New world test: a sub-behavior closes a door per object, the
  caller's events hear it, and the next call finds it closed.
- Counter-check: with the old call context the 3 tests fail.
- Gate on melektaus-atx: 239 files, 3600 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:38:06 +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 275e50af01 Let a tile report a touch instead of blocking it (#160)
A tile could be solid, one-way and scaled, but not report without
blocking. Water, lava, spikes or a goal zone needed a trigger object
of their own laid over the map. Now a tile has a switch Trigger, and
the collision events name the tile and the cell they met.

- A tile has the switch "Trigger (reports, never blocks)" under
  Solid. Its cells hold nothing up: no Move, no falling body, no
  ray. They raise On collision enter, stay and exit with the tilemap
  as other.
- The three events have the outputs tile, cell x and cell y. Without
  a tile, tile is empty and the cell is -1, -1.
- The three events have the input per cell. Off, the default, gives
  one event per area: cells of the same tile that touch side by
  side. On gives one event per cell.
- The scene view outlines trigger cells in yellow like a trigger
  collider, merged to rectangles but never with solid cells.
- Is touching with the map counts trigger tiles, and Set tile works
  while the game runs.
- Both systems find the touches with the same overlap check in
  runtime/systems/tileTriggers.ts, not through the physics engine.

Decided here:

- Trigger wins over Solid. A tile with both does not block, as with
  a collider.
- Water beside lava is two areas, and so are two ponds that touch
  only at a corner.
- An area names the cell nearest to the centre of the collider, and
  on exit the cell it named last.
- The solid cells of a map still do not name their tile.
- Normal, impulse and point are 0, as with a trigger collider.
- Collision scale and outlines shape a trigger cell like a solid
  one. One-way, friction and bounciness have no effect.

Tests:
- Gate on stein2: 205 files, 3104 tests (3067 + 37), exit 0
- e2e 39,40: 106 checks. e2e 1-8,39-43: 490 checks. Both on
  melektaus-atx. No full run, the sum of 1648 (1645 + 3) is
  reckoned and CI measures it.
- 37 unit tests in tests/core/tile-triggers.test.ts, nearly all
  against both systems
- Three e2e checks with a real mouse in block 40l-iv
- 24 unit counter-probes fail tests. Two did not at first, tests
  were added for them.
- Two browser counter-probes fail their checks
- Two screenshots looked at (yellow outline, the node with per cell)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:28:39 +02:00
adminandClaude Opus 5.5 14972d057f Refactoring round 8: hide data files, safe saves, Esc as Cancel (#85)
game-maker / gate (push) Successful in 3m53s
game-maker / suite (push) Successful in 11m55s
Recurring cleanup round over the code since round 7 (bc688f3). The
issue now also asks to fix bugs and small inconsistencies found on the
way, so unlike the rounds before, this one changes behaviour in a few
places. Duplicated code in api went from 23 clones with 228 lines to
none (jscpd, at least 50 tokens). In src it stays at 22 clones with
271 lines, the same before and after.

Bugs:
- nginx served project and snapshot files as they were, so anyone who
  knew a project id could read it without signing in. Every file the
  API writes now goes through AtomicFile (temp file, mode, rename).
  Projects, snapshots, releases, accounts and sessions are 0640, and
  nginx answers 403 for them.
- Any signed-in account could delete a shared image, font or sound,
  which projects of other accounts may wear. Deleting one is now for
  administrators only. The editor never deleted a file, so the
  client's delete is gone.
- Two saves of one project could run at once. The lock file was
  removed after each save, so a waiting save and the next one locked
  different files. The lock file now stays, and deleting a project
  takes the lock as well.
- Bytes that are no picture got a 500 instead of a 422, because the
  error handler also threw for warnings silenced with @.
- A session file was rewritten in place. A request reading it at that
  moment found it empty and was signed out. It is replaced atomically.
- Delete did nothing for a shape picked in the vector art list. Delete
  now goes through removeSelection, one case per kind of selection,
  and a kind without a case fails the type check.

Inconsistencies:
- Escape on a question does what its Cancel does. Cancel on "Delete
  project?" and "Restore snapshot?" leads back to the picker or the
  list like the other questions did, and so does deleting a project.
- Editor texts use American spelling throughout, like the port names:
  48 British spellings in 45 lines (colour, centre, initialised).
- objectName and groupName replace eleven copies of "own name, else
  the asset's" and three of "Group". One copy fell back to the object
  id instead of "?".

Duplicates, dead code, comments:
- routeFiles serves images, fonts and sounds instead of three copies.
  HashedFileStore holds what the three stores shared: keeping a file
  under its hash with its name, listing and deleting. The stores keep
  only which bytes they take and what a record says.
- AtomicFile replaces nine copies of temp file plus rename.
- removeShapes is the one way the canvas, the inspector button and
  Delete take shapes out.
- GraphInterpreter.fire was called by tests only. The 54 calls use a
  helper in tests/helpers.ts now.
- FileStore.delete, HttpFileStore.delete and the PluginApi type were
  unused. A doc comment in actions.ts sat above the wrong function.

Checked and left as is:
- solidCells is used by tests only. It is the per-cell view before the
  merge into rectangles, the one place the largest-scale rule shows.
- An undo checker ran all 124 commands of the unit tests and compared
  the project before and after undo. The only differences are empty
  ports, groups and notes lists where there were none, which the file
  format drops anyway.

Docs:
- README: the check sum.
- Wiki, own commit: API (admin-only deletes, 422, file modes, lock
  file), Home (the "Not yet" list named three things that exist),
  Concepts, Keyboard, Vector art, Architecture and Development, and
  the 38 characters Gitea flagged as ambiguous on eight pages.
- .gitignore: the lock files, and pasted-*.png in any folder, which
  keeps the screenshot archive out.

Tests:
- ./check.sh: 195 files, 2817 tests, exit 0 (+2 for the name helpers).
- PHP: accounts 52 in the web container and 53 in the CI image with
  the race test (+2), snapshots 15 (+1), releases 20.
- Full e2e run: ALL PASSED, 1586 checks (+6 in 12, 47, 49 and 50).
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks. The example
  project files were restored byte for byte afterwards.
- The file stores of HEAD and of this commit give byte-identical
  output for the live folders and for store, list and delete in a
  throwaway folder.
- Counter-checks, all fail as expected: a 500 for bad bytes fails 12,
  no admin check fails 49, removing the lock file again loses saves
  in 3 of 3 runs (151 to 170 of 321), Delete ignoring shapes fails 47,
  Escape that only closes fails 50, and dropping the shape case fails
  the type check (TS2366).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:12:55 +02:00
adminandClaude Opus 5.5 a7e793f6de Keep many spawned behaviors from eating the frame
A scene with 200 spawned gold pickups, each with a Pickup behavior of
97 nodes, dropped the game to about 14 fps. The graphs themselves ran
only a dozen nodes per pickup and frame. The cost was the lookup: for
every data input the interpreter searched all edges and all nodes of
the graph linearly and computed the port shape of the node again.

- New module core/graph/graphIndex.ts builds tables once per graph:
  nodes by id, edges by node and port, node types. Port shapes are
  cached as well.
- BehaviorSystem shares one cache between all its instances and
  clears it in syncFromModel. A graph changed during a run applies
  from the next frame on, as before.
- An interpreter without a cache, as in most tests, searches as
  before.

Measured on the scene Performance test (headless per frame, browser
fps on a throwaway copy of the project):

- Headless: about 21 ms down to about 3.6 ms per frame.
- Browser, about 505 entities: 13.6 fps up to 23 fps.
- Browser, about 320 entities: 32.3 fps up to 48 fps.
- After the fix behaviors take about 15 % of the browser profile.
  The rest is the editor mirroring the game into the model and
  rebuilding its panels, which is a separate change.

Tests:

- ./check.sh: 192 files, 2786 tests, CHECK EXIT 0.
- Full e2e run (local): ALL PASSED, 1551 checks.
- tests/core/graph-index.test.ts: 2 tests. The tables return the
  same as the search, in the same edge order. A graph changed during
  a run applies from the next frame.
- Counter-check: without clearing the cache in syncFromModel the
  second test fails with ['before'].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:36:10 +02:00
adminandClaude Opus 5.5 5fbc6eb088 Set the order in which event nodes run across behaviors (#264)
game-maker / gate (push) Successful in 3m42s
game-maker / suite (push) Failing after 11m7s
Which of two events ran first depended on hidden lists: the order of
the behavior instances and the order in which nodes were created. A
graph had no way to say that one On update must run before another,
and an On update of one behavior could run before a key event of
another.

- A frame now runs event kind by event kind over all behaviors: On
  disable and On enable, key down, key up, key held, timer, collision
  enter, stay and exit, animation finished, click, mouse wheel and at
  last On update.
- All 17 event nodes have a field Order, a whole number, default 0,
  negative allowed. Within one kind a lower Order runs earlier, no
  matter which behavior holds the node.
- On a tie the current hierarchy decides from top to bottom, then the
  order in which the nodes were created. The old rule that a behavior
  enabled later goes to the end is gone.
- The node shows Order: 5 only when the value is not 0, so existing
  graphs look the same.
- The rule lives as pure functions in core/graph/eventOrder.ts. The
  interpreter can now fire a single event node.

Changes in existing projects:

- An object destroyed in a frame gets no further event in that frame.
  On disable and On destroy still come at the end of the frame.
- New instances get all their On start first, then all On enable.
  Teardown runs all On disable first, then all On destroy.
- State events follow right after the event kind that caused them.
- Four unit tests had fixed the old order and are adjusted:
  two in behavior-events, one in physics-system, one in key-repeat.

Tests:

- ./check.sh: 191 files, 2784 tests, CHECK EXIT 0.
- GM_STEPS=1-6: ALL PASSED, 280 checks.
- Full e2e run (local): ALL PASSED, 1551 checks, measured.
- tests/core/event-order.test.ts: 11 tests for the rule, the field on
  all 17 nodes, order across behaviors, key before update, two nodes
  in one graph, On start, hierarchy after late enable and reorder at
  runtime, state events, save and load.
- e2e 6z-viii, 2 checks with real mouse and keyboard: Order -1 set in
  the inspector shows on the node, and a run logs B, A although B is
  lower in the hierarchy.
- Counter-checks: 5 in unit tests and 1 in the browser, all fail as
  expected. The originals were restored and checked with cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:21:31 +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 38e0f656be Split a struct output into one pin per field (#256)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Successful in 10m45s
Reading one field of a struct needed its own Expand node, even where
the graph already knows the struct type. A split output shows the
fields directly on the node, so a Find … Match test becomes a single
Equals and the wires keep running left to right.

- Right-clicking an output pin offers "Split into <Type> fields" when
  the type is known and "Split as …" for every other struct asset.
  The output is replaced by one pin per field with the field's kind.
- The type is known for typed struct variables, typed Pack/Expand,
  fields of another split and item of For each, Find, Match and Item
  at over a typed list (portSplits.ts).
- Right-clicking a field pin offers "Recombine". Splitting drops the
  edges of the whole output, recombining drops the field edges. One
  undo step brings them back.
- The split is stored as GraphNode.splits and saved with the graph.
  Copy and paste carry it along.
- Field pins follow the struct asset like a typed Expand. A new field
  gets a pin, a removed field loses pin and edge, a renamed field keeps
  its edge. Deleting the asset keeps the last fields. The delete dialog
  counts splits, so it now says "node(s)" instead of "Pack/Expand
  node(s)".
- Renaming a variable carries its split and the field edges along.
- At run time the node still produces the whole value. A field pin
  reads its field like Expand, a missing field reads as empty.
- Limits: outputs only, one level, behavior graphs only.

Tests:
- ./check.sh: 185 files, 2706 tests, CHECK EXIT 0
- e2e GM_STEPS=1-6,54,55: ALL PASSED, 303 checks
- New port-splits.test.ts with 12 tests. e2e 6z-vi with 3 checks
  using a real mouse: split Find.item, wire item.Name into the test
  and Match.item.Amount into the log (logs 7), recombine.
- Counter-checks: without field reads in the interpreter, without
  rename in the split, without moving edges on rename and without
  cleanup on field removal one unit test fails each. In the browser,
  without field reads nothing is logged and without type inference
  the split entry is missing.
- Check total in README: 1520.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:33:38 +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 98211a8d54 Let a variable hold an object across behavior runs
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Failing after 9m2s
A spawned object was only reachable in the run that spawned it. The
Spawn output could not go into a variable because "object" was a
port-only kind, so the next run had no way back to the instance.

- "Object" is now a variable kind. The reference lives for the run
  only. The file stores the declaration and an empty value.
- The live reference sits in slots beside the variable bag
  (graph/objectSlots.ts), not in it. The bag is the behavior's props,
  and syncToModel would have written an object into the scene.
- A destroyed object, or one from a restarted or left scene, reads as
  nothing. A dead reference is dropped from its slot.
- Keep (#229) hands the slots of kept variables on to the next call
  of a sub-behavior.
- Properties of kind Behavior reads and writes another behavior's
  object variable through the same slots.
- The inspector shows "(null)" for it and offers no input.
- A persistent Variables asset with an object variable shows a
  warning under "Persistent". Saved values never keep the object,
  every run starts it at (null).

Objects inside lists and struct fields are left out on purpose.
Emptiness is checked with Equals against an empty b. A node "Is
valid" does not exist yet.

The test helper graphEditor().variables() now also reads the kinds
of a Variables asset a node points to.

Tests:
- ./check.sh: 169 files, 2463 tests, CHECK EXIT 0
- e2e 6 + 23: ALL PASSED, 146 checks (new 23k, 2 checks)
- e2e 32: ALL PASSED, 18 checks (new 32f-ii)
- 10 unit tests in tests/core/object-variable.test.ts, one of them
  runs a persistent asset over two runs
- 5 counter-probes (value in bag, isAlive always true, no Keep
  hand-over, Properties without slots, object in saved values) each
  fail their test
- e2e check total 1406, computed only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:15:16 +02:00
adminandClaude Opus 5.5 21d7149756 Let a nested behavior without a start node run from On start (#230)
A behavior built as a plain behavior, with On start and no Behavior
start, did nothing when another behavior called it through Run
behavior. The node only showed a warning. Such a behavior should run
as a call too, without the author having to rebuild its entry.

- New callEntries() in nested.ts returns the Behavior start node or,
  when there is none, every On start node. callProblem uses it too.
- GraphInterpreter.call() follows all entries one after the other,
  in graph order, in the same frame and with the same variables.
- Behavior end still returns what reaches it. A call through On start
  has no inputs, because only the start node declares them.
- It is a fallback only. With a Behavior start present, On start
  nodes stay out of the call.
- The node warning now reads "No start or On start node - does
  nothing" and only shows when neither exists.
- Keep and local state machines behave as before.

Tests:
- ./check.sh: 163 files, 2396 tests, CHECK EXIT 0 (before 2392).
- 4 new unit tests in tests/core/nested-behavior.test.ts: the flow
  after On start runs, the return value arrives, several On start
  nodes run in graph order, a start node keeps On start out.
- e2e 19: ALL PASSED, 35 checks. Only the expected warning text
  changed, the check sum stays at 1385.
- Counter-checks: without the fallback 3 tests fail. With only the
  first On start exactly the order test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:13:14 +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 d3563897d8 Two compilers that walk a graph the same way walk it through one emitter (#85)
game-maker / suite (push) Failing after 2m22s
game-maker / gate (push) Successful in 2m53s
Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection,
duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on
FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own
contract. The three Create*Command share ImportFileCommand, the three renames share
RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in
portSection.ts replace in-file clones, and three comments that told history now state the rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:28:28 +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
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 656d61f187 Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The
running game reads the graph by reference each frame, so a new node, edge,
changed value, disconnect or undo takes effect from the next frame. Stop
discards these edits like everything else changed in play. A new On start
node does not fire for a behavior that has already started.

During play every data output shows the value it last produced, in the
highlight colour and refreshed ten times a second. Values are kept per
graph, because node ids are only unique within one; with several
instances the most recent value wins. The Variables node reads the live
variable bag, so only its own outputs are kept, as a snapshot.

Each time an exec edge fires, a bead runs along it in 0.3 s, at most one
every 0.15 s per edge, following the same curve the edge is drawn with.
Beads run on game time and freeze while paused; the game logic is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:19:07 +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 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
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 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 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