From f0673d19575287157a0b53df10b2818e7d2655e3 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Sep 2026 20:42:20 +0200 Subject: [PATCH] Move the reference documentation into the wiki, keep the README short (#61) The twelve sections of the 992-line README are now wiki pages (https://git.nockal.com/meleki/game-maker/wiki, wiki commit 9be1294), content unchanged. The README keeps what it is, the quick start and an index of the pages. --- .prettierignore | 4 +- README.md | 1022 ++------------------------------------- src/editor/shortcuts.ts | 2 +- 3 files changed, 53 insertions(+), 975 deletions(-) diff --git a/.prettierignore b/.prettierignore index a9a5a667..27bcd3f6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,6 @@ data/ index.html package-lock.json -# Markdown is left alone: Prettier pads table cells to equal width, which turns the -# README's long glossary rows into 1000-character lines. +# Markdown is left alone: Prettier pads table cells to equal width, which turns a +# long table row into a 1000-character line. *.md diff --git a/README.md b/README.md index 0424c752..fb9005be 100644 --- a/README.md +++ b/README.md @@ -6,987 +6,65 @@ all in one program, all drawn inside a single WebGL canvas. Live: · Widget playground: -## Concepts - -| Term | Meaning | -|---|---| -| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`) and typed properties. Assets have **no position**. | -| **Texture** | An uploaded image. Scenes, textures, animations and state machines are the four types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See *Textures* below. | -| **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See *State machines* below. | -| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) – titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | -| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run – **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents). | -| **Scene object** | An asset placed into a scene: a link to the asset + a **position** (`x`, `y`, `z`) + sparse **overrides** of the asset's properties + an optional **parent** (another object of the scene). Scenes cannot be placed. | -| **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** – its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see *Editing a container*) or from an instance through **Apply to container**. | -| **Selection** | One or more scene objects. Click selects, `Ctrl`/`Shift`-click adds/removes, dragging on empty scene space draws a rubber band (objects whose position lies inside are selected). Dragging a selected object moves the whole selection; `Delete` removes it; the inspector offers **Create container** and **Remove** for several objects. | -| **Behavior** | Logic as an asset: a **node graph** plus the **variables** it declares. Placed like anything else (a diamond symbol in the scene); its parent is the **owner** the graph acts on. | -| **Run in editor** | A switch only behaviors have (default off). With it on the behavior keeps running while the project is **not** playing – and unlike Play it then edits the *saved* model: what it moves or writes is a real change, autosaved and not on the undo stack. See *Running without Play*. | -| **Collider** | A shape (box or circle) for collision detection, placed as a **child** of the object it belongs to (its *body*). Drawn as a green outline in edit mode only. | -| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. | -| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` – the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. | -| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. | -| **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. | -| **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. | -| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** – the saved model is never touched. | - -**The camera defines what is rendered.** A camera asset has a view size (`width` × `height`, -default 640×360) and a `zoom`; the frame it covers in world units is `size / zoom`. The Scene -tab draws that frame as an outline. The Game tab renders only that frame – fitted into the -view with black letterboxing – and objects outside it are culled. Of several cameras the first -one in hierarchy order that is **switched on** (`enabled`, its parents included) is the active -one, so switching it off hands the view to the next; with none on the tab stays empty and says -*No active camera*. Without playing the tab previews the scene through that same camera, so it -shows what Play would show, minus the editor symbols. Play has no built-in behaviour (no input, -no movement): everything that happens is defined by behavior graphs. - -**Toolbar and panels.** The toolbar holds the **Project** menu (new project, switch project, -*Delete this project…*), the project name (type to rename), the **Scene** picker for the active -scene, Undo/Redo, Play/Pause, and on the right the notice line, the save status and the frame -rate. The **Assets** panel's `+` creates an asset; right-clicking a row offers *Set as active -scene* (double-click or `Enter` does the same), *Create sprite from texture*, *Create animation -from texture* and *Delete…*. In -**Scene objects** it offers *Select asset*, *Add collider*, *Create container*, *Unpack -container*, *Unparent*, *Expand all* / *Collapse all* and *Remove*. - -**The console.** Under whichever centre tab is showing sits the **Console** panel, always -visible and resized by the divider above it – dragged all the way shut it keeps its own header -to be dragged open by. It mirrors everything that reaches the browser console: a `Log` node's -message, Pixi's warnings, an uncaught error, and the editor's own notices (a failure in red, a -hint in plain text; the *Playing · …* status line is a mode rather than an event and stays out). -Each line carries the clock time of its call (`HH:MM:SS`, muted, in its own column, so the -message stays exactly what was logged). The newest 500 lines are kept, *Clear* empties them, -and new lines follow the tail only while the tail is what you are looking at. - -**Editing while playing.** Play opens the Game tab but leaves the editor fully usable: the -Scene tab shows the live positions (symbols, gizmos and dragging included), the hierarchy and -inspector show and edit the running objects, the graph can be changed, undo/redo work on the -play session's own history. All of it happens in the runtime copy: values a behavior changes -appear as overrides (gold, ↺ resets to the asset value), objects a graph destroys vanish from -the lists, objects a graph spawns (`Spawn` node) appear in them – selectable, editable, still -only in the runtime copy –, and Esc/Stop brings back exactly what was saved. Autosave pauses -while playing. - -**Pausing.** The toolbar's *Pause* button (or `Ctrl+Enter`) holds the running world: it is no -longer ticked, so behaviors, timers and collisions stand still and the world clock stops with -them. It is a sub-state of play mode, not a third mode – the runtime copy, the panels and the -keyboard behave exactly as while playing, and everything the editor can do during play it can -do during a hold. Keys and clicks that arrive while it is held are **dropped** rather than -queued, so resuming does not fire a frame's worth of input at once. Stop clears the hold, so a -paused game can never outlive its run. - -**Running without Play.** A behavior switched to **Run in editor** goes on running while the -project is not playing (`src/editor/EditorRun.ts`, ticked by the Editor next to the play session). -The switch is an ordinary property, so it can be set on the behavior asset and overridden per -object. What it runs is a `World` over the **saved** model, and that is the whole difference to -Play: a position or variable it changes is a real edit – it is autosaved, the panels show it, and -**undo does not take it back**. Which is why the switch is off by default and why it belongs on -behaviors that arrange or build content rather than on ones that simulate. - -**Input goes to the tab you are looking at.** While the **Scene** tab is on screen the keyboard and -the mouse are the editor's – typing a name or walking the hierarchy with the arrow keys must not -steer a game at the same time. In the **Game** tab, where no key means anything to the editor, they -reach the run instead: `On key …`, `Key is down` and `On click` work there, and the tab says so in -its top-left corner. Leaving the tab (or focusing a field in it) hands the keyboard back and lets -every held key go, so nothing keeps walking with nobody pressing anything. What the run does *not* -get: - -- **structural changes to the content of a placed container** – such content is resolved from the - template, not stored (see *Containers*), and the editor run must not bake it out, since that is - itself an edit. Its behaviors do run, and what they move or set is kept as an `instanceEdits` - entry on the instance; but a `Destroy` of resolved content is dropped from the running world - without being saved, and comes back the next time the world is rebuilt. -- **a second life** – only the behaviors of the **loaded** scene run, and only in edit mode; Play - stops the editor run, since that world runs every behavior anyway. An exception in a graph, or a - `Stop game` node, ends the run and puts the reason in the notice line instead of repeating it - every frame. - -## Behaviors (node graphs) - -A behavior asset holds a Blueprint-style graph edited in the **Behavior** tab of the centre -area (the **Scene** tab shows the editable scene, the **Game** tab the running game). Ports come in two kinds: **exec** (white -arrows, control flow) and **data** (coloured circles; `number`, `boolean`, `string`, -`object`, `list`, `struct`, `any`). Event nodes start a pulse; the interpreter follows exec edges and pulls -data inputs lazily from connected outputs. Unconnected data inputs use the inline value -shown on the node and editable in the inspector. - -- **Variables** are the behavior's own property schema (`BehaviorAsset.variables`): they - appear in the inspector, can be overridden per placed instance, and each running instance - gets its own mutable copy. One node reads and writes them, `Variables`: its settings are a *list* - of variables, and each one is an output that can be read at any time – no flow has to reach it – - and is read at the moment it is asked, so a read after a write sees the new value. A variable - switched to **Set** in the inspector is an input too, written when the flow arrives; the node has - its flow pins only while at least one is. Unconnected and with nothing typed, it writes the zero - of its kind (0, false, empty), and the node shows that value beside the name. All of them are written together, their inputs read - first, so one node swaps two variables. `count = count + 1` is one `Variables` node feeding its - own output through an `Add` back into its input. The ports are picked from the behavior's - variables rather than named, they take their type from the declaration, and renaming a variable carries them (and their edges) along. - `Properties` is the same node for the - object it is handed (fixed input `object`). Its **Type** setting, above the list, decides what - `+ Property` offers: *Sprite*, *Text*, *Collider*, *Camera* and *Container* offer their fields, - *Behavior* the variables of the behavior picked below it (renaming such a variable carries the - port along, in any graph), and *Any* offers the kinds (Number, Text, …) and leaves the name to be - typed. A picked property keeps its field and default – an unconnected `color` is a colour field - starting white, shown on the node as `#ffffff`, and a camera's `width` is its whole-pixel view - width. Switching the type keeps the ports; one the new type lacks is marked. The node shows its - type as a setting row (*Type: Sprite*, plus *Behavior: Game* under that type). Textures and - scenes are no type here: they are not objects in a running game. A variable is a - **number, a text, a yes/no, a colour, a texture, a state machine, a list or a struct**; a list declares **nothing** about its items – - each one is a number, a text, a yes/no, a colour, a texture, a state machine or a struct and says so itself, so one list may - hold several kinds at once. A list is - compared by its items, never by identity – otherwise every reload would look like an override. - There is no list *of objects*: a list is stored in the project file and an object is a live - handle that no reload survives. A set of objects is a list of their names, walked with `Repeat` - and `Object by name`. A list property is shown as a button saying how many items it holds; - clicking it opens the **List** tab, which gives each item a row with the same widget the - inspector would use for that kind – a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list - belongs to is still on screen. Which list the tab shows is a `ListSlot` (asset property, object - override or node setting, plus the `field` of a struct that holds one) on the editor state, - because no tab may reach into the `Editor` to switch itself on. An **unconnected `list` input** - of a node is the same button and the same tab: its inline value lives in the very same params - bag a setting does, and the slot stops resolving the moment an edge feeds the port. What an item holds - is **its own** and is chosen there: the **kind mark** in front of a row (`#`, `Aa`, `✓`, `◧`, `▦`, `⊞`) - is the picker – a click opens the menu, with no chevron, since the mark has to stay as narrow as - its column – and `+ Item ▾` asks for the kind of the one it appends. Because the kind lives in the - value, every such change is an ordinary **value edit**: one command, one undo step, and no - neighbouring item touched. A **struct item** carries its own shape the same way: its fields are - renamed, retyped, added (`+ Field ▾`) and removed on its own rows, which grows that one item and - not the list. Three kinds need saying, because JSON cannot tell them from the value beside them: a - **colour** is a number, and a **texture** or a **state machine** is an id, which is a text. Such a *tagged* kind wears its - name as the one key of a small object – `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the - picker a sprite's `texture` property has – a whole picture, one frame of a sheet, or an animation), - `{"$statemachine": "doors"}`. - A field name cannot start with `$` (`isValidFieldName`), so a tag can never collide with a struct - item of one field. Either reads as what it wraps on a data - edge (`Item at`), the graph having neither colours nor pictures of its own, and `Set item` leaves - the slot the kind it was. A **texture is a kind wherever a kind is picked** – a variable, a struct - field, a list item, an Any port of `Properties`, a field of `Pack` – and the `Texture` node hands - one into the graph, as `Number` and `Text` do a number and a text; a **state machine** is the same - kind of kind, with the `State machine` node. **Every kind is one row of one - table** (`core/properties/kinds.ts`): its word and mark, what it travels as on an edge, its empty - value, how a value is read as it, whether it is tagged and where it may be picked. The kind lists, - the `+ …` menus, the tags, the port fields and the literal nodes all follow from the row, so a new - kind is that row plus, if it names something in the project, its picker. Declared (a variable, a struct - field of one, a port) it needs no tag: the declaration says what it is, and the value is the id – - the way a colour field is a number. A declared port of kind colour or texture carries the field - that edits it and travels as a number or a text, since the graph has neither; a port declared as - the number a colour travels as still names the property (`isPortOf`). A **struct** variable is the other way round: few fields, so they fit under - each other right in the inspector. Its head row renames it and adds fields (`+ Field`), each - field row below carries its own name, its value and a remove – the port editor `Pack` already - has, one level down and with a value column – and a field may itself be a **list**, which is - the one count button that opens the tab one level deeper. Adding, renaming or removing a field reaches the - declaration, the behavior's own value **and every instance override** in one command, so no - stored value keeps a shape its declaration no longer promises. A placed instance draws every - variable, whatever its kind, with the very same rows (`buildVariable`), minus the renaming, the - kind chooser and the `+ Field` / `×` buttons: it may override the values, not declare them. A - struct's ↺ / ↑ sit on its head row, because an override covers the whole bundle rather than a - single field. All of it sits on **one column grid** with the rest of the inspector: mark (28 px), - gap and name (90 px) are exactly as wide as a plain row's label (124 px), a nested field's name gives up what - its indent takes, and names are cut with `…` rather than pushing their value out of line. -- **Owner** = parent: a placed behavior is usually parented to the object it controls - (inspector → Parent, or drag it onto that object in the hierarchy; dashed line in the - viewport). The `Owner` node returns it; `Self` returns the behavior itself; `Parent` walks up - from any object. -- **Auto-repeat on `On key down`**: two settings on the node, *Repeat after (ms)* and *Repeat - interval (ms)* – milliseconds, so both stay whole numbers. A delay of 0 leaves the node as it was – one fire per press; with a delay set, a - key that stays held fires again after it and then every interval (0 = every frame). That is the - sideways slide of a Tetris piece, or a held arrow walking a menu. The browser's own key repeat is - deliberately not used: its delay and rate are an operating-system setting, not a game's. Both - rows stay off the node body while the delay is 0, so a plain key node keeps its single line. -- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On update`, `On key down/up/held`, - `On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner – anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`, - `Is asset` (is this object an instance of asset X – how you tell what you collided with), - `Variables`, `Properties`, `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`, - `Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`, - `Substring`, `Find text`, `Equals`, - lists `List` (a constant), `Fill` (one value repeated – how an empty board is made), `Count`, - `Item at`, `Set item` (a *new* list, the one that came in is left alone), `Sub list`, - `Join lists`, `Index of`, structs `Pack`/`Expand`, - `Length`, `Distance`, `Normalize`, `Axis input` (four keys → a - normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`, - `Mouse position`, `Mouse button`, `Is hovered`, `Is touching` · - actions `Move` (dx, dy, scale, dt – connect an event's dt for per-second movement), `Set position`, `Spawn` - (asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef` - (ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and - inspector fields are generated from it. A port may declare a `default` for its inline value - (e.g. `Move.scale = 1`), shown on the node and in the inspector. -- **Ports that are not fixed by the type**: a `NodeDef` may say `declaresPorts` (the node carries - its own list, edited in the inspector) or `derivePorts` (the node works them out from its - settings and from the graph around it). `resolveDef(node, graph, lookup)` is what applies both, - so the canvas and the inspector see a definition that fits *this* node; a node whose ports are - derived reads them back off that definition, which is why `run` is handed it. `Sequence` keeps - one free output below the last connected one – connecting the spare grows it, unplugging the - bottom edge shrinks it again (keys `a`, `b`, `c`, … up to 26). `Run behavior` takes its ports - from the behavior it names, and a behavior's `Start`/`End` node declares them. Pointing a call - at a different behavior **takes the edges of the ports that go away** and gives them back on - undo: an edge on a port that no longer exists draws as nothing but survives every save, and - would come back to life once the port returns. -- **Structs**: several named values travelling as one, so a bundle needs one edge instead of one - per part. `Pack` declares its fields as inputs, `Expand` reads them back out as outputs, a - behavior port may be declared `struct` – which is how a bundle crosses a call – and a - **variable** may be one, which is how a bundle is stored. A `Pack`/`Expand` can take its fields - from a variable that already has the shape (**From variable**: a struct variable, or the first - struct item a list holds – a list declares nothing, so its value is the only source) instead of - having them typed again; that adds the fields it does not - have yet as one undo step and leaves the ones it has alone. Writing a struct variable keeps the - fields it declares: what arrives fills the ones it names, the rest stay put (`mergeStruct`), so - one node can set part of a bundle without erasing it – "the same piece, one row lower" is a - `Pack` of that one field. Depth is bounded by the *value*, not by convention: a list item is a primitive, a - tagged colour, texture or state machine, or a struct of flat ones (`listItems.ts`), a struct field says what it holds - (`StructFieldKind`, primitives plus `list`), and the list a *field* holds takes flat items only. - Forbidding just that one combination is what keeps the shapes finite: - `list → struct → primitive` and `struct → list → primitive` are the two deepest a stored value - goes – exactly what `asStoredValue` lets through an untyped slot – so the deep compare and the - editor stay finite. The List tab **offers only the kinds its slot can hold** (`ListAccess.kinds`): - on a list of its own all six, on a list a struct field holds the five flat ones. Offering the - struct there would add an item the next write drops – which reads as the menu entry doing nothing. A live entity is - an object on a data edge too, so it carries a `Symbol` brand (`ENTITY_HANDLE`) that no struct - field name can impersonate; nothing may hand a property value to `String()` (`textOf` instead), - or a struct would read as `[object Object]`. A struct port has no inline value (its parts come - from a `Pack`), and compatibility is per *kind*, not per shape – any struct fits any struct port. -- **Loops**: `Repeat` runs its `body` output a number of times, counting `index` up from 0, and - continues at `done`. Data nodes are cached for the length of one event pulse, but a loop round - drops that cache, so an expression built on `index` really changes with it. That is what makes - a graph able to work through a list: the Tetris example draws its 200 board cells that way, - `Object by name` taking the name the round put together (`"c" + index`). Two guards, not one: - a pulse may take 100 000 steps (a loop legitimately runs long), but exec edges may only nest - 1000 deep – that is what catches a flow feeding back into itself before the call stack does. -- **A behavior as a node** (nesting): a behavior that has a **Start** and an **End** node can be - run as a step of another behavior's graph. Every input declared on the start node becomes an - input port of the calling node, every output declared on the end node one of its outputs; the - node is drawn under the behavior's own name, and the palette lists it there too, so a call is - one pick. Declaring a port keeps both sides in step – renaming carries the typed-in value along, - removing takes the edges hanging on it in *every* graph that calls the behavior. A call is a - **call, not an inlining**: the nested graph runs in its own pulse cache with a fresh copy of its - variable defaults, so values travel in and out through the ports and nothing else. That makes a - call repeatable and keeps a nested behavior free of hidden state – for state, use a behavior - object in the scene (see *Cross-object state* below). A behavior that would run itself raises an - error instead of looping; there is one start and one end node per behavior. - Three ways in and out of a call: **drag** a behavior out of the asset panel onto the graph and it - lands as a call where you let go; **double-click** a call node to open the graph it runs. The - way down stays above the graph as a **breadcrumb** (`Game › Move › Other`): click a crumb to go - back there; the first one returns to where you started and the bar goes away. Opening a behavior - any other way – the asset panel, the scene, the picker – ends the trail. And a call that cannot - do anything **says so on the node** – no behavior picked, the behavior deleted, - or no start node in it. The last one is the one worth naming: without a start node there is no - way in, so the call is skipped and the flow simply carries on at its `exec` output. A missing - *end* node is not a defect – a call is always synchronous, "done" is the nested flow running out, - and the end node only decides what comes back. -- **Cross-object state**: a behavior's variables *are* the properties of its object, so another - graph reads and writes them with `Object by name` → `Properties` of type *Behavior*. That is - how the example game keeps one coin counter for the whole run: picking a coin up is one node, - `coins` fed through `Add` back into itself. -- **Mouse**: the Game tab feeds the pointer to the world in world units. `On click` fires for - the behavior whose owner was hit – colliders decide when there are any, otherwise the sprites - and texts of the owner and everything below it, so a button made of a box plus a label works - as one. A switched-off screen cannot swallow clicks. A behavior **without an owner** has no - shape that could be hit, so it takes every click inside the camera frame instead (the - `world x`/`world y` outputs say where) – that is the "click anywhere" listener. -- **Idiomatic movement** (the demo's "Mover", five nodes): `On update → Move(Owner, dx = Axis - input.x, dy = Axis input.y, scale = speed, dt = On update.dt)`, `speed` being a `Variables` node. -- **Idiomatic platformer** (the example game's "Player"): `On update` adds `gravity · dt` to a - `vy` variable and moves the owner by `(axis.x · speed, vy)` with *Collide with solids* on; - `blocked y` while falling means standing, so `vy` goes back to 0 and `onGround` becomes true; - `On key down (Space)` sets `vy = −jumpSpeed` when `onGround`. -- **Editing**: right-click the canvas for the searchable palette, drag port → port to connect - (types are checked; a data input holds one link and is replaced), drag nodes, right-click a - node or port to delete/disconnect, `Delete` removes the selection, wheel zooms. Everything is - undoable. -- **Mouse and selection work as in the scene view.** The **middle button pans** from anywhere, - a node under the pointer included (shift-drag on empty canvas does too); the left button never - pans. Dragging on empty canvas pulls a **rubber band** instead, which selects every node it - touches – nodes are large rectangles, so touching counts, unlike the scene view's test of an - object's position. Ctrl/Shift-click adds or removes single nodes, dragging any node of a - selection moves all of them, and `Delete` removes all of them – one undo step either way. -- **Dragging an edge tells you where it would land** before you let go: the port it would connect - to wears a white ring and the line snaps to it. Dropping on a node's *body* rather than a port - takes that node's first free compatible port – or, when none is free, the last compatible one, - whose link is then replaced. A drop into empty space opens the palette filtered to the node - types this edge could reach at all, and the picked node arrives already wired; node and link go - in as **one** undo step (`core/graph/dropTarget.ts` decides all of this, the canvas only draws - it). The dangling edge stays on screen while that palette is up, so the connection being made is - still visible while its node is chosen. -- **A link can be hidden**, so a connection across the whole graph does not have to be drawn as a - line across the whole graph. Right-click a port → *Hide link*; the curve is replaced by a short - stub at each end, ending in a dot captioned with the node at the *other* end. The connection is - untouched – hiding is drawing only, and `folded` on the edge is the entire change. The curve - comes back, dimmed, **while either of its two nodes is selected**, and always while a pulse is - travelling it, so the live trace never loses its thread. Clicking a dot selects the node at the - far end, which both answers "where does this go?" and lends the curve back; - right-clicking it offers *Show link* and *Disconnect* (`editor/graph/folding.ts` holds the - visibility rule, the canvas only draws it). **A port's hidden links share one mark** – drawn per - link, several of them would stack stub, dot and caption on the very same pixels. The mark then - grows a little, is captioned `Add +2`, and one click selects *every* far end at once, which is - also what lends all of its curves back; the menu counts what it acts on (*Show 3 links*), and - does it as one undo step. -- **A node can be collapsed** to its title bar: the chevron in front of the title (it leaves the - selection alone, like the hierarchy's), or right-click → *Collapse node* / *Expand node*, which - acts on the whole selection as one undo step. Every link of one side then meets at a single - point on the title bar, and its hidden links share one mark per side. It narrows to its title - (never wider than an open node, where a long title is cut). A collapsed node has no - ports to grab, so a link dropped on it goes nowhere – expand it to wire it. Like hiding a link, - this is drawing only: `collapsed` on the node is saved with the project. A collapsed - `Variables` or `Properties` node, titled just that while open, is titled by what it does, since - its ports are out of sight: - *Set 2 variables* when more than one is written, else *Get 2 variables* when more than one is - only read, else *Set speed* / *Get speed*, and *No variables* when it has none. -- **Header bar** (the same one the Texture and Container tabs have): the behavior picker, - **Fit** (which zooms out far enough to hold every node, measuring each by its own drawn - height – `editor/framing.ts`), and the size of the graph as "N nodes · M links". -- **Snap** (header checkbox, off by default): a drag drops each node it carries on the nearest - 24 px grid line – the same grid the background draws. It takes effect on the next drag only, so - switching it on never shuffles a graph that is already laid out, and a node nobody drags never - moves. Every node of a selection snaps **for itself**, so dragging a group is how you line one - up; the offset is a decision about rectangles and lives with the others in `editor/framing.ts`. -- **Selection-following, not selection-bound**: selecting a behavior asset, a placed behavior - or one of its nodes shows that graph; a selection about anything else (a sprite, nothing at - all) leaves the tab on the behavior you were editing, because checking a sprite's position - must not empty your work. Only a behavior that no longer exists is dropped, and then the tab - falls back to another one – the picker is how you switch on purpose. -- **Live view**: Play always switches to the Game tab; switch to Behavior while playing and - the exec edges and nodes that just ran light up – the same canvas draws editor and game, so - this costs nothing extra. Each fire of an exec edge also sends a bead along it (0.3 s, at most - one per 0.15 s and edge): a key press is a single bead, `On update` an even chain. Beads run - on game time, so a paused game holds them where they are. Every data output shows, in the same colour, the value it last - carried (`0.017 dt`, `Pause box object`), refreshed ten times a second. With several objects - running the same behavior it is the one that produced last; in a loop, the last round. An - output nothing has asked for yet shows nothing. The graph stays editable meanwhile: the interpreter reads the runtime - copy's graph on every pulse, so an edit takes effect on the next frame and is discarded with - the run. A new `On start` node does not fire for a behavior that has already started. -- **Runtime**: `BehaviorSystem` creates one `GraphInterpreter` per behavior entity; graphs only - touch world entities. A runaway pulse throws (see *Loops* for the two limits) and stops play - with a notice. - -**Placing an asset: drag it in.** Drag a row out of the **Assets** panel and drop it on the -view – the Scene tab places into the active scene, the Container tab into the open template, so -what you see is what you fill. While the pointer is over a view, an outline shows the footprint -the object will have and a crosshair the origin it snaps to; it turns **red** when the drop would -be refused, which today means a container that would end up inside itself (`placementRejection`, -`src/core/model/placement.ts` – the same function the command runs, so the preview cannot promise -a placement the drop then denies). Picking a container up selects it, but the Container tab does -*not* follow that while a drag is in flight: the target must not swap out from under the pointer. -Dragging is the only way to place: a double-click on a scene makes it the active one, on anything -else it does nothing. - -**Editor symbols are always on top.** The viewport draws scene content (sprites) in an -`objects` layer and editor symbols (camera icon, labels) in an `overlay` layer above it, so a -sprite can never hide a symbol regardless of z or hierarchy order. A viewport label keeps a -constant size on screen however far you zoom, and both kinds of them – the object names of the -Scene tab and the frame names of the Texture tab – read that size from one token -(`Theme.viewportLabelSize`); the two `viewportLabel` styles differ in colour only. - -**Z-order is part of the position.** `position.z` is the draw order of a scene object: higher -`z` is drawn on top; objects with equal `z` keep their hierarchy (tree) order. It is edited in the -inspector next to X/Y, undoable like any move, and applies in edit and play mode alike. - -**Hierarchy (parenting).** Every scene object may have a parent in the same scene -(`SceneObject.parentId`). Positions – including `z` – are **local to the parent**; the world -position is the sum along the chain (`src/core/model/hierarchy.ts`). Children move with their -parent and are removed with it (also when the parent's asset is deleted); undo brings the whole -subtree back. Re-parenting keeps the world position by recomputing the local one, and cycles -are rejected. In the **Scene objects** panel children are indented below their parent; drag a -row onto another row to parent it, onto the empty space below the rows (or right-click → -Unparent) to move it to the top level; the inspector offers the same as a **Parent** menu. -Dragging into the **gap** above or below a row – its outer quarter, marked with a line instead of -an outline – orders the object among *that* row's siblings rather than putting it inside, and -re-parents it on the way when the gap belongs to another parent. Since equal `z` falls back to -this order, that is also how two objects at the same depth are put in front of each other. A drag -that lands where it started changes nothing and costs no undo step. -A row's **name turns gold** when the object deviates from what it follows – its container -template, or its asset – and also when anything below it does, so a folded subtree still shows -that something inside was changed. It is the same gold the inspector paints an overridden -label in. -Rows with children **fold**: click the chevron (or press ←/→, or double-click the row) to open -and close a subtree, right-click → **Expand all** / **Collapse all** for the whole scene. A -scene opens fully collapsed – which rows are open is view state of the panel, never stored in -the project – and selecting an object anywhere (viewport, `Add collider`, `Create container`) -unfolds the way down to it and scrolls it into view, so a selection is never hidden. In -play mode entities carry the same `parent` link: moving a parent entity moves its subtree, -`Destroy` removes it. `Get position` exposes both the local and the world coordinates. - -**Editor offset (scene view only).** Every scene object additionally carries an -`editorOffset` – an x/y shift of where it is *drawn, selected and dragged* in the Scene tab, -and nothing else. The game, collisions and the graph never see it; it is there to pull apart -objects that sit on top of each other, such as the stacked title/win/lose screens of a game. -It cascades like a position (offsetting a container offsets its content) and is edited in the -inspector below X/Y/Z, undoable, with ↺ back to 0. It belongs to the placed object, so -`Create container` and placing a container never carry it into the template. - -## Colliders and collisions - -A **collider** is an asset (`shape` box/circle, `width`/`height` or `radius`) placed like any -other object – typically as a child of the sprite it belongs to, so its position is the offset -from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add -collider* to get one with one click: a box collider sized like the -sprite, parented at its centre. Colliders can be reused across objects, overridden per instance -and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each -other). They are editor symbols: green outlines in edit mode, invisible in play mode. - -Play runs a `CollisionSystem` (`src/runtime/systems/CollisionSystem.ts`) before the behaviors: -every frame it tests all collider pairs (world positions applied; touching edges do not count) -and sorts them into *enter*, *stay* and *exit*. Detection never pushes anything apart. A -behavior receives `On collision enter/stay/exit` for the colliders of its **owner** (its -parent; itself when top-level): the outputs are `other` (the other body), `other collider` -and `collider` (the own one). `Is touching(a, b)` answers the same question on demand – an -object stands for its collider children, a collider for itself. The geometry lives in -`src/core/properties/collider.ts` so editor and runtime agree on it. - -**Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting -(default off). When enabled it moves an object one axis at a time and, after each axis, pushes -its *solid* colliders back to just touch any solid collider they ran into – so the other axis -still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which -axes hit (always `false` with the setting off). A collider with -**Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces -collision events but never blocks. An overlap that already exists is never resolved (no jolt -when objects start stacked or inside a wall) but also never deepened, so such an object can -always leave. A move longer than the thinnest collider involved is split into sub-steps – a -fast object cannot jump across a thin wall and end up on the other side. Colliders of the -moving object and of anything parented under it travel with it and cannot -block it. `Move` without the setting and `Set position` ignore colliders entirely. - -## Textures - -A sprite is a coloured box until it is given a picture. The picture is a **texture asset**, -and the sprite points at it through the ordinary property `texture` — which is the whole -design: because it is an ordinary property, instance overrides, **Apply to container**, -**Reset to asset**, undo and `Properties` work on it without a single line written for them. -One coin instance can wear a different picture than its nine siblings, and swapping a texture -at runtime is a graph node that already exists. - -**Getting an image in.** **Dropping image files onto the canvas** or **Ctrl+V** pasting one -uploads it and makes the asset in one go; several files at once are imported one after another, -and a file that fails does not stop the rest. - -**+ → Texture** asks a different question and so opens a different dialog: not the machine's -file system, but **every picture the server already holds**, as a grid of thumbnails. Files are -never collected (below), so a picture one project dropped is still there for the next to pick -up, and uploading the same bytes twice buys nothing. Picking one and pressing **Use image** -makes a texture asset on that file; **Upload…** is the way in for a picture 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 rather than making a second one. - -**Where it goes.** The image is uploaded as raw bytes to `POST ?r=textures` and stored as -`data/textures/{sha256}.{ext}` — the file is named after its own content. That name buys four -things: the same picture uploaded twice is one file, a stored file can be cached forever -(a name cannot come to mean different bytes), the project document holds a short string -instead of megabytes of base64, and deleting a texture asset is undoable for free because -nothing on disk is removed. The price is that files outlive the projects that referenced them. - -**Colour tints the picture.** `color` multiplies the texture, so it defaults to **white** -(= no tint) and a picture shows its own colours. Tinting is worth having: one white coin -picture becomes ten differently coloured coins, per instance, for nothing. Without a texture -`color` is simply the fill of the box, as before. - -**`filter`** is the texture's only property: *Smooth* (default) or *Pixel art*, which is the -difference between crisp and mush when a small image is scaled up. Switching it needs a -`style.update()` on the texture source, not just the assignment: Pixi's `scaleMode` setter -writes the filters but leaves the style's cached resource id in place, and that id is the key -the GPU sampler is looked up under — so without it the picture only changes on a reload. - -### Frames — cutting a sheet up - -The **Texture tab** shows one texture with its **frames** on top: named rectangles in image -pixels. Drag on empty picture to cut one, drag it or its eight handles to adjust it, and edit -the exact numbers in the inspector; `Delete` removes the selected frame. **Slice…** cuts a -regular grid at once — you give columns and rows (not a cell size: everyone knows how many -tiles their sheet has, and deriving the cell size from the picture cannot disagree with it), -with optional offset and spacing for padded sheets, and the dialog says how many frames of -what size the numbers would produce before it commits. - -A sprite then wears **a frame instead of the whole picture** — but through the *same* -property. `texture` holds either a texture id (the whole image) or a frame id, because frame -ids come from the same generator and are unique across the document. That is what keeps one -picker, one property and one set of instance overrides, and what makes every sprite written -before frames existed still mean exactly what it meant. The picker lists each sheet followed -by its frames as "Sheet / Frame", and it is searchable, because one sliced sheet adds an entry -per cell. - -Frames are intrinsic data on the texture, like a scene's objects and a behavior's graph — not -properties, since nothing about them is overridable per instance. All edits go through -commands, so cutting, dragging, slicing and re-slicing are undoable, and one drag is **one** -undo step. Deleting a frame leaves the sprites pointing at it: a dangling reference draws the -plain colour and comes back by itself on undo. Deleting the whole texture counts the sprites -wearing *any* of its frames, not just the ones naming the texture directly. - -**Fitting the sprite to the picture.** The *Width* and *Height* rows of a sprite carry a ↔ / ↕ -button offering two things: **Fit to 64 × 48** sets that one side from the other and the picture's -proportions — the frame's rectangle when it wears a frame, the whole image otherwise — and **Set -to 64 × 48 px** puts both sides at the picture's own pixel size. Like ↺ and ↑ the button exists -only when it would do something, and follows the same rule for how many: no offer means no button, -one is a direct click, both make it the menu form ↔ ▾. So a sprite without a picture, or one -already in proportion *and* at pixel size, carries nothing. Either action is an ordinary property -write, so on an object it becomes an override, and setting both sides is still one undo step. The -proportions are compared with a relative tolerance rather than exactly — of the 40 000 picture -sizes up to 200 × 200, 3135 do not survive the round trip through a division, and an exact test -would leave the button standing after its own click. The pixel size is compared exactly, because -those two numbers are written as they are. - -**A sprite straight from a texture.** Right-click a texture in the asset list → *Create sprite -from texture* makes a sprite asset that wears the whole picture at its pixel size, named after the -texture (numbered like any name clash, so `Coin` gives `Coin 2`) and selected. It is the same as creating a sprite, picking the texture and clicking *Set to -W × H px*, but in one undo step. - -Rendering a frame is a texture matrix on the fill, not a Pixi sub-texture — a Graphics fill -ignores a `Texture`'s `frame` (measured, see `viewport/textureFill.ts`). One consequence is -worth having: a sheet stays **one** GPU upload however many frames of it are on screen. - -**Nothing ever breaks on a missing picture.** While an image loads, and for a reference that -dangles because the texture was deleted, the sprite draws its plain colour — exactly what it -looked like before textures existed. So there is no placeholder, no flicker and no error path. -Deleting a texture warns how many sprites reference it (a texture is never *placed*, so -counting placed instances would report "unused" for a picture a dozen sprites are showing). - -### Animations - -An **animation asset** is a list of pictures played one after another. Each entry is what a -sprite's `texture` can hold – a whole texture or one of its frames – so an animation may mix -frames of several sheets, and a picture listed twice is held twice as long; there is no -per-frame duration beyond that. Its two properties are **Frames per second** (default 10) and -**Playback**: *Loop*, *Once* (stops on the last frame) or *Ping-pong* (back and forth, without -showing the turning frames twice). - -**A sprite wears it through `texture`** – the same property, the same picker (animations are -listed first, marked "(animation)") and the same instance overrides as a texture or a frame. -The sprite stays what is drawn, sized, tinted and collided with; the animation is only a picture -that changes over time. *Set to W × H px* takes the size of the first frame. The fastest way in -is right-clicking a texture → **Create animation from texture**, which plays its frames in -order (the whole picture when it has none), named after the texture, in one undo step. - -**The Animation tab** plays the animation above a strip of its frames. `▶` / `❚❚` pauses on the -frame on screen, and resumes from the selected one. Clicking a tile pauses on it, dragging a -tile into another gap reorders the frames, `Delete` removes the selected one, right-click offers -*Duplicate frame* and *Remove frame*, and **+ Frames ▾** inserts after the selection: each -sheet's frames at once, or single pictures. The selected tile belongs to the tab, not to the -editor's selection, so the inspector keeps showing the animation's two properties meanwhile. -The preview fits each frame on its own until the wheel zooms it about the pointer; from then on -every frame is drawn at that one scale, centred, so frames of different sizes show their real -proportion. The middle button (or `Shift`+drag) pans, **Fit** returns to fitting, and over the -strip the wheel scrolls the tiles instead. - -**It runs in play only.** The Scene and Container tabs show the first frame: the edit mode has -no clock and redraws on changes only. The `AnimationSystem` gives every sprite its own clock, kept on the entity and -not in `props` (the world writes changed props back as overrides). The one rule that makes it -usable from a graph: **a different animation starts from its first frame, the same one set again -runs on** – so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every -frame without freezing the animation on frame one. The system ticks before the behaviors, so -**`On animation finished`** fires in the frame a *Once* animation reaches its end, exactly once, -with the sprite as `object`. Like collisions it is heard by the behaviors of the sprite's owner -subtree – the sprite itself, or a sprite placed next to the behavior below the same object – and -by a behavior without an owner for every sprite; its *Animation* setting narrows it to one. -There is no node to restart an animation that is already running; that is left for when a game -needs it. - -Deleting a texture counts the animations that play its frames besides the sprites wearing it; -deleting an animation counts the sprites wearing it. Either leaves dangling references that draw -the plain colour and come back on undo. - -### State machines - -A **state machine asset** is a named set of states and nothing more, yet: its graph holds one -`State` node per state (a title and a *Name* setting, no ports) and no edges. Transitions, -events like "on state entered" and a highlight of the current state while playing are later -steps. The states are edited in the **State machine** tab, which is the graph canvas the Behavior -tab uses – so placing, dragging, the header's **Snap** checkbox, renaming in the inspector, -`Delete` and undo are the graph's. -Selecting the asset opens the tab (a behavior, texture or container asset does not switch tabs – -a machine has nothing to show anywhere else). Right-click on its canvas offers a `State` and -nothing else, named by its number (`State 3` for the third); no behavior palette offers one. - -**A behavior reads it through two nodes**, both with a `machine` input that is a picker while -unconnected (or takes a `State machine` node, the literal every kind has): - -- **`State branch`** grows **one exec output per state**, named after the state, and continues - along the one the machine is in. Before `Set state` has run the machine is in **no state**, and - the flow ends there – there is no start state. A state the machine no longer has (deleted, or of - another machine) ends it too. -- **`Set state`** is the mirror image: **one exec input per state**, one exec output. The input the - flow *arrives through* names the new state – so `OnStart → Set state (Open)` and `On key down → - Set state (Closed)` are two edges into one node. The interpreter hands every node the exec input - a pulse entered through (`run(…, entry)`); this is the first node to read it. - -The ports are keyed by the **state's node id** and labelled by its name: renaming a state keeps -every edge, deleting one leaves its edges dangling like any port that is gone. Which machine the -ports belong to is read off the graph, not off a running value – the inline pick, or a -`State machine` literal wired in; a variable or an `Item at` feeding the input yields no state -ports, exactly as `Run behavior` knows its behavior only as a setting. Picking another machine -orphans the edges of the old states, the way any setting that decides a node's ports does. - -**The current state is the behavior instance's own**, kept beside its variables -(`NodeContext.states`, machine id → state id): two crates running the same behavior are in -states of their own, and a behavior run through `Run behavior` starts in no state, as it starts -with fresh variables. A state machine is a **kind** like a texture – a variable, a struct field, -a list item (tagged `{"$statemachine": id}`), a port – and a `statemachine` asset is never placed. - -**Limits.** nginx caps request bodies at 1 MB, so the editor shrinks images over 2048 px per -side and re-encodes to WebP when they are still too big — but an image that already fits is -uploaded byte for byte, because re-encoding pixel art would destroy it. Anything still too -large is refused with a clear message instead of a broken upload. The server accepts PNG, -JPEG, WebP and GIF, deriving the type from the content rather than from anything the client -says, and rejects images over 8192 px per side. - -## Editing a container - -The **Container tab** shows one container's template with the *same* viewport the Scene tab uses – -`ContainerCanvas` subclasses `SceneViewport` and swaps nothing but which asset holds the objects, -so picking, rubber band, dragging, gizmos, labels and parent lines are not written twice. It adds a -cross at the **origin**, because that is the point the container is held by when placed: where the -content sits relative to it is the thing being authored. A template has no background of its own – -painting one would invent the scene it is not in. - -The **Scene objects** panel lists whatever the visible tab edits, so in the Container tab it lists -the template, with the same drag-to-parent, context menu and folding as a scene; the inspector, -`Delete` and `Ctrl+G` work on template objects like on placed ones. None of that is a special case: -a scene asset and a container asset are both `ObjectHolder`s, and every object command takes a -`holderId` that is either one. - -**Header bar**: the container picker (searchable), **Fit** (frames the content, origin always -included) and the size of the template as "N objects". Placing needs no button of its own: an -asset dragged from the panel onto this view goes into the template, and a container dragged onto -its own template is refused while it hovers rather than at the drop. -Like the Behavior tab it follows the selection without being bound to it – the container asset, one of -its placed copies or one of its template objects shows it, anything else leaves the tab on what you -were editing, and only a deleted container makes it fall back to another one. Selecting a template -*object* also switches to the tab, the rule a graph node and a texture frame already follow: it -exists in no other view. Selecting the asset itself does not. - -Placing a container anywhere creates **one** object. Its content is never copied: `resolveObjects` -(`src/core/model/instance.ts`) weaves the template in whenever the holder is read, one level per -nesting step, and the resolved objects get derived ids – `~