A tile could be solid, one-way and scaled, but not report without
blocking. Water, lava, spikes or a goal zone needed a trigger object
of their own laid over the map. Now a tile has a switch Trigger, and
the collision events name the tile and the cell they met.
- A tile has the switch "Trigger (reports, never blocks)" under
Solid. Its cells hold nothing up: no Move, no falling body, no
ray. They raise On collision enter, stay and exit with the tilemap
as other.
- The three events have the outputs tile, cell x and cell y. Without
a tile, tile is empty and the cell is -1, -1.
- The three events have the input per cell. Off, the default, gives
one event per area: cells of the same tile that touch side by
side. On gives one event per cell.
- The scene view outlines trigger cells in yellow like a trigger
collider, merged to rectangles but never with solid cells.
- Is touching with the map counts trigger tiles, and Set tile works
while the game runs.
- Both systems find the touches with the same overlap check in
runtime/systems/tileTriggers.ts, not through the physics engine.
Decided here:
- Trigger wins over Solid. A tile with both does not block, as with
a collider.
- Water beside lava is two areas, and so are two ponds that touch
only at a corner.
- An area names the cell nearest to the centre of the collider, and
on exit the cell it named last.
- The solid cells of a map still do not name their tile.
- Normal, impulse and point are 0, as with a trigger collider.
- Collision scale and outlines shape a trigger cell like a solid
one. One-way, friction and bounciness have no effect.
Tests:
- Gate on stein2: 205 files, 3104 tests (3067 + 37), exit 0
- e2e 39,40: 106 checks. e2e 1-8,39-43: 490 checks. Both on
melektaus-atx. No full run, the sum of 1648 (1645 + 3) is
reckoned and CI measures it.
- 37 unit tests in tests/core/tile-triggers.test.ts, nearly all
against both systems
- Three e2e checks with a real mouse in block 40l-iv
- 24 unit counter-probes fail tests. Two did not at first, tests
were added for them.
- Two browser counter-probes fail their checks
- Two screenshots looked at (yellow outline, the node with per cell)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The issue asked for three nodes: Set layer, Set collides with and Layer
of. A probe showed that the finding behind the first two is outdated.
The Properties node of type Collider already writes both settings while
the game runs, and both collision systems see the change at once. As
agreed, only Layer of is built, and writing stays with Properties.
- Layer of is a data node with the input object and the outputs name
and number. The name is what the project calls the layer.
- NodeContext.collisionLayerOf answers the number, and the new field
collisionLayers on GraphLookup brings the names to the node.
- layerHolderOf in solids.ts says whose layer counts. An object stands
for its first collider child, a collider or tilemap for itself.
- Decided here: a collider that is switched off counts too, because
its layer is set either way.
- Decided here: without a collider the name is empty and the number
is -1.
- The node is a read, not a run, so it answers at the moment it is
asked. Right after a Set in the same flow it names the new layer.
- The output number was not asked for. It fits the layer port of
Properties as it is.
Changing the layer during a run had no test at all so far. Five tests
now hold it in both systems.
Tests:
- Gate: 198 files, 2929 tests (2914 + 15), exit 0
- e2e 1-6: 285 checks passed (06: +2 for block 6z-x)
- Five unit tests for the change during a run: a mover is blocked,
passes and is blocked again, a ray with a layer filter follows the
change, and a dynamic body falls through the ground in Play
- Six unit tests for Layer of, three against each system, and four
cases in the node catalog
- 6z-x finds the node in the palette, checks its ports and logs
"Ghost" and "1" for the owner's collider in a Play run
- Two counter-probes for the change during a run fail 1 and 4 tests,
and only new ones, so those places were unguarded before
- Six counter-probes for Layer of fail 2 to 3 tests each. All eight
ran on stein2, not in the container.
- The browser counter-probe fails 6z-x with "Layer 1" and "1"
- The e2e sum of 1619 is calculated (1617 measured in CI run 276,
plus 2). Neither the full suite nor the smokes ran for this state.
- Not looked at in the browser: that the layer port of Properties
shows the same picker as the inspector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Issue #193 asked where friction, bounciness and density of a tile
live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a
collider could not use the outline drawn on a texture frame (#141).
Both gaps are closed here.
- A collider has a fifth shape "Frame collision" with a Frame row.
The frame is picked like a sprite's texture and stored as the
value 'frame'.
- The shape is linked, not copied. Redrawing the outline in the
texture tab changes every collider using that frame, in the editor
and on the next play.
- The frame centre sits on the collider origin, one image pixel is
one unit. The shape turns, scales and mirrors like any other.
- Every outline ring blocks. Each ring is split into convex parts the
same way a polygon is.
- Without a frame, with a whole texture or with a frame that has no
outline, the collider is drawn red as a box and blocks nothing.
- Move, Rapier, raycast, detection, hit test, drawing and footprint
all resolve the frame. At runtime an entity looks the frame up
live in the current project.
- A tile has Friction and Bounciness next to Solid and One-way. The
rows only show when Solid is on. Defaults and mixing rules match
the collider. Density is left out because a tilemap never moves.
- Tilemap cells are grouped by surface as well as by one-way angle.
Ice and stone never merge into one rectangle, and every cell brings
its surface to Move and Rapier.
Findings:
- A tile with a collision scale below 1 is no longer flush with a
neighbour of another surface. It is inset there like at an open
side, the same as next to one-way tiles today.
- A bounce test first measured 0.84. The ball had landed on the hero
at the origin, not on the tile. On the tile it measures 0.993.
Tests:
- ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917).
- 14 new unit tests in tests/core/surface.test.ts. The Move tests run
against the editor run and the physics system.
- 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider
has frame size, a walker climbs the slope in the frame).
- GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks.
- Three counter-probes bite: Move ignores the tile surface (2 fail),
Rapier ignores it (1 fails), frame shape falls back to a box
(5 fail).
- Check sum 1198 is computed (1195 + 3), the full run belongs to CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>