Commit Graph
134 Commits
Author SHA1 Message Date
adminandClaude Opus 5 ae0247d86f Lists add and delete items right in the list header and rows
The layer, frame and shape lists still had their add and delete
buttons scattered below the list, each in its own layout. They now
live in the shared SortableList, so every list works the same way.

- SortableList gets an optional header with the title on the left
  and a + on the right, plus an optional x per row next to the eye.
- The keepOne option hides the x on the last remaining row, so a
  pixel art image and a tilemap always keep one layer.
- Pixel art layers and frames and tilemap layers use header, + and
  x. Only Merge down and Duplicate stay below the lists.
- Vector art shapes get the x but no +. Add shape stays below the
  list and Delete shapes stays for a multi-selection.
- The section headings in the pixel art column use the same style
  as the list titles and the inspector.

Findings:

- The wait loop added to check 47i for #196 only waited for the
  right half of the shape. If the texture landed between the two
  reads, the left half was still blue and the check failed. It now
  waits for both halves.

Tests:

- ./check.sh: 138 files, 2052 tests, exit 0.
- GM_STEPS=39,40,47,48 (fresh): ALL PASSED, 156 checks (was 154).
- GM_STEPS=47,48 (fresh, final): ALL PASSED, 86 checks.
- Two new e2e checks: x on a shape (47) and x on a frame (48). The
  tilemap check in 40 now uses the x instead of Delete layer.
- Counter-check: with the x not reporting to its owner, exactly the
  two new checks fail.
- Check total 1266 in README and wiki is computed (1264 + 2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:18:26 +02:00
adminandClaude Opus 5 592ea942d9 Vector shapes can fill with gradients and textures (#186)
game-maker / gate (push) Successful in 2m14s
game-maker / suite (push) Failing after 7m14s
A vector shape could only be filled and outlined with one flat colour.
Shading, glows and patterned surfaces had to be faked with stacks of
shapes. Every shape now has a paint type for its fill and its outline.

- Fill type: solid colour, linear gradient, radial gradient or
  texture. Line type offers the same without texture.
- Colour stops are edited on a bar in the inspector, as agreed in the
  issue. A click on the bar adds a stop in the colour the gradient
  already has there, so adding never changes the picture. At least two
  stops remain.
- The new Paint tool in the tab shows two handles for the selected
  shape: axis start and end, centre and radius, or the corners of one
  texture tile. "Handles: Fill / Outline" picks which paint is edited.
- Moving a whole shape moves its gradients and tile with it. Every
  gesture is one undo step.
- A texture fill always tiles, because Pixi turns a non-repeating
  pattern into a repeating one anyway. When switched on, one tile
  covers the whole shape.
- Only uploaded textures and frames can be a fill, never a drawing.
  A drawing filled with itself would rasterise itself mid-raster.
- The fingerprint of a drawing includes the texture and frame a fill
  points to, so the picture redraws when the image is swapped.

Findings:

- graphics.destroy(true) also destroyed the textures of the fills.
  With a texture fill it deleted the tile slice and the next redraw
  crashed on reading 'scaleMode' of null. It is now destroy().
- The gradient cache destroyed old gradients itself, which gave 20
  Pixi warnings "destroyed while still bound". They are now handed to
  Pixi's texture GC with autoGarbageCollect switched on. The console
  of module 47 is empty again, the last CI log had 0 of them.

Tests:

- ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986)
- npm run build: exit 0
- GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44),
  0 console warnings
- 25 new unit tests, 10 new e2e checks in block 47i
- Four counter-checks bite, originals restored and verified with cmp
- Check sum 1232 in README and wiki, calculated (1222 + 10)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:05:02 +02:00
adminandClaude Opus 5 1116818011 Vector shapes can be combined by union, subtract and more (#187)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m51s
Building a shape out of simpler ones meant drawing the result point by
point. The Vector Art tab now selects several shapes at once and
combines them with a boolean operation into one ordinary shape.

- Ctrl/Cmd+click in the tab and Ctrl or Shift+click in the shape list
  add a shape to the selection. Shift stays pan in the canvas.
- A COMBINE block in the inspector offers Union, Subtract, Intersect
  and Exclude. Subtract removes every upper shape from the lowest one.
- The result replaces the lowest selected shape and keeps its name,
  colours, stroke and place in the order. It is one undo step.
- Curves are flattened to 0.2 canvas pixels, a circle gets about 48
  points. Open lines and single shapes cannot be combined, the buttons
  are off and a hint gives the reason. An empty result is rejected.
- Delete now removes every selected shape, and the selection drops
  shapes that no longer exist.
- The geometry comes from clipper2-ts 2.0.1, pinned. It has no
  dependencies and speaks even-odd directly. Only the engine module
  is imported, the bundle grows by 55 kB instead of 110 kB.

Findings:

- clipper2-js was tried and dropped. It pulls in Angular 15 with a
  high npm audit finding and no fix.
- Pixi finds holes by containment, not by even-odd depth. An island
  inside a hole would be drawn as a hole again. Result rings are
  emitted outer ring before its islands, and a test mirrors Pixi's
  rule.
- A depth sort turned out to be dead code in a counter-check and was
  removed. A pixel check in 47h stayed green without the subtract
  and now checks both colours.

Tests:

- ./check.sh: 134 files, 1986 tests, exit 0 (before 133 / 1968)
- npm run build: exit 0
- GM_STEPS=47, freshly seeded: ALL PASSED, 44 checks (before 39)
- 18 new unit tests, 5 new e2e checks in block 47h
- Five counter-checks bite, originals restored and verified with cmp
- Check sum 1222 in README and wiki, calculated (1217 + 5)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 11:56:41 +02:00
adminandClaude Opus 5 78595ec80f A selected vector shape has its own inspector rows (#129)
game-maker / gate (push) Successful in 2m12s
game-maker / suite (push) Successful in 7m45s
Stage 4 of #129. Until now the inspector showed the rows of the asset
when a shape was selected, so there was no way to give a shape a colour,
an outline or a place in the drawing order without editing the project
file. The rows for a shape now sit where every other selection puts its
rows.

- SHAPES lists every shape of the asset, topmost row drawn last, the
  same way a layer list reads. Selecting the asset itself shows the same
  list below its own rows, with no shape selected.
- Each row paints its shape with the commands the rasterizer uses
  (shapeDrawing plus paintDrawing) into a 22 px swatch, the whole canvas
  scaled in, so the row also says where on the canvas the shape sits.
- The four order buttons from the issue (forward, to front, backward, to
  back) sit in two rows of two, and a row can be dragged to another
  place with a marker line. They are disabled when there is nothing to
  move.
- VECTOR SHAPE carries name, fill, opacity and an outline checkbox, the
  checkbox adding and removing the stroke instead of hiding it; line
  colour and width appear with it. A dragged colour slider is one undo
  step, the style rows going through previewingHandlers like every other
  row.
- A line width of 0 is no stroke, which the command rejects, so the row
  clamps at 0.1.

Findings:

- locate('Outline') aims beside the control, because it gives the centre
  of the label text and a wider label pushes a fixed offset out of the
  field. __gm.inspectorField(label) knows the centre of the control.
- A click into a text field sets the cursor and selects nothing, so a
  typed hex landed appended to the old one and parseHex returned null
  without anything failing - typeText only checks that the text is
  contained. A Ctrl+A goes first now.
- Four buttons side by side cut "Backward" to "Backw...", which only the
  screenshot showed. Two rows of two now, as in the tilemap panel.

Tests: ./check.sh 128 files, 1897 tests (was 127/1883). Full e2e suite
locally instead of only CI because the change sits in the inspector and
touches every selection: ALL PASSED - 1179 checks (was 1172), module 47
at 24 checks (was 17). Three counter-checks hold: not reversing the list
fails 1 unit test and 2 e2e checks, dropping the gap correction when
dropping a row fails 2 unit tests and the drag check, and a constant
signature kills module 47 because the outline rows never appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:29:40 +02:00
admin 3d7ad65f34 Vector art has a tab to draw in, which is stage 3 of #129
game-maker / gate (push) Canceled after 37s
game-maker / suite (push) Canceled after 37s
Stages 1 and 2 gave the asset a model and a picture, but the only way
to put a shape into it was a test calling mutate.  The tab closes that
gap: what you draw there is what the sprite in the scene carries, with
no extra step, because the fingerprint changes and the picture registry
of #185 redraws.

- The tab has a header with Select, Pen, Grid and Fit, a chequerboard
  behind the canvas and the canvas itself as a frame.  It paints with
  the same commands as the rasterizer, paintDrawing being exported
  from viewport/vectorArtPicture.ts for it, so the tab cannot drift
  away from the picture.
- Pen sets points on a click and pulls the handles out on a drag, both
  of them mirrored; a click on the first point closes the ring and Esc
  ends an open line.
- Select drags a point, a handle or a whole shape, a click on an edge
  puts a point there, a double click takes one away, and Alt turns a
  corner round and back.  Delete removes the selected shape.
- The rules grew inside polygonEdit.ts instead of a second module, as
  the plan says, so a collider polygon and a frame outline get the
  same behaviour where it applies.
- A point on a curved edge splits the curve by de Casteljau instead of
  cutting it: both halves together draw exactly what was there before.
- A curved edge is walked instead of measured over its chord, or a
  click on the bow of a curve would grab nothing; the same walk carries
  the test for the inside of a ring.
- One gesture is one undo step.  The preview writes the model directly
  and the old state is put back on release before the command runs,
  which is the pattern the frame outlines of #141 already use.
- state.ts gained a selection kind "shape".  The inspector shows the
  rows of the asset for it, the shape list with colour, opacity and
  stroke being stage 4.

One counter-check was no counter-check: disabling the pen's closing
tolerance by multiplying it with 0 left the run green, and rightly so,
because the closing click lands on exactly the screen coordinate point
0 came from, so the distance is 0 and <= 0 is true.  Only an
unsatisfiable condition really switched it off, and then 2 checks fail.
A second trap on the way: a false && in front of the guard breaks the
TS narrowing, npm run build bails out, and two runs went against the
old bundle.

Tests: check.sh 127 files, 1878 tests, EXIT 0 (was 126 / 1860).
Full e2e suite ALL PASSED - 1172 checks, module 47 alone 17 (was 10).
18 new unit tests in tests/editor/ring-edit.test.ts and 7 new e2e
checks in blocks 47c and 47d.  Two counter-checks hold: no de
Casteljau fails 1 unit test, measuring a curve over its chord fails
another.  Originals checked back with cmp and rebuilt.
2026-09-20 21:34:14 +02:00
adminandClaude Opus 5 4f55f85ad4 Collision layers and masks: a collider carries a layer and a mask of the layers it meets, and the pair only meets when both masks name the other's layer - the rule is one pure module (src/core/model/collisionLayers.ts) with layerBit, layersMeet and the names a project starts with, so the three places that have to agree read the same function: the editor run through CollisionSystem, the Solids sweep per pair and Rapier, where the mask becomes the collider's interaction groups; the project file gained collisionLayers: ["Default"] (default one layer, no formatVersion bump), the collider schema two new kinds - layer as a dropdown of the project's layers and layerMask as a menu of tickboxes that stays open while it is ticked (Menu.ts learned rows that do not close on a pick) - and the names are edited through "Collision layers..." in the toolbar's Project menu, not a Project tab, which is the decision I made myself because the question ran into the MCP timeout: the dialog (src/editor/CollisionLayersDialog.ts) holds up to 16 names, Add layer appends "Layer <n>" and Apply writes all of them as one undo step (SetCollisionLayersCommand); a mask read from a file goes through a check that refuses anything that is not a finite number, because Number(null) is 0 and a collidesWith: null would silently have meant "meets nothing"; 9 new e2e checks in module 8 cover the menu entry, the dialog, Add layer, Apply and its undo, both inspector rows, the tickbox menu, and the mover walking straight through the crate that 8c blocked on once their layers no longer meet, and 21 new unit tests cover the rule, the schema and the command (#152)
game-maker / gate (push) Successful in 2m41s
game-maker / suite (push) Failing after 8m21s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:31:22 +02:00
adminandClaude Opus 5 8e4bb6e1f9 User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
game-maker / gate (push) Successful in 2m5s
game-maker / suite (push) Failing after 6m22s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 12:50:52 +02:00
adminandClaude Opus 5 010b6a7f6c A state can transition to itself: make transition and a second click on the same state connects a self transition, taking it fires On state exited and then On state entered every time, for local and global machines, while Set state into the current state stays silent as before; only states may connect to themselves, every other node is still refused, one self transition per state; the loop is drawn over the middle of the top edge with its arrow pointing down and its name at the apex, the preview already shows it while armed, and the inspector lists it once under Transitions out; new test hook transitionLabelAt, e2e 26e5 (#163)
game-maker / gate (push) Successful in 1m56s
game-maker / suite (push) Successful in 6m45s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:10:02 +02:00
adminandClaude Opus 5 5385d9e152 The one-way tolerance angle is a field: a one-way collider and a one-way tile gain One-way angle (0 to 90 degrees, default 45), a contact blocks when its normal is within that angle of the blocking side, without a value the limit is bit-identical to before, and the rule holds for Move, Raycast and dynamic bodies in Rapier; tiles with different angles never merge into one rectangle, the scene view draws the angle as a cone in the middle of the thick side, and the inspector shows Blocks from and One-way angle only while One-way is on, through the new schema option shownWith (#159)
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:13:29 +02:00
adminandClaude Opus 5 4728f4fab5 A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
game-maker / gate (push) Successful in 1m43s
game-maker / suite (push) Successful in 6m8s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:58:03 +02:00
adminandClaude Opus 5 c2fd536aaf The texture field only offers pictures: each texture with its frames, no animations any more; an animation id left in an older project shows as a missing reference (#143)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 6m8s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:14:01 +02:00
adminandClaude Opus 5 5046ddb810 The tilemap painting tools move out of the inspector into their own panel beside the scene view, shown only while exactly one placed tilemap is selected in the scene tab; the free viewports keep their content in place on screen when they are resized (#142)
game-maker / gate (push) Canceled after 0s
game-maker / suite (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:46:23 +02:00
adminandClaude Opus 5 f53279d065 The active tilemap layer can be renamed from the painting section, and a Show setting fades or hides the other layers in the scene view while the tilemap is selected (#138)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 6m4s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:56:32 +02:00
adminandClaude Opus 5 bfbcbe604c The tile list in the tilemap section is a palette of pictures: each tile shows its lone tile (the four convex corners for floor and wall, the picture itself for none) with its name below, laid out with the image-picker grid (#139)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m15s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:15:51 +02:00
adminandClaude Opus 5 732f6518ae The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 5m56s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 08:01:07 +02:00
adminandClaude Opus 5 33a7b1b63e Tilemaps are a new placeable asset type that is painted with autotiles in the scene view, with layers, brush, eraser and rectangle, and is drawn at runtime and in the export (#128)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m51s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:41:47 +02:00
adminandClaude Opus 5 2a390e52ff The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
game-maker / gate (push) Successful in 1m36s
game-maker / suite (push) Successful in 5m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:19:53 +02:00
adminandClaude Opus 5 beeea355d9 Animation rows can target behavior variables and shader parameters (#104)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m38s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:04:29 +02:00
adminandClaude Opus 5 b278605c53 Keys carry an easing for the span to the next key: linear, step, ease in, ease out, ease in-out (#101)
game-maker / gate (push) Successful in 1m32s
game-maker / suite (push) Successful in 5m36s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 07:27:44 +02:00
adminandClaude Opus 5 4ed2edd0f0 An animation is a value a graph can pick, and playing a running one makes it win again (#97)
Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:47:31 +02:00
adminandClaude Opus 5 5af0a1c83a An animation's rows are edited on a timeline: double click sets a key, keys are banded, dragged and deleted, the ruler scrubs (#98)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:38:34 +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 bf0be66c9d A frame with borders tiles unless told to stretch
game-maker / gate (push) Successful in 1m32s
game-maker / suite (push) Successful in 5m47s
The fill of a sliced frame defaulted to `Stretch`. It is `Repeat` now, and the Fill dropdown lists
`Repeat` first.

The saved document keeps writing only the fill that differs from the default, so the key it
carries is now `stretch`, and a frame without it repeats. A frame saved before with the key
absent and meaning stretch would therefore tile; none of the stored projects has one - both sliced
frames on the server carry `fill: "repeat"` explicitly, so they draw as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:13:11 +02:00
adminandClaude Opus 5 be8f2331de A frame that keeps its corners can be stretched to any size (#41)
game-maker / suite (push) Successful in 6m21s
TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.

The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.

A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.

The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:04:09 +02:00
adminandClaude Opus 5 b9f1a9d319 A graph that grew past one screen needs somewhere to say what a part of it is for (#38)
game-maker / suite (push) Successful in 6m13s
The graph could hold any number of nodes and said nothing about which of them
belonged together. Groups and comments are two separate things, as decided in
the issue: a group is a titled box that carries the nodes it encloses, a
comment is free text that carries nothing.

Both live as optional arrays on Graph, not as registry nodes. A comment that
were a real node would run through the interpreter, show up in the drop
palettes and be compiled into a shader — the wrong place for a piece of text.
As arrays on Graph they exist in all four graph kinds at once, and a document
without them means exactly "no groups", so no format bump.

Membership is computed when the box is grabbed, never stored: the center of a
node inside the box travels with it, and a node dragged out is simply out. A
box inside a box travels with the outer one, its own nodes included. A group
is grabbed by its title bar only — its body stays transparent to the pointer,
or a rubber band could never be drawn across the nodes it holds. Deleting a
box deletes the box; the nodes stay where they are, because a box that
silently took ten nodes with it would be the most expensive surprise in the
feature.

The color is a free picker, and the title color follows from it: relative
luminance per WCAG, threshold at 0.179 where the two contrast ratios meet —
not 0.5, which would pick white on mid grey where black reads better.

Resizing snaps the far edge to the grid, not the width, so the corner that is
not being dragged stays put. The minimum size rises in whole steps for that
reason: 80 is no multiple of 24, so a box squeezed to the stop would have sat
off-grid in exactly the mode meant to keep it on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:24:34 +02:00
admin 1e9d21cd5d A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap
happens after the tick, and the last wish of a tick wins. Exactly one
thing crosses over: the SharedState, so global variables and global
machines keep what they know while everything local is built anew.
Loading the scene that is already running rebuilds it, which is what
"reset the level" means when the score must survive.

A run starts either from the start scene (a new project field, set from
the asset panel and marked with an arrow in the list) or from whatever
scene is open. The toggle next to Play says which question is asked and
remembers the answer in localStorage: it is a way of working, not part
of the game.

Sounds no longer end because a scene ends. A voice belongs to the
behavior that started it and stops right after On destroy, so the scene
swap is no longer a special case at all. For the enemy that plays its
explosion and destroys itself in the same breath there is now a
detached input on Play sound.

The scene selector stays usable while a run plays, because a double
click in the asset panel already did the same thing unguarded. It moves
what the editor looks at, not what the run plays.
2026-09-15 13:24:26 +02:00
admin 5671a38044 A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server
content-addressed under its sha256, the project keeps a named asset that
points at it, and nothing is ever re-encoded. The format is read from the
file's own signature, not from what the browser claims - the same .m4a
arrives as audio/mp4, audio/x-m4a or as nothing at all.

Playing is two nodes. "Play sound" takes the asset and may raise or lower
its volume; every call is its own voice, so two jumps in a row are two
jumps heard. Its loop input is a choice of three - from the asset, on,
off - because a checkbox has to spend one of its two positions on "not
set" and would lose a case: an asset that loops could then never be
played once.

Below the graph core stays browser-free: NodeContext hands out an asset
id, BehaviorSystem resolves it against the project and folds the asset's
own volume and loop into the call, and only viewport/SoundLibrary knows
about Web Audio. A run without a sink plays nothing instead of throwing.

The CI browser is muted, so the ear is no witness: the debug hook counts
started and stopped voices, and step 34 reads that counter.
2026-09-15 07:54:28 +02:00
rootandClaude Opus 5 8143130ed2 A shared value belongs to no behavior, so it becomes an asset of its own (#91)
game-maker / suite (push) Failing after 8m59s
This reverses the design of the previous commit. `PropertyDef.scope` is gone
without replacement: a behavior variable is an object property again, without
exception, and sharing is something you point at rather than something you
declare twice.

A **Variables asset** is declarations and nothing else -- no graph, no position,
never placed, no tab of its own (`TAB_OF_ASSET` is already `Partial<>`, so that
costs no line). A `Variables` node names one through its new `variablesAsset`
setting, empty meaning the behavior's own variables as before, and then reads
and writes one value per key for the whole run. Its property list follows the
chosen source, which was the actual request: a value another graph declared is
now offerable, where before every participant had to declare the name itself.

That deletes more than it adds (-72 lines net): `globalConflicts` and its
run-stopping kind clash, `SetVariableScopeCommand`, the scope picker, both
object-inspector filters and the "shared by the whole run" note on the placed
object. A typo is no longer a silent second variable -- naming an asset is
picking from a list. And two Variables assets may carry the same key without
meeting, because the asset is the key, not the name. The one shape it copies is
the one that already existed: `Set state` picks its machine the same way.

Lifetime is unchanged. The bag is built at `BehaviorSystem.start()` from the
declarations and never written back, so playing in the editor cannot change the
project -- which is the whole reason a counter must not be an object property
(`World.syncToModel` would `commit()` it, and the coin counter would stand at 7
for good).

Full symmetry across that gap, without mirroring anything. Two named wires:
`Inspector.liveVariables` shows the values as they stand while a run is on and
falls back to the declarations when it ends, and `adoptDeclarations()` takes a
key into the run *only where the document changed it since the last sync*.
Without that condition any unrelated project edit would snap a running counter
back to its starting value; a counter-test bends exactly that and falls over.
Nothing was ever cached, incidentally: `GraphInterpreter` only pulse-caches
nodes without a `read`, and the variable node has one. The wire was missing.

The state machine `Scope` row is now an ordinary `enum` row through
`createFieldRow`. It was hand-built from an `HStack` with a ghost dropdown and
began ~34px left of every value above it; the explanations beside `Local` and
`Global` are gone from the menu, the sentence below the row keeps saying it.
Machine scope is otherwise untouched -- `StateMachineAsset.scope`,
`SetMachineScopeCommand`, `stateBag()` and its persistence all stand, so #59
stays closed as built and `Scope` as a type now carries machines alone.

Six counter-proofs, each biting at its own guard. One of them did not bite at
first: the suggestion list was computed twice, in `nodeSection` and
`portSection`, so only the browser test covered the request itself. It is one
named function now, `variableChoices`, with a unit test of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:05:36 +02:00
rootandClaude Opus 5 d1fcfe8b81 A shared value belongs to the run, so a declaration carries its scope (#91)
A variable or a state machine declared `global` keeps one value for the whole
run instead of one per behavior instance. The scope sits on the declaration --
`PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence
meaning `local` -- and `local` is never written, so every project made before
this saves back unchanged.

**The name is the key.** Two behaviors declaring a global `score` mean one
variable; a behavior declaring `score` locally keeps its own. Kinds that
disagree about one global name stop the run before it starts, with the two
behaviors named; starting values that disagree are a warning on the behavior
that declares it, and the first declaration wins (`globalConflicts`).

The runtime needs no redirection layer. A global variable falls out of
`schemaForObject`, so it never reaches `entity.props` -- and then the whole rule
is "whoever declares the name takes their own value" (`graph/sharedState.ts`).
The `Variables` node is untouched and stays transparent: switching a variable to
global breaks no existing graph, because `GraphInterpreter.readAll` builds a
fresh object per read anyway and only asks `port.key in all`.

That same cut answers the inspector. A shared value has no owner, so the placed
object shows no row for it and can hold no override -- `SetVariableScopeCommand`
takes the deviations that were there and undo gives them back.

Both places say it in words rather than leaving a glyph to be guessed: the
declaration explains the mark below the list, and the placed object names the
variable it no longer holds instead of claiming the behavior declares none --
which is what it said before, untruthfully. Screenshots of the e2e run turned
that up; no check had been looking at it.

`SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from
the *declarations* rather than from any instance (a global variable exists
before its behavior is placed), and is thrown away with the run: nothing is
written back, not even in the editor run. `GraphInterpreter.call` passes it on
through `...ctx` while nested calls keep getting fresh local variables, which is
why `program.defaults` is stripped of global keys.

Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or
the inspector does not rebuild after the switch -- and a state machine has no
rows at all, so its scope is appended to the signature by hand. And the object
inspector does not go through `schemaForObject` for behaviors, it reads
`asset.variables` directly; filtering in one place was not enough, and no unit
test covered that path. The e2e caught it (`global: true` where false belonged),
and the rule now lives in one named function, `objectRows`.

Scopes `object` and `scene` are deliberately not built. The object case already
has a route (`Object by name` -> `Properties`), and a run knows exactly one
scene, so `scene` would be indistinguishable from `global` until #39 exists.

Closes #59: a machine's scope is a word on the machine asset, not a setting per
`Set state` node, so the same machine cannot live in two places at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:22:53 +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 Opus 5 86b5363a01 Fonts: a new asset type, picked from the server or dropped in, worn by text objects (#74)
game-maker / suite (push) Successful in 9m10s
A font asset holds a file and nothing else, the twin of a texture: the server keeps it
content-addressed under data/fonts/{sha256}.{ext}, with the format read from the file's own
four-byte signature (finfo reports fonts inconsistently across libmagic versions). A text
wears one through its ordinary Font property, so overrides, reset, undo and the Properties
node come for free; no font means the editor's own typeface.

The picker lists everything the server holds and renders each row in the face it offers.
The family name is gm-font-<hash16>, never the name inside the file, so two "Regular" cannot
collide. WorldViewport.syncText is the single place that draws it, which covers scene, game
preview and play at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:09:59 +02:00
rootandClaude Fable 5.1 016e83c9b5 Transform: scale, rotation and mirror as properties, world transform as a matrix chain, oriented colliders (#76)
game-maker / suite (push) Successful in 9m5s
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.

The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.

Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 17:25:06 +02:00
rootandClaude Opus 5 5c1c3c6fd6 Properties node: name each shader param by its shader (#66)
game-maker / suite (push) Successful in 8m23s
The + Property menu of the Properties node offered every shader's params
for every sprite. Each shader row now carries its shader name, and a name
shared by two shaders lists both: "brightness (Adjust colors, Neon glow)".
Sprite-owned rows stay unlabelled.

The note hangs off a map keyed by the def object, not by the name -- a
sprite property and a shader param may share a name (#63), and keying by
name would label the wrong row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:05:01 +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 Fable 5.1 ce42d16657 Refactor round 2: fold duplicated command and inspector code
- applyCommands sit on ObjectEditCommand with a revert hook instead of
  five copies of the same preamble and undo
- actionIf replaces six identical Reset/Apply menu blocks, addNameField
  the three name rows, assetLink the two asset links
- requireStructuralObject replaces ten two-line preambles and drops the
  duplicate holder lookup
- __gm.objectsOf had no caller; the SMB remnant in e2e.sh is gone

Proven: check.sh 797 tests, full e2e 633 checks, smokes 24/19/20 against
the same bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 16:33:24 +02:00
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +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 8a7185948d Make texture and colour full kinds and drop the load-time migrations
- `Texture` and `Color` can be picked as a variable kind, a struct field
  kind and a declared port kind; a new `Texture` node hands a picture id
  into the graph. Declared, a texture is its bare id – the `$texture` tag
  stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
  `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
  `migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 14:06:14 +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 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 5f8a308d62 Drop the collider and delete-node buttons from the inspector
Adding a collider now lives only in the Scene objects context menu, deleting a node on
the Delete key and the node context menu. The e2e checks that pinned the inspector button
now drive the hierarchy menu instead, including that its entry is disabled on template
content and on a placed container without raising a notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:54:07 +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 631cf54c72 Give every behavior variable one row shape on asset and instance
buildStructBlock only covered structs, so a placed instance drew numbers,
text, bools and lists through createFieldRow on a 64 px name column while
its struct block sat on the asset's grid below it. variableRows.ts now
builds the row for every variable kind on both sides; the edit option
decides what the owner may change (asset: rename, x, item kind; instance:
revert and push). Label and Button take a fixed width so the mark column
no longer shifts names when a list shows its item-kind picker.

The two structFields calls that became dead with 9f48257 are gone. The
e2e steps locate the Enabled toggle through __gm.inspectorField instead
of clicking a fixed offset from its label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 07:27:29 +02:00
root 9f482573e5 Show a placed object's struct the way its asset does
The instance inspector drew its own, flatter version of a struct: one row per
field, no bundle head. It now draws the same block as the asset side, because
there is only one implementation left - both call buildStructBlock. What the
instance does not get is everything that touches the shape: renaming a field,
'+ Field', and the per-field '×'. The shape belongs to the asset; a placed
object overrides values. The revert and override marks sit on the head rather
than on a field, since an override covers the whole bundle at once.

Underneath this, editing a single field of an unoverridden struct used to blank
its siblings: structFieldsOf filled the untouched fields from spec.read, which
is object.overrides[key] and therefore undefined until something is overridden.
structOf then handed back the empty shape. PreviewedValue carries 'current' now
- the resolved value the row actually shows - and the fields come from that.
Without it the old path stays, which is right for the asset side, whose prop
bag is dense.
2026-09-10 23:05:02 +02:00
root 14f542365e Build a Pack or Expand node's ports from a struct variable
The port editor gets 'From variable' next to '+ Field': pick a struct variable
of the behavior and its fields land as ports. It only adds - a field the node
already declares stays as it is, because it may carry an edge - and the entry
is disabled once there is nothing left to add. A list of structs counts too:
it declares the shape of every item, which is exactly the bundle a Pack builds
for it. A color field arrives as a number port; the graph has no color type.

AddPortsCommand is new underneath: adding five ports had to be one undo step,
and it checks every key before it appends a single one, so a collision does
not leave half the list behind. AddPortCommand is a three-line subclass of it,
which leaves its sixteen call sites untouched.
2026-09-10 21:35: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
rootandClaude Opus 5 f60358c951 Let a list hold structs, and read or write variables in bundles
A list item may now be a struct. The depth is bounded in the declaration
rather than by a comment: FieldKind (number|text|boolean|color) says what a
struct field holds, ListItemKind adds struct on top and says what a list
holds, so list -> struct -> primitive is the deepest storable shape and
sameValue stays finite. A struct item renders inline in the List tab, which
is why it needs no path in the slot address.

Four paths silently assumed a list item was a primitive: sameValue compared
items with ===, textOf would have printed [object Object], asListItem turned
a struct into text on Set item and Fill, and Index of used indexOf, which is
reference equality and could never have found a struct.

Tetris now runs on bundles. shapes is 112 {x, y} structs instead of 224 loose
numbers, piece and the candidate are structs, so Next move hands back a whole
candidate and each strand packs only the field it changes, since mergeStruct
leaves the rest standing. Declared behavior ports drop 49 -> 35, the master's
variables 18 -> 13, its graph 84/94 -> 69/76 nodes/edges.

Get variables and Set variables declare their ports from a list of variable
names, the way Pack and Expand already do, so one node can read or write
several at once. Renaming a variable rewires them, because there the port key
is the variable name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:01:56 +02:00