A video used to run on its own clock as soon as a sprite wore it. An
animation could switch the sprite on and off, but never say which
picture shows at which frame. Cutting a video in an animation needs
the opposite: the animation sets the time and the video follows.
- Sprites wearing a video or a clip get two rows, Video clock and
Video time. They show only while the texture holds a video.
- With Video clock on Driven the video does not play by itself. It
shows the picture at Video time, counted from the clip's start.
Past the end it goes round with Loop and holds the last picture
without.
- Video time is a number like any other, so keys, step, easing and
curves give stills, slow motion and ramps.
- A driven sprite gets a player of its own, keyed by the wearer's
name. The same clip can run twice at different times.
- Following has two ways (`videoFollow.ts`). Running forward at 0.25x
to 4x the video plays at that rate and is nudged. A still, a scrub
or a step backwards seeks, and only the newest seek counts.
- Sound is heard only near 1x. The animation tab now plays it too, so
a cut can follow the sound. Scrubbing is silent.
- Scene, game view, the inspector's PREVIEW and the shader preview
show the driven picture. In a running game Play animation sets it.
Found on the way:
- After a scrub the video in the animation tab kept running. The
preview redrew only while the playhead moved, so nobody asked for
the video once it stood. `AnimationEditor.tick` now redraws it every
frame while it shows a video.
- Every sprite has two more defaults (`videoClock: 'free'`,
`videoTime: 0`). Eight tests in five files pinned the old list and
are updated.
Tests:
- Unit: video-clock and video-follow new, 4 files with 42 tests green.
- e2e 66 (melektaus-atx): ALL PASSED, 22 checks, 3 new. 66i: two
sprites on one clip stand at 0.2 s red and 0.7 s green. 66j: a
scrub to frames 2, 7, 12 and 3 shows red, green, blue, red, and
each colour still stands 0.7 s later. 66k: playing runs 10-30 ms
behind at rate 1.00-1.03 and is not muted.
- Counterchecks: a seek that keeps the rate turns the seek test red.
Not seeing Driven turns 66i, 66j and 66k red.
- Gate (melektaus-atx): exit 0, 297 files, 4546 tests.
- Not tested in the browser: Play animation in a running game.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A video becomes an asset like a sound: uploaded once, converted to one
format by a worker of its own, cut into clips in its tab, and worn by a
sprite through its Texture row. Only Chrome is a target, without alpha.
- Server: POST ?r=videos queues the upload under the hash of its bytes,
so the MP4's name is known at once. The worker (VideoWorker over
VideoFormat) writes H.264/AAC, yuv420p, constant frame rate, a
keyframe every 15 frames, at most 1920x1080, with a poster and the
details beside it. A failed job says why and is tried again on the
next upload.
- An upload may be up to 2 GB. It is streamed from the request to disk
and hashed there, so php-fpm holds none of it in memory. The other
routes keep their 256 MB.
- The worker runs in the container game-maker-video on stein2, a
compose project of its own (tools/video/), and in CI beside the PHP
server. The CI image has ffmpeg for it.
- Model: asset type video with size, length, frame rate and sound, and
clips like a sound's. The clip helpers and commands serve both.
- A sprite's Texture row offers videos and clips after the pictures.
resolvePicture names a playing video by a key, and the texture library
hands it to a VideoLibrary with one player per key.
- The editor shows a placed video as a quiet preview. Play starts it
over at its clip's start and lets it be heard through the master bus,
a pause holds it, Esc makes it quiet again.
- Video tab: the picture, a ruler, the clip strip and a track on the
Sound tab's time axis. A drag spans a clip, a handle moves an edge,
Play plays the picked clip.
- The picker shows a video on its way with its progress and adopts an
upload from it once it is ready. Dropping a video file does the same.
Decided on my own:
- Two sprites wearing the same clip share one player and show the same
picture. Control per instance belongs to the follow-up with nodes.
- A video plays in real time, the game's time scale does not slow it.
- The container may use 3 of the 4 cores of stein2.
Findings:
- The first real upload (279 MB) got 413: the API's own ceiling of
256 MB for every request, not nginx or the Brandmauer.
- Chrome has no picture of a video that never played, and a Pixi
VideoSource drawn before the first frame stays empty for good. The
texture is made in the first frame callback (openVideo).
- A player nobody asks for in 2 s is let go. The inspector's PREVIEW
asked only on an edit, so with the sprite not on screen its texture
was destroyed under it and the batcher threw on every frame. The
preview now redraws every frame while it shows a video, like a Time
shader. A let-go texture keeps an empty source instead of none.
- VideoSource.load() reloads the element and cuts off an earlier
play(). Playing starts after loadeddata.
Tests:
- PHP videos.test.php: 68 checks, with the real ffmpeg
- 300 MB upload through Brandmauer and nginx: 201 in 21 s, php-fpm at
13.7 MB RSS at most
- Unit videos.test.ts: 20 tests
- e2e 66 (stein2, atx was off): ALL PASSED, 18 checks; e2e 04 with the
new asset count green
- Gate (atx): 294 files, 4520 tests. serialize.test.ts used video as
its example of an unknown type and now uses a made-up one.
- Counter-checks: a keyframe gap of 30 fails the PHP check, a key that
keeps the video's end fails 2 unit tests, a clip without its end
fails "plays only its stretch" in e2e 66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A Load scene destroyed every object of the old scene. A player, a HUD
or a music object had to be built again in each scene, with its state
pushed through shared variables. A top-level object with the new switch
Keep on scene load now travels into the next scene as the same entity,
with everything below it.
- The switch is a common property right under Enabled, off by default,
on the asset and on a placed object. The inspector shows the row on
top-level objects of a scene only, as answered in the issue.
- runtime/keptObjects.ts holds the rules as pure functions.
keptEntities picks what travels, objectsToBring copies the stored
objects as the run parents them, and bringObjects puts them into the
next scene.
- World.instantiate takes a handed entity as it is instead of making
one. The next syncToModel then writes the run's values into the
scene.
- BehaviorSystem.release takes the running instances out before the
old world is destroyed, and the next system adopts them. They hear
no On destroy and no second On start, and their sounds play on.
- PhysicsSystem.resume gives a dynamic body the velocity and spin it
had.
- Duplicates: the object brought along wins. The next scene does not
build its own object with the same id, which is the way back into
the scene the object came from.
- Decided here: the next scene does not build a top-level object of
the same asset that is kept itself either. The issue only names the
id. With the id alone a level that places its own player would hold
two after a swap.
- Decided here: kept objects land behind the scene's own, the switch
is no animation row, and collisions are found anew.
A finding, fixed along with it: Load scene did not build a scene as it
is stored, but as the run had left it. A moved object stayed moved and
a destroyed one stayed gone. The way back into a scene needs a clean
build.
- GameRun copies a scene's objects and groups before it first builds
it and puts them back before it builds it again.
- GameRun.end puts every such scene back. The player calls it on stop,
so spawned objects no longer stay for "Click to play again".
- The consequence: an edit made in a scene during Play is gone the
next time that scene is loaded.
Tests:
- Gate: 199 files, 2944 tests (2929 + 15), exit 0
- e2e, full run: 1625 checks passed (1619 + 6), exit 0
- Smokes: 24 / 19 / 21, exit 0
- 15 unit tests in tests/core/keep-on-scene-load.test.ts
- Six e2e checks in module 35: the switch is set with a real mouse,
the child has no row, the object is in the new scene, its counter
still reads 1, its sound plays on, and the stored scenes are
unchanged after Stop
- Eleven tests with fixed row lists only count the new schema row
- 16 unit counter-probes all fail tests, run on stein2. One did not
fail at first (same id not left out), it has its own test now.
- The browser counter-probe fails 4 of the 6 new checks
- Not looked at in the browser: the switch as a port of the Properties
node, and a released game with a kept object
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading filled every missing asset property with its `default`. For
Sort with parent that is `false`, while the gold mark and Reset compare
against `initial`, which is `true`. So every sprite, text and particle
source saved before #232 counted as changed once loaded, and each save
wrote the `false` into the project file.
- normalizeBag leaves a missing key missing when it has its own
`initial`. It still reads as `false`, so older projects draw as
before, but they no longer count as changed.
- e2e module 4 failed in CI on three checks since 8b44f97. Its Reset
click hit the extra Reset of Sort with parent instead of Height.
- The #232 test deleted the key on the object instead of going through
the load path. It now checks that loading keeps the key missing and
marks nothing. serialize.test.ts no longer expects the filled value.
Tests:
- ./check.sh: 167 files, 2435 tests, CHECK EXIT 0
- e2e 1-5 chained: ALL PASSED, 141 checks (before the fix the same
three module 4 checks failed locally, as in CI)
- Counter-check: without the fix the extended test fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The fill of a sliced frame defaulted to `Stretch`. It is `Repeat` now, and the Fill dropdown lists
`Repeat` first.
The saved document keeps writing only the fill that differs from the default, so the key it
carries is now `stretch`, and a frame without it repeats. A frame saved before with the key
absent and meaning stretch would therefore tile; none of the stored projects has one - both sliced
frames on the server carry `fill: "repeat"` explicitly, so they draw as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.
The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.
A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.
The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sounds follow the road fonts already paved: the file goes to the server
content-addressed under its sha256, the project keeps a named asset that
points at it, and nothing is ever re-encoded. The format is read from the
file's own signature, not from what the browser claims - the same .m4a
arrives as audio/mp4, audio/x-m4a or as nothing at all.
Playing is two nodes. "Play sound" takes the asset and may raise or lower
its volume; every call is its own voice, so two jumps in a row are two
jumps heard. Its loop input is a choice of three - from the asset, on,
off - because a checkbox has to spend one of its two positions on "not
set" and would lose a case: an asset that loops could then never be
played once.
Below the graph core stays browser-free: NodeContext hands out an asset
id, BehaviorSystem resolves it against the project and folds the asset's
own volume and loop into the call, and only viewport/SoundLibrary knows
about Web Audio. A run without a sink plays nothing instead of throwing.
The CI browser is muted, so the ear is no witness: the debug hook counts
started and stopped voices, and step 34 reads that counter.
This reverses the design of the previous commit. `PropertyDef.scope` is gone
without replacement: a behavior variable is an object property again, without
exception, and sharing is something you point at rather than something you
declare twice.
A **Variables asset** is declarations and nothing else -- no graph, no position,
never placed, no tab of its own (`TAB_OF_ASSET` is already `Partial<>`, so that
costs no line). A `Variables` node names one through its new `variablesAsset`
setting, empty meaning the behavior's own variables as before, and then reads
and writes one value per key for the whole run. Its property list follows the
chosen source, which was the actual request: a value another graph declared is
now offerable, where before every participant had to declare the name itself.
That deletes more than it adds (-72 lines net): `globalConflicts` and its
run-stopping kind clash, `SetVariableScopeCommand`, the scope picker, both
object-inspector filters and the "shared by the whole run" note on the placed
object. A typo is no longer a silent second variable -- naming an asset is
picking from a list. And two Variables assets may carry the same key without
meeting, because the asset is the key, not the name. The one shape it copies is
the one that already existed: `Set state` picks its machine the same way.
Lifetime is unchanged. The bag is built at `BehaviorSystem.start()` from the
declarations and never written back, so playing in the editor cannot change the
project -- which is the whole reason a counter must not be an object property
(`World.syncToModel` would `commit()` it, and the coin counter would stand at 7
for good).
Full symmetry across that gap, without mirroring anything. Two named wires:
`Inspector.liveVariables` shows the values as they stand while a run is on and
falls back to the declarations when it ends, and `adoptDeclarations()` takes a
key into the run *only where the document changed it since the last sync*.
Without that condition any unrelated project edit would snap a running counter
back to its starting value; a counter-test bends exactly that and falls over.
Nothing was ever cached, incidentally: `GraphInterpreter` only pulse-caches
nodes without a `read`, and the variable node has one. The wire was missing.
The state machine `Scope` row is now an ordinary `enum` row through
`createFieldRow`. It was hand-built from an `HStack` with a ghost dropdown and
began ~34px left of every value above it; the explanations beside `Local` and
`Global` are gone from the menu, the sentence below the row keeps saying it.
Machine scope is otherwise untouched -- `StateMachineAsset.scope`,
`SetMachineScopeCommand`, `stateBag()` and its persistence all stand, so #59
stays closed as built and `Scope` as a type now carries machines alone.
Six counter-proofs, each biting at its own guard. One of them did not bite at
first: the suggestion list was computed twice, in `nodeSection` and
`portSection`, so only the browser test covered the request itself. It is one
named function now, `variableChoices`, with a unit test of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A variable or a state machine declared `global` keeps one value for the whole
run instead of one per behavior instance. The scope sits on the declaration --
`PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence
meaning `local` -- and `local` is never written, so every project made before
this saves back unchanged.
**The name is the key.** Two behaviors declaring a global `score` mean one
variable; a behavior declaring `score` locally keeps its own. Kinds that
disagree about one global name stop the run before it starts, with the two
behaviors named; starting values that disagree are a warning on the behavior
that declares it, and the first declaration wins (`globalConflicts`).
The runtime needs no redirection layer. A global variable falls out of
`schemaForObject`, so it never reaches `entity.props` -- and then the whole rule
is "whoever declares the name takes their own value" (`graph/sharedState.ts`).
The `Variables` node is untouched and stays transparent: switching a variable to
global breaks no existing graph, because `GraphInterpreter.readAll` builds a
fresh object per read anyway and only asks `port.key in all`.
That same cut answers the inspector. A shared value has no owner, so the placed
object shows no row for it and can hold no override -- `SetVariableScopeCommand`
takes the deviations that were there and undo gives them back.
Both places say it in words rather than leaving a glyph to be guessed: the
declaration explains the mark below the list, and the placed object names the
variable it no longer holds instead of claiming the behavior declares none --
which is what it said before, untruthfully. Screenshots of the e2e run turned
that up; no check had been looking at it.
`SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from
the *declarations* rather than from any instance (a global variable exists
before its behavior is placed), and is thrown away with the run: nothing is
written back, not even in the editor run. `GraphInterpreter.call` passes it on
through `...ctx` while nested calls keep getting fresh local variables, which is
why `program.defaults` is stripped of global keys.
Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or
the inspector does not rebuild after the switch -- and a state machine has no
rows at all, so its scope is appended to the signature by hand. And the object
inspector does not go through `schemaForObject` for behaviors, it reads
`asset.variables` directly; filtering in one place was not enough, and no unit
test covered that path. The e2e caught it (`global: true` where false belonged),
and the rule now lives in one named function, `objectRows`.
Scopes `object` and `scene` are deliberately not built. The object case already
has a route (`Object by name` -> `Properties`), and a run knows exactly one
scene, so `scene` would be indistinguishable from `global` until #39 exists.
Closes#59: a machine's scope is a word on the machine asset, not a setting per
`Set state` node, so the same machine cannot live in two places at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`deserializeProject` builds each asset's effective schema in pass 1 and coerces
`props` against it in pass 2. For a behavior that schema was
`[...COMMON_PROPS, ...variables]` -- but `runInEditor` lives in
`ASSET_SCHEMAS.behavior`, not in `COMMON_PROPS`, so `normalizeBag` dropped the
value as undeclared. The flag therefore survived no save: every reload switched
it back off.
The fix is the full schema, `schemaFor('behavior')`, which is what
`schemaForAsset` hands out everywhere else.
Found by a round-trip test comparing the whole project rather than one field. An
existing test had the loss written down as its expectation
(`{ enabled, speed }`), which is why nothing ever reported it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.
The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.
Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.
Co-Authored-By: Claude Fable 5.1 <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>
Images are uploaded to a content-addressed blob endpoint and stored as
data/textures/<sha256>.<ext>; the project only keeps the file name. The
sprite points at a texture asset through an ordinary property of the new
kind `asset`, so instance overrides, "Apply to container", "Reset to
asset", undo and `Set property` work without any extra code. The new kind
also replaces the ad-hoc asset picker the graph nodes faked as an enum.
Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V.
Images wider than 2048px are downscaled client-side and re-encoded to
WebP if needed, because nginx caps uploads at 1MB; an image that already
fits is uploaded byte for byte so pixel art is never resampled.
The sprite colour default changes from red to white, since colour tints
the texture multiplicatively and a tint of white shows an image as it is.
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>
- SceneObject.editorOffset (x/y): shifts only where an object is drawn in
the Scene tab, cascades to children, undo-able with a reset action.
Never written into container templates.
- On click without an owner now fires for any click inside the camera
frame instead of hit-testing the (editor-only) behavior symbol.
- seed-jumprun: coins are a "Coin" container (sprite + trigger), nested
containers are expanded recursively, Destroy targets the coin's parent,
and the clock uses a Ceil node instead of ToText.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Engine additions that the example game needed:
- `enabled` as a shared property of every placeable asset (default on, an
override on the scene object). A switched-off object is not drawn, does not
collide and its behavior does not run - and neither does anything parented
below it. Disabled objects are now hidden in the scene view as well, so the
editor shows what the game shows; they stay in the hierarchy panel, which is
where they are selected and switched back on.
- Asset type `text` (text, fontSize, color, align), rendered in scene and game.
- Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`,
`Is hovered`, hit-tested against colliders, else sprite/text footprints.
- New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text,
Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game.
Tooling: shared CDP harness (tools/harness.mjs), central project registry with
a guard that keeps destructive scripts off the playground (tools/projects.mjs),
plus smoke suites for the demo scene and the jump-and-run game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Position is now { x, y, z }; higher z draws on top, ties keep hierarchy order
- Viewport sorts the objects layer deterministically (syncDrawOrder)
- Inspector shows a Z field; asset properties that differ from the schema
default are marked bold with a reset button (ResetAssetPropertyCommand)
- isModified() helper, unit tests, E2E checks and seed updated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript.
- core: asset model (Scene/Camera/Sprite), scene objects with sparse
overrides, property schema + resolve, command stack (undo/redo),
serialization and project store interface (16 unit tests)
- ui: custom in-canvas widget toolkit (panels, fields, color picker,
list/scroll, popup) with a hidden <input> keyboard bridge
- editor: asset/hierarchy panels, schema-driven inspector, toolbar
- viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan
- runtime: world/entity instantiation for play mode, input, camera follow
- api: PHP JSON project store with revision counter (409 on conflict)
- tools: CDP-based e2e smoke test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>