A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.
- The project has a list of up to 16 render layers, edited in Project
settings next to the collision layers. It is a list of its own, read
by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
`Render layer` under their switches. Its default `Inherit` takes the
parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
camera is the first one switched on without Overlay, so two plain
cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
one see-through view per overlay camera over itself. That view is a
`WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
takes the list it sets.
Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).
Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
red main sprite at z 1000, in the preview and in the running game.
A `UI` sprite in front of the main camera and a `Default` one in
front of the overlay stay away. Without Overlay the second camera
draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page of a released game always drew the camera at Auto, the
largest whole step that fits. A camera of 300x180 in a window of
1280x800 stands at step 4 and leaves a border left and right. The
issue asks for Fit next to it. Answered there: a setting of its own
rather than the scale of the Game tab, and no switch for the player.
- The project has a field gameLinkZoom, 'auto' or 'fit'. A document
without it reads as 'auto', so nothing changes for a project that
is there already. No format bump.
- Project settings has a row "Link zoom" with a menu of Auto and
Fit. It is an undo step of its own, SetGameLinkZoomCommand.
- GameScreen hands the field to cameraView where it passed a fixed
'auto'.
- gameScale, the scale of the Game tab, is not touched and does not
reach the link.
- The released copy is frozen, so the change reaches the link only
with "Update release".
Decided here:
- The row is called "Link zoom", not "Game link zoom" as proposed.
The longer label is wider than the label column of the dialog and
pushed its button 12 px out of line with the others.
- The undo step keeps the name "Game link zoom", which is what the
tooltip of the undo button shows.
- A note under the row says "The released game at the largest whole
step that fits, or filling the window."
Tests:
- Gate on melektaus-atx: 211 files, 3227 tests (3219 + 8), exit 0
- e2e 60 on melektaus-atx: 19 checks, all pass
- e2e 35,46,59,60: 74 checks, all pass
- No full run and no smokes. The sum of 1671 (1668 + 3) is reckoned,
1668 is what CI counted for 9e869df.
- Eight unit tests: the default, saving and loading, values that
are not a choice, undo, gameScale left alone, the factor of Fit
for a window larger and smaller than the camera, and the rule
behind Apply.
- Three e2e checks in block 60i of module 60, with the real mouse:
Auto to Fit in the dialog, update the release, open the link
without a session and measure scale, frame and pixels. Fit reads
4.267 where Auto reads 4.
- Nine unit counter-probes and three browser counter-probes all fail
their tests.
- Not checked in the browser: Fit in a window smaller than the
camera, which has a unit test, and Fit under a display scale above
100 %. Nothing was looked at after the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the behaviors fixed, the scene Performance test still ran at
about 23 fps with 500 entities. The rest of the frame went to the
editor: every spawn or destroy rebuilt the hierarchy with all its
rows, redrew the scene view behind the game tab and refreshed every
panel. The game itself is cheap. What the panels show of it now
follows a preset in the project settings.
- New section While playing in the project settings with three
presets: Live, Balanced and Minimal. Every project reads as
Balanced, including those saved before the setting existed.
- Live keeps today's behavior. Balanced brings hierarchy and panel
values up to date 4 times a second and stops redrawing the hidden
scene view. Minimal updates hierarchy and values only on pause and
throttles the graph tabs as well.
- The graph and state machine tabs stay live under Balanced. At 4
updates a second the beads on exec edges stutter, and the tabs
draw only while on screen. This differs from the issue proposal.
- The game itself is never throttled. Pause, step and stop bring
every panel up to date at once, and a change of structure that
falls between two updates is kept for the next one.
- The rule lives in editor/playThrottle.ts as panelPolicy and
PanelClock. PlaySession asks it after every frame.
- Objects spawned at the top level go into a hierarchy group named
after their asset, so hundreds of gold pieces fold into one row.
Groups start folded.
Measured on a throwaway copy of Performance test, 3 s each, fps:
- About 500 entities: Live 38.6, Balanced 56, Minimal 60.6.
- About 315 entities: Live 56.9, Balanced 60.5, Minimal 60.6.
- p95 frame time at 500 entities: 60 ms, 41 ms and 23 ms.
Tests:
- ./check.sh: 193 files, 2799 tests, CHECK EXIT 0.
- GM_STEPS=1,18,59: ALL PASSED, 54 checks.
- Full e2e run (local): ALL PASSED, 1562 checks.
- tests/editor/play-throttle.test.ts: presets, update rates, kept
structure changes, flush, spawn group and loading old projects.
- New e2e module 59: preset chosen with the mouse, measured update
rates, scene view behind the game tab, Minimal with pause.
- Module 03 now expects spawned objects in their group row after the
next update.
- Counter-checks: 4 in unit tests and 2 in the browser, each failing
as expected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Load scene sets a flag on the World like Stop and Restart do; the swap
happens after the tick, and the last wish of a tick wins. Exactly one
thing crosses over: the SharedState, so global variables and global
machines keep what they know while everything local is built anew.
Loading the scene that is already running rebuilds it, which is what
"reset the level" means when the score must survive.
A run starts either from the start scene (a new project field, set from
the asset panel and marked with an arrow in the list) or from whatever
scene is open. The toggle next to Play says which question is asked and
remembers the answer in localStorage: it is a way of working, not part
of the game.
Sounds no longer end because a scene ends. A voice belongs to the
behavior that started it and stops right after On destroy, so the scene
swap is no longer a special case at all. For the enemy that plays its
explosion and destroys itself in the same breath there is now a
detached input on Play sound.
The scene selector stays usable while a run plays, because a double
click in the asset panel already did the same thing unguarded. It moves
what the editor looks at, not what the run plays.
A reference search over every export and method in src/ found code that
only tests still called, or that was never called at all:
- RemoveSceneObjectCommand: the product only removes through the
multi-object command; the tests now use that one too.
- NodeRegistry.requireNodeDef: a second function of the same name in
commands/require.ts raises a CommandError instead of throwing, and is
the one the product uses. The twin invited the wrong import.
- isTextureAsset, listContainers, AssetTabEditor.show,
FocusManager.destroy, KeyboardBridge.selectAll and the getters
ColorField.value, ListView.selected, NumberField.number.
Also: Theme.controlActive now matches the resting control colour, and a
new note asks what editing the graph during play would take.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
findPropertyUsages walks override sites, so Project.ts imported instance.ts
while instance.ts imported Project.ts – the repo's only value-level import
cycle. The queries now sit in usages.ts and Project.ts is a base type module
again. requireScene has had no caller since holderId replaced sceneId.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scene now stores only the container object; its content is resolved from
the template on every read, with deviations kept as sparse instanceEdits on
the container object. This drops the propagation bookkeeping entirely --
templateSync.ts is gone and the three structural gaps (add, remove, reparent)
cannot diverge any more.
Values stay editable on an instance, structure does not; Unpack writes the
content into the scene, keeping ids. Play mode bakes containers into the
throwaway copy before entities are built. No migration: formatVersion is 2
and the seeders write the new format.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Container tab reuses SceneViewport against any object holder, so a
container template is edited exactly like a scene. Right-click no longer
changes the selection, which previously made nesting impossible: opening
the menu on a container loaded it, and "Place in container" then targeted
itself.
Template edits (move, rename, override) now reach already-placed instances
per slot, through a shared templateSync used by both the apply path and the
plain object commands. Copies the user has touched keep their own value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.
ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.
__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
measured against the existing style); Markdown excluded because table-cell
padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
"this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
tests in one go.
Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.
Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
events now; the DOM handlers are thin adapters.
Compare fell back to == without a configured operator although the schema
default is <; aligned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>