Commit Graph
72 Commits
Author SHA1 Message Date
rootandClaude Fable 5.1 a32b7c6daa Adjust colors: brightness adds instead of multiplying (#62)
game-maker / suite (push) Failing after 4m1s
The shipped shader graph gains a `brightness - 1` offset node, so 1 stays
neutral and brightness lifts every channel by the same amount; contrast keeps
multiplying, saturation keeps mixing with the luma. Fixture, seeds, unit and
e2e expectations follow (twelve nodes, 1.25 -> #80c0ff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:58:50 +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 6141d07d4f Behavior lifecycle: On enable, On disable and On destroy events (#42)
game-maker / suite (push) Successful in 7m29s
Order per instance: On start -> On enable -> On update ... -> On disable
-> On destroy. On enable fires right after On start and whenever the
behavior comes back on (itself and its parents); On disable in the frame
it goes off, the instance stays frozen. On destroy fires when the entity
leaves the world (Destroy node, child of a destroyed object) and when the
world is torn down on stop/restart, preceded by On disable if still on.
A new On start node added while playing still does not fire for a
running instance (kept as is, documented in the wiki).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 21:21:35 +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 656d61f187 Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The
running game reads the graph by reference each frame, so a new node, edge,
changed value, disconnect or undo takes effect from the next frame. Stop
discards these edits like everything else changed in play. A new On start
node does not fire for a behavior that has already started.

During play every data output shows the value it last produced, in the
highlight colour and refreshed ten times a second. Values are kept per
graph, because node ids are only unique within one; with several
instances the most recent value wins. The Variables node reads the live
variable bag, so only its own outputs are kept, as a snapshot.

Each time an exec edge fires, a bead runs along it in 0.3 s, at most one
every 0.15 s per edge, following the same curve the edge is drawn with.
Beads run on game time and freeze while paused; the game logic is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:19:07 +02:00
rootandClaude Opus 5 495756b500 Remove exports and methods nothing uses any more
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>
2026-09-11 20:15:49 +02:00
rootandClaude Opus 5 0376ae175d Title a collapsed Variables or Properties node by what it does
Collapsed, the node reads "Set 2 variables", "Get width" or "No properties";
expanded it is always "Variables" / "Properties". The Properties node shows
its object type (and behavior) as a line under the title instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 19:53:43 +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 7c63c884ef Replace the five variable nodes with one Variables node
A single node now lists any number of variables. Every entry is an output
that reads the current value whenever it is pulled; an entry switched to
Set is also an input and is written when the flow arrives. Exec pins only
exist while at least one entry writes, and all inputs are read before any
write, so one node can swap two variables.

Get/Set variable, Get/Set variables and Variable are gone. Old project
files are converted on load: each old node becomes a Variables node with
the same variables, edges and typed values.

A written port without a typed value now shows the zero of its kind on
the node body, matching what the inspector shows and what the run writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:56:24 +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 e9fa592078 Add the Variable node and show the variable name in node titles
Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).

The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:34:09 +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 1e2285c545 Let a graph node collapse to its title bar
A chevron in front of the title toggles it without touching the selection,
and right-click offers Collapse node / Expand node for the whole selection
as one undo step. A collapsed node draws its title bar alone; every link of
one side meets at a single point on it, and hidden links there share one
mark per side, since per-port marks would stack on the same pixels. It has
no ports to grab, so a link dropped on it connects nothing.

Like hiding a link this is drawing only: collapsed is saved on the node and
an expanded node leaves no trace in the project file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 08:59:19 +02:00
root 14f542365e Build a Pack or Expand node's ports from a struct variable
The port editor gets 'From variable' next to '+ Field': pick a struct variable
of the behavior and its fields land as ports. It only adds - a field the node
already declares stays as it is, because it may carry an edge - and the entry
is disabled once there is nothing left to add. A list of structs counts too:
it declares the shape of every item, which is exactly the bundle a Pack builds
for it. A color field arrives as a number port; the graph has no color type.

AddPortsCommand is new underneath: adding five ports had to be one undo step,
and it checks every key before it appends a single one, so a collision does
not leave half the list behind. AddPortCommand is a three-line subclass of it,
which leaves its sixteen call sites untouched.
2026-09-10 21:35:30 +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 fe3c63c48c Collapse Tetris' variable clusters, and give the long runs a home
Next move held four clusters of Set variable in a row, fourteen single nodes
in all; clear-lines read three variables one after the other. They are now
one plural node each, which takes Next move 40 -> 30 nodes and clear-lines
25 -> 23. Every remaining single node was checked and really is single: ok/no
are the same variable in opposite branches, take/keep the same one across a
write, and the three key setters hang on three different events.

Two contracts carry those merges, both pinned as tests. Set variables writes
per port with writeTyped exactly as the singular node does, so a Pack{rot}
still merges instead of overwriting - without it, turning would have erased
the other three fields. And the interpreter collects every input before the
node runs, so no input may come from a variable the same node writes; a data
node is evaluated once per pulse and keeps its answer. That second rule is
invisible in the game, because OnUpdate re-reads each frame and heals it, so
it lives in a unit test rather than in the smoke.

/opt/node22 and /opt/npm are gone from the tree. build.sh ran into the empty
path once they were moved away; the other seven mentions were documentation.

e2e.sh and check.sh replace the bare npm calls. A long run does not survive
the end of its shell - four full e2e runs died mid-suite with oom_kill 0 -
so e2e.sh start hands the run to a transient game-maker-e2e.service, which
does. e2e.sh status reports the running job or the last result, and calls a
log without a closing line ABORTED rather than reporting the passes it got
to. Both scripts end on an explicit exit line, because set -o pipefail inside
a script does not protect the caller: measured, ./check.sh | tail still
reports 0. Logs carry a timestamp and land in logs/, which is ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:00:28 +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 3c76106fd2 Let a Sequence grow an output as you connect the last one
The Outputs setting is gone. A Sequence now keeps one free output below the
last connected one: nothing plugged in is one output, connecting the spare
grows a new one, unplugging the bottom edge shrinks it back.

It counts the last *connected* port rather than the number of edges, so
unplugging a middle one leaves a gap instead of silently dropping an edge.

Removing the setting also removes a bug it caused. Typing in the Outputs field
did nothing and the arrow keys did nothing, while dragging the value worked:
onInput fires per keystroke, the preview writes into node.params, that emits
'project', and Inspector.refreshValues() sees the node's signature change --
the port list is part of it -- so it rebuilds and destroys the row being typed
in. A drag survives because it keeps receiving globalpointermove on the old
widget. refreshValues() has no focus guard where refreshLive() does; the trap
is still there for the next typed field that changes a node's shape.

Ports that depend on the edges around a node mean derivePorts needs the graph,
not just the params, so resolveDef and portSignature take it too. And a node
whose ports are derived cannot know how many it has from its type, so run is
handed the definition resolved for it.
2026-09-10 08:32:14 +02:00
root b6a609ae24 Let a node work out its own ports
Ports that vary per node already existed, but only for the three nested-behavior
types, and the rule for them sat as a switch on type names in nested.ts – the
one place in the project that knew node types by name. It dissolves into two
optional contracts on NodeDef, so a type carries its own rule:

  declaresPorts  the node holds an editable list, shown as a section in the
                 inspector – behavior start/end, and now Pack/Expand
  derivePorts    the node computes them from its settings – Sequence from a
                 count, Run behavior from the behavior it names

Sequence gains an Outputs setting (2 to 26). Its keys stay a, b, c and count on
with d, e, so the edges of the sequences already out there survive.

A param can now take a port away, and the edge on it would stay: it draws as
nothing yet survives every save, and comes back to life if the port returns.
SetNodeParamCommand takes those edges with it, which also closes the same leak
that switching a Run behavior node's target has always had.

The port editor addresses its node by id rather than by type, so two Pack nodes
can live in one graph, and it calls the rows Fields when they are not a
behavior's signature.
2026-09-10 08:14:18 +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 11981b40d0 Give the three asset tabs one base, and move the viewports into the editor
src/viewport/ held three classes only src/editor/ ever used, and framing.ts sat
under graph/ although the scene view wants the same bounds sum. The graph, the
texture and the container tab repeated the same header, subscription and
disposal code with two silent divergences: the graph tab said "nothing shown"
with null instead of '', and the texture tab refell back to the first asset on
a selection change but not on a project event.

These arrive together because GraphCanvas ties them: it reads framing from its
new home, hands its fold marks to folding.ts, and its '' change is what
GraphEditor must agree with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 07:14:38 +02:00
rootandClaude Opus 5 a2f2f8465c Take the cross-cutting queries out of the model's base module
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>
2026-09-10 07:14:28 +02:00
root 338ff4389f Type a repeat delay as whole milliseconds
The two On key down fields read in milliseconds now, and say so in their
labels. A number field shows three decimals unless a property asks for
fewer, so both params set decimals: 0 – the field reads 300, not 300.000,
and rounds to whole milliseconds while typing or scrubbing. The clock
behind it still counts seconds; the conversion sits at the one place that
reads the properties.
2026-09-10 05:59:39 +02:00
rootandClaude Opus 5 3ab8d88ad6 Let On key down repeat while the key stays held
A held key fired once and then nothing, so sliding a Tetris piece sideways or
walking a menu meant building a timer next to every key node. Two settings on
the node do it now: Repeat after and Repeat interval, both in milliseconds so
the usual values stay whole numbers (300, 50) instead of 0.3 and 0.05. A delay
of 0 keeps the node exactly as it was, an interval of 0 means every frame.

The browser's own auto-repeat is deliberately not used - Input.press has always
dropped it - because its delay and rate are an operating-system setting of the
player's, not a game's. The repeat is built from the measured hold time instead,
per node, so two nodes on one key keep their own clocks. The rule itself is a
pure function in keyRepeat.ts and is checked without a world.

Both rows stay off the node body while the delay is 0: two lines of "0" on every
key node in every graph would be noise, and the screenshot said so. NodeDef gains
hiddenParams for that; the inspector still shows them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:44:00 +02:00
rootandClaude Opus 5 4b0919a224 Let a link be hidden, and shown again by selecting either end
A link can be folded away from the canvas: instead of a curve running clear
across the graph, each end grows a short stub with a mark naming the node at
the far side. The curve comes back while either of its two nodes is selected,
so following a connection is a click rather than a search, and a link carrying
a live pulse is always drawn so the trace keeps its thread.

Marks are collected per port, not per link, because several hidden links on one
port used to stack stubs, dots and labels on identical pixels. A bundle reads
"Move +1" and its dot selects every node the port feeds, as one undo step.

The whole thing is one optional flag on the edge; the interpreter never sees it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:19:59 +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
rootandClaude Opus 5 c6f907b4e0 Make a call node say why it does nothing, and easier to place
A behavior node that cannot run looked exactly like a healthy one: no
start node, no behavior picked, a deleted behavior or a self-call were
all silently skipped. The call still had to be forgiving at runtime -- a
dangling reference must not abort a game -- so the fix is diagnosis, not
an exception: describeProblem() names the one reason, the node draws it
in red under its title, and the inspector repeats it where you pick the
behavior. A missing end node is deliberately not a problem: a call is
always synchronous, and the end node only decides which values come back.

Two ways in: a behavior dragged from the asset panel lands in the graph
under the pointer, and double-clicking a call node opens the graph it
runs. Both reuse machinery that already existed -- the asset panel
already starts the drag, and list views already report double clicks.

The e2e module needed its assets at the head of the asset list: after 18
modules the list is long enough that a newly appended row scrolls out of
view, and the press then lands on the panel behind it, starting no drag
at all. That looked exactly like a rejected drop and cost two full runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 20:40:56 +02:00
root 6702ff8abe Let a behavior be a node in another graph
A behavior that has a Start and an End node can now be dropped into any
other graph as a node of its own. What is declared on the start node
becomes the inputs of that node, what hangs on the end node its outputs;
the port list is derived from the sub graph, not copied, so declaring a
port in one graph grows the node in the other without touching it. The
node carries the behavior's name and the palette lists behaviors by
name, so a call is one grab instead of a node plus a dropdown.

An embedded behavior has no object and therefore no place for
variables: every call starts on the asset defaults and communicates
only through its ports. That is the conservative direction - state can
still be added later, and shared state already has a home in a behavior
object in the scene. Node ids are unique per graph only, so each call
gets its own pulse cache; without that, two graphs would collide.

Renaming a port used to silently drop the value typed on the calling
node. It is carried along now, project wide, like the edges.
2026-09-09 20:01:49 +02:00
rootandClaude Opus 5 4f78c7518c Give the graph a loop, and build Tetris with it
A Tetris example project needs to draw 200 board cells and scan rows
for a full line. Neither is expressible without iteration, so the node
system gets a Repeat node plus the missing text side: Substring, Find
text and To number. The interpreter drops its data cache once per
round -- otherwise every round would recompute with the first round's
values.

Object by name takes its name as an input now, not just as a setting;
addressing "c" + index is impossible otherwise. An unconnected input
reads the stored setting, so saved projects are untouched.

The board itself is a 200-character text, "." for empty. That makes
both operations of Tetris a single expression instead of a loop, and
each frame attempts exactly one move -- rotate beats sideways beats
fall -- so the fit check exists once in the whole graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 19:10:41 +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
rootandClaude Opus 5 37fff5dabf Play through the first camera that is switched on
The game view took whatever camera came first in the scene, ignoring the
switch, so `enabled = false` on a camera did nothing and a second camera
never got its turn. `World.activeCamera()` now decides: of the cameras
switched on – the whole parent chain counted, as for any object – the
first in hierarchy order, and none on renders nothing at all. It is asked
per frame, so a graph switching cameras hands the view over immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:13:30 +02:00
root 88ec367ab0 Offer the pictures the server already holds when adding a texture
"+ → Texture" opened the machine's file dialog, so a picture that was uploaded once
but lost its asset – an undone import, a deleted asset – was unreachable from the
editor while still sitting on the server. Nothing collects those files, by design.

It now opens a grid of every picture the server holds, with thumbnails: pick one and
"Use image" makes a texture asset on that file, "Upload…" is the way in for bytes the
server does not have yet and lands in the same import as a drop. A tile captioned with
an asset name is one this project already wears – picking it selects that asset instead
of making a second one, since two assets on one file would fight over `filter`.

- `GET ?r=textures` now reports `width`/`height`; a texture asset holds the pixel size,
  and reading it off the GPU texture would tie the asset's correctness to load timing.
- `TextureLibrary.load(file)` loads without touching `scaleMode` – a thumbnail must not
  restage the filter of a picture the scene is drawing with.
- The grid geometry lives in `texture/imageGrid.ts` as pure functions; inside the widget
  no test could reach it.
- The part after an upload is now one function shared by the drop import and the picker,
  so "one file = one asset" is stated in a single place.

12 new E2E checks; the strongest one undoes an import, leaving the file on the server
with no asset naming it, and picks exactly that file back out of the dialog.
2026-09-09 15:55:15 +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
root 75d377af39 Place assets by dragging them from the panel into the view
The context menu entry is gone; dragging replaces it entirely. The drop
target is the view under the pointer, not the active tab, so the same
gesture works for scenes and for container templates.

While dragging, an outline shows the footprint the object will have plus
a crosshair on its origin, turning red when the drop would be refused.
Preview and command share one function (placementRejection), so the
outline cannot promise something the drop then rejects.

A left-press used to select the list row immediately. Since the container
tab follows the selection, it switched to the very container being dragged
and every drop would have nested it inside itself. The list now selects on
release without a drag: picking something up is not the same as choosing it.
2026-09-09 13:09:13 +02:00
root ebad3fd530 Make an edge drag say where it will land, and offer nodes when it lands nowhere
Dragging an edge now rings the port it would connect to and snaps the line
there. A drop on a node's body takes that node's first free compatible port,
or the last compatible one when none is free, replacing its link. A drop into
empty space opens the palette filtered to the node types the edge could reach
at all, and the picked node arrives wired - node and link as one undo step,
so a single undo takes back the whole gesture.

The decision of what fits what lives in core/graph/dropTarget.ts as pure
functions, tested directly; the canvas only draws the answer. The palette
takes an optional filter rather than knowing about drags.

Verified remote on the Windows box (GPU): ALL PASSED, 284 checks.
2026-09-09 12:29:57 +02:00
root b87fcdfbad Mark overrides in gold instead of bold, and up the hierarchy
An inspector label whose value deviates from its base was drawn bold, which is
easy to miss next to the regular labels and says nothing in the hierarchy, where
a folded subtree could hide a change entirely.

The colour now lives as a theme token (`override`, #f2c84b) used by both the
inspector labels and the hierarchy rows. Whether a row deviates is decided by
`src/core/properties/deviation.ts`: `objectDeviates` compares an object against
what it follows -- the container template for content, the asset otherwise --
and `deviationMarks` carries the mark up to every ancestor, so a folded subtree
still shows that something inside was changed.

`deviationMarks` takes the stored holder, not the resolved one: in the resolved
view container content looks like an ordinary object and `instanceTargetOf`
would compare against the asset, marking every instance.

Covered by 3 unit tests and 5 e2e checks, including one that strips an object's
own overrides to prove the row stays gold for a deviating child and restores
them byte for byte. Full suite remote on the RTX 4070: 275 checks, all passed.
2026-09-09 10:54:25 +02:00
rootandClaude Opus 5 4568c6581b Add pixel-size fit and drag-to-reorder in the hierarchy
The aspect button now carries two offers, so it becomes a menu: fit one
side to the texture ratio, or set both sides to the exact pixel size as a
single undo step.

Dropping a row on the outer quarter of another row inserts it into that
gap instead of parenting into it, reordering among the siblings there.
Order is the stored order, so ReorderObjectCommand is a splice; equal z
falls back to tree order, which makes this the way to draw one object in
front of another. Rejected on container content like the other structural
edits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:20:46 +02:00
rootandClaude Opus 5 f40b8a0523 Fix instance lookups that trusted stale data
Two regressions from the template-resolution rework, plus the harness gap
that hid them:

- `instanceTargetOf` decided from the shape of the id. Unpacking keeps the
  derived ids, so unpacked content still counted as template content. It now
  asks whether the object is spelled out in the holder.
- The parent picker captured its holder when the row was built. Since
  `viewOf()` returns a derived copy, the menu listed the objects of that
  moment (measured: 40 in the model, 17 in the menu). It resolves on open.
- `evaluate()` swallowed page exceptions, so a mutate that died half-way let
  every later check pass or fail for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:59:22 +02:00
rootandClaude Opus 5 ead649eec3 Resolve container content from the template instead of copying it
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>
2026-09-08 21:09:53 +02:00
rootandClaude Opus 5 9d93020bd8 Add a Container tab and propagate template edits
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>
2026-09-08 20:19:54 +02:00
rootandClaude Opus 5 0b34bdd990 Fit sprite size to the texture aspect ratio
Add ↔ next to Width and ↕ next to Height in the inspector. Each sets its
field so the sprite matches the ratio of its assigned texture or frame,
reading the frame's own rect rather than the sheet's.

The buttons reuse the existing FieldActions convention, so they simply
do not appear when no texture is assigned, when the sprite is already
proportional, or on assets whose schema has no texture property.

Comparison is a cross product with a relative tolerance: 3135 of the
40000 size pairs up to 200x200 do not survive a round trip through a
division, so an exact check would leave the button standing after its
own click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:27:38 +02:00