Commit Graph
100 Commits
Author SHA1 Message Date
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 b90b62886b Put one item into a list, a struct staying whole, with Add item (#250)
A list could only grow through Join lists, and that takes its b apart:
a struct becomes one item per field and a list is joined. A list of
structs such as loot could not grow at all.

- Add item returns a new list with value as exactly one more item. A
  struct or a list goes in whole. The list that came in is left alone,
  like with Set item.
- index -1 or unset appends. Otherwise the item goes in before that
  index. An index outside the list appends, so the value is not lost.
- The rule that an item takes the kind of its slot moved out of Set item
  into fitToSlot. Both nodes use it, Set item behaves as before.
- No e2e check and no browser look. The node is covered by unit tests.

Tests:
- ./check.sh: 180 files, 2634 tests, CHECK EXIT 0
- 7 catalog cases, 4 cases in list-of-structs.test.ts, and index in the
  whole-number inputs of texture-kind.test.ts
- Counter-check: taking the value apart like Join lists fails 3 tests
- This commit alone: tsc and the three touched unit files pass
  (337 tests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:07:04 +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 3a3401f8ac Let the sort knob win over the corner handle it sits on (#228)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 9m57s
The CI run for 30de4d3 failed on e2e 37b-ii: dragging the sort knob of
a nine-slice frame resized the frame instead (h 18, lifted null). The
knob's middle sits exactly 8 px left of the frame, which is also the
edge of the corner handle's 8 px tolerance. Until now only the rounding
of the pointer decided which of the two won. The slimmer header bars of
#245 moved that rounding to the corner handle's side. The bug itself
has been there since #228.

- sortLineAt bounds the knob where it is drawn, from a gap to the reach
  left of the frame, instead of by the handle tolerance.
- TextureCanvas passes that gap in image pixels, scaled by the zoom.
- The corner handle stays reachable at the corner itself and to the
  right of and below it.

Tests:
- ./check.sh: 178 files, 2576 tests, CHECK EXIT 0
- GM_STEPS=1,37: ALL PASSED, 26 checks (37b-ii: lifted 6, h 24)
- New unit test: the knob's middle belongs to the knob even inside
  the corner handle's reach. It fails with the old bound.
- Full e2e run not repeated. 37b-ii was the only failure in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:36:40 +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 a90ef4c22f Draw the game view letterbox in the editor background colour
The bars around the camera frame in the Game tab were pure black.
They now take Theme.colors.background, so the tab blends in with the
rest of the editor instead of framing the game in a black box.

- GameViewport drops LETTERBOX_COLOR and uses the theme colour for
  both the letterbox and the model frame.
- e2e 3f checked "the crate is drawn, not letterbox" against
  #000000. With the new colour that would pass whatever is drawn,
  so it now compares against #1b1d22.

Tests:
- ./check.sh: 178 files, 2575 tests, CHECK EXIT 0
- GM_STEPS=1-3,46: ALL PASSED, 94 checks (3f reads #e0553f)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:57:28 +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 3abb6894a7 Let a container sort its content with its parent (#243)
A container is not drawn, so it had no place in the draw order. The
chain from its content to the parent of the container broke there, and
a container under a character could not be drawn together with it.

- A container gets the switch Sort with parent under Z. It defaults to
  off, also for new containers, so projects draw as they did.
- With the switch on, the whole content is drawn in the slot of the
  container's parent. The content's own switch does not count there.
- Containers in a row pass the content on as long as each has the
  switch on. On the top level or under a tilemap it has no effect.
- drawSlots follows the new sortsWith field of a draw item. Scene view,
  game and picking all use drawSlots, so all three agree.

Found on the way: a scene object hung directly under a container
spells the container out, and its template content is dropped. That is
the existing spelledOut rule, the tests now keep both cases apart.

Tests:
- ./check.sh: 178 files, 2573 tests, CHECK EXIT 0
- 7 new unit tests in sort-with-parent.test.ts: the switch, template
  content, override per container, a chain, top level and tilemap,
  game and picking
- e2e 5b-iii: a real click on the switch in the inspector, with undo
- 4 counter-probes over #242 and #243 bite (2 unit, 2 browser)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:12:40 +02:00
adminandClaude Opus 5.5 074bda2247 Give texts, tilemaps, particles and containers the turn knob (#242)
The knob on its stem turned and scaled only sprites and colliders. Every
other type with Scale and Rotation had to be typed into the inspector.

- Text, tilemap and particle source get the knob above their box. Edge
  and corner grips stay with sprite and collider, the only types with a
  width and a height of their own.
- A container gets a thin frame round all of its content, turned and
  scaled with it, and the knob sits above that frame. It turns and
  scales round the container's origin, so the content swings with it.
  A container with no content gets the knob above its symbol.
- contentFrame in sizeGizmo.ts is the pure rule for that frame.
  sizeGizmoEditor.ts wires it in.
- One pull is one undo step, as for sprites.

Not clicked in the browser: scaling (as opposed to turning) of a
container by the knob. Only the shared code path covers it.

Tests:
- ./check.sh: 178 files, 2573 tests, CHECK EXIT 0
- 2 new unit tests for contentFrame
- e2e 29k, 4 checks: knob on text, tilemap and particles, frame round
  the container's content, a real pull turns the container by 20.9
  degrees round its origin with the content on the computed pixel,
  one undo takes it back
- 4 counter-probes over #242 and #243 bite (2 unit, 2 browser)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:12:40 +02:00
adminandClaude Opus 5.5 98b9b300d9 Zoom the scene view further in, up to 64 device pixels
The scene view stopped at 24 device pixels per world pixel, which is
too little to work on single texels of small pixel art.

- FREE_ZOOM_STEPS gains the steps 32, 48 and 64. All of them are whole
  numbers, so every texel keeps the same width on screen.
- The tile editor takes the same steps, so its fit can now land above
  24 on a small board.
- The unit test and the step list in e2e 39 follow the new top step.

Tests:
- ./check.sh: 178 files, 2573 tests, CHECK EXIT 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:12:01 +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 483c34bb5c Let Destroy keep the children and hand them back as a list
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Successful in 9m41s
Destroying a container used to take everything below it along. Now
Destroy can remove just the object itself and pass its direct children
on, so a behavior can keep working with what was inside.

- New input keep children (bool, default off). When on, only the
  object goes. Its direct children move up to the object's parent, or
  to the top level if there is none. Grandchildren stay where they are.
- New output children (object list) with the direct children that were
  kept. It is empty when keep children is off.
- The children keep their place on screen. carriedTransform folds the
  destroyed object's position, rotation and scale into their own, the
  same as reparenting in the editor.
- Behaviors and the rigidbody of the object are always destroyed with
  it and are not in the list. Moved up they would steer or simulate
  the parent.
- World remembers which children it reparented, and syncToModel writes
  their new parent into the scene. A reload of the model keeps it.
- Limit, read from the code only: in the editor run the contents of a
  placed container are not baked. Moved children there last for the
  run only.

Tests:
- ./check.sh: 170 files, 2478 tests, CHECK EXIT 0
- e2e 34 (the module with a Destroy node): ALL PASSED, 21 checks
- world-sync: world matrix of child and grandchild unchanged under a
  rotated and scaled parent, parent survives syncFromModel, top level
  case, already destroyed object gives []
- destroy-keep-children: real graph Destroy(Owner) -> Log(children)
  logs Lid, Label without the grandchild Nail
- 3 counter-probes, all caught: no world transform (1 test fails), no
  parent in the scene (1), behaviors and rigidbody moved up (2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:45:27 +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 74b9c60661 Stop marking older sprites as changed after loading (#232)
game-maker / gate (push) Successful in 3m9s
game-maker / suite (push) Successful in 9m36s
Loading filled every missing asset property with its `default`. For
Sort with parent that is `false`, while the gold mark and Reset compare
against `initial`, which is `true`. So every sprite, text and particle
source saved before #232 counted as changed once loaded, and each save
wrote the `false` into the project file.

- normalizeBag leaves a missing key missing when it has its own
  `initial`. It still reads as `false`, so older projects draw as
  before, but they no longer count as changed.
- e2e module 4 failed in CI on three checks since 8b44f97. Its Reset
  click hit the extra Reset of Sort with parent instead of Height.
- The #232 test deleted the key on the object instead of going through
  the load path. It now checks that loading keeps the key missing and
  marks nothing. serialize.test.ts no longer expects the filled value.

Tests:
- ./check.sh: 167 files, 2435 tests, CHECK EXIT 0
- e2e 1-5 chained: ALL PASSED, 141 checks (before the fix the same
  three module 4 checks failed locally, as in CI)
- Counter-check: without the fix the extended test fails

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:35:43 +02:00
adminandClaude Opus 5.5 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 17a6fd0339 Move the shader test sprite off a name plate so CI passes again
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m24s
Every suite run since 50bddf6 failed the same 14 pixel checks in step
28. The test sprite sat right of the camera, where the Crate pair
container from an earlier step has its symbol. Since 50bddf6 names
stand on dark plates, and that plate reaches into the sprite's upper
left corner. The thin grey name before let the sampled pixels through.
The plate covers them.

- The sprite now sits up left of the camera (-190, -105 instead of
  +180, +100), a spot no earlier step fills.
- The guard that samples a 9x9 grid over the sprite stays. It named
  the plate as -0.4,-0.3:#1b2f57, the sprite blue under a 70 percent
  near-black plate.
- The app is right: symbols and their names draw above objects.

Tests:
- Reproduced in the CI image with an empty data folder, GM_STEPS=1-28:
  the same 14 FAILs with the same colours as CI tasks 303 to 313.
- Full CI job in the CI image after the move: ALL PASSED, 1385
  checks, smokes 24 / 19 / 21, E2E EXIT 0, SMOKES EXIT 0.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:41:17 +02:00
adminandClaude Opus 5.5 03e7a78c3d Keep a tilemap with outlined solids at 60 fps in scene and play
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Failing after 8m33s
A project with three tilemap layers, two of them with collision on
water and hill tiles, ran at 33 to 52 fps. A CPU profile showed that
almost all of the time went into the collision geometry of the map.
It was rebuilt from scratch every frame although nothing had changed.
The animated water makes the views redraw every frame, so the cost
was never skipped.

- New module tilemapMemo keeps what is derived from the cells: the
  tiles of a layer, the blocking cells, the rectangles and polygons.
  It is reused as long as the content stays the same.
- A brush stroke paints into the existing cells object. The memo
  therefore compares the content cell by cell, not the identity.
  The comparison walks two arrays in insertion order.
- Clipped outlines are kept per outline list. The model always
  replaces that list instead of changing it.
- PhysicsSystem no longer rebuilds the shapes of a tilemap each tick
  while its cells, placement and settings are unchanged. Before, it
  also built a JSON key from all world shapes every tick.
- The overlaid layers after Set tile in a run stay stable. Otherwise
  the memo would miss on every call after the first edit.

The #184 work only helped Move and raycasts, not this path.

Measured on stein2 (i5-7500T, HD 630), script time per frame:

- Scene, nothing selected: 17.7 ms, 52 fps -> 5.8 ms, 60 fps
- Tilemap selected: 26.2 ms, 38 fps -> 8.6 ms, 60 fps
- Play: 29.9 ms, 33 fps -> 8.7 ms, 60 fps

About 3 ms per frame remain. The content comparison costs roughly
60 us per 700-cell layer and runs about 40 times a frame. The mesh
geometries in TilemapField are still rebuilt every frame.

Tests:
- ./check.sh: 162 files, 2377 tests, CHECK EXIT 0 (before 2370)
- e2e 1-5 and 39-43: ALL PASSED, 289 checks
- 7 new unit tests in tests/core/tilemap-memo.test.ts
- 5 counter checks (content ignored, no memo, outline by kind only,
  physics always rebuilds, layer settings ignored) each fail their
  test. All originals are back, checked with cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:23:18 +02:00
adminandClaude Opus 5.5 021a6dcb56 Zoom the tile tab in the same steps as the scene view
game-maker / gate (push) Successful in 2m58s
game-maker / suite (push) Failing after 8m34s
The tile tab zoomed freely, so the pixels of a tile could end up
unevenly wide at odd factors. Scene view, container tab and animation
preview already snap to FREE_ZOOM_STEPS. The tile tab now does the
same, so every pixel editor zooms alike.

- The wheel steps through FREE_ZOOM_STEPS, counted in device pixels.
  The board position is rounded to whole device pixels.
- New wheelSteps() in src/viewport/view.ts turns wheel events into
  steps: one notch is one step, a trackpad gathers 50 px per step.
  FreeViewport uses it too, so the rule exists only once.
- Fit takes the largest step at which the whole board fits. Refitting
  on a new tab or board size stays as before.
- panAndZoom takes an optional zoom rule instead of limits. Graph and
  node board keep zooming freely.
- A saved view from before the steps snaps to the nearest step.
- The cap parameter of frameBoxes from 4027afc has no user anymore.
  It is removed together with its unit test.

Fit rounds down, and below 100 percent the steps are far apart. In the
small e2e window the 48 px board lands at 25 instead of 41 percent,
the 16 px board at 100 instead of 124 percent.

Tests:
- check.sh: 161 files, 2370 tests, exit 0 (-1 cap test,
  +1 wheelSteps test)
- e2e 1-5 and 39: all passed, 169 checks. Three checks in 39 now
  expect steps: Fit on a step with the next one too large, one notch
  goes 0.25 to 0.5, 16 px tiles refit to at least 100 percent.
- Counter check: a free Fit factor fails exactly those three checks
  (zoom 0.413 and 1.2396).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:30:41 +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 5dbac3a8df Add a Select node that picks one of two values by a condition
Choosing between two values meant a Branch with the whole action
twice behind it, for example two Play animation nodes that differ
only in the animation. Every later change had to be made in both.
Select picks the value, so the action stays single.

- Select is a data node with the inputs condition, false and true
  and the output value. It passes on "true" if the condition holds,
  else "false". An unconnected condition counts as false, and an
  empty picked input gives null.
- The ports are named false and true, not a and b, so nobody has to
  guess which input is which. The order false, true follows Unreal's
  Select and Blender's Switch.
- false, true and value share one type through `sharedType`, like
  First not empty. With two Animation literals plugged in, value
  takes their type and fits Play animation directly.
- Both inputs are always evaluated. Data nodes have no side effects,
  so that does not matter.

Tests:
- ./check.sh: 160 files, 2363 tests, CHECK EXIT 0 (before 2356).
- 5 cases in tests/core/node-catalog.test.ts and 2 in
  tests/core/shared-port-type.test.ts.
- Counter checks: with the inputs swapped all 5 catalog cases fail,
  without sharedType exactly the type test fails.
- No e2e change, the sum stays at 1380.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:39:27 +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
adminandClaude Opus 5.5 50854389bb Let a tilemap layer sort its rows by Y with the objects (#228)
A tilemap layer was one piece in the draw order, so a figure stood
either in front of a whole layer of trees or behind all of it. For a
top-down game the trees in front of the figure's feet have to cover
it and the ones behind must not.

- A new "Sort by Y" box under the layer list sets `sortByY` on the
  painted layer. Every change is its own undo step.
- Such a layer comes as one draw slot per row. On its `z`, the rows
  and all objects on that `z` are sorted by where they stand: the
  bottom of an object's box, the bottom of a row. Ties keep the
  hierarchy order, and other `z` values are untouched.
- A layer drawn as one piece and the map's own node stay behind the
  sorted ones on the same `z`, since neither has a single place to
  stand. Ground belongs on a lower `z` or stays unsorted.
- A new tile property "Sort offset" (whole rows, -16 to 16) lets a
  crown sort with the trunk below it.
- `TilemapField` draws a container per row for such a layer, and
  picking takes the row into account, so a click picks what is drawn
  in front. Scene view, game view and play share the same rule.

Limits: on a rotated map "down" is world Y. A layer sorted by Y costs
about one draw call per row and tile instead of one per tile.

Tests:
- ./check.sh: 160 files, 2350 tests, CHECK EXIT 0 (before 2340).
- 10 new unit tests in tests/viewport/sort-by-y.test.ts.
- e2e 39-43: ALL PASSED, 148 checks. New check 40p-iii ticks the box
  and finds the slot of row 0, undo brings back the whole layer.
- Counter checks: without the Y sort 5 unit tests fail, with the
  offset ignored 5, with no row in picking 1. If TilemapField draws
  no rows, exactly 40p-iii fails in e2e.
- The e2e sum in the README is 1378, counted, not yet seen in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:32:37 +02:00
adminandClaude Opus 5.5 ffb5bbe039 Stop a key getting stuck when Shift changes it while held (#227)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Failing after 8m37s
Input remembered keys under KeyboardEvent.key. Pressing a, adding
Shift and letting go sent "A" on release, which matched nothing, so
"a" stayed down until the next release without Shift.

- New rule keyName in src/core/keys.ts lowercases a single character.
  So a and A are the same key, also under Caps Lock.
- Input remembers per physical key (e.code) under which name it went
  down and releases exactly that name. So 1 released as ! is freed
  too, which lowercasing alone does not cover.
- On key down, up and held, Key is down and Axis input compare with
  the same rule. A node that already stores A reacts to a.
- Recording a key in the inspector stores Shift+A as a.
- Limit: holding Shift before pressing 1 still sends !, and a node on
  1 does not react. Nothing gets stuck by it.

Tests:
- 3 new unit tests in tests/core/behavior-events.test.ts: the flow
  from the issue, 1 released as !, and a node on A hearing a.
- Without lowercasing exactly the issue test and the A test fail.
- Without the code memory exactly the 1/! test fails.
- ./check.sh: 159 files, 2340 tests, CHECK EXIT 0 (before 2337).
- No e2e change, the check sum stays at 1377.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 18:06:16 +02:00
adminandClaude Opus 5.5 c1a6d63edb Cover which tilemap layer and texture frame get a collision fill
game-maker / gate (push) Successful in 3m27s
game-maker / suite (push) Successful in 9m47s
f4cc681 fills tilemap solids only for the selected map and layer, and
texture collision only for the selected frame. Neither the scene view
wiring nor the texture canvas had a test for that.

- __gm.tilemap.solids() now reports the cells that were filled, and
  __gm.texture.drawInfo() counts the filled outlines.
- e2e 40l-iii: a selected tile fills nothing. The map selected with
  the upper layer fills only cell (4,0), with the lower layer only the
  3x3 block. Two selected objects fill nothing again.
- e2e 42: a selected frame fills its one outline. Deselected it fills
  none, while the outline stays visible.
- README check total goes from 1375 to 1377. CI confirmed 1375, the
  new total is counted, not yet measured.

Tests:
- ./check.sh: 159 files, 2337 tests, CHECK EXIT 0
- e2e 39-43: ALL PASSED, 147 checks (before 145)
- Counter checks: a scene view that fills every map fails only the
  tilemap check. A texture canvas that fills every frame fails only
  the texture check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 13:22:45 +02:00
adminandClaude Opus 5.5 8f29b88cc9 Make the actor smoke reach every sub pixel edge position again
Since #225 the game view opens at 100 % instead of fit. The actor walks
150 px/s, so its top edge only lands on .0 or .5 of a pixel. The check
for the frame top edge needs 0.6 to 0.9 and gave up in 6 of 7 CI runs.
The picture itself was fine: the pixels above and at the edge were the
same in every red run.

- While nudging the actor into place the smoke slows time to 0.08 with
  __gm.setTimeScale. The actor then moves 0.2 px per frame and can
  reach any position inside a pixel.
- Time scale goes back to 1 before the edge is measured.

Tests:
- Old smoke on the same seeded project fails locally like CI
  (fraction 0.08).
- New smoke passed three times in a row (fraction 0.80, 21 checks).
- The actor project file was saved first and restored byte exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 13:22:43 +02:00
adminandClaude Opus 5.5 f4cc681df4 Fill texture and tilemap collision only where it is being edited
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 9m24s
The faint fill on every frame outline and on every tilemap made the
scene and the texture editor look green all over. Like the collider
before, the fill now shows only on what is selected, and a bit
stronger, so it marks the thing being worked on.

- Texture collision: all frame outlines stay visible. Only the
  outlines of the selected frame are filled, at alpha 0.3 instead of
  0.15 on every frame.
- Tilemap: the outline still shows where the whole map blocks. The
  fill appears only while the tilemap alone is selected, and only for
  the part the layer chosen in the panel adds. Solid is 0.3 and
  one-way 0.15, before 0.12 and 0.06 on every map.
- New query layerSolids(layerId) in items.ts. It builds the same
  solids for one layer and runs only for the selected map.
- The small landing wedge on one-way edges stays filled. It belongs
  to the edge marker, not to the area.

Not covered by tests: the texture canvas and the scene view wiring
that picks the map and the layer. Neither was looked at in a browser.

Tests:
- ./check.sh: 159 files, 2337 tests, CHECK EXIT 0 (before 2334)
- e2e 1-17,39-43: ALL PASSED, 668 checks
- 3 new unit tests in tests/viewport/tilemap-fill.test.ts
- Counter-check: with layerSolids ignoring the layer, exactly the
  layer test fails. Original restored, checked with cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:23:13 +02:00
adminandClaude Opus 5.5 495d87847d Tint a collider's area only while the collider itself is selected
game-maker / gate (push) Successful in 2m59s
game-maker / suite (push) Failing after 9m56s
A collider filled its area with a faint green in the scene view all the
time. It lay over every sprite a collider sits on and made the art look
washed out. The outline alone shows where a collider is. The area only
matters while one works on that collider.

- drawItem takes a selected flag. Only a selected collider is filled,
  every other one is drawn as its outline.
- The fill is stronger than before, alpha 0.3 instead of 0.12, so the
  selected area is easy to see. The value is COLLIDER_FILL_ALPHA in
  src/viewport/symbols.ts.
- The scene view says what is selected through isSelected. The game
  view keeps the default and never fills.
- Only the collider object itself counts. Selecting its body leaves
  the area unfilled.

Tests:
- 2 new unit tests in tests/viewport/collider-color.test.ts read the
  Pixi instructions: outline only when not selected, fill at 0.3 and
  outline when selected.
- Counter check: with the fill drawn regardless of selection the
  outline-only test fails. The original is back per cmp.
- ./check.sh: 158 files, 2334 tests, CHECK EXIT 0.
- e2e 1-17,28-30: no FAIL. e2e 39-43: ALL PASSED, 145 checks.
  A first run left out 39, which 40 builds on, and failed 8 checks
  in 40 for the missing tile only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:05:29 +02:00
adminandClaude Opus 5.5 4b7e8614a8 Keep play start and game view scale in the project (#226)
Both settings lived in the browser's localStorage, so they did not
travel with the project and differed per machine. They now belong to
the project like the symbol scale. There is no migration, old projects
load with the defaults.

- New module src/core/model/playSettings.ts reads playFrom and
  gameScale on load. Unknown or missing values fall back to the start
  scene and 100%.
- src/editor/settings.ts is gone.
- Play starts in the project settings dialog is an undo step
  (SetPlayFromCommand). It is only recorded on a real change.
- The game view scale is saved with the project but is no undo step,
  so Ctrl+Z never undoes a zoom. A scale picked during play is written
  to the stored project and survives Stop.
- Save states of persistent Variables assets (saves.ts) stay in the
  browser on purpose. They are runtime data of the game, not settings.

Tests:
- 6 new unit tests in tests/editor/play-settings.test.ts: defaults,
  save and load, broken JSON, undo of Play starts, scale without undo
  step, scale picked during play survives Stop.
- e2e 03g checks the value in the project, nothing in localStorage and
  no new undo step. 35f also checks project.playFrom. No new checks,
  the sum stays at 1375.
- Counter checks: no write to the stored project in play, no read on
  load, and an undo step for the scale each fail exactly their test.
- ./check.sh: 158 files, 2332 tests, CHECK EXIT 0
- e2e 1-17,28,30,35,38-40,46: ALL PASSED, 752 checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 11:38:18 +02:00
adminandClaude Opus 5.5 5afb70a92e Let the project set the size of scene view symbols
game-maker / gate (push) Successful in 3m0s
game-maker / suite (push) Failing after 9m27s
Pixel art projects draw at very different sizes, so the fixed 18 px
icons of behaviors, containers and rigidbodies were too big in some
projects and too small in others. The size now lives in the project
settings and travels with the project.

- New field Symbol scale in the project settings dialog, below Play
  starts. It takes 0.1 to 4 with two decimals and defaults to 1.
- The value is stored as project.symbolScale. Older projects load
  with 1, and values out of range are clamped on load.
- Every change is its own undo step through a project command.
- The factor scales the icon size, the spacing of icons that share a
  spot, the label offset and the selection frame of icon-only symbols.
- The footprints in bounds.ts that Fit uses stay as they were. The
  hit area follows the icon, so clicks match what is drawn.
- uiHooks reports the icon size so e2e can measure it.

The e2e sum in the README is 1375. That is 1374 plus one new check,
and 1374 itself has not been confirmed by CI yet.

Tests:
- ./check.sh: 157 files, 2326 tests, CHECK EXIT 0 (before 2320).
- New tests/core/symbol-scale.test.ts with 5 tests (default, save
  and load, clamping, undo, icon frame), one more in
  project-settings.test.ts.
- e2e 46f: dialog opens with 1, 0.5 plus Apply gives a 9 px icon,
  undo gives 18 again. Module 46: ALL PASSED, 13 checks.
- Counter checks: without loading the value the save and load test
  fails. With the icon ignoring the factor exactly 46f fails. Both
  originals restored per cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:55:42 +02:00
adminandClaude Opus 5.5 5399ef17e5 Snap the scene view zoom to pixel exact steps
The wheel zoomed smoothly and fit computed any factor, so the scene
view almost never showed pixel art with even texels. Under Windows
scaling even zoom 1 was off. The zoom now only takes steps that give
every texel the same number of screen pixels.

- The zoom counts device pixels per world pixel. freeView divides by
  the renderer resolution, so zoom 1 is one screen pixel per texel.
- FREE_ZOOM_STEPS: 1/8, 1/4, 1/2, then 1 to 6, 8, 12, 16 and 24.
- The wheel moves one step per notch. A trackpad collects its travel
  and steps every 50 px.
- Fit rounds down to the next step. A saved zoom from before snaps to
  the nearest step on a log scale.
- Pan, drag, grid and resize use CSS pixels per world unit, so they
  still follow the pointer under Windows scaling.
- Always on, no toggle, as decided in the session.

Tests:
- 4 unit tests: zoom 1 at 125%, whole steps, wheel steps and ends, fit
  rounding and snapping of a saved zoom.
- Counter-check: without the ratio in freeView the 125% test fails.
- check.sh: 156 files, 2320 tests, CHECK EXIT 0.
- e2e 1-17,28,30,38-40: 726 checks passed on the steps before the
  final values (up to 8). Not rerun on the final steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:40:54 +02:00
adminandClaude Opus 5.5 22a89ed0f0 Keep game view steps pixel exact under Windows display scaling
At 125% Windows scaling the game view drew 100% as 1.25 screen pixels
per camera pixel, so pixel art showed texels of uneven width even at
the round steps. The steps now count device pixels instead of CSS
pixels.

- gameScaleFactor, cameraFactor and cameraView take the device pixel
  ratio. 100% is one screen pixel per camera pixel, 200% two.
- Auto picks the largest step that fits in device pixels. Fit reports
  its factor in device pixels too, e.g. 156% at 125% scaling.
- Fixed steps start the frame on a whole device pixel.
- The game view reads the ratio from the renderer resolution.

Limits: main.ts caps the resolution at 2, so scaling above 200% is
stretched by the browser again. A changed ratio at runtime only counts
after a reload, as before.

Tests:
- 2 unit tests at 125%: 100% is one device pixel on a whole pixel,
  auto and fit count device pixels.
- Counter-check: without the conversion the 100% test fails.
- check.sh: 156 files, CHECK EXIT 0 on the combined working tree.
- No e2e: the harness runs at devicePixelRatio 1, same math as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:40:43 +02:00
adminandClaude Opus 5.5 2592873d76 Pull nearest tilemap uvs a tenth of a texel in, 0.02 still showed lines
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Successful in 9m30s
At 0.02 texel the lines between tiles of a nearest tilemap were gone at
the zooms tried first, but still showed at others. The user raised the
inset to 0.1 by hand and checked it in the browser.

- A tenth of a texel is still far below half a texel, so the texel
  picked at every pixel centre stays the same.
- The comment names the new value.

Tests:
- tilemap.test.ts follows the value: the inset test expects 0.1/96,
  the sliver test allows up to 0.25 texel from the edge, and the quad
  test checks its uvs to 2 digits instead of 3.
- ./check.sh: 156 files, 2314 tests, CHECK EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:44:26 +02:00
adminandClaude Opus 5.5 5bf1ee7a4e Let Equals match a number against a typed number like 130 (#224)
The float tolerance from the last commit never reached the case it was
built for. The inputs of Equals are of type any, and their inline field
is a text field. A typed 130 therefore arrives as the string "130", and
the tolerance only applied when both sides were real numbers. The node
fell back to comparing "129.99999999999997" with "130" as text.

- A number against a text that parses as a number now uses the same
  1e-9 rule as two numbers.
- An empty text does not count as 0.
- Two texts are still compared as text, so "1.0" and "1" stay
  different.
- Compare was not affected, its inputs are real number fields.

Tests:
- 3 new cases in node-catalog.test.ts: 129.99999999999997 vs "130"
  is equal, 0 vs "" is not, "1.0" vs "1" is not.
- Counter-check: with the old rule (two real numbers only) exactly the
  "130" case fails. Original restored and compared with cmp.
- ./check.sh: 156 files, 2314 tests, CHECK EXIT 0 (before 2311).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:44:25 +02:00
adminandClaude Opus 5.5 9d6a0aebd3 Let Compare and Equals treat float noise as equal numbers
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Failing after 9m47s
A graph that checks "time - late == 6" after On timer (#224) failed,
because the difference comes out as 5.999999999999972. Both values are
sums of measured frame times, and they round along different paths.
The only workaround was a Round node in front of every comparison.

- New helper nearlyEqual in values.ts. Two numbers count as equal when
  |a - b| <= 1e-9 * max(1, |a|, |b|). The tolerance grows with the
  size of the values, so it holds for hours of summed dt.
- Compare uses the rule for all six operators. "<" and ">" only hold
  when the values are not nearly equal, "<=" and ">=" also hold when
  they are. So "==" and ">" are never true at the same time.
- Equals uses the same rule when both inputs are real numbers. Objects
  still compare by identity, everything else as text. The node
  description says so.
- The tolerance is fixed, there is no parameter on the node. Physics
  values come from float32 in Rapier and are not covered on purpose.
- The log display is unchanged and still prints the raw number.

Tests:
- ./check.sh: 156 files, 2311 tests, CHECK EXIT 0 (before 2300).
- 11 new cases in node-catalog.test.ts, including the value from the
  log against 6, growth with magnitude, 6.001 != 6 and 1e-6 > 0.
- Counter-check exact compare: exactly the 8 noise cases fail.
- Counter-check "<"/">" without the exclusion: exactly 2 cases fail.
- No e2e change, the check sum stays at 1374.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:30:45 +02:00
adminandClaude Opus 5.5 6fecb2e841 Tell On timer how late its frame is, so time minus late is exact (#224)
game-maker / gate (push) Successful in 2m55s
game-maker / suite (push) Failing after 9m22s
A timer can only fire on a frame. It fires on the first frame past each
due time, so Time.time at that moment is the due time plus up to one
frame. On a display that is not exactly 60 Hz this delay creeps up by
a millisecond every few seconds and falls back to zero once it reaches
a whole frame. The issue saw 82.009 after 82 ticks. The timer itself
does not drift, it keeps the remainder when it fires.

- On timer gets a number output late. It is what is left of the
  timer's clock after taking one interval, so time - late is exactly
  the due time.
- When one long frame catches up several ticks, each gets its own
  late. The first one was due longest ago and is the latest.

Tests:
- New unit test in behavior-events: late 0.125 after a frame past
  1.0 s, 0 on the dot, 1.5 and 0.5 for two ticks caught up in one
  frame.
- Counter-check: late taken before the interval is subtracted fails
  exactly that test. Original restored with cmp.
- ./check.sh: 156 files, 2300 tests, CHECK EXIT 0 (was 2299).
- No e2e change. The e2e modules only wire the exec of On timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 07:44:53 +02:00
adminandClaude Opus 5.5 aef1eba917 Stop thin lines between tiles of a nearest tilemap at odd zooms
game-maker / gate (push) Successful in 2m55s
game-maker / suite (push) Successful in 9m21s
At zoom levels that do not land quarter edges on whole screen pixels,
a tilemap whose texture uses Nearest showed one-pixel lines in the
wrong colour. The float uv of a pixel centre right at a quad edge
could round onto the first texel of the neighbouring quarter in the
sheet, which is usually a different tile. Terrain sheets showed it
most, because every tile is four quads there.

- Nearest now pulls the uvs 0.02 texel inside each quarter. That is
  far below half a texel, so every picked texel stays the same and
  pixel art keeps its size.
- Linear keeps its half-texel inset.
- The first draft used 0.01. The user raised it to 0.02 after
  checking the result in the browser.

Tests:
- ./check.sh: 156 files, 2299 tests, CHECK EXIT 0 (before 2298).
- New unit test: every nearest uv sits strictly inside its quarter,
  by less than 0.05 texel. Two older tests now expect the inset.
- Counter-check: with no inset for nearest the new test and the
  inset test fail.
- No e2e check. The user checked the result in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 07:26:46 +02:00
adminandClaude Opus 5.5 9ef360ba1b Let the game view show the picture at a chosen scale (#225)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 9m28s
The game view always stretched the camera picture to fit the pane.
Pixel art came out blurred at odd factors, and there was no way to
see the game at its real size. The view now has a header with a
scale drop-down, and 100 % is the default.

- The header of the new GamePane offers Auto, Fit, 25 %, 50 %,
  100 %, 200 % and 400 %. Auto and Fit show the factor they land
  on, e.g. "Auto · 100%", and follow the pane when it resizes.
- 100 % means one screen pixel per pixel of the camera resolution
  (width x height). The camera zoom scales the world on top of
  that, so a zoomed camera still fills the same frame.
- Auto takes the largest fixed step that fits the whole picture
  and falls back to 25 % when nothing fits. Fit keeps the old
  behaviour.
- A picture larger than the pane is cropped around the camera
  centre. Fixed steps start on a whole pixel so pixel art stays
  sharp at 100 %.
- The choice is kept in the browser like "Play starts". Nothing
  is written into the project.

Found on the way: the first draft applied the step to the world
instead of the camera resolution. The camera zoom then had no
effect at 100 %, which the existing check 12c caught (zoom 1.37
measured as scale 1).

The CI smokes were not run locally, because that would reseed
projects the user works in. All three smoke cameras are 640x360
and fit the 670x383 harness view at 100 %.

Tests:
- ./check.sh: 156 files, 2298 tests, CHECK EXIT 0 (before 2290).
- 8 unit tests in tests/viewport/view.test.ts for whole-pixel
  steps, cropping, Auto and Fit with fallback, and zoomed cameras.
- 7 e2e checks in 03g. e2e 1-17,28,30,38-40: ALL PASSED, 727
  checks.
- Counter-checks: no cropping, no rounding, no factor reported to
  the header, and no zoom share each fail their own tests.
- Check total: 1367 (CI at c1a702b) + 7 = 1374, computed only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 07:02:48 +02:00
adminandClaude Opus 5.5 c1a702bc39 Add a terrain autotile kind for 6x4 sheets like TD Plains
game-maker / gate (push) Successful in 3m7s
game-maker / suite (push) Successful in 9m54s
Sheets such as "TD - Tileset - Plains" draw an autotile as a 6 x 4
block: a 3 x 3 nine-slice, a vertical and a horizontal strip and a
single tile on the left, the inner corners on the right. The floor
kind only knew the RPG Maker 2 x 3 layout, so these sheets could not
be used as autotiles at all.

- New TileKind "terrain" with 16 px tiles in a 6 x 4 block. All 47
  neighbour combinations are resolved per quarter, so the renderer,
  the collision and the palette stay untouched.
- Whole tiles are used where the sheet draws them: the 16 side cases,
  the four single notches and the two diagonal pairs. Hand-drawn
  strips and single blocks therefore look as drawn.
- Only the missing cases are composed, for example an edge with a
  notch or two notches side by side. They take the side tile and put
  the notch quarters in.
- A notch also takes the quarter above or below from its notch tile
  when fill would stand there. Tall cliff notches keep their wall.
  On ground and stones fill meets fill, so nothing changes there.
- The label rule for floor and terrain now runs through one notch
  function. Floor still passes the MZ table test.

Not touched: the tile board only refits on open and on Fit. Switching
an open tile to a kind with another tile size leaves it small until
Fit is pressed. That was the case before.

Tests:
- ./check.sh: 156 files, 2290 tests, CHECK EXIT 0 (before 2283).
- e2e 39,40: ALL PASSED, 90 checks (before 86). 39j checks 47 tiles,
  pixels of single, notch, pair and composed tiles, and undo to floor.
- Counter-checks: without whole notch and pair tiles one unit test
  and one e2e check fail. Without the taken quarter one of each
  fails. Originals restored by cmp and rebuilt.
- Check sum: 1363 measured by CI plus 4 is 1367, computed only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 06:14:51 +02:00
adminandClaude Opus 5.5 32cb5b4ba5 Size, turn and scale a selected sprite or collider in the scene (#223)
game-maker / gate (push) Successful in 2m54s
game-maker / suite (push) Successful in 9m14s
A sprite or collider could only be sized, turned and scaled through
the inspector. The scene view now draws a size gizmo on the selection,
so the common edits happen where the object is.

- Edges and corners set Width and Height. The opposite side stays
  where it was, so the position moves by half the change. Sizes land
  on whole units, at least 1.
- A circle collider has edge handles only and stays round. A polygon
  or frame collider keeps its own corner editing and gets no size
  handles.
- One knob on a stem above the top edge turns or scales. The first
  5 px of the drag decide which: out from the centre scales, sideways
  turns. The choice holds until the release, so one drag never does
  both. Turning and scaling share one handle by the user's choice.
- Shift snaps a turn to 15 degrees. The turn adds up the swept angle,
  so it goes on past a half circle instead of jumping back.
- The values are overrides of this placement, like "Fit to sprite".
  A value equal to the asset's own clears the override. One drag is
  one undo step.
- A handle reaches 7 px outwards but only half that into the object.
  Otherwise a press near the edge of a small sprite sizes it instead
  of moving it.
- The rules live in src/editor/sizeGizmo.ts without Pixi. The handles
  are drawn and driven by src/editor/viewport/sizeGizmoEditor.ts. The
  test hook __gm.sizeGizmo() reports them in screen pixels.

Found on the way:
- The first e2e run failed the play-mode drag check in 41b. It grabs
  the 40 px crate 6 px inside its bottom edge, and the full grip zone
  took that as a resize. The half zone inside fixes it.
- A first gizmo check failed because the sprite's right edge lay
  outside the scene view (x 940, view ends at 925). The test was
  wrong, the sprite now sits further left.

Tests:
- ./check.sh: 156 files, 2283 tests, CHECK EXIT 0 (before 2273).
- 10 unit tests in tests/editor/size-gizmo.test.ts.
- 6 new e2e checks in 29j: handles present, edge resize with the
  opposite edge fixed, undo, knob turn, knob scale that ignores a
  later sideways pull, circle radius.
- e2e 1-16,29,39-43 on the final state: ALL PASSED, 663 checks.
- Counter-checks: deciding the knob mode on every move fails the
  "does not turn it" check. Not moving the centre on an edge drag
  fails both edge checks. A full grip zone inside fails the new unit
  test. Originals restored.
- Check total 1357 + 6 = 1363, computed. CI measured 1357 at d257298.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:37:17 +02:00
adminandClaude Opus 5.5 d257298bc7 Keep a dragged rigidbody on the mouse while the game plays
game-maker / gate (push) Successful in 2m55s
game-maker / suite (push) Successful in 9m16s
Dragging a sprite with a dynamic rigidbody in the scene view during
play barely moved it. Only a quick drag and release got through. A
dragged object should follow the mouse as if it had no rigidbody and
start from rest when it is let go.

- The scene view reports the dragged objects as held through
  EditorState. The game tick no longer writes its own position over a
  drag position that has not been read in yet. This affected every
  object in play, not only rigidbodies.
- A held dynamic body turns kinematic and is set straight to the drag
  target. Gravity and velocity no longer pull it away between two
  mouse moves, and the "moved directly" warning stays quiet.
- A body counts as held when the object itself, an object above it
  or its rigidbody is dragged.
- On release the body turns dynamic again with linear and angular
  velocity set to 0.

Not changed: a press on an unselected crate still lands on its
same-sized collider (rule from #203). Dragging while play is paused
and in the editor run is untouched.

Tests:
- ./check.sh: 155 files, 2273 tests, CHECK EXIT 0 (before 2271).
- e2e 41 alone: 21 checks. e2e 1-16,39-43: 641 checks, all passed.
- 2 unit tests: a held body stays where the drag puts it and starts
  from rest, also for a grip shorter than one physics step. The tick
  does not overwrite a drag position that is not read in yet.
- 1 e2e check in 41b: the crate is lifted 150 units with the real
  mouse, holds exactly at -3490 for a second, then falls from rest
  (1.3 px, then 5.1 px) with an empty log.
- Counter-checks: without holding in physics the crate falls back to
  the ground and the old warning shows. With the tick writing back
  the unit test fails. Without zeroing, a short grip keeps 490 px/s
  and the test fails.
- Check total 1356 + 1 = 1357, calculated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:16:06 +02:00
adminandClaude Opus 5.5 d444b27fae Let behaviors read and set tilemap cells while the game runs (#135)
game-maker / gate (push) Successful in 2m59s
game-maker / suite (push) Successful in 9m22s
A behavior could move sprites around but had no way to look at or
change the tilemap it was standing on. Doors, breakable blocks and
"is there ground ahead?" checks all need that.

- New nodes Tile at, Set tile, World to cell and Cell to world. The
  conversion honours position, rotation, scale and mirroring of the
  placed map. A small Tile literal node lets a graph compare what it
  read against a fixed tile.
- The layer is picked by name from all tilemap layers in the project
  through a new field kind. "First layer" takes the map's first one.
  Because layer is an input, a text can also be wired in.
- Each placement keeps only its edited cells in liveTilemap.ts,
  layered over the asset's cells. A second placement of the same map
  stays untouched, and the asset itself is never written.
- A read sees a Set tile from earlier in the same flow. Drawing picks
  up the edit at once and neighbouring autotile edges adapt.
- Solids see the edits straight away, so a Move in the same frame
  already hits the new cell. Rapier rebuilds its shapes on the next
  step.
- After Stop the map is back to what was painted. The editor run shows
  the edit in the scene view while it lasts and does not touch the
  project.
- An unwired tilemap input means no map, like Get position. Cells
  outside the grid are refused and done reports false.

Tests:
- ./check.sh: 155 files, 2271 tests, CHECK EXIT 0 (before 2245).
- tests/core/tile-access.test.ts (14), one physics test for a floor
  set at runtime, catalog entries for the five nodes.
- e2e 39,40: all passed, 86 checks (before 81), five new in 40s.
- Counter-checks: collision ignoring edits fails 3 unit tests, the
  solids cache missing same-tick edits fails only the "at once" test,
  drawing ignoring edits fails the 2 drawing checks, the editor run
  not reporting a new tile fails only its check.
- 40s uses a still tile. The animated fixture tile redraws every frame
  and hid the last counter-check at first.
- Check total 1351 + 5 = 1356, computed. CI confirmed 1351 at cb32b4b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:45:29 +02:00
adminandClaude Opus 5.5 cb32b4bc23 Draw scene symbols as white panel icons that make room for each other
game-maker / gate (push) Successful in 2m54s
game-maker / suite (push) Successful in 9m5s
The scene view drew its symbols from its own SVGs, each in a type
colour and some on a plate. They looked heavier than the panel icons
and were hard to read on busy ground. Symbols of different objects
also sat on top of each other whenever their positions met.

- Every symbol is now the panel icon of its kind, one file for both
  places. The scene-only SVGs in src/viewport/icons are gone.
- Symbols are white in a thin dark outline with no plate behind them.
  haloSvg widens each shape of the file by its own stroke and draws
  that silhouette dark under the icon, so no second file is needed.
- A butt stroke end would leave its end without outline. The halo caps
  such strokes square, and the rigidbody arrow shaft is now a filled
  rect so its top edge is outlined exactly.
- Symbols are 18 world units across, stay upright and are clickable.
  A click selects the object, a drag moves it.
- symbolLayout places every symbol on its object's position. Symbols
  that overlap stand on a ring round the middle of their positions,
  evenly spaced one icon apart. A ring that reaches into another
  symbol takes it in until nothing overlaps. Ownership plays no part.
- Names sit beside their symbol, on the outside of a ring. Selection
  frames and parent links follow the symbol.
- A collider icon no longer shows trigger or invalid state. The
  outline colour of the shape still does.
- A lone collider sits on its sprite's middle again. A press there on
  an unselected sprite drags the collider, so e2e 02 and 16 now grab
  the sprite beside its middle. Hook __gm.container.objectRect helps
  16 find that spot.

Tests:
- check.sh: 2245 tests, CHECK EXIT 0 (+8 for layout and halo)
- e2e 1-16,31,39-43: 670 PASS, 2 FAIL. Both were sampling too
  strictly for the thinner, smaller icons. 5c looked for the exact
  outline colour, which the thin outline only reaches blended at this
  zoom. 31n started the ring scan one pixel past the smaller ring.
  Both are fixed, and e2e 5,31 then passed with 48 checks.
- 5c checks white with an outline and none of the old colours, 41b
  checks the ring positions and that a click on a symbol selects its
  object, 31n checks the now white particle ring.
- Counter-checks: without the halo only 5c fails, also with the looser
  outline match. Without the layout the ring and click checks in 41b
  fail. Originals restored by cmp.
- The e2e check total stays 1351.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:07:47 +02:00
adminandClaude Opus 5.5 e3d47ca9c1 Show a clickable symbol for colliders in the scene view
game-maker / gate (push) Successful in 2m59s
game-maker / suite (push) Successful in 9m34s
Colliders were the only placed part without a symbol in the scene
view. A collider that sits exactly on its sprite was hard to find and
could only be picked by its outline.

- The symbol is the box with four corner handles from the asset
  panel. It stays upright and has a fixed size in world units.
- colliderColor() holds the colour rule of the outline (green,
  trigger colour, red for a shape that collides with nothing). The
  outline and the symbol share it.
- The symbol sits 28 units left of the collider centre. The sprite
  centre stays free for grabbing the sprite, and a rigidbody symbol
  on the same spot is not covered.
- The symbol has its own 26x26 hit area and the outline's handler.
  A click selects the collider and a drag moves it. sceneStack counts
  it as a hit under the pointer.
- The hook __gm.sceneIcon(id) also returns x, y, color, screen and
  clickable.

Findings:
- With the clickable symbol on the sprite centre, four checks in 01
  and 09 dragged the collider instead of the crate. That is why the
  symbol moved beside the centre.

Tests:
- ./check.sh: 153 files, 2237 tests, CHECK EXIT 0 (was 152 / 2234).
- New unit test for colliderColor. svg-icons.test.ts lists the
  collider symbol.
- e2e 41b: 3 new checks (beside the centre without and with a
  rigidbody, click selects the collider).
- e2e 1-16,39-43: ALL PASSED, 635 checks. e2e 41 alone: 20 checks.
- Counter-probes: no symbol fails both placement checks. A symbol
  without hit area fails only the click check.
- Check total 1348 + 3 = 1351, computed, not yet measured by CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:18:10 +02:00
adminandClaude Opus 5.5 abf3b1f618 Pick an existing collider or rigidbody from the object menu (#222)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m52s
"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.

- "Add collider…" and "Add rigidbody…" open a second menu at the same
  spot: a search field, "Create new …", a separator, then every asset
  of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
  the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
  the collider commands. An object without a resolvable asset is
  greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
  sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
  collider can now sit on several sprites. A value equal to the asset
  value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
  90 degrees and gives a circle the radius that fits (min/2). It is
  greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
  "Add Component" in Unity. That costs one click more than before.

Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
  existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
  centring each fail at their own check. Breaking the override rule
  fails 2 unit tests.
- CI confirmed 1345 checks at 2cf7202 (task 265). New total 1348,
  calculated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 18:29:34 +02:00
adminandClaude Opus 5.5 2cf72022eb Ping an asset from its inspector link instead of selecting it (#221)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 9m2s
A single click on an asset link used to select the asset. That swapped
the inspector over to the asset, so the object being edited was gone
and the second click of a double-click landed on a different row. A
click now pings the asset in the asset panel, like Unity does in its
Project window, and a double-click opens it.

- state.pingAsset(id) emits a new `ping` event. It runs no command and
  leaves the selection alone, like openAsset.
- ListView.ping(id) draws an accent overlay on the row. It stays full
  for 200 ms and is gone after 1 s, driven by onFrame. destroy cancels
  the animation.
- The asset panel expands the group, scrolls to the row and pings it.
- The capture listener and followedLink in the inspector are gone.
  The inspector is no longer rebuilt, so pressCount on the label is
  enough. followsPress and pressOf are no longer exported from ui/.
- The "Asset" and "Part of" rows of a scene object were a ghost button
  that selected the asset. They are now a link label with the same
  ping and open behaviour, next to a read-only field with the name.
- The link logic lives in linkLabel() in fieldFactory.ts. createFieldRow
  and the asset row share it.
- TextField gets a readOnly option: no focus, no caret, no hover.
- New hook __gm.assetPing(). inspectorLink also reports the field
  next to the label (value, readOnly).

Findings:
- Renaming an asset while its object was selected left the old name
  in the inspector. This was already the case with the old button.
  The refresh now updates the name.

Tests:
- ./check.sh: 151 files, 2228 tests, CHECK EXIT 0 (before 150 / 2226).
  New: tests/ui/list-ping.test.ts for pingAlpha and pingAsset.
- e2e 1-16,28 on the final build: ALL PASSED, 575 checks.
  28l-ii has 9 checks instead of 5.
- Counter-checks, each failing exactly at its check: ping also selects,
  no follow-up frame (alpha stays 0.55), inspector never opens, field
  takes focus, name not refreshed, label not a link.
- Check total 1345, calculated. CI last counted 1336 at fd6af84.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:55:31 +02:00
adminandClaude Opus 5.5 1aae97b335 Open a sprite's texture or shader from the menus and inspector (#221)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 8m58s
A sprite's texture and shader were only reachable by hunting for them
in the asset panel. Every place that shows such a reference now leads
straight to it.

- Object menu (hierarchy and scene view): "Open texture" and "Open
  shader" sit right below "Open asset" when the sprite has one. The
  object's own values count, so an overridden shader opens the
  override.
- Asset context menu: the same entries at the top.
- Inspector: every asset field with a value shows its label as a blue,
  underlined link. One click selects the asset, a double click opens
  it. An override stays gold and gets the underline too.
- The first click rebuilds the inspector, so the second one lands on a
  different row. The inspector catches that second click before any
  row sees it.
- New pure rule `references.ts`: the asset behind a reference field. A
  frame leads to its sheet. Kinds without a tab (palette, font, sound)
  get no menu entry.
- `followsPress` in `clicks.ts` keeps the double-click window and
  tolerance in one place.
- `activateAsset` in `actions.ts`: opening means the same everywhere
  as a double click in the asset panel, so a scene also becomes the
  active one. The asset panel uses it too.

Tests:
- ./check.sh: 150 files, 2226 tests, CHECK EXIT 0 (before 148/2214).
- 12 new unit tests: 6 for the reference rule, 4 for the link in
  createFieldRow, 2 for followsPress.
- 5 new e2e checks in 28l-ii: both menus, the link's look, click
  selects, double click opens.
- e2e 1-16,26,28,35,39,40 on the final build: ALL PASSED, 732 checks.
- Counter-probes: no "Open ..." entries, no underline and a second
  click never recognised each fail exactly at their checks.
- Check total 1341 (CI counted 1336 at fd6af84), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:44:19 +02:00
adminandClaude Opus 5.5 9b62d572b4 Stop the bead spacing check failing on one slow frame
CI task 261 at fd6af84 went red on a single check in module 18: "an
exec edge firing every frame carries an evenly spaced chain of beads"
measured a gap of 0.611 instead of 0.5 +- 0.1. The code did not
change. The tolerance was tighter than one slow frame.

- A bead leaves on the first frame at least 0.15 s of game time after
  the one before, which is 0.5 of the 0.3 s trip. The gap is therefore
  that plus up to one frame.
- One 33 ms frame right after the screenshot gives exactly 0.611.
- The check now wants a gap above 0.49 and below 0.5 plus one frame of
  up to 50 ms.

Tests:
- e2e 18 on the final build: ALL PASSED, 33 checks.
- Counter-probe with a minimum spacing of 0.1 instead of 0.15: the
  check fails with 0.333, and only this check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:44:15 +02:00
adminandClaude Opus 5.5 fd6af84474 Keep the texture tab smooth with many collision outlines (#220)
game-maker / gate (push) Successful in 3m0s
game-maker / suite (push) Failing after 8m37s
With a large sheet the texture tab stalled on every mouse move. A
profile over a pan with 1728 frames showed two causes. Half of the time
went into convexPieces, which split every outline into convex pieces
only to pick its colour. And every pointer move during a pan rebuilt
and re-tessellated the whole overlay geometry.

- TextureCanvas draws on two layers. `sheet` holds every frame except
  the selected one and is rebuilt only when the zoom, the selection or
  one of those frames changes. `overlay` holds the selected frame, its
  handles and the rubber band of a new frame.
- Both layers are drawn without the pan. A pan only moves the
  container and places the name labels again.
- SheetCanvas has a new hook `panned()`. By default it calls `redraw()`
  as before, so Pixel Art and Vector Art are unchanged.
- The validity of an outline is remembered per outline and comes from
  polygonProblem, with the same result. This works because the model
  replaces an outline list on every change and never edits it in
  place, the same as frameOutline.ts already relies on.
- The name plates stay below the outlines and the texts on top. Checked
  on a screenshot: the corner handle next to a label sits on top.
- New debug hook `__gm.texture.drawInfo()` with the rebuild count, the
  outline count and the screen positions of outlines and names.

Measured with a throwaway probe, 1728 frames, 120 moves, main thread
time per move, before and after in the same browser:

- Middle-click pan: 76.9 ms -> 4.7 ms (4.4 ms without frames)
- Dragging a frame: 51.7 ms -> 10.1 ms
- Wheel zoom: 52.2 ms -> 30.9 ms
- Selecting another frame: 67.6 ms -> 41.0 ms

Findings:

- Wheel zoom and selection still rebuild `sheet` once on purpose. That
  stays a single hitch per notch or click on very large sheets.

Tests:

- ./check.sh: 148 files, 2214 tests, CHECK EXIT 0.
- e2e 1-16,37,39-43 on the final build: ALL PASSED, 641 checks.
- Three new checks in 42c-ii. A corner drag does not rebuild the other
  frames. An undo on an unselected frame reaches the picture (1 -> 2
  outlines). A pan moves outlines and names by exactly 40/25 px without
  a rebuild.
- Counter-checks: a missing outline in the cache key and a pan that
  does not move each fail at their check. The selected frame in the
  key fails alone with 9 rebuilds during an 8-step drag.
- Check total 1333 -> 1336 (confirmed by CI at ceaa44e, task 259).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 15:57:13 +02:00
adminandClaude Opus 5.5 ceaa44e5b8 Add roles and a user management tab for admins (#180, #179)
game-maker / gate (push) Successful in 2m50s
game-maker / suite (push) Successful in 8m57s
Until now every account could do the same, and managing accounts
meant running make-user.php on the server. Accounts now carry a role,
and admins get a tab in the editor to create, rename, re-role, reset
and delete accounts. Both issues share the same server files, so they
land in one commit.

#180 Roles:
- Every account is admin or user. A missing or unknown role counts as
  user, on the server and in the client.
- auth/session and auth/login return {user, role}.
- Auth::requireAdmin() answers everyone else with 403.
- make-user.php takes the role as a third argument. Without it an
  existing account keeps its role, so a password reset from the CLI
  never demotes an admin.
- Admins see and save other users' projects like their own. Saving
  keeps the original owner.

#179 User management:
- The account menu gets "User management" (admins only) and "Plugins"
  (everyone). Both open the same closable tab, accounts on top and
  plugins below. The fixed Plugins tab is gone.
- Per account: Rename, Role, Password (no old password needed) and
  Delete with a confirmation. "New user..." creates one.
- Each account lists its projects with an Open button.
- Delete is refused while the account still owns projects.
- Renaming moves the projects along and ends every session of the
  account. transfer takes the same lock as save.
- On their own row an admin can only set the password. The last admin
  cannot be demoted or deleted. The server enforces both.
- The account rules live in AccountRules.php and accountRules.ts as
  pure functions, so they are testable without a server.

Findings:
- At 1280 px the Delete button reached past the column edge and was
  not clickable. The name column is flexible now, and module 49 checks
  the layout.
- The server lets an admin delete other users' projects, but the
  editor has no button for it yet. Left open in the issue.

Tests:
- ./check.sh: 148 files, 2214 tests, exit 0 (was 147 / 2204).
- New tests/php/accounts.test.php, 51 checks, plain PHP. CI runs it
  as its own step in the gate job, the host has no PHP.
- New e2e module 49 with 18 checks. CI creates the e2e-admin account
  in serve.sh. Modules 14, 38, 46 and the scaffold follow the new tab.
- e2e 1-16,37,38,45,46,49: all passed, 581 checks.
- Counter-checks: owner on save, admin view, default role and self
  delete in PHP fail 7, 5, 5 and 1 checks. Unchecked requireAdmin,
  a wider column and the caller as owner each fail exactly their
  e2e check.
- Check sum 1314 -> 1333 (+1 in 38, +18 in 49), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:59:04 +02:00
adminandClaude Opus 5.5 30fb3a2316 Dragging previews once per frame instead of on every move (#213)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 8m43s
Scene view, texture tab and graph called state.preview() on every
pointer move. Each call rebuilds panels, the editor run and generated
images, so a drag in a busy project cost up to 34 ms per move. The
model is still written on every move. Only the preview now waits for
the next frame.

- New FramePreview in src/editor/framePreview.ts. request() goes into
  the move handler, cancel() into the release. The rule lives in one
  place instead of six copies.
- Scene view: object drag, polygon corners and tilemap strokes use it.
- Texture tab: frames, slice edges and collision corners use it.
- Graph: nodes, groups and comments use it through GraphDrags. That
  covers the state machine, shader and particle tabs as well.
- Vector art is unchanged. Its frame already writes the model since
  #129.

Findings:
- The texture tab paid twice per move (232 project events for 120
  moves). Pixi sends a second pointer event 4 to 8 ms after each real
  move as a hover update. It now lands in the same frame for free.
- Inspector scrubbing and gradient stop dragging still preview on
  every input. Both lie outside the three canvases of the issue.

Measured with a throwaway probe, 120 moves at 8 ms, 120 extra objects:
scene 26.2 -> 18.5 ms/move, texture 34.2 -> 17.9, graph 28.8 -> 17.0.
Project events dropped from 120/232/119 to 48/48/41.

Tests:
- ./check.sh: 147 files, 2204 tests, exit 0 (before 146 / 2199).
- 5 new unit tests in tests/editor/frame-preview.test.ts, including
  the real GraphDrags. Counter-checks (direct preview in the graph,
  no cancel on release, one key per name) each fail the expected test.
- e2e 1-16,29,31,36,37,39-43: 708 checks passed.
- e2e 1-16,26,28: 634 checks passed.
- No new e2e check, the check sum stays at 1314.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:08:11 +02:00
adminandClaude Opus 5.5 f9d5e8bca5 Move the one-way rule and collider geometry out of physics (#215)
game-maker / gate (push) Successful in 2m48s
game-maker / suite (push) Successful in 8m46s
PhysicsSystem had 940 lines. The step logic shares its state with
the engine handles of parts and bodies, so a separate class would
need almost the whole system as its interface. That part stays. Two
pieces without own state move out instead. Behaviour is unchanged.

- passesOneWay in oneWay.ts is the one-way rule as a pure function.
  It can now be tested without Rapier.
- bodyGeometry.ts holds the collider shapes and the pose maths.
- PhysicsSystem is down to 820 lines.

Tests:
- ./check.sh: 146 files, 2199 tests, CHECK EXIT 0
- 5 new unit tests for the one-way rule without an engine.
- New tests/core/body-geometry.test.ts: a capsule turned by 90
  degrees went unnoticed before. Both tests fail against that bend.
- Counter-checks on landing hold and teleport detection fail in
  3 to 6 tests each.
- e2e 1-28,31,36,39-43 on the final state: ALL PASSED, 1060 checks
- Each of the four #215 commits was checked green on its own in a
  scratch worktree: 2190, 2192, 2192, 2199 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:20:07 +02:00
adminandClaude Opus 5.5 ed29b2f3dc Split the debug hook into one file per topic (#215)
debugHook.ts had grown to 1097 lines of unrelated test hooks. They
now live in seven files under src/editor/debug/, grouped by topic.
debugHook.ts only assembles them and is down to 86 lines.

- The shape of window.__gm is unchanged for tools/. Measured in the
  browser before and after: 120 top-level keys and 313 leaves with
  their arity, byte-identical JSON.
- The Text import is explicit in the helpers. Without it the name
  fell back to the DOM type and instanceof would never have matched.

Findings:
- The comment for vectorArt sat above pixelArt. It is back in place.
- An orphaned duplicate comment on assetRow is gone.

Tests:
- ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (unchanged)
- e2e 1-16,37,39-43,47,48: ALL PASSED, 734 checks
- The shape probe turns red against a deliberately misnested hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:20:06 +02:00
adminandClaude Opus 5.5 b830f9c747 Split wiring, drags and the frame layer out of GraphCanvas (#215)
GraphCanvas had grown to 1406 lines. Wiring, dragging and the frame
views each carry their own state, so they now live in their own
classes behind a narrow host interface. Behaviour is unchanged.

- GraphCanvasHost is what the new classes see of the canvas.
- Wiring owns dragging a pin, the palette on a drop into empty space
  and "Make transition".
- GraphDrags owns moving nodes and frames, including snapping.
- AnnotationLayer holds the frame views. The test hooks stay on the
  canvas and read the views through the layer.
- BeadClock moves into beads.ts and can now be tested without a
  browser.
- The canvas builds host, wiring and drags in its constructor because
  state is a parameter property and not set during field init.
- GraphCanvas is down to 1019 lines.

Tests:
- ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (+2 for BeadClock)
- e2e 1-28,31,36: ALL PASSED, 932 checks
- New e2e check in 36k: a frame caught by the rubber band is also
  drawn as selected. No check covered that before. Against a bent
  build it fails with selected: false.
- Counter-checks for drop target, snapping and beads fail exactly
  the expected checks.
- Editor check sum is now 1314 (CI measured 1313 at 487b3aa).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:20:05 +02:00
adminandClaude Opus 5.5 e31db3df1d Split tile painting and polygon corners out of SceneViewport (#215)
SceneViewport had grown to 1216 lines. Three blocks carried their own
state and only needed a few things from the view, so they now live in
their own classes. Behaviour is unchanged.

- SceneViewHost is the narrow interface the new classes see of the
  view: state, coordinate mapping, redraw and the preview trigger.
- TilemapPainting owns the paint stroke, the rectangle preview and
  the flood fill, with press/move/release called in the old order.
- PolygonCorners owns hovering, dragging, inserting and deleting the
  corners of a polygon collider.
- TilemapSolids draws the solid outlines of tilemap layers.
- SceneViewport is down to 668 lines.

Findings:
- PaintStroke.holderId was written but never read. It is gone.

Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (unchanged)
- e2e 1-16,29,31,39-43: ALL PASSED, 679 checks
- Counter-checks in the browser: bending the brush, the corner drag
  and the solid outline each fails exactly the expected checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:20:03 +02:00
adminandClaude Opus 5.5 487b3aa5b4 Painting over identical tiles no longer adds an empty undo step (#212)
game-maker / gate (push) Successful in 2m45s
game-maker / suite (push) Successful in 8m43s
A rectangle drawn over cells that already hold exactly that tile
pushed a command that changed nothing. Undo then seemed to do nothing
and the project was marked as modified. PaintTilesCommand.changes()
was meant to prevent that, but nothing called it since #128.

- New paintTiles() in tilemapCommands.ts returns the command for a
  finished stroke or fill, or null when no cell changes. It is the
  first caller of PaintTilesCommand.changes().
- All three paint paths in the Scene View go through it: brush and
  eraser, rectangle (both in endStroke) and flood fill (fillFrom).
- Flood fill was not affected, because floodFill already returns []
  for the same tile. The guard is there anyway, as the issue asked.
- A rectangle that changes even one cell stays a normal undo step.

Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (before 2187).
- 3 new unit tests in tests/core/tilemap.test.ts: same rectangle
  twice gives no command, a wider rectangle still does, a fill that
  finds nothing gives no command.
- 1 new e2e check in 40d: the same rectangle again, then one undo
  must take 9 cells to 0. A redo restores them for later blocks.
- GM_STEPS=39,40 on the final build: ALL PASSED, 81 checks.
- Counter-probe with changes(...) || true: exactly the two unit
  tests fail, and in the browser exactly the new check fails with
  {"afterUndo":9}. Original restored via cmp (IDENTICAL), rebuilt.
- Editor suite check total: CI measured 1312 at 0e85856, new total
  1313 (calculated) in README and wiki.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:56:17 +02:00
adminandClaude Opus 5.5 0e8585665a Load the 500 weight and draw missing glyphs as SVG icons (#214)
game-maker / gate (push) Successful in 2m47s
game-maker / suite (push) Successful in 8m41s
The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.

- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
  new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
  icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
  icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
  arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
  the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
  DOMParser.
- Tests find icons through the SVG id. The new hook
  __gm.locateIcons(id) and the harness helpers findIcon/clickIcon
  replace locate() on the old characters.

Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
  screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.

Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
  character in src/ exists in every shipped face. The cmap reader
  matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
  icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
  for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
  e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
  back, the struct icon removed (one unit test each). In the browser,
  500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at 056a622, +2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:13:22 +02:00
adminandClaude Opus 5.5 056a622524 Refactoring round 6: shared sheet canvas, fewer copies (#85)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 8m55s
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).

- SheetCanvas is the new base of the texture, pixel art and vector art
  canvases. It holds background, mask, hint, panning, wheel zoom, fit,
  view save and restore and the screen mapping. Pixel art keeps its
  whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
  animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
  contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
  commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
  GeneratedPictures.has and SnapToggle.snapping. Tests now check the
  same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
  The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
  wiki pages.

Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
  has not been called since #128. The rectangle tool therefore records
  a no-op undo step when it paints the same tile over itself. Left as
  is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
  and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
  could become 10 without a red test. tests/core/raycast.test.ts now
  covers it.
- CI measured 1308 editor checks at d7c0b7c, not the 1307 counted for
  #164. The sum is now 1310 (1308 + 2), computed.

Tests:
- ./check.sh: 144 files, 2143 tests, exit 0 (before 2142).
- e2e 1-16,37,39-43,47,48 fresh: ALL PASSED, 729 checks.
- e2e 1-13,47,48 with the new checks: ALL PASSED, 518 checks.
- Counter-checks, each restored via cp and cmp: halved panning fails
  both new pan checks; unclamped mix fails 1 test, no blue channel 3;
  coarser hash 3; broken reorder undo 2; triggers setting forced on 2;
  bent ray length 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 06:57:43 +02:00
adminandClaude Opus 5.5 d7c0b7cfdb Variables and state machines can reset with every scene (#164)
game-maker / gate (push) Successful in 2m43s
game-maker / suite (push) Successful in 8m40s
A shared variable or machine used to live either per object or for the
whole run. Levels need a third lifetime: shared like global, but built
fresh from the declarations whenever a scene is loaded.

- New scope `scene` for Variables assets and state machines. A missing
  field keeps the old meaning: `global` for assets, `local` for
  machines, so existing projects do not change.
- Returning to a scene that was visited before is fresh as well. There
  is no memory per scene.
- A `scene` machine announces its start state again after a scene swap,
  so `On state entered` in the new scene hears it. A global machine
  stays quiet because it never left its state.
- The rule lives in two pure functions in `sharedState.ts`
  (`carriedForward`, `restartedAssets`). BehaviorSystem only calls them.
- The inspector shows a Scope dropdown on Variables assets and a third
  step on the machine dropdown, with a note on what `scene` means.

Decisions:
- `scene` and `persistent` exclude each other (variant A). The choice
  dialog was cancelled, so this is an assumption to revisit. A
  scene-scoped asset hides the Persistent switch, and switching to
  `scene` drops the flag (undo restores it). The guard sits in
  `isPersistent` itself, so no path can save values the scope throws
  away anyway.

Findings:
- `clickText('Scene')` hits the Scene tab, not the menu entry. One e2e
  check was red because of that, and its partner was falsely green
  because it compared `undefined` with `undefined`. Both use `clickLast`
  now.

Tests:
- ./check.sh: 144 files, 2142 tests, CHECK EXIT 0 (before 143 / 2133)
- 9 new unit tests in tests/core/scene-scope.test.ts
- e2e 32 fresh: ALL PASSED, 17 checks
- e2e 1-3,17,18,26,32,35 fresh: ALL PASSED, 213 checks
- 3 new e2e checks (35: scene-scoped asset resets on swap, 32h: dropdown,
  note, hidden Persistent switch)
- Counter-checks bite: ignoring `restarted` fails 2 unit tests and the
  new check in 35, the old announcement rule fails 1, dropping the scope
  guard in `isPersistent` fails 1. Originals restored, cmp IDENTICAL.
- Check sum 1307 (1304 + 3) is computed. Last CI measurement: 1293.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 06:06:53 +02:00
adminandClaude Opus 5 6e3e8d23c5 Combining vector shapes keeps the curves it did not cut (#208)
game-maker / gate (push) Successful in 2m44s
game-maker / suite (push) Successful in 8m42s
Until now every boolean operation on vector art threw the curves away.
The shapes were flattened into line segments, the operation ran on those
segments, and the result came back as a polygon. A circle that nothing
even touched came out of an Exclude with 40 corners instead of its four
points and handles. Now the result carries the original curves again,
and only the edges the operation really drew stay straight.

- While flattening, every sample point remembers which curve it came
  from and where on that curve it sits. The operation runs on the same
  segments as before, so nothing about the boolean result changes.
- When a run of result points walks along one of those curves, that
  piece is cut out of the original curve again with de Casteljau. This
  is an exact cut, not a fit through the points.
- An edge whose two ends sit on the same curve but whose middle does
  not follow it stays straight. Without that rule a cut-off cap would
  bulge back into the shape.
- A closed contour keeps at least three points, so a lens shape does
  not collapse into two.
- The rule lives in its own module, src/core/model/vectorCurves.ts.
  vectorBoolean.ts only hands it the table of sampled points.

Findings:

- The two intersection points themselves sit up to 0.09 canvas pixels
  off the original curve. Clipper cuts the flattened segments, not the
  curves, so this was the case before as well and stays inside the
  0.2 px sampling tolerance.
- Refining those points with Newton steps on the curve was measured and
  gave nothing back (0.02952 px instead of 0.02978 px), because the
  error is in the intersection itself and not in the estimate. The code
  was removed again.
- Theme.ts rowHeight 22 -> 24 is in this commit. The change is not mine,
  it was already in the working tree; gate and module 47 are green with
  it.

Tests:

- ./check.sh: 143 files, 2133 tests, exit 0 (was 2127)
- e2e module 47 on a fresh seed: all passed, 59 checks (was 57)
- 6 new unit tests: untouched circle, untouched side plus a straight cut
  edge, a cut in the middle of a curve, a chord crossing one curve
  twice, two overlapping circles, the three point minimum
- 2 new e2e checks in block 47p: point count and handles from the model,
  plus a pixel pair on the round edge
- Four counter-proofs, all bite: no reconstruction -> 5 unit tests and
  the e2e check fail with 35 points and nothing curved; no chord test ->
  the cap bulges back to 28.28; no three point rule -> the lens test
  fails; handles halved -> the pixel check reads background

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

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

Findings:

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

Tests:

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:15:21 +02:00
adminandClaude Opus 5 1a3babf9c5 First not empty gets a setting for what counts as empty (#206)
game-maker / gate (push) Successful in 2m46s
game-maker / suite (push) Successful in 8m46s
The node treated 0, false and an empty text alike, which is right for a
missing behavior parameter but wrong the moment a real 0 travels through
the graph. A setting now says how much counts as empty, so the same node
works for both cases.

- The new parameter "Empty means" has three steps: "Any empty value"
  (the default, the behaviour so far), "Nothing there (0 and false
  count)" and "Only a missing value".
- The default keeps every existing graph as it is, including the common
  setup where an unset animation parameter arrives as an empty text.
- The rule lives in isEmptyValue(value, mode) in values.ts; the node only
  passes the step through. An unknown setting behaves like the default,
  the same way Compare does.
- The node description is shorter because the old one was cut off in the
  inspector.

Findings:

- In the "Nothing there" step a struct is empty only when its fields are
  empty under the same step, so a position (0, 0) stays a real value.

Tests:

- ./check.sh: 142 files, 2113 tests, exit 0 (before 2104).
- 9 new cases in the node catalog table: the edge cases per step (0,
  false, empty text, empty list, position (0, 0), unconnected a) plus an
  unknown setting.
- Two counterproofs hold: letting "Nothing there" act like "Any" drops
  the 3 cases with 0, false and the position; removing the "Only a
  missing value" step drops the 2 cases with the empty text and list.
- Seen in the browser: the node shows "Empty means: Any empty value" and
  the inspector dropdown lists all three steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:24:01 +02:00
adminandClaude Opus 5 d98c2898b1 A new node takes the first of two values that is not empty (#205)
game-maker / gate (push) Successful in 2m46s
game-maker / suite (push) Successful in 8m38s
A graph that reads an optional value needed a chain of Equals and
Branch just to fall back on a default. The new node does that in one
step: it passes a through unless a is empty, in which case b comes
out. Both ports are any, so it works for numbers, text, lists,
structs and objects alike.

- `First not empty` sits in the data category with two any inputs
  `a` and `b` and one any output `value`. Are both empty, b comes
  out, so the second input doubles as the default.
- `isEmptyValue` in values.ts holds the rule as a pure function:
  nothing connected, 0, an empty text, false, an empty list, and a
  struct whose fields are all empty.
- The struct case is what covers a position: behavior graphs have no
  vector type of their own, so (0, 0) arrives as a struct. `{x: 0,
  y: 2}` therefore counts as set, `{x: 0, y: 0}` as empty.

Findings:

- A real 0 or false on a is passed over. That follows from counting
  0 as empty and is written down in the issue.
- The output stays any. Ports that inherit their type from an input
  do not exist in the graph; `Item at` has the same shape.

Tests:

- ./check.sh: 142 files, 2104 tests, exit 0 (was 2094).
- 10 new catalog cases: a set, a unconnected, 0, "", false, an empty
  list, a position (0, 0), a struct with one field set, an object,
  and both empty.
- Counter-check: with structs never empty and 0 a real value exactly
  those two cases fail, the rest stays green. Originals restored and
  compared with cmp, then rebuilt.
- No e2e check: single data nodes have no e2e coverage here, the
  palette fills itself from BUILT_IN_NODES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:58:30 +02:00
adminandClaude Opus 5 8c5846f956 Right-click in the scene view opens the object menu (#204)
The object menu lived only on the hierarchy rows. Now a right-click in
the scene view opens the same menu for the object under the cursor.

- The menu moves out of HierarchyPanel into src/editor/objectMenu.ts.
  Hierarchy and scene view call the same function, so their entries
  cannot drift apart. Only the hierarchy adds "Expand all" and
  "Collapse all".
- Which object the menu is for follows the press rule from #203. A
  selected object under the cursor, even a hidden one, keeps the
  selection and the menu acts on all of it. Otherwise the topmost
  object is selected first.
- The rule is the pure function menuTargetOf in src/viewport/pick.ts,
  next to pressOn.
- Empty space, including an empty tilemap cell, opens no menu.
- The container tab gets the same, because ContainerCanvas extends the
  scene view.

Tests:
- ./check.sh: 142 files, 2094 tests, exit 0 (was 2090)
- e2e 39,40 fresh: ALL PASSED, 80 checks
- e2e 1-16,39,40 fresh: ALL PASSED, 565 checks
- New block 40r (3 checks): menu on a hidden selected crate, topmost
  object without a selection, no menu on an empty cell.
- Counter-check: a right-click that always takes the topmost object
  fails 2 unit tests and the hidden-crate e2e check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:47:31 +02:00
adminandClaude Opus 5 ad2a2d7921 Clicks in the scene view pick what is drawn under the cursor (#203)
Pixi's own hit test decided what a click in the scene view selected.
A tilemap caught clicks over its whole area, even on empty cells and
over sprites sorted above one of its layers. A press always grabbed
the topmost object, so a hidden selected object could not be dragged,
and there was no way to reach an object underneath another one.

- New pure rule in src/viewport/pick.ts. It lists the objects under
  the cursor front to back, in the order they are drawn.
- A tilemap counts only where it has a tile. Its depth there is the Z
  of the highest layer with a tile at that spot. An empty cell is
  empty space, so a click clears the selection or starts a marquee.
- Pressing on any selected object, hidden ones included, drags the
  selection. Otherwise the press selects the topmost object and drags
  it, as before.
- A click without drag on a selected object selects the next one
  underneath. After the lowest it starts again at the top.
- New debug hook __gm.sceneStack(x, y) returns that list.

Decisions:
- With a paint tool the selected tilemap paints anywhere on its grid,
  even under a sprite. With empty cells as empty space an empty cell
  could not be painted otherwise.
- Icons such as colliders and cameras stay in front. A second click
  now reaches the sprite under a collider.

Tests:
- ./check.sh: 142 files, 2090 tests, exit 0 (was 141 / 2080)
- 10 new unit tests in tests/viewport/pick.test.ts
- GM_STEPS=39,40: ALL PASSED, 77 checks
- GM_STEPS=1-16,29,39-43: ALL PASSED, 626 checks
- 4 new checks in block 40q: sprite over a lower layer, empty cell,
  cycling crate, map, crate, dragging the hidden selected crate.
- Counter test: whole-area tilemap hits and always-topmost presses
  fail 4 unit tests and all 4 new checks.
- Check total 1290 (1286 + 4), computed, not yet counted by CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:09:29 +02:00
adminandClaude Opus 5 d9c71c2ce7 e2e checks follow the new theme instead of fixed sizes (#202)
game-maker / gate (push) Successful in 2m38s
game-maker / suite (push) Successful in 8m31s
The suite job for 0d20776 died in module 06. The new font and theme
moved the layout, and three checks relied on the old one.

- 06: the ranking step right-clicked the spot where step 6d had just
  dropped a Log node. Whether that click hits the node depends on how
  the layout snaps it. It now asks for a fresh empty spot.
- 14 and 27 expected a shut panel to keep 30 px. The header is
  Theme.rowHeight + 4, now 26. A new hook __gm.panelHeader hands the
  checks the real height, so the next theme change does not break
  them again.

Tests:
- ./check.sh: 141 files, 2080 tests, exit 0
- Local full run before the fix: 06 passed with the new spot, only the
  three header checks in 14 and 27 failed.
- GM_STEPS=1-14,27 after the fix: ALL PASSED, 457 checks
- No new checks, the total stays at 1286.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:29:10 +02:00
adminandClaude Opus 5 0d207769b9 Focused text fields show their outline on every edge (#202)
game-maker / suite (push) Failing after 1m15s
game-maker / gate (push) Successful in 2m15s
The focus outline of a text field was only visible at its corners.
The mask that clips the text covered the whole field, border included,
so the 1 px stroke was cut away along every straight edge. Only the
rounded corners bent inwards far enough to survive. With the larger
radius the new theme tried, this became obvious.

- TextField puts text, selection and caret into their own container.
  Only that container is masked. Background and outline are not.
- NumberField and the rename field in lists inherit the fix.
- The theme radius goes back to 2 px.
- New debug hook __gm.focusedRect() returns where the focused widget
  sits, so a check can read its border.

Tests:
- ./check.sh: 141 files, 2080 tests, exit 0
- GM_STEPS=2: ALL PASSED, 24 checks (one new)
- The new check in 2c reads the middle of all four edges of the
  focused name field. All four must be the focus blue.
- Counter-check with the old full-field mask: the check fails, all
  four edges read #24272e.
- Editor check total is now 1286 (1285 + 1), computed, not measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:11:42 +02:00
adminandClaude Opus 5 940a78e8ee The editor ships its own font, Source Sans 3 and Code Pro (#202)
Theme named Inter and JetBrains Mono, but neither was ever shipped,
so every machine drew the editor in whatever it had installed. The
e2e runs measured text in a different face than the user saw.

- Five Latin woff2 faces live in src/ui/fonts/ next to their OFL
  licences: Source Sans 3 400/500/600/700 and Source Code Pro 400.
- fonts.ts loads them through FontFace, and main.ts waits for them
  before the first widget exists, so Pixi measures the real face.
- A face that fails to load is reported in the console and the
  system fallback in Theme takes over.
- Theme is retuned by hand: 12 px body, 22 px rows, smaller gaps
  and padding, radius 8, and weight 500 for bold and headings.
- The scene background default follows Theme.colors.graphBackground.
  This makes core import from ui for the first time.

Findings:
- The 500 file is in the folder but fonts.ts does not register it
  yet, so weight 500 currently falls back to the 400 face.

Tests:
- ./check.sh: 141 files, 2080 tests, exit 0.
- e2e not run. Checks that measure widths or row positions are
  expected to need updating for the new metrics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:55:22 +02:00
adminandClaude Opus 5 28b1afa7af Panning the scene view no longer redraws the whole scene
game-maker / gate (push) Successful in 2m22s
game-maker / suite (push) Successful in 8m38s
A middle-mouse pan used to run the full redraw on every frame. It
collected every object from the project, cleared and redrew every
shape, re-sorted all children and made Pixi re-upload every buffer,
although only the camera had moved. The user saw the frame rate drop
while panning.

- WorldViewport gets moveCamera(). When zoom and view size are
  unchanged it only moves the world and redraws the overlays drawn in
  screen pixels. collectFrame, the shape redraw and the re-sort drop
  out of a pan frame.
- Every full redraw remembers the box each object is culled by. A pan
  that moves an object across the edge of the view still takes the
  full redraw, so objects appear and vanish as before.
- Grid, selection outlines, rays and the container origin mark move
  into a shared drawOnScreen hook. It runs after every full redraw and
  after every pan. The parent links stay in afterItems because they
  are drawn in world units.
- Wheel zoom keeps the full redraw, because line widths follow the
  zoom. A cache for collectFrame was left out: it would save about
  1 ms and risk stale data.
- debugInfo() reports redraws and cameraMoves, and __gm gets a hook
  that forces a full redraw, so a check can compare against it.

Findings:
- Chrome already coalesces mouse moves to one per frame (500 moves
  gave 183 redraws in 189 frames). A throttle was tried and removed
  again, because its counter-check did not fail.
- A check in module 03 compared the whole gameInfo() object and broke
  on the new counters. It now compares camera, objects and rendering.
- The stall without ?vsync most likely comes from the desynchronized
  context in src/main.ts. It cannot be reproduced headless.

Tests:
- Measured on Jump and Run, main-thread time per frame, 3 runs of 240
  moves: pan 5.7-7.4 ms before, 3.8-4.8 ms after. Plain mouse moves
  stay at 2.1-2.4 ms. 208 of 240 moves took the fast path.
- ./check.sh: 141 files, 2080 tests, exit 0.
- GM_STEPS=1-16,29,42,43: 535 checks, 1 failed (the module 03 test
  above). After the fix GM_STEPS=1-5: all passed, 130 checks.
- 3 new e2e checks in 5d: 12 pan steps give 12 camera moves and 0
  full redraws, pixel rows after the pan match a fresh redraw, a box
  panned out of view and back is drawn again.
- Counter-checks: overlays not redrawn on pan gives "3 of 3 rows
  differ". Edge test disabled keeps the box visible with 0 redraws.
- Check total 1285 (1282 confirmed by CI task 229, plus 3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:38:05 +02:00
adminandClaude Opus 5 d2d63e1681 e2e runs always test the build that was just made
The test browser keeps Chrome's default profile, disk cache included,
and nginx sends no Cache-Control header. Right after `npm run build` a
run could load the index.html of the previous build and test old code.
It showed up while measuring the scene view pan: a new debug hook
returned NaN although server and bundle already had it.

- signIn() turns the network cache off via
  Network.setCacheDisabled before the first page load.

Tests:
- GM_STEPS=5 (fresh seed): ALL PASSED, 10 checks.
- ./check.sh: CHECK EXIT 0, 2080 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:00:49 +02:00
adminandClaude Opus 5 fe894c630d Pixel art tools show only useful options and remove any colour (#201)
game-maker / gate (push) Successful in 1m42s
game-maker / suite (push) Successful in 8m15s
The pixel art side bar showed every tool option all the time, the
palette used only part of the column width and "Remove" could only
drop the last colour. Now each tool shows what it needs, the palette
fills the column and any colour can be removed.

- Options per tool live in toolOptions.ts. Brush shows for pencil,
  eraser, line and unfilled shapes. Filled shapes shows for rectangle
  and ellipse. Contiguous shows for fill. Picker has none.
- All options share one row with a fixed height, so nothing below
  jumps when the tool changes.
- The palette picks its column count from the width and stretches the
  swatches until the row ends flush. At 1280 px that is 11 per row
  instead of 8.
- The colour field takes the space the two buttons leave, so "Remove"
  is no longer cut off.
- "Remove" deletes the selected colour. Pixels of that colour turn
  transparent, later indices move down by one in every image that uses
  the palette, and palette plus images change in one undo step.
  Transparent itself cannot be removed.

Decisions:
- Pixels of a removed colour become transparent, like erasing
  (chosen by the user before the build).

Findings:
- The Filled checkbox slid away under the pointer. The brush button
  sat to its left and vanished when Filled turned on, so the second
  click missed. The brush button now sits to the right, and a check
  clicks the same spot twice.
- One start-up FAIL "debug hook present" came back never in three
  later runs. It is the known start flake.

Tests:
- ./check.sh: 141 files, 2080 tests, exit 0 (before 140 / 2070)
- 10 new unit tests in tests/editor/pixel-palette.test.ts
- GM_STEPS=48 (fresh seed): ALL PASSED, 40 checks, 4 of them new
- Counter-check: indices not shifting and brush always visible make
  4 unit tests and exactly the 2 matching e2e checks fail
- Check total 1282 (1278 + 4) in README and wiki, computed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:58:31 +02:00
adminandClaude Opus 5 41772e53f7 Variables assets can keep their values from one run to the next (#166)
game-maker / suite (push) Successful in 8m21s
game-maker / gate (push) Failing after 3h5m52s
A game needs a high score or an unlocked level to survive the end of
a run. As agreed in the issue there are no save or load nodes. A
Variables asset gets a switch instead, and the existing Variables
nodes read and write the values as before.

- A "Persistent" checkbox in the Variables inspector. Toggling it is
  one undo step, and absent means not persistent.
- The behavior system loads the saved values at start and writes them
  after every frame that changed one, so a closed tab loses nothing.
- Values live in localStorage per project, not in the project file.
  Another browser or person starts fresh.
- Play and "Run in editor" save apart, so trying a behavior in the
  editor leaves the Play save alone.
- Saved values are read through the current declaration. A removed
  variable drops out, a new one starts from its default, and a changed
  kind is converted.
- "Clear saved values" in the inspector clears this asset in both
  areas. The main menu gets "Clear saved data" for the whole project.
  Clearing takes effect from the next run on.

Decisions:
- The switch covers the whole asset, not single variables. Mixing
  both means two assets.
- A changed default in the inspector shows only after the save is
  cleared. That is what persistent means.

Findings:
- The first e2e run died at "debug hook present" right after a Vulkan
  error from Chrome. The second run passed, so it was a GPU flake at
  start-up.

Tests:
- ./check.sh: 140 files, 2070 tests, exit 0 (before 138 / 2055).
- 15 new unit tests in tests/core/saved-values.test.ts and
  tests/editor/saves.test.ts.
- GM_STEPS=32,46 fresh: ALL PASSED, 24 checks. Six new checks in
  module 32 go through the UI. Module 46 knows the new menu entry.
- Counter-checks both fail as expected. Loading switched off: 2 unit
  tests and 2 e2e checks fail. Editor run writing into the Play area:
  1 e2e check fails.
- Check total 1278 (1272 measured by CI at bbbf341, plus 6).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:23:57 +02:00