Commit Graph
70 Commits
Author SHA1 Message Date
adminandClaude Opus 5 6a31f5e0ad One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:36:11 +02:00
adminandClaude Opus 5 32057a96fc Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
game-maker / gate (push) Successful in 1m52s
game-maker / suite (push) Successful in 6m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:56:25 +02:00
adminandClaude Opus 5 1b5e386b60 Move with collision reacts along the contact normal: it travels to the surface, stays flush and slides the rest of the move along it with Coulomb friction (the mean of both colliders, a tile counts as 0.5), a circle now rolls over an edge instead of catching on it, pushed dynamic bodies move off along the normal, Move gains the outputs hit, normal x and normal y, an object without its own collider moves freely, and editor run and play share the same path through Solids; contact point and normal come from our own f64 geometry (contactBetween), because Rapier's castShape lands 3e-6 px inside the wall in f32 (#150)
game-maker / gate (push) Successful in 1m48s
game-maker / suite (push) Successful in 6m23s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:52:11 +02:00
adminandClaude Opus 5 78899b1ace Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
game-maker / gate (push) Successful in 1m46s
game-maker / suite (push) Successful in 6m17s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:11:43 +02:00
adminandClaude Opus 5 bffa508ee7 A tile gains Collision scale: below 1 each blocking cell pulls in on the sides without a blocking neighbour and stays flush towards its neighbours, so a wall of bushes becomes a narrower wall without gaps and the inner corner of an L closes cleanly; above 1 the cell grows at its open sides; on several layers the largest value counts, and the merged rectangles work in fractions of a cell with exactly the old result at 1 (#140)
game-maker / gate (push) Successful in 1m43s
game-maker / suite (push) Successful in 6m27s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:00:07 +02:00
adminandClaude Opus 5 4728f4fab5 A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
game-maker / gate (push) Successful in 1m43s
game-maker / suite (push) Successful in 6m8s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:58:03 +02:00
adminandClaude Opus 5 c2fd536aaf The texture field only offers pictures: each texture with its frames, no animations any more; an animation id left in an older project shows as a missing reference (#143)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 6m8s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:14:01 +02:00
adminandClaude Opus 5 4c411c1ff3 Tilemaps can block movement: a tile gains a Solid switch, the solid cells of all layers are merged into rectangles and the placed tilemap takes part in the collision system as one body, reported as other in collision events (#132)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m3s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 09:47:03 +02:00
adminandClaude Opus 5 732f6518ae The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 5m56s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 08:01:07 +02:00
adminandClaude Opus 5 33a7b1b63e Tilemaps are a new placeable asset type that is painted with autotiles in the scene view, with layers, brush, eraser and rectangle, and is drawn at runtime and in the export (#128)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m51s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:41:47 +02:00
adminandClaude Opus 5 1a9f07c4ac Autotiles are a new asset type with a tab that shows every tile of the floor (2x3) or wall (2x2) block and an animated sample pattern (#118)
game-maker / gate (push) Successful in 1m50s
game-maker / suite (push) Successful in 5m59s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:35:29 +02:00
adminandClaude Opus 5 2a390e52ff The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
game-maker / gate (push) Successful in 1m36s
game-maker / suite (push) Successful in 5m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:19:53 +02:00
adminandClaude Opus 5 4ed2edd0f0 An animation is a value a graph can pick, and playing a running one makes it win again (#97)
Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:47:31 +02:00
adminandClaude Opus 5 193378e6a9 An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes.

- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
  property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
  between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
  relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
  start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
  builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:02:25 +02:00
adminandClaude Opus 5 94a0a578ed A border drawn in pixels is as wide as the texture says a pixel is, so a small sheet can frame a large sprite (#96)
A sliced frame drew its borders one source pixel to one sprite unit. Pixel-art sheets are small:
a 16px corner on a 400-unit button is a thin line, and the only way around it was a larger image.

The texture now carries `Nine-slice scale` (default 1), next to `Filter`. It sets how many sprite
units one border pixel takes, for every sliced frame of that texture. The UVs stay as they are -
a corner shows the same pixels, only larger or smaller. When the sprite is too small for both
borders they are still scaled down together, now starting from the scaled size. With `Repeat`
the tile size scales too, otherwise a doubled border would sit beside a pattern of the old size.

The value is a schema row, so inspector field, saving and undo come without their own code.
Anything that is not a size above 0 reads as 1, and documents without it draw as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:09:43 +02:00
adminandClaude Fable 5.1 cc6db5f350 Three stores, three importers and three pickers that differ only in their type are one of each (#85)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:38:54 +02:00
admin c990dbcb26 A name that a sprite already owns cannot name a property, and a division by zero cannot answer (#63, #64)
game-maker / suite (push) Failing after 11m29s
#63: a shader Property node named "width" quietly took over the sprite
row of the same name. isSpriteProp lives in PropertySchema now and is
derived from FULL_SCHEMAS.sprite instead of being a second list that
drifts. isValidPropertyName rejects those ten names, and the compiler
reports it at the node as its own problem: "width" is a sprite
property, told apart from "not a word" and from a duplicate.

#64: Divide emitted (a / b) and handed NaN to the driver. It emits
divide(a, b) now, with the guard in the prologue next to straight(),
scalar and per channel: zero divided by zero is zero. That was already
the house rule in the behavior and particle graphs - only the shader
had never been told. Clamp comes along as a shader node for the same
reason: the behavior catalog has had it all along.
2026-09-15 18:43:40 +02:00
admin 1e9d21cd5d A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
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.
2026-09-15 13:24:26 +02:00
admin 5671a38044 A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
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.
2026-09-15 07:54:28 +02:00
rootandClaude Opus 5 8143130ed2 A shared value belongs to no behavior, so it becomes an asset of its own (#91)
game-maker / suite (push) Failing after 8m59s
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>
2026-09-14 18:05:36 +02:00
rootandClaude Opus 5 d1fcfe8b81 A shared value belongs to the run, so a declaration carries its scope (#91)
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>
2026-09-14 16:22:53 +02:00
rootandClaude Opus 5 d5e67cd80c A shader costs a source nothing, because a field was already one draw call (#88)
game-maker / suite (push) Successful in 10m9s
A particle source can wear a shader asset the way a sprite does, and it is the
cheapest place in the editor to wear one: a shaded sprite gives up its batch and
costs a draw call of its own, while `ParticleContainerPipe` breaks the batch for
every field regardless. Five hundred shaded particles are the one call five
hundred plain ones were.

The fragment the compiler emits needed no change, only a second vertex stage:
Pixi's particle vertex is GLSL ES 1.00 and ours is ES 3.00, so it is translated
rather than invented. What it sees is a particle's picture, not its life — the
buffer carries five fixed attributes and takes no sixth, so age, index and seed
stay on the graph, which is what the graph is for.

Two things the issue did not ask about:

`Shader.resources` is an accessor over the names its constructor declared, so a
slot that is not declared becomes a loose JS property and is never bound. The
adaptor writes `uTranslationMatrix` into whichever shader the container names —
without an empty block for it the vertex gets no matrix and the particles land
elsewhere.

`schemaOf` in serialize.ts hung on `schema !== schemaFor('sprite')`, an identity
check a source never satisfies, so its shader values would have been dropped on
save.

A source is the one asset drawing rows from two graphs, so a name can arrive
twice. Its own row wins and the uniform keeps its default; nothing is rejected
on the shader's side, because the collision exists only at this one wearer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:06:45 +02:00
rootandClaude Opus 5 78baf0c719 A source wears an icon without becoming decoration (#89)
game-maker / suite (push) Successful in 9m49s
Behavior and Container draw an icon because isSymbolType() is true for them,
but that predicate does not mean "draws an icon" — it means "is editor
decoration". Four places read it that way: collectWorldItems, GameViewport.
collectFrame and syncDrawOrder all drop symbols out of the running world and
the game picture, and placementOf() gives them an upright unscaled box.
Listing 'particles' there would have handed the source an icon and taken away
its particles, in the scene view as well as in the game.

So the two questions are two functions now. hasSceneIcon() says a type draws
an icon; isSymbolType() keeps meaning decoration. The icon itself is a
SceneViewport decoration, which is editor-only for free because GameViewport
never syncs decorations.

A ring (r 10, #c26a1f) with four sparks, upright and unscaled like the other
symbols, its middle left open — unlike Behavior and Container a source draws
something of its own, and the field is densest exactly there. The name sits
beside it and a dashed line runs to the parent, same colour.

Found while writing the tests: the source's grab box scaled with its transform
while the icon did not, so the two came apart. placementOf() returns only the
translation for 'particles', making the box the upright 24 x 24 that covers
its icon. The source's own transform still feeds every particle untouched.

824 checks (821 + 3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 08:25:49 +02:00
rootandClaude Opus 5 8ae20d044f Time is an argument, so speed scales it and a prewarm offsets it (#87)
game-maker / suite (push) Successful in 9m48s
Speed and Prewarm are not state and not a second loop: both are arithmetic
on the clock argument the field already passes to every particle formula.
fieldTime = elapsed * timeScale + prewarmOffset, a pure rule next to
liveRange, called at the one place ParticleField.sync forms its time, so
preview and world inherit it together.

Prewarm offsets by one lifetime because that is the smallest offset which
fills a continuous emitter; a burst is dead at time >= lifetime, so the
same offset would skip it entirely and it is suppressed there.

The tab's slider counts wall-clock seconds, so its span divides by the
scale. A missing row falls back to 1 / off, so old project files need no
migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 07:31:07 +02:00
root 0f9e1611d1 A particle is a function of its age, not a thing that is stepped (#73)
game-maker / suite (push) Failing after 4m22s
The asset type `particles` is placeable and carries a graph at the same time —
`behavior` was the blueprint. Because a continuous emitter gives birth to particle n
at time n/rate, the whole field is a function of the clock rather than of the frames:
no ring-buffer bookkeeping, exact time jumps, reproducibility as a measurable property.

compile.ts follows compileShader's frame but emits JS closures into a Float64Array
instead of GLSL, and without eval; a node with two consumers is evaluated once.
Drawing is one Pixi ParticleContainer per source, so one draw call.

Collision, wind and braking in flight are out for good — that is the price of the
formula model, not an omission.

The tab count goes from nine to ten, which is exactly the case #84 was built for one
day earlier: at 1280 px the row no longer holds every tab, so guard 14b now asks for
hidden.length > 0 instead of === 0.
2026-09-14 05:44:25 +02:00
rootandClaude Opus 5 86b5363a01 Fonts: a new asset type, picked from the server or dropped in, worn by text objects (#74)
game-maker / suite (push) Successful in 9m10s
A font asset holds a file and nothing else, the twin of a texture: the server keeps it
content-addressed under data/fonts/{sha256}.{ext}, with the format read from the file's own
four-byte signature (finfo reports fonts inconsistently across libmagic versions). A text
wears one through its ordinary Font property, so overrides, reset, undo and the Properties
node come for free; no font means the editor's own typeface.

The picker lists everything the server holds and renders each row in the face it offers.
The family name is gm-font-<hash16>, never the name inside the file, so two "Regular" cannot
collide. WorldViewport.syncText is the single place that draws it, which covers scene, game
preview and play at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:09:59 +02:00
rootandClaude Fable 5.1 016e83c9b5 Transform: scale, rotation and mirror as properties, world transform as a matrix chain, oriented colliders (#76)
game-maker / suite (push) Successful in 9m5s
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>
2026-09-13 17:25:06 +02:00
rootandClaude Opus 5 5c1c3c6fd6 Properties node: name each shader param by its shader (#66)
game-maker / suite (push) Successful in 8m23s
The + Property menu of the Properties node offered every shader's params
for every sprite. Each shader row now carries its shader name, and a name
shared by two shaders lists both: "brightness (Adjust colors, Neon glow)".
Sprite-owned rows stay unlabelled.

The note hangs off a map keyed by the def object, not by the name -- a
sprite property and a shader param may share a name (#63), and keying by
name would label the wrong row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:05:01 +02:00
rootandClaude Fable 5.1 06e1494f94 Textures: Linear/Nearest labels; prove the filter pass does not resample pixel art (#68)
game-maker / suite (push) Successful in 8m13s
Dropdown labels are now Linear (default) and Nearest; the stored values
linear/nearest are unchanged, old project files load as before.

e2e 12c-ii measures in Play at camera zoom 1.37 (screen scale 1.434): a
Nearest edge sampled across seven pixels is byte-identical with and without
a neutral Adjust colors shader, and brightness 0.5 on the same path darkens,
so the pass really runs there. New hook __gm.gameRect(id).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 12:53:21 +02:00
rootandClaude Fable 5.1 42cafa7543 Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
game-maker / suite (push) Failing after 3m59s
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>
2026-09-12 22:41:14 +02:00
rootandClaude Fable 5.1 e7d82bd1a9 State machines: asset, tab, State branch and Set state nodes (#51)
A state machine is an asset whose graph holds one `State` node per state
(no ports, name as title), edited in a new "State machine" tab that reuses
the graph canvas: right-click places "State n", snap toggle shared with the
Behavior tab via SnapToggle. `statemachine` is a value kind like texture
(port, literal node, variable, struct field, tagged list item).

Behaviors use it through `State branch` (one exec output per state) and
`Set state` (one exec input per state; the interpreter now hands `run` the
entry port). States live per behavior instance in NodeContext.states, with
no initial state. Ports derive from the inline picker or a wired
`State machine` literal; BehaviorLookup became GraphLookup with
behavior(id) and machine(id).

The Graph tab is labelled "Behavior" (key unchanged). Unit tests
(state-machine, kinds, catalog, ports), e2e step 26 and README added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 18:00:23 +02:00
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +02:00
rootandClaude Opus 5 3ed8b68ecf Describe every value kind in one table instead of spreading it over the code
A kind like colour or texture used to be spelled out in five parallel
lists, its port type, its empty value, its glyph, its label and its list
tag - 22 literal places for texture alone. core/properties/kinds.ts now
holds one row per kind and the rest is derived from it, so adding a kind
is one row. The five literal nodes collapse into literals.ts, the six
kind menus into one helper.

Number, Text, Boolean is now the one order everywhere; the variable and
port menus used to list Boolean before Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:47:17 +02:00
rootandClaude Fable 5.1 8a7185948d Make texture and colour full kinds and drop the load-time migrations
- `Texture` and `Color` can be picked as a variable kind, a struct field
  kind and a declared port kind; a new `Texture` node hands a picture id
  into the graph. Declared, a texture is its bare id – the `$texture` tag
  stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
  `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
  `migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 14:06:14 +02:00
rootandClaude Fable 5.1 11c900c998 Drop the per-list item declaration; every item carries its own kind
A list no longer says what its items are. ListDef.items, ListDef.fields,
ListFieldDef.items and SetListItemsCommand are gone; the single seam is
src/core/properties/listItems.ts. JSON already tells number, text, boolean
and struct apart, so only two kinds need a tag: {"$color": n} and the new
{"$texture": id}. Field names cannot start with "$", so a tag never
collides with a struct field. On a data link a texture item arrives as
its bare id; the graph has no images.

List tab: the mark in front of each row is the kind picker, "+ Item"
asks for the kind, both are ordinary value edits (one undo step). A list
inside a struct field only offers the flat kinds because the depth rule
forbids a struct item there (ListAccess.kinds); the menu used to offer
it and silently do nothing.

Legacy lists that declared items: 'color' are tagged on load.

Actor demo: the four walk animations and four standing frames live in
two list variables of texture items indexed by facing (Item at), so the
graph can be rebuilt in the editor. The stand frames get their grid ids
back; 44 nodes instead of 46.

Also: the user's edits to on-start-during-play.md and the new note
reseed-because-of-list.md.

Verified: check.sh 794 tests, full e2e 631 checks, smokes 24/19/20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:35:02 +02:00
rootandClaude Opus 5 1135bf4562 Play sprite animations from an animation asset worn through texture
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.

A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.

The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.

Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:52:34 +02:00
rootandClaude Opus 5 58f19a3451 Replace the property nodes with one Properties node
Get/Set property, Get/Set properties, Set enabled and Set text are gone.
A single Properties node takes a fixed "object" input; every chosen
property is an output that reads the current value, and one switched to
Set is also an input written when the flow arrives. Exec pins only exist
while at least one entry writes.

The node carries a type (Sprite, Text, Collider, Camera, Container,
Behavior, Any) that decides what "+ Property" offers: the fields of that
type, the variables of the chosen behavior, or for Any a kind to pick
and a name to type. Each port takes the default and field of its type,
so a Camera width starts at 640. Switching the type keeps the ports and
marks those the new type lacks. At runtime the type is only an editor
aid; the node writes to whatever object it gets.

Old project files are converted on load. The type is inferred from the
keys: the first object type that has them all, else the one behavior
that declares them all, else Any. enabled belongs to every object, so a
converted Set enabled lands on Any.

The jump-run and tetris seeders use Properties throughout; draw-piece
switches its cell on in the same node that sets the colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:40:47 +02:00
rootandClaude Opus 5 854ae2e756 Add Get/Set properties nodes and a breadcrumb for opened sub-behaviors
Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.

The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.

Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.

The "Add node..." header button is gone; nodes come from the context menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:07:07 +02:00
rootandClaude Opus 5 8f92a06c84 Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.

Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
  the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing

Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:00:30 +02:00
rootandClaude Opus 5 9212e61c4c Create a sprite asset from a texture's context menu
The sprite takes the whole image at its pixel size and the texture's name,
numbered like any other clash. CreateAssetCommand now accepts starting
properties; they go through the schema in execute, so an unknown key or a
bad value is refused before the project is touched, and one undo removes
the asset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 09:57:19 +02:00
rootandClaude Opus 5 10d030376a Finish the struct and list leftovers: inline lists, list fields, item kinds
An unconnected list input is a button into the List tab instead of a number
field; a struct field can hold a list, so ListSlot carries an optional field
name and the three owner cases only say what sits at a key; and a list
variable picks its item kind in the inspector (marker doubles as the chooser),
so a list of structs no longer has to be born from a project file.

The declaration keeps the storable shapes finite: a struct field may be a list,
but that list holds primitives only, so list->struct->primitive and
struct->list->primitive are the two deepest forms.

check 674, e2e 483, jump-run 24, Tetris 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:36:15 +02:00
rootandClaude Opus 5 f60358c951 Let a list hold structs, and read or write variables in bundles
A list item may now be a struct. The depth is bounded in the declaration
rather than by a comment: FieldKind (number|text|boolean|color) says what a
struct field holds, ListItemKind adds struct on top and says what a list
holds, so list -> struct -> primitive is the deepest storable shape and
sameValue stays finite. A struct item renders inline in the List tab, which
is why it needs no path in the slot address.

Four paths silently assumed a list item was a primitive: sameValue compared
items with ===, textOf would have printed [object Object], asListItem turned
a struct into text on Set item and Fill, and Index of used indexOf, which is
reference equality and could never have found a struct.

Tetris now runs on bundles. shapes is 112 {x, y} structs instead of 224 loose
numbers, piece and the candidate are structs, so Next move hands back a whole
candidate and each strand packs only the field it changes, since mergeStruct
leaves the rest standing. Declared behavior ports drop 49 -> 35, the master's
variables 18 -> 13, its graph 84/94 -> 69/76 nodes/edges.

Get variables and Set variables declare their ports from a list of variable
names, the way Pack and Expand already do, so one node can read or write
several at once. Renaming a variable rewires them, because there the port key
is the variable name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:01:56 +02:00
rootandClaude Opus 5 0449218cb6 Let a variable declare struct fields
The value type was already there: StructValue, textOf, sameValue,
writeTyped/mergeStruct and the struct port type came with the first
part, and Get/Set variable carry any-ports, so a bundle already flowed
through the graph unchanged. What was missing was the declaration.

PropertyDef gains kind 'struct' with a list of fields, VARIABLE_KINDS a
fifth kind, variableDef a fifth parameter. A declared field is a
primitive - a list field would need a nested editor and therefore the
path in the slot address, the same step "a list of structs" is waiting
for. coerceValue now returns PropertyValue rather than FieldValue, the
first value type that is not a field; none of its five callers break.

In the inspector a struct variable is a block, not a tab. The list tab
exists because hundreds of rows fit in no inspector row; a struct is the
opposite case - few fields that fit under each other. It is the port
editor Pack already has, one level down and with a value column: type,
key and default value in one place. A placed object sees the same fields
as plain value rows: overriding yes, declaring no.

Adding or removing a field touches the declaration, the default, the
behavior's value and every instance override in one command, so a stored
value can never keep a shape its declaration no longer promises. The row
handlers read the bundle at the moment of the change rather than closing
over a snapshot - a field writes the whole bundle, so with a snapshot
the second edited row revives the first one's old value. defSignature
carries the fields, otherwise a new row appears only at the next
rebuild.

Fixes a fixture fault this uncovered: step 10 pushed its text object
into the scene without instanceEdits, which overrideSites walks with
Object.entries. AddStructFieldCommand is the first caller to reach it
from that state; the step was green alone and failed three checks in the
full suite.

Moves the dynamic-ports note to done - all three of its parts are built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 12:29:01 +02:00
rootandClaude Opus 5 4d07946c74 Lay out a scroll container's content when the content changes
A short list opened after a long one drew all its rows stacked on one
point. The fault was not in the list tab: rebuild() handed the row stack
to setContent() while it was still empty, so the content was sized to
max(rows, viewport) = the viewport. The rows arrived after that, and for
a short list the same formula yields the same height, so resize() saw no
change and skipped layout(). A long list changes the height and never
showed it.

relayoutContent() now runs whenever the content changed, rather than
leaning on resize()'s change detection - that detection is the wrong
instrument for "the children moved".

The list tab was the only affected caller: the panels use ListView,
which positions its rows itself, and the inspector fills its body
before setContent.

Also drops the comma line under the list. Every section that can carry a
list supplies openList, so the comma fallback in fieldFactory was
unreachable; listToText/listFromText go with it. The list-value test now
covers coerceListItem through coerceValue instead, so the boolean
spelling stays guarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 10:58:40 +02:00
root 2cf788eedd Let a value be a bundle of named fields
PropertyValue was number | string | boolean | list. A struct joins it, which
costs two things the typechecker found rather than I did.

An object on a data edge used to mean a live entity, so asEntity would have
taken a struct for one and asString would have read .name off it. EntityHandle
now carries a symbol brand – a symbol so no field name can impersonate it, and
it never serializes, which suits a thing that exists only while the game runs.
It is built in exactly one place.

String(value) would have written a struct as [object Object] at 39 call sites.
They go through textOf(), which is identical for primitives.

normalizeGraph dropped everything but primitives from node params, so the List
node lost its value on every save ([3,1,4] round-tripped to undefined).
Variables were spared: they have a schema, params have none.
2026-09-10 08:14:18 +02:00
rootandClaude Opus 5 a9c3111377 Let a variable hold a list
A variable could only be a number, a text or a yes/no, so every set of values had
to be smuggled through a string - which is where Tetris got its unreadable
"03066326048763714" colours from. A list is now a value of its own, with an item
kind (number, text, yes/no, colour) that decides how the inspector reads the one
comma-separated line it is typed as, and eight nodes to work with: List, Fill,
Count, Item at, Set item, Sub list, Join lists, Index of.

Two traps came with it. An array is typeof 'object', so every place that told a
value from an entity had to learn the difference, or a list would have been
passed around as a game object. And a value was compared by identity in three
places - with lists that marks everything as an overridden property after each
reload, so those comparisons now compare contents.

There is no list of objects: a list is written to the project file and an object
is a live handle that no reload survives. A set of objects stays a list of names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:26:31 +02:00
root 9c4148dc1b Hold a running game, and let the editor run reach into containers
Pause is a sub-state of play, not a third mode: the toolbar button and
Ctrl+Enter stop ticking the world, the clock included. A held frame still
drops the input edges, so resuming does not fire a pause worth of keys in
one go.

The editor run missed the case it is most wanted for. hasEditorRun only
looked at the stored scene objects, so a behavior inside a placed
container never started one; the world now builds from resolveObjects and
writes back through objectRef, keeping the move as an instance edit
instead of baking the instance out.

Keys and clicks go to the run while the Game tab is on screen, where no
key means anything to the editor, and are handed back on leaving it -
held keys released, so nothing keeps walking unpressed.
2026-09-09 18:16:28 +02:00
root 8f31afac37 Let a behavior run while the editor is open
A behavior object carries a "Run in editor" switch next to Enabled. When
it is on, the behavior ticks outside of Play and works on the saved model
directly - Unity semantics: what it moves is a real change, autosaved and
not undoable.

Input stays with the editor, so On key never fires while typing. The
editor run never bakes placed containers into the model, since that would
silently drop their instanceEdits.

syncToModel reports whether it wrote anything, and the dirty flag is only
raised on change - otherwise a behavior moving something every frame
would trigger ~60 saves a second.
2026-09-09 17:13:07 +02:00
root e9634f0f0d Paint an asset's name gold while any of its values deviates
The inspector already marked an asset value that differs from its schema
default, but the Assets panel gave no hint, so a changed asset looked
like an untouched one until you opened it.

The rule sits next to the existing objectDeviates as a pure function:
assetDeviates asks whether any marked property differs, markedAssetSchema
says which rows the inspector marks at all. buildAssetSection now builds
its rows from that same list, replacing the behavior/rest split it used
to carry, so the list cannot claim a deviation the inspector then fails
to show.

A behavior's variables are deliberately not in it. Their default is
declared on the variable itself, which means the asset value *is* that
default; counting them would paint the fixture's Mover gold while the
inspector beside it marks nothing.
2026-09-09 15:21:39 +02:00
root d7c8d3e579 Measure a deviation against the base, not the innermost template
An instance edit was dropped as soon as its value matched the template
object. With nested containers the template is not what the value falls
back to: the outer container holds an edit for the object below it, and
that one wins. If the inner template happened to carry the same value the
user typed, the edit was thrown away and the value jumped to whatever the
container in between says.

Reported against the Jump-and-Run project: a coin narrowed to 8 inside
"level", then set back to the sprite's own 24 in the scene, snapped back
to 8. Reproduced through the inspector before touching anything.

InstanceTarget now offers `base`: the template with the edits of every
container in between woven in. Everything that judges deviation uses it -
setOverride/setName/setPosition, both Revert commands, and the gold marks
in the inspector. Writing still goes into the real template, which is
where "apply to container" belongs.

`base` is built on every read. Apply writes into the template and then
asks whether the edit still deviates, so a base captured beforehand would
answer for the template as it was - the e2e suite caught exactly that.

Also in here, because it corrects the commit before it: placing an asset
by double-clicking its row is gone, as asked for in asset-doppelklick.md.
Dragging is the only way to place now. That took AssetPanel.place, the
PlacementTarget interface, Editor.placementTarget and the two worldCenter()
methods that existed only to feed it. A double-click on a scene asset still
makes it the active scene.
2026-09-09 13:45:27 +02:00