Commit Graph
341 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 a5ae21481b Type point values, snap and scrub the preview in the curve tab (#248)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m9s
Building a curve by mouse alone was fiddly: there was no way to put a
point on an exact value, no grid, and the running preview could not be
stopped at a chosen moment. The hand cursor over points also looked
like a link instead of a drawing tool.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:45:23 +02:00
adminandClaude Opus 5.5 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 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 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 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 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 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