Commit Graph
263 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 9b5eeaab57 Empty a deleted tile's cells on every tilemap (#268)
Deleting a tile left the cells painted with it in the tilemaps. They
drew nothing, blocked nothing and did not show in the delete dialog,
but still read as a tile for "Tile at" and stopped a fill.

- DeleteAssetCommand empties the cells of a deleted tile on every
  layer of every tilemap in the same step. Undo puts the tile and its
  cells back together.
- The delete dialog counts them, for example "3 cell(s) on 1
  tilemap(s) are painted with it and become empty." (cellsPainted).
- presentTile treats a cell naming a missing tile as empty. "Tile at"
  (World.tileAt) and floodFill use it, so cells already in existing
  files read and fill as empty. There is no migration.
- The brush, the rectangle and the fill check the tile in hand. A
  tile deleted after it was picked no longer paints new invisible
  cells, the editor says "Choose a tile to paint with." instead.
- Unchanged on purpose: the picker still reports such a cell as
  naming a tile that no longer exists, and erasing removes it.

Tests:
- ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6).
- GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t).
- Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4).
- Counter-checks, all fail as expected: 5 in unit tests (delete
  without emptying, undo without cells, fill and "Tile at" without
  presentTile, wrong map count) and 3 in the browser (dialog without
  count, delete without emptying, brush without the check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:00:22 +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 bc688f3efc Refactoring round 7: split BehaviorSystem, fewer copies (#85)
game-maker / gate (push) Successful in 3m50s
game-maker / suite (push) Successful in 12m3s
Recurring cleanup round over the code since round 6 (056a622), 105
commits. Behaviour is unchanged. Duplicated code in src went from 29
clones with 229 lines (0.32 %) to 14 clones with 109 lines (0.15 %),
measured with jscpd and the same settings as round 6.

- BehaviorSystem.ts shrinks from 941 to 589 lines. Collecting the
  pulses (keys, timers, collisions, animations) moved to
  behaviorPulses.ts. The NodeContext the nodes receive moved to
  behaviorContext.ts. Imports from outside stay the same, TimeScaled
  is still exported from BehaviorSystem.ts.
- PointKindButton and drawPointGrip replace the "Point:" button with
  its menu and the point and handle drawing that the curve and the
  vector art tab each had on their own.
- lerpVec, cubicAt and splitCubic in src/core/util/vec.ts replace four
  copies of the cubic curve and two de Casteljau splits.
- mapNested in Properties.ts is the one walk over lists and structs
  that liveObjects and objectRefs did separately. withFieldRenamed
  and withoutField replace three copies of renaming a struct field
  and two of removing one.
- jsonBody and staleRevision build the JSON body and turn a 409 into
  StaleRevisionError for all stores. The auth store turns a 401 into
  its error in one place.
- Smaller helpers: structAssetFrom for the two "Make struct type"
  commands, shownObject in the inspector, selectedItem for the size
  gizmo and the polygon corners, solidTiles in the tilemap collision,
  requireVariablesAsset and requireBehaviorVariable.
- Left as is on purpose: the plain forwarding from the collision and
  physics systems to Solids, the pointer wiring of the canvases and
  the identical last lines of the two make commands.
- Removed unused code: tilemapTileIds and tilemapsUsing were never
  called since #128 and #131. EditorState.runtimeProject became unused
  with GameRun in #266. The assetId getter of the make commands was
  read only by a test.
- 60 values used only in their own file lost their export. The shader
  and particle node catalogues stay exported as in round 6.
- Five comments that told history now describe the current state. The
  doc comment of BehaviorSystem sits on the class again.
- README lists the struct, clip area and curve assets and mentions the
  game link from #266.

Findings:
- Deleting a tile that is painted on a tilemap leaves its cells in
  place, and they draw nothing. The delete dialog does not mention the
  tilemaps. That was probably the job of the unused tilemapsUsing.
  Left as is, because fixing it changes behaviour.
- The solid filter for tile surfaces was untested. A bent filter
  stayed green. A new test in tests/core/surface.test.ts now fails
  against it.

Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (one test removed with
  tilemapsUsing, one added for tile surfaces).
- Full e2e run: ALL PASSED, 1580 checks.
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks, all passed.
  The example project files were restored byte for byte afterwards.
- Counter-checks, 12, all fail as expected: 10 in unit tests (among
  them mapNested, splitCubic, cubicAt, staleRevision, the stay pulse,
  the late timer and the time scale), 2 in the browser (Point button
  always enabled fails 47, selectedItem without a hit fails 29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:16:37 +02:00
adminandClaude Opus 5.5 cab1c0177f Show only the game view in fullscreen, in the editor and the link (#267)
game-maker / gate (push) Successful in 4m10s
game-maker / suite (push) Failing after 11m53s
A game is easier to judge and to play without the editor around it.
The Game tab and the game link now both have a button that puts the
game view itself into fullscreen, not just the browser window.

- The Game tab header has a fullscreen button on the right. It hides
  toolbar, panels, tab headers and console and keeps the zoom from the
  picker. A running game keeps running.
- A corner button at the top right, or Esc, leaves fullscreen and puts
  the view back into the tab where it was.
- The game link shows the same corner button. Over a running game it
  hides after 2 s without mouse movement and returns on the next move.
- A click on the corner button never reaches the game.
- Stop ends fullscreen. Stop switches back to the Scene tab, and any
  switch away from the Game tab ends fullscreen, so no empty screen is
  left behind.
- src/viewport/fullscreen.ts wraps the Fullscreen API and holds the
  rule for when the button shows. FullscreenCorner.ts is the button
  used by both pages. Two SVG icons are new.
- Editor.showGameFullscreen moves the GameViewport into the root while
  fullscreen is on.

Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests).
- e2e 60: 2 new checks for the corner button on the game link.
- e2e 61 (new): 4 checks for fullscreen in the editor.
- Full e2e run (local): ALL PASSED, 1580 checks.
- Counter-checks fail as expected: rule without "no game running",
  no stopPropagation (60h), no end on tab switch (61c), no reparenting
  of the view (61a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:44:14 +02:00
adminandClaude Opus 5.5 24d360aeec Add zoom steps and a larger start caption to the game link (#266)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Failing after 12m45s
The released game only ever zoomed in whole steps up to 400%, so a
small camera on a large screen stayed far smaller than it could be.
The "Click to play" caption was drawn in the 11 px panel title size
and was easy to miss.

- GAME_SCALE_STEPS gains 300%, 500% and 600%. The game tab picker,
  Auto in the editor and the game link all read the same list.
- The start caption takes its size from the new Theme.captionSize
  (16) instead of a constant in GameScreen.
- The player page keeps the default arrow cursor at all times. It no
  longer switches to the hand over the caption.

Tests:
- view.test: Auto for 320x180 in 2000x2000 is now 6, with cases
  for 3 and 5. play-settings.test takes 2.5 and 8 as invalid steps.
- e2e 60 checks the caption against the panel title size and the
  arrow cursor before and after the start. Module 03 follows the new
  step list.
- ./check.sh 2808 tests, CHECK EXIT 0 (before the last Theme tweak,
  which changes no test). Full e2e run ALL PASSED, 1574 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:14:54 +02:00
adminandClaude Opus 5.5 4194f3e150 Share a link that plays the game without the editor (#266)
A project could only be played inside the editor, behind the login.
To show a game to someone else there was no way short of an account.
A release freezes the saved project under a random token and serves
it on a page that holds nothing but the game view.

- Menu -> Release... opens a dialog with the state of the release,
  the link with Copy link and Open, Publish or Update release, and
  Take down behind a confirmation.
- Publish saves first and refuses while anything is unsaved, like
  snapshots. Updating keeps the link. Take down kills it, and a new
  Publish hands out a new token.
- The link is play.html?game=<32 hex>. The page needs no session,
  paints Theme.colors.background, zooms with Auto and waits for
  "Click to play" so that sound and keyboard focus are allowed.
- The game always starts in the start scene. Persistent variables
  are stored per link in the browser. Plugins are not loaded, and the
  dialog warns about them and about a project without a scene.
- GET ?r=releases/{token} is the only route besides auth/* that works
  without a login. The public answer carries the token as project.id,
  so the project id stays hidden. Release files are written with 0640
  and nginx does not serve them directly (measured 403).
- Starting, the frame and Load scene moved into runtime/GameRun.ts.
  Play in the editor and the play page run through the same code.
- WorldViewport moved to src/viewport/ and no longer needs the
  EditorState. Pointer forwarding (gamePointer.ts) and setting up
  textures, fonts and sounds (projectFiles.ts) exist only once.
- Vite builds two pages. The play page loads 4 kB of its own code
  plus the shared chunk (Pixi, runtime, drawing, 653 kB) and Rapier,
  without the editor.

Found on the way, not fixed here: data/projects/<id>.json answers 200
without a login because the files are 0644.

Tests:
- ./check.sh: 194 files, 2808 tests, exit 0.
- tests/php/releases.test.php: 20 checks, also added to CI.
- New e2e module 60 with 10 checks, played in a browser without a
  session. Module 46 counts the menu entries and was updated.
- Full e2e run: 1572 checks passed.
- Counter-checks: 3 unit, 2 PHP and 3 browser mutations all fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:30:38 +02:00
adminandClaude Opus 5.5 94c1055895 Throttle what the editor shows while a game runs (#265)
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Failing after 11m9s
With the behaviors fixed, the scene Performance test still ran at
about 23 fps with 500 entities. The rest of the frame went to the
editor: every spawn or destroy rebuilt the hierarchy with all its
rows, redrew the scene view behind the game tab and refreshed every
panel. The game itself is cheap. What the panels show of it now
follows a preset in the project settings.

- New section While playing in the project settings with three
  presets: Live, Balanced and Minimal. Every project reads as
  Balanced, including those saved before the setting existed.
- Live keeps today's behavior. Balanced brings hierarchy and panel
  values up to date 4 times a second and stops redrawing the hidden
  scene view. Minimal updates hierarchy and values only on pause and
  throttles the graph tabs as well.
- The graph and state machine tabs stay live under Balanced. At 4
  updates a second the beads on exec edges stutter, and the tabs
  draw only while on screen. This differs from the issue proposal.
- The game itself is never throttled. Pause, step and stop bring
  every panel up to date at once, and a change of structure that
  falls between two updates is kept for the next one.
- The rule lives in editor/playThrottle.ts as panelPolicy and
  PanelClock. PlaySession asks it after every frame.
- Objects spawned at the top level go into a hierarchy group named
  after their asset, so hundreds of gold pieces fold into one row.
  Groups start folded.

Measured on a throwaway copy of Performance test, 3 s each, fps:

- About 500 entities: Live 38.6, Balanced 56, Minimal 60.6.
- About 315 entities: Live 56.9, Balanced 60.5, Minimal 60.6.
- p95 frame time at 500 entities: 60 ms, 41 ms and 23 ms.

Tests:

- ./check.sh: 193 files, 2799 tests, CHECK EXIT 0.
- GM_STEPS=1,18,59: ALL PASSED, 54 checks.
- Full e2e run (local): ALL PASSED, 1562 checks.
- tests/editor/play-throttle.test.ts: presets, update rates, kept
  structure changes, flush, spawn group and loading old projects.
- New e2e module 59: preset chosen with the mouse, measured update
  rates, scene view behind the game tab, Minimal with pause.
- Module 03 now expects spawned objects in their group row after the
  next update.
- Counter-checks: 4 in unit tests and 2 in the browser, each failing
  as expected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:53:04 +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 0c41cb97b1 Clip the children of an object to a clip area (#260)
game-maker / gate (push) Successful in 3m43s
game-maker / suite (push) Failing after 11m7s
An inventory grid could not scroll, because nothing could cut a child
off at an edge. A game also had no way to hear the mouse wheel. Both
are needed for a scrolling list, a dialog text or a window with more
content than room.

- A new object type Clip area has width and height and turns, scales
  and mirrors with its parent. The size gizmo drags width and height.
- Everything below a clip area, on every level, is drawn only inside
  its rectangle, in the Scene view as in the game. Nested areas cut to
  their intersection. The rule works on a convex polygon, so a turned
  area cuts right as well.
- On click, Is hovered and On mouse wheel do not hit a cut-off part.
  The Scene view does not pick it either.
- In the Scene view the area is a dashed frame that is grabbed only at
  its edge, so a click inside lands on the content. The eye under
  Shape hides the frame. In the game the area is invisible.
- While an object below an area or one of its parents is selected, the
  Scene view shows the cut-off part at 30 % opacity and it can be
  grabbed there. The rule lives in core/properties/clip.ts.
- The Properties node knows the type Clip area.
- The new event node On mouse wheel fires by the rule of On click. Its
  delta counts in notches, positive is down, and a frame's turns
  arrive as one value. It also gives x and y.

Limits:

- Only sprites and texts show the faded rest. Tilemaps and particles
  are cut and can be grabbed when open, but are not shown faded.
- Masks by texture shape are out, as noted in the issue.

Tests:

- ./check.sh: 2773 tests, CHECK EXIT 0.
- Full e2e run (local): ALL PASSED, 1549 checks, measured.
- tests/core/clip-areas.test.ts: 12 tests for the cut rule, scene and
  game, save and load, hits, the wheel with and without owner, the
  notch conversion and the open rule.
- e2e 58, 5 checks with real mouse and wheel: the scene cuts (74 of 74
  red inside, 0 outside), a click on the cut part picks nothing, the
  selected rest is faded and grabbable, the game cuts, one notch
  logs 1.
- e2e 04 and 46 count the object types and the eye rows. They now
  include Clip area.
- Counter-checks: 6 in unit tests and 3 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 07:08:00 +02:00
adminandClaude Opus 5.5 89aa92d2e3 Put a shader on a text and draw pixel fonts sharp (#169)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Failing after 11m40s
A text could not wear a shader, and a pixel font was always smoothed.
Pixi's Text takes no custom shader. It also shares one texture between
texts of the same content and style, and the filter is not part of that
key, so two equal texts with different filters would overwrite each
other.

- TextPicture rasters a text into a texture of its own and draws it
  like a sprite. Shader, time animation and edge mode work without a
  special path. A new text, font or size rasters again and keeps the
  shader.
- The old texture is dropped only after the shader holds the new one.
  Before, Pixi warned about a destroyed texture that was still bound.
- A text has a Shader row with the shader's property rows below, as a
  sprite has. Overrides, saving, animation and the Properties node
  (type Text) know them. A shader parameter named like an own row of
  the text loses to that row, as on the particle source.
- The font asset has a Filter row, Linear or Nearest. The row existed
  three times already (texture, vector art, pixel art) and is now one
  helper filterRow.
- Nearest rasters the text 8 times finer and cuts every 8x8 block to
  one texel: at least half covered is full, else empty. Chrome on
  Windows smooths canvas text sideways through DirectWrite whatever
  filter we set later. The cut removes those half pixels on every
  system, and the texture stays one texel per world unit.

Found on the way:

- On a font that is no pixel font thin strokes break apart under
  Nearest. For a pixel font at its own size this does not matter.

Not done:

- The preview in the Shader tab does not take a selected text as its
  picture, it shows the test picture then.

Tests:

- ./check.sh: 189 files, 2759 tests, CHECK EXIT 0.
- text-shader.test.ts with 10 tests: rows and name precedence, a
  clamped override, save and load, animation, the font filter, and
  sharpCoverage on a smoothed edge and at the texture border.
- e2e 30 has 7 new checks: red glyphs through the shader picked with
  the mouse, the shader survives a new text, red in the game, soft
  edges under Linear, clean 4x4 blocks under Nearest, and half covered
  texels (Linear 238, Nearest 0).
- Full local run before the Nearest cut: all passed, 1543 checks.
  GM_STEPS=1,30 on the final bundle: all passed, 32 checks. The sum
  1544 in the README is counted from these two runs, not measured.
- Counter-checks fail as expected: in the browser no shader on a text
  fails 30h-30j, an always linear sampler fails the Nearest check, the
  old 1:1 raster leaves 107 half covered texels and fails 30k-ii.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:01:28 +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 f84defb9b6 Pick an object of the same scene for an Object variable (#258)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 10m2s
An Object variable could only be set by a graph while the game runs.
A placed object always showed "(null)", although the other objects of
its scene are there to choose from and the runtime can already find
any object by its id. Now the placed object stores a reference to one
of them, and the running game turns it into the real object.

- Stored as {"$object": "<id>"}. The empty placeholder stays as it
  was, so old projects load unchanged and empty still means "(null)".
- The row of an Object variable on a placed object is a dropdown with
  None and every object of the scene, indented like the Hierarchy.
  The same goes for Object fields in structs and Object items in
  lists, also in the Variables and List tabs.
- The behavior asset keeps "(null)". An asset has no scene, so "Apply
  to asset" is gone from Object rows.
- The runtime resolves the reference when the Variables or Properties
  node reads it. A destroyed target reads as null, a missing one shows
  "(missing)" in the row.
- Inside a container only objects of the same container are offered.
  The reference keeps the template id, so every placement finds its
  own copy.
- Deleting the target clears the reference in the same undo step.
  Paste points references inside the pasted group at the copies and
  leaves outside references alone. Create container and Unpack carry
  references along with the new ids.
- A value a graph writes while running stays in the run. The scene
  keeps the picked object, also in "Run in editor".

Found on the way:

- Setting an override coerced every object slot to empty, in plain
  values, struct fields and list items. The shared objectPlace() now
  keeps the reference.

Not done:

- No eyedropper to pick in the scene, the dropdown is enough for now.
- Deleting a template object in the container editor does not clean up
  references in other scenes. They read as null there.

Tests:

- ./check.sh: 187 files, 2724 tests, CHECK EXIT 0
- e2e 1-9, 51, 57: ALL PASSED, 389 checks
- e2e 1, 57 on the final bundle: ALL PASSED, 17 checks
- 13 unit tests in object-references.test.ts, 5 checks in e2e 57
- Counter-checks: 8 unit breaks fail their tests, the browser break
  without a pick list fails 57b-57d

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:35:17 +02:00
adminandClaude Opus 5.5 e8b3ac0a3b Keep behavior variables private to their own graph (#257)
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Successful in 10m56s
A behavior often needs working state such as a timer or a counter.
Until now every variable showed up on each placed object and in the
Properties menu, so the inspector filled up with values nobody should
touch. A private variable works like any other inside its graph but
is not offered anywhere outside.

- Each behavior variable gets a lock switch next to Keep. It starts
  off, so existing projects stay as they are (`privateVariables`).
- A placed object shows no private variable, neither in the
  inspector nor while the game runs. If all are private, it says so.
- Switching a variable to private drops its overrides on every
  placed object in one undo step. Switching back does not restore
  them.
- A running game does not write a private value back as an
  override, and a project change during the run does not reset it.
- `+ Property` no longer offers private variables. A port that
  names one by hand still reads and writes it without a warning.
- The default value of a private variable is read-only in the asset
  inspector. Its shape (name, fields, type, delete) stays editable.
- Keep is now a pin icon instead of a word. With the lock added, the
  header of a list variable had no room left and the lock covered
  `+ Item`.

Not done: the animation track picker still offers private variables.

Tests: ./check.sh -> 186 files, 2711 tests, CHECK EXIT 0.
e2e 1,19,20,21,56 -> ALL PASSED, 119 checks. New module 56 has
7 checks with a real mouse. Counter-checks fail as expected: unit
without the write-back filter, the sync guard, dropping overrides or
loading the flag. Browser with all variables on the object, an
unfiltered menu, or no value lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:54:57 +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 e1d3c0cadb Keep items a running game puts into a typed list
game-maker / gate (push) Successful in 3m28s
game-maker / suite (push) Successful in 10m48s
A typed global list lost the items the game had added to it. The item
showed up in the inspector for a moment and then was gone again.

- Since #254 the project hands out a typed list as a fresh copy each
  time, with the `$struct` marks stripped.
- While a game runs, every change to the project runs
  `adoptDeclarations`. It took a declared value over when it differed
  from the last one it had seen, compared with `!==`.
- The fresh copy never equals the last one by identity. So the empty
  list from the project overwrote the live list on every project
  change, for example when the game writes something back to the scene.
- The comparison now uses `sameValue`. A real edit to the variable
  during a run still reaches the game.

Tests:
- ./check.sh: CHECK EXIT 0
- New unit test in scope.test.ts, red without the fix.
- e2e 55i: a graph appends to a typed global list, then another asset
  changes during the run. With the fix {"filled":2,"afterEdit":2},
  without it afterEdit is 1 and the check fails.
- GM_STEPS=1,55: ALL PASSED, 26 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:00:22 +02:00
adminandClaude Opus 5.5 b0d0868f55 Give a list one struct type so every item keeps its shape (#254)
game-maker / gate (push) Successful in 3m28s
game-maker / suite (push) Successful in 10m38s
A list of structs used to let every item carry its own fields. Adding
an item meant typing the same fields again, and nothing kept the items
in step with the struct asset from #253. A list can now name a struct
type for its items, and the asset keeps them in shape.

- A list variable and a list field get the row Items with Mixed, every
  struct asset and Make struct type. Make struct type collects every
  field any item has, so no item loses a field.
- A typed item carries its type under the key `$struct`
  (ITEM_TYPE_KEY). listItems does not count it as a field.
- A list that holds anything but structs refuses a type, also when
  the offending item sits in an override. The console names the list
  and the count, and the list stays as it was.
- In a typed list `+ Item` adds an item with the defaults, without the
  kind menu. Field names are locked, values stay editable.
- The rows of a typed item take their kind from the struct asset
  (declaredKind in InlineList.ts). An asset field shows the asset
  picker instead of a text field, as reported in the issue.
- A struct item in a mixed list gets its own Type row. Switching a
  list back to Mixed keeps the type on each item.
- syncStructTypes brings marked items into shape and carries renames
  into them, also inside overrides and nested lists. One undo step.
- The delete dialog of a struct asset also counts typed lists.
- The game never sees the key. World and sharedState strip it with
  withoutItemTypes and bagWithoutItemTypes, so a struct stays a
  plain object at runtime.
- Add item and Set item get the setting Type. With a type they only
  accept a struct with exactly its field names (fitsType in
  SetItem.ts). Anything else is left out and logged.
- The item indent from #252 is back to its old width, the narrow one
  squeezed the value fields.

Not done: writing a variable does not check the type, and the List
node offers no type.

Found on the way: a test helper in list-of-structs.test.ts called
nodes without params. It now passes the same arguments as the
interpreter.

Tests:
- ./check.sh: 183 files, 2679 tests, CHECK EXIT 0
- e2e GM_STEPS=1,21,22,32,54,55: ALL PASSED, 150 checks
- e2e GM_STEPS=1-6,21,22,32,54,55 before the follow-up: 404 checks
- New: tests/core/typed-lists.test.ts, e2e module 55 with 13 checks
- Counter-checks bite in unit and browser, originals restored by cmp

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:23:00 +02:00
adminandClaude Opus 5.5 61bbb96a96 Define a struct once as an asset and reuse its shape (#253)
game-maker / gate (push) Successful in 3m30s
game-maker / suite (push) Successful in 10m33s
A struct used to declare its fields wherever it stood. The same shape
in two variables meant typing it twice, and Pack/Expand copied it once
without staying in sync. A Struct asset now holds the shape in one
place, and every struct that picks it as its type follows it.

- New asset type "Struct" (not placeable, group Structs, own icon).
  The inspector shows its fields with name, kind and default value,
  and "Open" shows them in the variables tab.
- Struct variables and struct fields get a Type row: own fields, any
  Struct asset, or "Make struct type". With a type the fields come
  from the asset and cannot be added, renamed or removed there. The
  values stay per variable, overrides on placed objects still work.
- Pack and Expand get the setting Type. With a type their ports are
  the fields of the asset and read-only in the inspector.
- A change to the asset reaches every typed place in one undo step
  (syncStructTypes). A new field arrives with its default. A removed
  field goes along with its port, edge and typed value. A renamed
  field keeps its value in variables, overrides and on ports with
  edges, also through nested types.
- Undo for these edits records plain assignments (Recorder), because
  the reach of a change is not known in advance.
- Cycles such as Loot in Loot, also through another type, are refused
  and not offered in the picker.
- Deleting the asset names how many places use it. Afterwards each
  place keeps its fields as its own.
- Loading reshapes a stale copy in a project file to its type.
- "Make struct type" creates an asset from an existing struct and
  links the place to it.

Differs from the plan:

- A changed default value applies to new fields and newly typed
  places only. Existing values stay.
- Removing a single field shows no warning. Undo brings it back.

Typed lists are left for #254. Lists stay dynamic for now.

Found by e2e: after deleting or renaming the type the inspector did
not rebuild and kept showing the old name. Fixed, the check was red
before.

Tests:
- ./check.sh: 182 files, 2661 tests, CHECK EXIT 0
- GM_STEPS=1,4,21,22,54: ALL PASSED, 176 checks
- 14 unit tests in tests/core/struct-types.test.ts
- New e2e module 54 with 14 checks, real mouse. Module 04 counts
  23 asset types.
- Counter-probes: without the rename pass 4 unit tests fail, without
  reshaping values 8, without the cycle rule 1. In the browser,
  without spreading changes and with + Field despite a type, 3
  checks fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:04:40 +02:00
adminandClaude Opus 5.5 4857bb75e6 Show lists in the inspector and variables in their own tab (#252)
game-maker / gate (push) Successful in 3m24s
game-maker / suite (push) Successful in 10m22s
The List tab could show one list at a time and was the only place to
see its items. A list now shows its items right where it lives, and a
new Variables tab shows all variables of an asset or placed object.

- InlineList draws the items of a list under its row in the inspector,
  in struct fields and in the Variables tab. A list inside a list or a
  struct field is drawn the same way, one step indented.
- The index column is the indent. Items sit under their list without an
  own margin, and the index column is 16 px wide instead of 28 px.
- Above 20 items (INLINE_LIMIT) the rest collapses into "... N more -
  open in tab". During a run the rows show the values from the game.
- ListHead puts "N items" and "+ Item" in one row. "N items" opens the
  list in a tab. A separate Open button squeezed the counter to zero
  width in the narrow inspector column.
- VariablesEditor shows every variable of a behavior asset, a variables
  asset or a placed behavior. On an asset it edits the declarations. On
  a placed object an edit only overrides that object, with the usual
  reset and push buttons.
- Opening a list inside a tab adds a new tab and keeps the open one.
  valuesKey gives each list its own document, so a list that is already
  open is shown instead of opened twice.
- Titles read "Pickup · Variables" or "Listy · nums · [3]". The tab
  closes when its asset or object is deleted.
- Lists on node inputs stay inline only, a node owns no variables.
- The List tab, ListEditor and the __gm.list hook are gone.

Tests:
- ./check.sh: 181 files, 2645 tests, CHECK EXIT 0
- GM_STEPS=1,21,22,32: ALL PASSED, 123 checks
- e2e 21 is rewritten for inline lists and the new tab, 22 and 32 are
  adapted. Sum in README: 1486 (+6 in 21, -1 in 22), counted by hand.
- Counter-probes fail as expected: sub-lists not told apart (1 unit
  test), sub-list replacing the open tab, all items drawn inline (60
  rows instead of 20), no live values (0 rows at 24 items in a run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:56:21 +02:00
adminandClaude Opus 5.5 92fad6b9e3 Show the list a running game fills in the list tab (#251)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m16s
While playing, the inspector said "2 items" for a list a graph had
filled, but clicking it opened an empty list tab. The inspector reads a
Variables asset from the running game, the list tab only read the list
stored in the project. It did not follow the run's ticks either.

- resolveList takes the live values. For a Variables asset it uses the
  running game's value while one runs, the project's otherwise. This is
  the inspector's rule.
- ListEditor refreshes on runtime ticks about 10 times a second, and
  not while a field in the tab is being typed in, like the inspector.
- Editor hands the list editor the inspector's liveVariables.
- __gm.list.info() also returns rows, the rows the last build drew.
- Editing in the tab while playing goes into the run's copy of the
  project, as in the inspector. A very long, growing list rebuilds all
  rows up to 10 times a second.

Tests:
- ./check.sh: 180 files, 2634 tests, CHECK EXIT 0
- e2e GM_STEPS=1,32: ALL PASSED, 30 checks
- e2e 32b-ii, 2 checks: a graph adds an item every frame, the test
  pauses and clicks "N items" with a real mouse. The tab draws 26 rows
  for 26 items, grows to 50 after resuming and is empty after Stop.
- Counter-checks: without live values the tab draws 0 rows for 23
  items, and without the refresh it stays at 23
- README: 1481 editor checks, counted by hand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:07:14 +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 a5ae21481b Type point values, snap and scrub the preview in the curve tab (#248)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m9s
Building a curve by mouse alone was fiddly: there was no way to put a
point on an exact value, no grid, and the running preview could not be
stopped at a chosen moment. The hand cursor over points also looked
like a link instead of a drawing tool.

- Number fields t and v in the header show the selected point. Typing
  or dragging on them goes through the same rules as a mouse drag, so
  points never overtake and handles stay clamped. One undo step each.
  t is greyed out on the two end points.
- The magnet button from Vector art snaps points and handles to a
  0.05 grid in t and v, when placing on free space and when dragging.
- Points and handles keep the default arrow in the curve tab and in
  Vector art. The pixel art palette dropped pointer as well, which is
  beyond the issue. Collider corners never had it.
- The preview now starts off. A vertical time line with a knob under
  the t axis sets t when dragged sideways. Dragging the dot on the
  rail jumps to the first t where the curve reaches that value, or to
  the closest one if it never does.
- The rules are pure functions in curveView.ts (withCurvePointSet,
  snappedToGrid, shareAtValue, previewSecondsAt), the canvas only
  calls them.

Tests:
- ./check.sh: 180 files, 2616 tests, CHECK EXIT 0
- GM_STEPS=1,47,53: ALL PASSED, 87 checks
- 4 new unit tests in curve-view.test.ts, 4 new e2e checks in 53
  (53h-53k) and 1 in 47, all with real mouse and keyboard
- Counter-probes bite: without "first t" 1 unit test fails, with
  pointer, no snap, no field commit, no scrub or preview on 5 browser
  checks fail, pointer in Vector art fails the new 47 check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:45:53 +02:00
adminandClaude Opus 5.5 393b8ec7f9 Build your own easing curve and use it on keys and Lerp (#247)
game-maker / gate (push) Successful in 3m44s
game-maker / suite (push) Successful in 10m25s
The built-in eases cover the usual shapes, but a game often wants its
own: a coin that bounces twice, a door that overshoots and settles.
A new asset type holds such a curve, and keys and the Lerp node can
pick it instead of a built-in ease.

- New asset type "curve": points like in Vector art, x is t and y is
  the value. A new curve is the straight line from (0, 0) to (1, 1).
- One pure evaluation in src/core/model/curve.ts serves the timeline
  preview, the running game and the Lerp node.
- The curve always has one value per t. The end points stay at t 0
  and 1, points cannot pass each other, and a handle that reaches past
  its neighbour is shortened along its direction. Loading a hand-edited
  file applies the same rules.
- Curve tab: click on the line to insert a point that keeps the shape,
  click on free space to put one there, drag points and handles,
  double-click or Delete removes, Alt-click toggles round and sharp.
  Every step is one undo step.
- Presets: all 30 built-in eases plus Linear, fitted with adaptive
  subdivision. Each stays within 0.4 % of the built-in ease.
- Preview: a dot runs along the curve once a second, a second dot
  shows only the value. The Preview button stops it.
- Animation keys get "Curve" under Easing with a curve picker. Lerp
  gets "Curve" under Ease, and its result may overshoot b.
- A missing or unpicked curve runs linear. The inspector says so, the
  node shows it as a problem, and deleting a used curve warns with the
  number of keys and Lerp nodes.

Tests:
- ./check.sh: 180 files, 2612 tests, CHECK EXIT 0
- GM_STEPS=1-6,25,53: ALL PASSED, 309 checks
- New e2e module 53 with 13 checks, all with the real mouse. Expected
  values come from its own Bezier evaluation.
- Counter-checks bite: without Bezier evaluation 4 unit tests fail,
  without handle clamping 2. In the browser, a key that cannot find its
  curve, a click that sets no point and a handle drag that moves the
  point all turn checks red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:58:05 +02:00
adminandClaude Opus 5.5 320d3e27e1 Add a Lerp node that blends from a to b with an ease (#246)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Failing after 9m24s
Every animation in a behavior had to build its interpolation from
Subtract, Multiply and Add, once per axis. The gold pickup in
"Garden of Sacrifices" needed about 30 nodes for that alone. A single
Lerp node with an ease makes those chains one node each.

- Lerp has the inputs a (default 0), b (1) and t (0) and the output
  result. It sits in the data category next to Clamp.
- The Ease setting offers Linear, In, Out and In-out, all quadratic.
  The node row shows it as "Ease: Out".
- t is held in 0..1. A timer that overshoots in the last frame still
  lands exactly on b.

Tests:
- ./check.sh: 178 files, 2583 tests, CHECK EXIT 0
- 7 new cases in the node catalog test. Without the clamp on t,
  2 of them fail.
- e2e 6z-iii (2 checks): typing "lerp" in the palette adds a linear
  Lerp, and picking "Out" in the inspector reaches the node and its
  row. GM_STEPS=1-6: ALL PASSED, 267 checks.
- README check sum is 1459, computed, not counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:49:22 +02:00
adminandClaude Opus 5.5 30de4d35a8 Give the scene tab a header with Fit and the eye (#245)
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Failing after 9m15s
The scene view had no header bar of its own, and its eye sat in the
top bar. Switching between Scene and Game also showed two different
pictures, since nothing tied the free view to the camera.

- Theme.headerPadding (4) replaces Theme.padding in the top bar and
  in TabHeader, for the padding and for minHeight. Both bars shrink
  from 40 to 32 px. Nothing else uses the new value.
- The new ScenePane wraps the scene view in a header bar with Fit
  and the visibility eye. The eye is gone from the top bar, which
  keeps the menu, undo and redo.
- Fit with an active camera shows the world exactly as the Game tab
  does at its chosen step: the same zoom, and the camera centre on
  the same screen pixel. The rule is the pure freeViewOfCamera in
  viewport/view.ts, which also mirrors the letterbox snapping.
- Fit without a camera frames all content on a whole step, as
  before.
- New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there.
- The doc comment above Theme.treeIndent went away in the working
  tree along with this. It is committed as found.

Not checked: the two views only match while they have the same size.
With the tilemap panel open the scene view is narrower, and Auto may
pick a smaller step there.

Tests:
- ./check.sh: 178 files, 2575 tests, CHECK EXIT 0
- 2 new unit tests in view.test.ts: three cameras times four steps
  land on the same screen point as in the Game tab, Auto stays whole
- e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate,
  camera frame and view box match in both tabs
- GM_STEPS=1-3,46: ALL PASSED, 94 checks
- Counter-probes: without the centre offset the unit test fails,
  with Fit ignoring the camera both 3h checks fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:57:49 +02:00
adminandClaude Opus 5.5 e0b471c354 Open the add menu with a right-click in the asset panel (#244)
game-maker / gate (push) Successful in 3m24s
game-maker / suite (push) Successful in 10m9s
New assets could only be added through the + button. A right-click
on a category heading or on the empty space below the rows now opens
the same menu at the pointer.

- ListView gets the callback onListMenu. It fires on a right-click
  on empty space or on a heading. Only the asset panel has headings,
  so other lists are not affected.
- AssetPanel opens its + menu from there.
- A right-click on a heading does not fold or unfold the category.
  A right-click on empty space clears the selection like a left-click.
- A right-click on an asset still opens the menu of that asset.
- The empty list now says "No assets - press + or right-click".

Tests:
- ./check.sh: 178 files, 2573 tests, CHECK EXIT 0
- e2e 1-4: ALL PASSED, 134 checks
- 3 new checks in e2e 04 with real right-clicks: menu on the
  "Scenes" heading (21 types, heading stays folded), menu on empty
  space, and "Behavior" from that menu creates one, undone again.
- Counter-probe: without the menu both right-click checks fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:27:34 +02:00
adminandClaude Opus 5.5 38f009e035 Make e2e 40, 46, 51 and 52 pass in the full run again
game-maker / gate (push) Successful in 3m42s
game-maker / suite (push) Successful in 10m55s
The suite job failed on every push since 6015869. Each module passed on
its own, but the full run showed two kinds of fault.

- e2e 40 still expected the object menu and the empty-cell menu from
  before #240 and #241. It now expects Copy, Paste, Group... and Remove
  from group, and Paste alone on an empty cell.
- e2e 46g read the outline of the first collider in the scene. In the
  full run that is a leftover of module 43, off screen, so it read as
  hidden before any switch. The check now places a collider of its own
  next to its tilemap and removes both at the end.
- e2e 51 copied the seed's red and blue crate. In the full run module 09
  has packed both into a container. The module now places two crates
  with colliders of its own and removes them at the end.
- e2e 52 compared whole row lists against the seed scene. In the full
  run that scene holds about 50 more rows. The module now works in a
  scene of its own, which it removes at the end. It had never run in CI
  because 51 died before it.

The check in e2e 41 that failed once (task 347, a dragged body 15 px
off in its first sample) is left alone. It failed in 1 of the last 60
logs, while a local e2e run loaded the same host.

Tests:
- full e2e run in the game-maker-ci image with GPU, as the runner does:
  ALL PASSED, 1452 checks, E2E EXIT 0 (the README sum is now measured)
- GM_STEPS=1,46,51,52 on the host: ALL PASSED, 51 checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:12:40 +02:00
adminandClaude Opus 5.5 2eeabf7c23 Drag a whole selection and join existing groups in the hierarchy (#241)
game-maker / gate (push) Successful in 3m26s
game-maker / suite (push) Failing after 9m12s
Three follow-ups to the hierarchy groups. Dragging a row that belonged
to a multi-selection moved only that one row. The group dialog could
only create a new group. The group row drew its folder in front of the
label, unlike every other row in the hierarchy.

- Dragging a selected row moves every selected object. This holds for
  reordering, dropping into a group or between its members, dragging
  out of a group and dropping onto an object as new parent.
- The moved objects land side by side in hierarchy order, whichever
  row was grabbed. The whole move is one undo step.
- A selected child of a selected object travels with its parent, not
  on its own. Dropping the selection onto its own rows does nothing.
- Dragging a row that is not selected still moves only that row.
- hierarchyDrop.moveObjects replaces the single-object rule. One object
  is the case with one element, so the old drop tests hold unchanged.
- The group dialog has a "Group" dropdown: New group or an existing
  group. Picking a group greys out the name and turns the button into
  "Add". A group that holds all chosen objects already is not offered.
- Rename keeps using the dialog without the dropdown.
- ListRow draws an end icon at the very right and the hint left of it.
  Only the hierarchy uses iconAtEnd, so other lists do not change.
- New test hook __gm.hierarchyHint() gives the centre of a row's hint.

Not clicked in the browser: dragging a selection in the container tab
and onto an object as parent. Only the unit tests cover these.

Tests:
- ./check.sh: 178 files, 2564 tests, CHECK EXIT 0
- e2e 1 + 52: ALL PASSED, 27 checks
- 7 new unit tests in hierarchy-drop.test.ts, 4 new e2e checks in
  52i and 52j with real clicks and a real drag
- 4 counter-probes bite: drop rule ignores the selection (5 unit tests
  fail), icon column not kept free, dialog ignores the picked group,
  panel ignores the selection (only o2 lands in the group)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:19:12 +02:00
adminandClaude Opus 5.5 893b81f798 Select a range of rows with shift-click in the hierarchy (#241)
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Failing after 9m5s
Ctrl- and shift-click only added single rows to the selection. With
many objects spread over the hierarchy, picking a run of them meant
clicking each one. Shift-click now selects everything between the
anchor and the clicked row, as in a file explorer.

- The anchor is the row last clicked without shift, plain or with
  ctrl. A second shift-click redraws the range from the same anchor.
- Ctrl+shift-click adds the range to the current selection instead
  of replacing it.
- A collapsed group inside the range brings all its members. Children
  hidden under a collapsed parent stay out, since they are not shown.
- Without an anchor from the hierarchy, the primary object picked in
  the scene view serves as anchor. With no selection at all,
  shift-click adds the single row as before.
- The range rule is a pure function in hierarchyRange.ts, so it is
  tested without a browser. HierarchyPanel keeps the anchor.

Not checked in the browser: ctrl+shift-click and the anchor taken from
the scene view. Only the code path covers them.

Tests:
- ./check.sh: 178 files, 2557 tests, CHECK EXIT 0
- e2e 1 + 52: ALL PASSED, 23 checks
- 5 new unit tests in tests/editor/hierarchy-range.test.ts
- New e2e check 52h with real clicks
- Counter-check: without the range, 52h fails with ["o2","o5"]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:53:57 +02:00
adminandClaude Opus 5.5 53fa63441b Group scene objects in the hierarchy, for the editor only (#241)
game-maker / gate (push) Successful in 3m58s
game-maker / suite (push) Failing after 9m58s
A scene with a hundred crates fills the hierarchy with a hundred rows.
Groups bundle top-level objects under one folder row that folds away.
They exist for the editor only. Parenting, positions and the game do
not see them.

- Model: a scene or container template holds a list of groups, an
  object may carry a groupId. Old projects load unchanged. Loading
  drops empty groups and members that are no longer top level.
- Only top-level objects can join a group. Parenting a member takes it
  out of the group, and Undo puts it back. There are no groups in
  groups.
- Order: treeOrder lists the members together, where the first one
  stands. The same order decides drawing at equal z, in the editor and
  in the game, so the hierarchy and the picture cannot disagree. The
  stored object list stays untouched when grouping.
- Hierarchy: a group row shows a folder icon, the name and the member
  count. Clicking it selects all members and keeps it folded. Selecting
  a single member elsewhere unfolds it. Folding lasts for the session.
- Create with "Group..." in the object menu. A name dialog asks first,
  so creating is one Undo step and Cancel creates nothing.
- The group menu has Select members, Rename... and Ungroup. The object
  menu adds "Remove from group".
- Drag and drop: onto the group row appends, into a gap between members
  inserts there, next to a row outside leaves the group. Dragging the
  group row moves all members as a block. The drop rules live in
  hierarchyDrop.ts as a pure function.
- Copy and paste keeps a top-level copy in the group of its original.
  A container built from group members takes their place in the group.
- Clicking an object that is already selected together with others now
  keeps only that object selected. ListView narrows the selection on
  release, so a drag starting on such a row keeps the selection.

Not checked in the browser: groups in the container tab, "Remove from
group", Ctrl-click on a group row and a drag out of a multi-selection.
The code path covers them.

Tests:
- check.sh: 177 files, 2552 tests, CHECK EXIT 0
- e2e 1, 51, 52: ALL PASSED, 31 checks (new module 52)
- 19 new unit tests in object-groups.test.ts and hierarchy-drop.test.ts
- 9 counter-probes, all fail as expected (7 unit, 2 in the browser)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:17:46 +02:00
adminandClaude Opus 5.5 2b89292eb4 Copy and paste scene objects and graph nodes (#240)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Failing after 9m5s
Copy and paste only existed for animation keys. Building a scene or a
graph meant placing every object and node again by hand, even when the
same arrangement was needed twice.

- Ctrl+C copies the selected objects with their whole subtree, or the
  selected nodes with their links, groups and comments. Ctrl+V pastes
  into the scene, container or graph of the visible tab.
- The copy is a snapshot in the editor's own clipboard, in memory. It
  holds asset ids, so the system clipboard is not used. It is cleared
  when another project opens.
- A paste lands under the mouse pointer when it is over the view, and
  keeps its arrangement. Otherwise it lands one grid step beside the
  original. Each further copy that still exists moves one step on, so
  an undone copy does not count.
- Objects get new ids for the whole subtree. They paste into the one
  selected object and keep their look under the new parent. If that
  object is the original or an earlier copy, the paste lands beside it
  instead of nesting deeper with every Ctrl+V.
- A placed container stays a container. Content copied out of a
  container pastes as plain objects. Pasting a container into its own
  template is refused.
- Nodes paste only where the palette would offer them. A second Start
  or End, a State in a behavior or a behavior calling itself is left
  out, and a notice names what was dropped.
- Menus: Copy and Paste in the object menu, a new Paste menu on empty
  scene space, Copy in the node and box menus, Paste in the palette.
- Ctrl+V still imports a picture from the system clipboard when the
  copy does not fit the tab. While objects are copied, the scene and
  container tabs use Ctrl+V for them, as the timeline does for keys.

Not checked in the browser: paste in the container tab, in state
machine, shader and particle graphs, and from the hierarchy menu. The
shared code path covers them.

Tests:
- ./check.sh: 175 files, 2531 tests, CHECK EXIT 0
- e2e 51 (new): ALL PASSED, 11 checks with real keys and clicks
- 13 unit tests in tests/editor/clipboard.test.ts, one of them pins
  the grid step after undo found during e2e
- 7 counter-checks all fail as expected (6 unit, 1 in the browser)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:25:51 +02:00
adminandClaude Opus 5.5 7e829f737e Load every picture of a project ahead so animated tiles don't flicker
game-maker / gate (push) Successful in 3m26s
game-maker / suite (push) Failing after 9m6s
A texture was fetched only when it was drawn for the first time. Until
the file arrived, the tilemap skipped the cell for that frame. An
animated tile whose next step sat in another picture therefore blinked
away once on its first cycle, and sprite animations did the same.

- FileLibrary gets preload(files). It requests every file that is not
  held, loading or known to have failed. Each library reports what it
  holds through holds().
- src/core/model/projectFiles.ts lists the files a project names per
  library: uploaded textures, fonts and pixel art cels.
- App.ts runs the preload on every project event. That covers opening
  a project, new assets and edits from a collaborator.
- A new test hook __gm.heldPictures() reports which textures of the
  project are in memory.

Not done on purpose:
- The tilemap keeps no fallback frame while a step is still loading.
- Sounds still load on first play.

Not checked: the flicker itself was not reproduced in the browser, the
e2e check measures "held in memory". Modules 42, 44 and 47 read
textures from the project and were not run locally.

Tests:
- ./check.sh: 174 files, 2518 tests, CHECK EXIT 0
- e2e 39 + 40: ALL PASSED, 96 checks
- 3 new unit tests for preload and the project file list
- New e2e check 40k-ii: an unplaced picture is held after a reload
- Counter-probe: without the preload 40k-ii fails with
  "Autotiles: held false, unworn: held false"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:50:49 +02:00
adminandClaude Opus 5.5 6015869297 Let the scene view hide symbols, names and shapes per asset kind
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Failing after 9m33s
The scene view gets crowded once a scene holds many behaviors,
colliders and tilemaps. A new eye button in the topbar opens a list of
all placeable asset kinds, and each row has toggles for what the scene
view draws for that kind.

- The popup lists all 9 placeable kinds from Behavior to Tilemap, with
  the columns Symbol, Name and Shape. It stays open while switching.
- Sprite, Text and Tilemap draw no symbol, so their symbol toggle is
  greyed out. Their name toggle shows a new centred name tag, which is
  off by default.
- Shape exists for Collider and Tilemap. A hidden collider outline is
  neither drawn nor clickable. A hidden tilemap shape removes the
  collision outlines and the paint tint, the tiles stay visible.
- A hidden symbol takes no slot in the ring and draws no dashed link to
  its parent. A name that stays on moves to the centre of the object.
- The setting lives in the project as `sceneVisibility`, not in
  localStorage. Older projects load with the defaults, which match the
  view as it was.
- Switching has no undo step, like the game scale. It only changes the
  view. A change made during Play survives Stop.
- The rules sit in `src/core/model/sceneVisibility.ts`, the popup in
  `src/editor/panels/SceneVisibilityPopup.ts`. Four new SVG icons.

Not covered by a check: symbol off with name on for a kind with a
symbol, Text and Tilemap with names, and the paint tint with shape off.

Tests:
- ./check.sh: 173 files, 2515 tests, CHECK EXIT 0
- e2e 46: ALL PASSED, 17 checks. 46g clicks the popup, lays a small
  tilemap over the crate and removes it again.
- e2e 8 alone on a fresh seed: ALL PASSED, 23 checks. A red check in
  the partial run 2,8,46 came from the module selection.
- 8 new unit tests in tests/editor/scene-visibility.test.ts.
- 4 counter-probes, each fails its test: load ignores the saved value,
  Play skips the saved model, scene view ignores collider shape, scene
  view ignores tilemap shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:32:58 +02:00
adminandClaude Opus 5.5 66868de10a Show destructive menu entries in red text, not on a red fill
game-maker / gate (push) Successful in 3m13s
game-maker / suite (push) Successful in 9m44s
A red filled row in a context menu shouted louder than the action
deserves. The entry now looks like every other entry and only its
text is red, so the menu stays calm and the warning is still there.

- Menu: danger entries are ghost buttons with the new text style
  danger. On hover they get the same grey fill as all other entries.
- Theme: new text style danger in the normal font size. The existing
  warning style is small and did not fit a menu row.
- Button: two getters expose the drawn text colour and fill, so a
  test can measure the look instead of reading the danger flag.
- Test hook __gm.menuLooks() lists both per menu entry.
- Confirm dialogs keep their filled red Delete button. That is a
  separate widget and was not part of the request.

Tests:
- ./check.sh: 172 files, 2498 tests, CHECK EXIT 0
- e2e 25: ALL PASSED, 31 checks. New check in 25g opens the menu
  with a real right click: Delete row has text #e0553f and no fill,
  Insert frames keeps the normal text colour.
- Counter-check: with the old styling the check fails with text
  #ffffff on fill #e0553f. Original restored per cmp and rebuilt.
- Not checked: other menus (graph, assets) have no own check. They
  run through the same Menu code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:53:00 +02:00
adminandClaude Opus 5.5 89b95aa8ac Move "Delete row" from the inspector into the timeline menu
The context menu of the timeline is where the other row and key
actions live. A separate button in the inspector was easy to miss and
sat apart from them.

- The timeline menu gets "Delete row" as a danger entry after a
  separator. It deletes the row under the pointer, whether the click
  hits the row header or a cell. Below the last row or on the ruler
  it is disabled.
- A right-click on a row header now also selects that row, like a
  right-click on a key selects the key. A right-click in an empty
  cell keeps the selection so chosen keys stay available for Copy.
- The "Delete row" button in the "Timeline row" inspector is gone.
  "Delete keys" for a multi-key selection stays.

Tests:
- ./check.sh: 172 files, 2498 tests, CHECK EXIT 0
- e2e 25: ALL PASSED, 30 checks. New check in 25g deletes a row via
  a real right-click and restores it with all three keys via Undo.
- No counter-probe. No check covers the right-click into a cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:43:36 +02:00
adminandClaude Opus 5.5 0c5ac32b59 Let structs and lists nest to any depth (#239)
game-maker / gate (push) Successful in 3m52s
game-maker / suite (push) Successful in 10m24s
Structs and lists were capped at two fixed levels (list of structs,
struct with a list field). Anything deeper was dropped silently on
load, in node settings and when written to a variable. The cap came
from an early design choice, not from a technical need, so it is
lifted completely instead of adding a third fixed level.

- One kind list NESTED_KINDS replaces ListItemKind and
  StructFieldKind. The flat special cases in listItems.ts are gone,
  every value goes through one recursive path.
- A struct inside a struct is declared, like a top-level struct.
  Values coming in over an edge are brought into that shape. Struct
  items in lists still take their fields from the value.
- New valuePath.ts addresses any nested spot by keys and indices.
  ListSlot.field is now ListSlot.path.
- Field commands (add, rename, remove) take the path to the struct.
  They change declaration, own value and all overrides in one undo
  step, as before on the top level.
- Inspector: "+ Field" offers Struct and List on every level. A
  nested struct is a header row with its own "+ Field", its fields
  are indented one step further.
- List tab: a list item shows "n items" and opens one level deeper.
  The title shows the path, e.g. "Listy · nums · [3]".
- Pack in Pack now writes a struct into a struct field instead of
  text. Objects are kept at any depth and stripped at every exit.
- A list edit in a struct field without an existing object override
  now starts from the shown value instead of an empty struct.
- Old projects load unchanged. Save, load and save again gives the
  same result.

Tests:
- ./check.sh: 172 files, 2498 tests, CHECK EXIT 0
- e2e 21 + 22: ALL PASSED, 90 checks (new 21d-iii, 22f-ii,
  21d and 22f updated)
- 20 new unit tests in nested-values.test.ts and
  nested-list-slot.test.ts, three tests that pinned the old cap
  turned around
- 6 counter-probes, each fails 1 to 8 tests, originals restored
  and checked with cmp
- Not covered: a nested struct inside a struct item in the list
  tab has no e2e click

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:33:20 +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 923baf2557 Let Spawn take an asset over an edge and find a child by name (#237)
An asset could only be picked as a setting on the Spawn node. It could
not be wired in, held in a variable or put into a list. Finding an
object below the own one needed a scene-wide search by name.

- "Asset" is now a kind like Animation or Sound: port, variable, list
  item and a new "Asset" literal node, with its own cube icon.
- Spawn and Is asset have an `asset` input. It uses the key of the old
  setting, so a saved asset reads as the value of the input and older
  projects keep spawning without a migration.
- New node "Child by name" searches below an object level by level.
  A child wins over a grandchild of the same name. Nothing found or no
  object gives null.

Tests:
- ./check.sh: 168 files, 2453 tests, CHECK EXIT 0.
- 9 new unit tests in tests/core/asset-kind.test.ts, catalog cases for
  Spawn, Is asset, Child by name and the literal.
- e2e 6z-ii (2 checks): node rows and a real drag of the literal onto
  `asset`. e2e 6: 119 checks passed. Spawn checks in 3 and 9 passed.
- Counter-checks: depth-first search fails 1, Spawn reading only the
  old setting fails 3, runtime ignoring the parent fails 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:36:49 +02:00
adminandClaude Opus 5.5 1b3cb625a3 Let a text field select from right to left with the mouse (#236)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m36s
Dragging from right to left in a canvas text field only moved the
cursor. The hidden input behind the field got every selection sorted
and without a direction. Its selectionchange echo then handed back the
sorted range, and the anchor jumped to the mouse on the first move.

- KeyboardBridge sets the selection with 'backward' when the focus
  lies before the anchor, and reads selectionDirection when the input
  reports a change.
- TextField passes anchor and focus through unchanged. Its own
  sorting is gone.
- Shift+Left runs through the same path. The cursor should now sit at
  the moving end there too. This is not tested on its own.

Tests:
- ./check.sh: 167 files, 2435 tests, CHECK EXIT 0
- e2e 2: ALL PASSED, 25 checks. New check 2d-ii drags right to left
  in the name field and reads start 0, end 13, direction backward.
- Counter-check without the direction: 2d-ii fails with end 2, the
  reported symptom.
- e2e check total in README goes to 1401 (computed, not run in full).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:30:33 +02:00
adminandClaude Opus 5.5 085e6c8cce Keep edited list values when an item is deleted (#235)
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 8m55s
The list tab read the list once when it built its rows. Editing a
value does not rebuild the rows, so the delete button later filtered
that old copy and wrote it back. Every edit since the last rebuild was
lost. Adding an item only triggered it, because adding rebuilds the
rows and that state became the old copy.

- ListAccess.value in src/editor/list/slot.ts is now read on every
  access instead of once when the access is built.
- The kind switch of a row and the struct field buttons (field type,
  rename, remove field, + Field) read the item when clicked, not
  when the row was built. They had the same stale copy.
- New test hook __gm.list.removeAt(i) clicks the delete button of one
  row, because the tab shows many buttons with the same label.

Not covered by a test of their own: the struct field buttons and the
kind switch after an edit. The cause is the same, the fix is in code.

Tests:
- ./check.sh: 2435 tests, CHECK EXIT 0.
- Unit test in tests/editor/list-slot.test.ts keeps one access, edits
  a value, adds and edits an item, deletes another. All edits stay.
  It fails without the fix.
- e2e 21b-ii with real clicks: set row 2 to 66, delete row 0, expect
  ["11","66"], two undos take both back. e2e 21: 52 checks passed.
- Counter probe: with the old snapshot the check fails with
  ["11","44"], the reported bug.
- Check total in README is now 1400 (+2), counted, not measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:45:23 +02:00
adminandClaude Opus 5.5 8b44f97c35 Let a child sort with its parent so its Z stays local (#232)
game-maker / gate (push) Successful in 3m7s
game-maker / suite (push) Failing after 8m51s
An animated or set Z of -1 on a weapon put it behind everything on Z 0,
not just behind the player holding it. A child can now opt in to being
drawn at its parent's place, so its own Z only orders it against the
parent and its siblings.

- New property "Sort with parent" on sprites, texts and particle
  emitters, shown in the inspector right under Z. It is a normal
  property, set on the asset and overridable per object.
- With it on, the rest of the scene sees only the parent's Z and, where
  the layer sorts by Y, the parent's feet. Below 0 the child is drawn
  behind the parent, from 0 on in front.
- It works across levels: a grandchild with the switch hangs on the top
  object of the chain.
- It has no effect under a parent that is not drawn itself (container,
  behavior, camera, collider) or under a tilemap.
- New assets start with it on, saved ones read as off, so old projects
  draw unchanged. For that a schema row now has "initial" next to
  "default". The gold "changed" mark and reset follow "initial", so a
  fresh sprite is not marked as changed. No load migration is needed.
- parentGroups() in drawOrder.ts gives a slot only to the top of a
  chain and inserts the children there. Editor, play mode and picking
  all use it, so they agree.

Tests:
- 10 new unit tests in tests/viewport/sort-with-parent.test.ts.
- 8 existing tests list the sprite rows or defaults and got the row.
- New e2e check 5b-ii clicks the switch and checks order and undo.
- Counter-checks, all originals restored by cmp: switch ignored fails
  7, children not sorted by Z fails 6, no chain fails 1, "initial"
  ignored fails 9, row missing in the inspector fails e2e 5.
- ./check.sh: 166 files, 2426 tests, CHECK EXIT 0.
- e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:08:09 +02:00
adminandClaude Opus 5.5 fae827c087 Let an animation key Z so an object can pass behind another
The timeline offered X and Y as position rows but never Z. The list
of position rows was simply written without it when animations came
in with #97. A swinging weapon could therefore not go behind its
character.

- Z is the third position row, after X and Y, on every placed root
  or object. A scene and a behavior still offer no position.
- Play and preview already read and write any position row by name,
  so the one entry in POSITION_KEYS is all the code change.
- The draw order is sorted on every render, and play writes z back
  into the scene each frame. A keyed Z therefore takes effect at
  once. A blended 4.5 sits above 4 and below 5.

Tests:
- ./check.sh: 165 files, 2416 tests, CHECK EXIT 0.
- 3 new unit tests in tests/core/animation.test.ts: Z is offered
  after X and Y and not on a behavior or scene, a running game moves
  the object from behind to in front of another one, and play,
  scene write-back and preview agree on z.
- e2e 25: ALL PASSED, 29 checks. The new check reads X, Y, Z as the
  first rows of the row dialog. Check sum 1396 -> 1397.
- Counter-check: without z in the list all 3 new unit tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:08:00 +02:00
adminandClaude Opus 5.5 147f8ffbea Let animation keys ease with Sine to Bounce in, out or in-out
A key could only ease quadratically, which makes quick moves like a
sword swing feel soft. Animators know the Penner curves from CSS,
GSAP and Unity, so a key now offers all of them.

- Ten curves (Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back,
  Elastic, Bounce), each in, out and in-out, next to Linear and Step.
- The inspector shows two fields instead of one long list: Easing,
  and Direction only when a curve is picked. Switching the curve
  keeps the direction. Coming from Linear or Step starts at in-out.
- Old projects keep working. ease-in/out/in-out now read as Quad and
  are still saved under that name. New curves save as cubic-out,
  back-in-out and so on.
- Back and Elastic overshoot on purpose. A number property with min
  or max is clamped in play and preview, so a Back in from scale 0
  does not flip the sprite. trackWriteAt now takes the property
  definition, and animatedKind* is renamed to animatedDef*.
- The timeline marker still shows only the direction.
- In-out is built for every curve from its in-curve and its mirror.
  easings.net uses own constants for Back and Elastic in-out, so our
  Back in-out winds up about 5 % instead of 10 %.

Tests:
- ./check.sh: 165 files, 2413 tests, CHECK EXIT 0.
- 12 new unit tests in tests/core/easing-curves.test.ts: round trip
  of all 30 values, start/end/midpoint of every curve, reference
  values, only Back and Elastic leave 0..1, clamping in play and
  preview, unknown values are rejected.
- e2e 25: ALL PASSED, 28 checks. New 25b-ii (3 checks) clicks both
  fields and undoes the four choices. Check sum 1393 -> 1396.
- Counter-checks, each fails its test: no clamping (2), Quad saved
  as quad-* (2), mirrored in-out wrong (2), curve lookup with `in`
  instead of Object.hasOwn (1), no Direction field (e2e 25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:02:23 +02:00
adminandClaude Opus 5.5 94749b4f3e Let a project keep named snapshots and restore them (#231)
game-maker / gate (push) Successful in 3m6s
game-maker / suite (push) Successful in 9m38s
Autosave only keeps the latest revision. Before a risky change there
was no way to put a known good state aside and go back to it later.
The main menu now has "Snapshots..." to create, restore and delete
named copies of the open project.

- Server: SnapshotStore.php keeps each snapshot as a copy of the
  project JSON under data/snapshots/<project>/ (git-ignored). Four
  routes under projects/{id}/snapshots use the same owner check as the
  project, so an admin also sees foreign snapshots.
- Create writes the autosave first. With unsaved edits (play with open
  edits, failed save, conflict) no snapshot is made and a message says
  why. The rule is the pure function in src/editor/snapshots.ts.
- An empty name falls back to the local date and time.
- Restore asks first, then keeps the current state as "Before
  restoring <name>". It writes the snapshot back as a new revision and
  reloads the project, so the undo list is empty. A restore can itself
  be undone by restoring that snapshot.
- Deleting a project deletes its snapshots too. Images, sounds, fonts
  and pixel art are content-addressed and never deleted, so a snapshot
  can always reach them.
- Client: SnapshotStore and HttpSnapshotStore in src/core/store, the
  SnapshotsDialog, and the flows in ProjectSession. A stale revision
  (409) becomes StaleRevisionError.
- CI runs the new PHP test in the gate job next to accounts.

Not covered by e2e: the lock during play and the conflict case. Both
are unit tests only.

Tests:
- check.sh: 164 files, 2401 tests, CHECK EXIT 0.
- tests/php/snapshots.test.php: 14 checks, all passed.
- tests/editor/snapshots.test.ts: 5 tests for the time format, the
  blocker rule, the routes and 409.
- New e2e module 50 on a throwaway project: create, rename, restore,
  undo the restore, delete, then delete the project and get 404 on
  its snapshots. 46b expects the new menu entry. e2e 46,50: 20 checks
  passed. The check sum goes from 1385 to 1393 (computed).
- Counter-checks, originals restored and compared with cmp: blocker
  always allowed fails 1 unit test. No sorting fails 2 PHP checks.
  Restore without keeping the old state fails 50c and aborts 50d.
- The first sort test with 2 snapshots passed by chance half the time
  because of random ids. It now uses 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:21:52 +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.5 4027afc228 Let Fit fill the tile tab instead of stopping at 100 percent
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Failing after 8m34s
Fit in the tile tab left the board small in the middle of the tab.
It used frameBoxes, which never zooms past 1 so that two graph nodes
do not blow up to fill the screen. The tile board is measured in image
pixels, though, and with 16 px tiles it is only about 345 x 186 px.

- frameBoxes takes a new cap parameter, default 1. The graph and the
  node board keep their behaviour.
- The tile tab passes its zoom limit of 6, so Fit fills the tab up
  to 600 percent.
- TileCanvas remembers whether the view is still the one Fit set.
  While it is, a change in the size of the tab or of the board fits
  again. After a pan or zoom by hand the view stays until Fit.

Found on the way: switching the image to another tile size (48 to
16 px) kept the old zoom, and the board shrank to a third. The refit
above covers that too.

Tests:
- ./check.sh: 161 files, 2370 tests, CHECK EXIT 0 (was 2369).
- New unit test in graph-framing: with a raised cap Fit fills the
  view up to the limit. The graph test for "not past 100 %" holds.
- e2e 39: ALL PASSED, 31 checks (was 29). Fit fills the tab at 48 px
  (zoom 0.41) and fits again after the switch to 16 px (zoom 1.24).
- Counter probes: a cap of 1 and no refit on a new board size each
  fail exactly the 16 px check (zoom 1 and zoom 0.41).
- e2e sum in the README is 1384, computed, not yet seen in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:35:50 +02:00
adminandClaude Opus 5.5 50bddf68d4 Keep scene view names readable on plates and in rows of their own
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 8m37s
Symbol names in the scene view were plain grey text. On a background
of a similar colour they could not be read, and names of symbols close
together were drawn over each other.

- Each name stands on a rounded, nearly black plate at 70 % opacity,
  3 px padding left and right, 1 px top and bottom.
- The text is light (viewportLabel) instead of muted grey.
- The plate of a selected object gets an accent border.
- New rule stackLabels in src/viewport/labelRows.ts. A name that
  overlaps another name or an icon moves one row down, then one up,
  then two down and so on, until a row is free.
- Names are placed top to bottom, so the upper one keeps its row.
  A name only moves vertically and stays on the side of its icon.
- Names outside the view still claim their row. Otherwise the visible
  names would jump while panning.
- New hook __gm.sceneLabels() for the e2e check.

The frame names in the texture editor already had a plate, so they
are unchanged.

Not covered by tests: the accent border on selection. Readability on a
light background was only judged from a screenshot on a dark one.

Tests:
- 5 unit tests in tests/viewport/label-rows.test.ts: free names stay,
  the lower of two colliding names moves, the stacking order, names
  avoid icons, 30 crowded names never overlap.
- e2e 5c-ii: 8 symbols on one spot give 8 plates, no overlap with each
  other or with icons, each name on the side of its icon.
- Counter-checks: without the overlap test in the rule 4 of 5 unit
  tests fail. With the scene view ignoring the rows exactly 5c-ii
  fails.
- ./check.sh: 161 files, 2369 tests, CHECK EXIT 0.
- e2e 1-5: ALL PASSED, 140 checks. The e2e sum is now 1382 (computed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:05:39 +02:00
adminandClaude Opus 5.5 af6ac12ab5 Show sharp tile preview labels and paths with tunnels and crossings
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 9m20s
The mask labels under the tile preview sat inside the zoomed board.
Pixi rendered them at 10 px and stretched them with the zoom, and
each label was shrunk to the width of its tile on its own. They came
out blurry and in different sizes. The example map also only showed
a ring, so narrow paths and crossings were never previewed.

- The labels live in their own layer in screen space and are
  re-rendered at the fitting font size on every zoom.
- All labels share one size, at most the 10 px of the theme. Below
  6 px they are hidden.
- A label has two lines: the sides on top, the corners dimmed below.
- The example map keeps the ring cell for cell and adds a network of
  1 tile wide paths below it. It has straight 1xN and Nx1 pieces,
  curves, two T junctions, a crossing and a dead end.
- The map stands next to the grid in a wide panel and below it in a
  tall one. Fit picks the layout with the larger tiles, and only Fit
  changes it, so zooming does not make the board jump.
- The new hook __gm.tile.captions() reports size, scale and lines.

Found on the way: the taller map made Fit zoom out further, and 3
pixel checks on the 16 px terrain tiles failed. The side by side
layout fixed that.

Tests:
- ./check.sh: 160 files, 2364 tests, CHECK EXIT 0 (was 2363).
- e2e 39: ALL PASSED, 29 checks (was 28).
- New unit test: the example map has straights, a curve, a crossing
  with four notched corners, a T junction and a dead end.
- Counter check: with the labels back in the zoomed board the new
  caption check fails (scales [0.648, 1]).
- The e2e sum in the README is now 1381. It is calculated only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 06:18:41 +02:00
adminandClaude Opus 5.5 89048413f1 Let a texture frame set where a sprite stands for Y sorting (#228)
A sprite sorted by Y stood on the bottom of its box. With a shadow or
empty rows under the feet that is too low, so a figure slipped in
front of a tree it should stand behind. The line belongs to the frame,
not the texture, because one texture can hold several sprite sheets.

- New field `sortLine` on a frame: whole image pixels up from the
  frame's bottom edge, 0 to the frame height. 0 is stored as nothing,
  so old projects stay the same. Shrinking a frame keeps the line
  inside it.
- `feetOf` in drawOrder.ts lifts a sprite by the same share of its
  box. Scene view, game view, play and picking all use it. In an
  animation the line of the frame shown right now counts.
- The texture tab draws the selected frame's line dashed in yellow
  with a knob left of the frame. At 0 the line shares the bottom edge
  with the nine-slice border and a resize handle, so the knob is
  tested first. A line inside the frame can be dragged directly,
  tested after the nine-slice lines.
- The inspector has a new "Sorting" section with a "Sort line"
  field. Dragging or typing is one undo step each.

Limit: frames of one walk cycle need the same value, set by hand.
Otherwise the figure changes its place in the order while walking.

Tests:
- ./check.sh: 160 files, 2356 tests, CHECK EXIT 0 (before 2350).
- 6 new unit tests in tests/viewport/sort-by-y.test.ts.
- e2e 37 and 39-43: ALL PASSED, 162 checks. New check 37b-ii drags
  the knob up 6 px and undoes it, plus the inspector row.
- Counter checks: if feetOf ignores the line, exactly the 3 order
  tests fail. If the knob cannot be grabbed, 37b-ii fails.
- The e2e sum in the README is 1380, counted, not yet seen in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:34:37 +02:00