Files
feuerwerk/benchmark/engine-prompt.md
T
adminandClaude Opus 5.5 46c620ae3b
game-maker / gate (push) Successful in 2m48s
game-maker / suite (push) Failing after 13m34s
Give a behavior more ways in and out, and hear it as an event (#294)
A behavior run by another one had a single flow port at its Start
and at its End, so it could not branch for its caller. And it could
not be used as an event: a call needs a pulse at its flow input,
and the events of a called graph never fire. An input like
"Confirm" (Space or Enter) had to be built again in every graph.

- A declared port of a Start or an End node can be of the kind
  Flow. It sits above the values and shares their names, exec stays
  reserved. Only these two nodes declare one, a struct does not.
- Run behavior shows a flow port of the Start as a further flow
  input. A call arriving through it leaves the Start through the
  output of that name and not through exec.
- Run behavior shows a flow port of the End as a further flow
  output. The call goes on through the one the End was reached by
  last, and then through exec. exec fires always, as before.
- On behavior is a new event node without a flow input. The
  behavior it names runs at the node for as long as the behavior
  around it runs, with all of its own events and one set of
  variables per node.
- Its outputs are those of the End. It fires once the inner pulse
  is through, by the output the End was reached by last, exec only
  for End.exec. A pulse that never reaches the End is not seen.
- The value ports of the Start are its inputs, read anew for each
  inner pulse. The Start itself never fires there.
- It nests to any depth. Under a Run behavior it stays silent. A
  behavior hearing itself, however far round, is not started and
  the node says "A behavior cannot listen to itself".
- The palette offers every other behavior with an End once more as
  "On <name>". A node naming one without End says so.
- An On node removed while the game runs ends its behavior with On
  disable and On destroy. The sounds of the entity end only with
  the behavior placed on it.
- Renaming and deleting a declared port follows the edges at On
  behavior nodes as it does at Run behavior nodes.
- The flow outputs of the Start of a behavior placed on an object
  never fire, like its exec.
- The engine prompt of the benchmark names flow ports and On
  behavior.

Tests:
- Gate on melektaus-atx right before this commit: 215 files, 3319
  tests (3278 + 41), exit 0.
- e2e 19 on melektaus-atx: 39 checks (+4), all pass. e2e
  1-6,17-24,26,36,51: 628 checks, all pass.
- 19 unit tests in flow-ports.test.ts, 19 in on-behavior.test.ts
  on a real world, the rest in the node catalog.
- Four new e2e checks in module 19: a flow port declared by mouse
  at the End, On Confirm in the graph, the call leaving through the
  further output while playing, Enter and Space on a real keyboard.
- 19 unit counter-probes all fail their tests. One did not at first
  (a removed node kept its instance), a test was added. Two browser
  counter-probes fail their checks.
- One screenshot looked at: On Confirm with exec and pressed, the
  second flow pin at the calling node.
- The e2e sum of 1680 is counted (1676 + 4), the full suite and
  the smokes did not run.
- Not seen in the browser: flow inputs at the Start, nesting, the
  ring, values through On behavior, the palette entry. No test
  holds that the sounds of the entity go on when a heard behavior
  ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:33:34 +02:00

408 lines
59 KiB
Markdown

# Build a browser-based 2D game editor and runtime, from an empty folder to finished
You are starting in an empty directory. Your task is to build a complete 2D game-making tool that runs in the browser: an editor in which a person creates assets, places them into scenes, wires logic, shaders and particle effects as node graphs, and presses Play to run the game inside the editor. It sits one step below a full game engine: there is no scripting language, everything a game does is defined by node graphs.
This is a benchmark. It measures whether one agent, given one specification, can deliver the whole product in a working state. "Finished" therefore has a precise meaning here: every section of this document is implemented, the three example games in section 20 can be played from start to end in the editor, and the test suites you write along the way are green. A broad build in which half the features are stubs counts for less than a complete one. Nobody is available to answer questions while you work, so where this document leaves something open, decide in the way that best serves a person making a small game, write the decision down in `DECISIONS.md`, and keep going.
The specification is long because the product is. Read it all once before you write code: several parts (overrides, containers, the graph foundation) are used by almost everything after them, and getting them right early is much cheaper than retrofitting them.
## 1. How to work
The work will span many context windows. Protect yourself against losing your place:
- Keep `PROGRESS.md` in the repository root. It lists the milestones of section 21 with their state, what is in flight, and anything the next context window needs to know (a trap you found, a decision you made, a test that is flaky and why). Update it whenever you finish something, before you move on.
- Commit after every coherent step, with a message that says what a user gains. A commit is your save point when something later goes wrong.
- Build in the order of the milestones. Each one ends with its tests green and the earlier ones still green.
- Do not stop to ask for confirmation, and do not end your turn with a plan. If something blocks you (a package that will not install, a browser that will not start), find another way and note it in `PROGRESS.md`.
Verify your own work, because nobody else will before grading:
- Put every rule that can be stated without a screen into a pure function, and unit-test it. Examples of such rules: how a property value resolves through its override chain, how a container template resolves into objects, in which order a frame fires events, what an animation shows at a given time. A rule buried in a click handler can only be tested through a browser, which is slow and brittle.
- Every feature a user reaches through the interface gets at least one end-to-end test that drives the real interface with real pointer and keyboard input and then checks the result in the model or on screen.
- For pictures, read the pixels: shaders, particles, draw order, nine-slice and text rendering are checked by sampling the canvas, not by trusting that the code ran.
- When you write a test for a fix or a rule, break the rule once and confirm that the test fails. A test that also passes against broken code protects nothing. Make sure the deliberately broken version still compiles, otherwise your end-to-end run silently uses the previous bundle and reports green.
- Look at screenshots of what you built at the end of every milestone. Layout mistakes (overlapping labels, a panel that swallows clicks, text cut off) are invisible to assertions and obvious in a picture.
- Report honestly in `ACCEPTANCE.md` (section 22). If something is not done or not tested, say so there.
Write code for a reader: descriptive names, small modules, comments only where a constraint is not visible from the code. Do not add features, abstractions or configuration this document does not ask for.
## 2. Stack
The stack is fixed so that results are comparable, with one exception: the interface framework is yours to choose.
- **Language:** TypeScript in strict mode, for client and server.
- **Build:** Vite. **Unit tests:** Vitest. **End-to-end tests:** Playwright with Chromium.
- **Rendering:** PixiJS v8, used only for the views that draw game content: the scene view, the game view, the container view, the animation preview, the shader and particle previews, and the texture view. One WebGL context per visible view at most; browsers limit the number of contexts, so views that are not on screen release theirs or share one renderer.
- **Editor interface:** ordinary DOM. Panels, lists, the inspector, menus, dialogs, tabs, text and number fields, the console and the timeline are DOM elements, not widgets drawn into a canvas. The framework is your choice. React is the suggestion, because it is the most widely used and has a mature node graph library, but Svelte, Vue, Solid or no framework at all are equally acceptable; record the choice and the reason in `DECISIONS.md`. The node graph editor is DOM/SVG as well; you may use a library for it (`@xyflow/react` or its counterpart for your framework) or draw it yourself, as long as every rule in section 8 holds.
- **Physics:** Rapier 2D (`@dimforge/rapier2d-compat`). **Sound:** the Web Audio API directly.
- **Server:** one small Node process (plain `node:http`, Hono or Fastify) that serves the built client and a JSON API, and stores everything as files below `data/`. No database, no accounts, no sessions.
- **Layering:** keep three layers apart, because the tests depend on it. `core` holds the model, the commands, the property system, the graph definitions and interpreters, and every pure rule; it imports neither the interface framework nor Pixi nor the DOM and runs under Vitest in Node. `runtime` holds the world and its systems and may use Rapier and Web Audio behind interfaces that tests can replace. `editor` holds the interface components and Pixi views and contains as little logic as possible.
Required commands, which the person judging the result will run:
| Command | What it does |
|---|---|
| `npm install` | Installs everything. No global tools may be needed besides Node 22 and the Playwright browser. |
| `npm run dev` | Starts server and client for development and prints the URL. |
| `npm run build` | Type-checks and builds the client. |
| `npm start` | Serves the built client and the API on `PORT` (default 5173). |
| `npm run check` | Type check, ESLint, Prettier check and all unit tests. |
| `npm run e2e` | Seeds its own test projects, starts the server itself and runs all end-to-end tests headless. |
| `npm run seed` | Writes the three example games of section 20 into `data/` so they can be opened in the editor. |
## 3. The editor at a glance
The window has a toolbar on top, a left column with two panels (**Assets** above **Scene objects**, with a draggable divider), a centre area with a tab row, the **Console** below the centre area, and the **Inspector** on the right. All dividers drag.
- **Toolbar:** a menu (New project, Open project…, Project settings…, Clear saved data), undo, redo, Play/Stop and Pause in the middle, and on the right a notice line and the save status.
- **Assets panel:** every asset of the project, grouped by type with a count per group, groups foldable, sorted by name. A `+` button opens a menu of all asset types, split into those that can be placed in a scene and those that cannot. A click selects an asset (the inspector shows it), a double-click opens it as a document tab if its type has an editor. A right-click offers the actions that fit the type, and *Delete…*, which asks first and says how many places use the asset.
- **Scene objects panel (hierarchy):** the object tree of whatever the visible tab edits (the active scene or a container template). Children are indented and fold. Dragging a row onto another parents it, dragging into the gap between rows reorders it among siblings, dragging onto empty space unparents it.
- **Centre tabs:** **Scene** and **Game** are fixed. Every other tab is an open document: one behavior, state machine, shader, particle asset, texture, animation, container or curve. Several documents of one type can be open at once, each keeps its zoom and scroll for the session, a tab closes with its cross or a middle click, and deleting an asset closes its tab.
- **Inspector:** the properties of the selection as rows. What the selection is decides the sections: an asset, a placed object, a graph node, a texture frame, an animation key, a state transition.
- **Console:** every `Log` node message, every runtime warning this document mentions and uncaught errors, each with a clock time, the newest 500 kept, with *Clear*.
Selection, keyboard and undo:
- Everything a user changes in the project goes through a **command** with `execute` and `undo`. One user gesture is one undo step: one drag, one typed value, one paste, one delete of a multi-selection. `Ctrl+Z` undoes, `Ctrl+Shift+Z` and `Ctrl+Y` redo. While a value is being dragged or typed the model follows live, and the step is committed at the end of the gesture.
- `Delete` removes the selection, `Ctrl+C` / `Ctrl+V` copy and paste objects, graph nodes and animation keys, `Ctrl+G` creates a container from the selected objects, `Ctrl+Enter` plays and pauses, `Esc` stops a run and cancels a dialog.
- Number fields can be typed into, stepped with the arrow keys and scrubbed by dragging sideways. Numbers show at most three decimals without trailing zeros.
- Changes are saved automatically shortly after each edit; the save status says *Saved*, *Saving…* or why a save failed.
## 4. The model
### 4.1 Project and assets
A **project** is one JSON document: a name, a list of assets, project settings (start scene, which scene Play starts with, collision layer names), and a revision number. Ids are short unique strings generated once and never reused inside a document; frames of a texture and nodes of a graph draw from the same generator, so an id identifies one thing in the whole document.
An **asset** has an id, a name, a type and a bag of property values. Assets have no position. The types:
| Type | Placeable | Has its own tab | What it is |
|---|---|---|---|
| `scene` | no | Scene tab | An object tree, a background colour, gravity |
| `sprite` | yes | no | A coloured box or a picture |
| `text` | yes | no | A line or block of text |
| `camera` | yes | no | What the game view shows |
| `clip` | yes | no | A rectangle that cuts everything parented below it |
| `collider` | yes | no | A collision shape |
| `rigidbody` | yes | no | Hands its parent to the physics engine |
| `container` | yes | yes | A reusable group of objects (prefab) |
| `behavior` | yes | yes | A logic graph plus the variables it declares |
| `particles` | yes | yes | A particle graph; a placed one is a source |
| `texture` | no | yes | An uploaded image with frames |
| `font` | no | no | An uploaded typeface |
| `sound` | no | no | An uploaded audio file |
| `animation` | no | yes | A timeline over a root asset |
| `statemachine` | no | yes | States and transitions |
| `shader` | no | yes | A graph compiled to a fragment shader |
| `variables` | no | no | Shared variable declarations |
| `struct` | no | no | A named shape for struct values |
| `curve` | no | yes | A hand-drawn easing curve |
### 4.2 Properties, defaults and overrides
This is the mechanism almost everything else rests on, so build it once and use it everywhere.
- Every asset type has a **schema**: a list of property definitions (key, label, kind, default, optional min/max/step, optional choices). The inspector, validation on load, and the `Properties` graph node are all generated from the schema.
- An asset stores only the values that differ from the schema default.
- A **scene object** is an asset placed in a scene: `{ id, assetId, name, position {x, y, z}, parentId?, overrides }`. `overrides` is sparse: only keys whose value differs from the asset's value are stored. The value an object shows is: its override, else the asset's value, else the schema default.
- An override whose value equals what lies below it is removed at once, so "is overridden" always means "really differs".
- In the inspector the label of a deviating row is drawn gold, and the row offers **reset** (drop the deviation) and, on a placed object, **apply** (write the value into the asset and drop the override, so every other object following the asset picks it up). Both buttons exist only while the row deviates. Asset values that differ from the schema default are gold and resettable the same way.
- Every placeable type has the property `enabled` (default on). A disabled object is not drawn, does not collide, and its behaviors do not run, and neither does anything parented below it.
- Reference properties (a sprite's `texture`, a text's `font`, any `shader`) hold an asset id and are edited with a searchable dropdown. A reference to something that no longer exists never breaks anything: the sprite draws its plain colour, the text its default face, the row shows `(missing)`, and undoing the deletion brings it back.
- Loading a document normalises it: unknown keys are dropped, values are coerced to their kind, dangling parents become top level. There are no format migrations to write, the format starts with you.
### 4.3 Scene objects, hierarchy and transform
- Positions are **local to the parent**; the world position is the parent chain applied. `z` adds up along the chain and is the draw order: higher is in front, equal `z` falls back to hierarchy order.
- Sprites, texts, colliders, containers, clip areas and particle sources carry four transform properties besides their position: `scale` (one factor), `rotation` (degrees, clockwise on screen), `mirrorX`, `mirrorY`. They are ordinary properties (asset value, per-object override, writable from a graph) and they cascade to children the way a position does, composed as matrices. Cameras and behaviors are positioned by the chain but never turned or scaled.
- Re-parenting keeps the world position and look: the local position and, where the two parent chains differ in turn or size, the transform properties are recomputed. Cycles are refused.
- Removing an object removes everything below it; undo brings the subtree back.
- Sprites, texts and particle sources have **Sort with parent** (default on): the child is drawn in its parent's place in the scene order, and its own `z` only orders it against the parent and its siblings (below 0 behind the parent, otherwise in front).
- A **clip area** has `width` and `height`. Everything parented below it at any depth is drawn only inside its rectangle and can only be clicked there. Nested areas intersect. The area itself is invisible in the game and a dashed outline in the scene view. A scrolling list is a clip area with content moved by a graph.
### 4.4 Sprite, text, camera
- **Sprite:** `width`, `height`, `color`, `texture`, `shader`, plus transform. Without a texture it is a box filled with `color`; with one, `color` tints the picture (default white).
- **Text:** `text`, `font`, `fontSize`, `color`, `align`, `shader`, plus transform. Multi-line text is allowed.
- **Camera:** `width`, `height` (default 640 x 360), `zoom`. The frame it covers in world units is size / zoom. The first enabled camera in hierarchy order is the active one. The scene view draws each camera's frame as an outline.
## 5. Containers (prefabs)
A container asset holds a **template**: a tree of objects exactly like a scene's, centred on the container's origin. A scene and a container are both "object holders", and every object command takes the id of the holder it works in, so the hierarchy, the inspector, dragging, copy and paste and delete work on template objects without special cases. The Container tab is the scene view pointed at the template, with a cross at the origin.
The rules that make this a real prefab system:
1. **Placing a container stores one object and nothing else.** Its content is never copied. Whenever a holder is read, the template is woven in: the resolved objects get derived ids `<placement id>~<template object id>`, and for a container inside a container the path continues with `/`: `<outer placement>~<inner placement>/<object id>`. These derived ids are what the hierarchy lists, the view draws, the selection holds and commands address.
2. **Editing the template changes every placement at once**, structure included: add, remove or re-parent a template object and every placement shows it.
3. **A placement may deviate in values only.** Position axes, the name and property overrides of resolved content can be edited per placement. Only what deviates is stored, as sparse `instanceEdits` on the placed container object, keyed by the template path. An edit is dropped the moment its value equals its base. The base is the template with the edits of every container in between woven in: inside nested containers a value falls back to the nearest enclosing level, not to the innermost template.
4. **Structure of resolved content cannot be edited in the placement:** it cannot be removed, re-parented, or have objects parented into it. The interface greys those actions out and says why.
5. For resolved content the inspector's reset and apply buttons become menus when there is more than one base: **Reset to container** / **Reset to asset**, **Apply to container** (writes into the template, where every other placement still at the old value follows) / **Apply to asset**.
6. **Create container** (`Ctrl+G`) turns the selected objects into a new container asset: they move into the template and the scene keeps one placement at their centre, with world positions unchanged. **Unpack** does the reverse for one placement: its content becomes ordinary stored objects that keep their derived ids and no longer follow the template.
7. A container may contain other containers to any depth. A container that would end up inside itself, directly or through another, is refused, already while the drag hovers.
8. **Play bakes.** The run works on a throwaway copy of the project in which every placement is spelled out as stored objects with the same derived ids, because a running graph may destroy one of them.
9. A container whose `enabled` is switched off and on is how a game shows and hides a screen (title, pause menu, game over). `Object by name` still finds a disabled object, so a graph can switch it back on.
Object references (section 9.1) into containers follow one rule: a reference is stored the way the holder that owns it shows the object. In a template D, content `x` of a nested placement `q` is `q~x`; inside a placement `r` of D the same object resolves to `r~q/x`. A reference is always resolved from the place it sits in, the same way in the editor and in the running game. Removing an object, in a scene or a template, empties every reference to it in the same undo step: in the template, in the edits of every placement in every scene and container, and on outside objects that name the resolved copy. Deleting an asset does the same for every object it takes along.
## 6. Scene editing
- **Placing:** drag an asset from the Assets panel onto the scene or container view. An outline previews the footprint, and turns red where the drop would be refused.
- **Selecting:** a click selects the topmost object as drawn. Clicking a selected object again selects the next one below it. `Ctrl`/`Shift`-click toggles. Dragging on empty space draws a rubber band. Pressing on a selected object and dragging moves the whole selection as one undo step.
- **View:** the wheel zooms about the pointer, the middle button (or `Shift`+drag) pans, **Fit** frames the content. A grid is drawn behind the scene.
- **Symbols:** objects that draw nothing in the game (behavior, container, rigidbody, collider, camera, particle source) show an icon and a name label in the scene view, always above scene content, at a constant screen size, clickable and draggable like the object. A parented symbol shows a dashed line to its parent. Colliders draw their outline (green, amber for triggers).
- **Size gizmo:** a selected sprite or box collider has handles on its edges and corners that set width and height with the opposite side staying in place, and a knob above the top edge that rotates (`Shift` snaps to 15°). Values go into the object's overrides; one drag is one undo step.
- **Copy and paste:** `Ctrl+C` copies the selected objects with everything below them, `Ctrl+V` pastes under the pointer or one grid step beside the original. A placed container is pasted as a placed container. References among the copied objects point at the copies.
- **Context menu** (the same in the view and in the hierarchy): Select asset, Open asset, Add collider…, Add rigidbody…, Create container, Unpack container, Unparent, Copy, Paste, Remove.
- **Scenes:** a project has any number of scenes. The **active scene** is the one the editor shows (double-click a scene asset). The **start scene** is named on the project (right-click a scene → *Set as start scene*). A project setting says whether Play starts with the start scene (default) or the active scene.
## 7. Play mode and the runtime
- **Play** makes a runtime copy of the project (same ids, containers baked) and builds a **world** from the chosen scene: one entity per object, with its resolved properties as mutable state. The centre switches to the Game tab. **Stop** discards world and copy; the saved project is never changed by playing.
- The **Game tab** renders exactly the active camera's frame, letterboxed, at a scale chosen in its header: fixed steps (25 % to 600 %), *Auto* (largest step that fits) and *Fit*. Objects outside the frame are culled. With no enabled camera the tab says *No active camera*. While nothing is playing, the tab previews the scene through that camera.
- **The editor stays usable during a run.** The scene view, hierarchy and inspector show the running objects and edit the runtime copy; values a graph changed appear as overrides; spawned objects appear in the hierarchy and destroyed ones vanish; graphs can be edited and take effect on the next frame. Undo works on the run's own history. Autosave pauses.
- **Input goes to the tab on screen.** In the Game tab, keys and the mouse reach the game; in any other tab they are the editor's. Leaving the Game tab releases every held key.
- **Frame order:** animation system, physics and collision detection, behaviors (events in the order of section 8.6), then rendering.
- **Clock:** the world has one clock that starts each run at 0. The toolbar's Pause (`Ctrl+Enter`) holds the whole world, and input arriving while it is held is dropped. Particle sprays and time-driven shaders run on the same clock, so they stand still under a pause.
- **The game controls time itself** with graph nodes: `Pause game` (from the next frame only behaviors marked **Run while paused** are ticked; physics, animations, particles and shader time stand still), `Resume game`, `Is game paused`, `Set time scale` (1 is real time; at 0 everything is frozen but graphs keep running with a `dt` of 0), `Time scale`. A pause belongs to the scene and is lifted by `Load scene`; the time scale belongs to the run.
- `Load scene` swaps the running scene at the end of the frame. Objects, their behaviors and the sounds those behaviors started end; Variables assets with global scope, global state machines and the clock carry on. `Restart game` rebuilds the run from the saved model. `Stop game` ends it.
- A graph error (an exception in a node, a runaway loop) stops the run and puts the reason in the notice line and the console.
- While a run is on, the Behavior and State machine tabs show it live: exec edges that just fired light up, each data output shows the value it last carried, and the current state is marked.
## 8. Graphs and behaviors
### 8.1 The shared graph editor
Behaviors, state machines, shaders and particle assets are all node graphs and share one editor component:
- Right-click on empty canvas opens a searchable **palette** of the nodes this graph type allows. Search ranks an exact name match first, then names starting with the query, then names containing it, then descriptions; `Enter` takes the best match.
- Drag from a port to a port to connect. Types are checked while dragging; the port that would be taken is highlighted. A data input holds one edge, and a new one replaces it. Dropping an edge on a node's body takes its first free compatible port. Dropping it on empty canvas opens the palette filtered to nodes that could take the edge, and node plus edge go in as one undo step.
- Nodes are dragged, rubber-band selected, multi-selected with `Ctrl`/`Shift`, deleted with `Delete`, copied and pasted with their inner edges. The wheel zooms, the middle button pans, **Fit** frames the graph, an optional **Snap** aligns dragged nodes to a 24 px grid. The header shows "N nodes · M links".
- A selected node shows its settings and the inline values of its unconnected inputs in the inspector. An unconnected data input uses its inline value, which the node also shows next to the port name.
- **Groups** (a titled box that moves the nodes whose centre lies inside it when dragged by its title bar) and **comments** (a free text card) can be added from the palette. Deleting a group never deletes nodes.
- A node type is one definition object (ports, settings, a `run` or `emit` function) in one file; palette, port drawing, validation and inspector fields are generated from it. Ports can be fixed, declared on the node by the user, or derived from settings and the surrounding graph.
- When a derived port disappears while an edge still hangs on it (the state it stood for was deleted, the behavior it called changed), the edge is not silently removed: the node shows a **ghost port** in a warning colour with the edge dashed. Nothing runs through a ghost and nothing can be connected to it; *Disconnect* removes it, and restoring the cause makes the edge work again.
### 8.2 Behaviors
A behavior asset holds a graph and the **variables** it declares. It is placed like any object; its parent is the **owner** the graph acts on.
- Ports are **exec** (control flow) or **data** of kind `number`, `boolean`, `string`, `object`, `list`, `struct` or `any`. An event node starts a pulse; the interpreter follows exec edges and pulls data inputs lazily from connected outputs. Data results are cached for the length of one pulse; a loop round clears the cache so expressions built on the loop index change each round.
- Two guards protect the editor: a pulse may take at most 100 000 steps, and exec edges may nest at most 1000 deep. Exceeding either stops the run with a clear message.
- Each running behavior instance has its own copy of its variables. A behavior's variables are properties of its placed object: they show in the object's inspector, can be overridden per object, and another graph can read and write them through `Properties`. A variable marked **Private** is hidden from placed objects and cannot be overridden.
- Two switches exist only on behaviors: **Run while paused** (section 7) and, per variable of a called behavior, **Keep** (section 8.5).
### 8.3 Values and kinds
A variable, a struct field or a list item is one of: number, text, boolean, colour, texture, state machine, asset, animation, sound, scene, object, list, struct. Define every kind as one row of one table (label, how it travels on an edge, its empty value, how a value is coerced to it, its picker), and derive the kind menus, literal nodes and inspector fields from that table.
- A list declares nothing about its items by default: each item carries its own kind, so a list may be mixed. Kinds that JSON cannot tell apart are stored tagged, for example `{"$color": 16711680}`, `{"$texture": "<id>"}`, `{"$object": "<id>"}`. Field names may not start with `$`.
- Lists and structs nest to any depth. They are compared by content, never by identity.
- The inspector draws lists and structs inline: a list row shows its count and `+ Item`, each item is a row with the widget of its kind, a struct shows its fields as rows one indent step in. Editing an item is an ordinary value edit and one undo step.
- An unconnected input carries the zero of its kind (0, false, empty text, empty list); an unconnected `object` or `struct` input carries null.
### 8.4 Node catalogue
Implement all of these. Names are the palette names.
- **Events:** `On start`, `On enable`, `On update` (outputs `dt`), `On disable`, `On destroy`, `On key down` (with optional auto-repeat: delay and interval in ms), `On key up`, `On key held`, `On timer` (interval; output `late`), `On collision enter` / `stay` / `exit` (outputs `other`, `other collider`, `collider`, and for enter/stay `normal x/y`, `impulse`, `point x/y`), `On click`, `On mouse wheel`, `On animation finished`, `On state entered`, `On state exited`. Keys ignore case.
- **Flow:** `Branch`, `Sequence` (grows an output whenever the last one is connected), `Repeat` (`index`, `body`, `done`), `For each` (`item`, `index`, a `break` input read before every round, `done`), `Run behavior`, `Start` and `End` (declare the inputs and outputs of a behavior that is called, values and further flow ports alike), `State branch`, `Set state`, `Set state by name`, `State transition`.
- **Objects:** `Self`, `Owner`, `Parent`, `Object by name`, `Child by name` (nearest level first), `Objects by name`, `Children` (optionally recursive), `Children by name`, `Objects of asset`, `Name of`, `Is asset`, `Is enabled`, `Get position` (local and world).
- **Values:** `Variables`, `Properties`, the literals `Number`, `Text`, `Boolean`, `Texture`, `State machine`, `Sound`, `Scene`, `Animation`, `Asset`.
- **Math and logic:** `Add`, `Subtract`, `Multiply`, `Divide` (by 0 gives 0), `Modulo`, `Min`, `Max`, `Abs`, `Sqrt`, `Clamp`, `Floor`, `Ceil`, `Round`, `Lerp` (with ease: Linear, In, Out, In-out, or a curve asset), `Random`, `Length`, `Distance`, `Normalize`, `Compare`, `Equals`, `And`, `Or`, `Not`, `Select`, `First not empty`, `Time`. `Compare` and `Equals` treat two numbers as equal up to a relative 1e-9.
- **Text:** `To text`, `To number`, `Join text`, `Substring`, `Find text`.
- **Lists and structs:** `List` (constant), `Fill`, `Count`, `Item at`, `Set item`, `Add item`, `Sub list`, `Join lists`, `Index of`, `Pack`, `Expand`. List nodes return new lists and leave their input alone.
- **Input:** `Key is down`, `Key was pressed` and `Key was released` (true for the one frame of the edge), `Axis input` (four keys to a normalised direction), `Mouse position`, `Mouse button`, `Is hovered`.
- **Actions:** `Move` (dx, dy, scale, dt; optional *Collide with solids*, section 11), `Set position`, `Spawn` (asset, x, y, z, optional parent; outputs the object), `Destroy`, `Emit`, `Play sound`, `Stop sound`, `Play animation`, `Stop animation`, `Pause animation`, `Load scene`, `Log`, `Stop game`, `Restart game`, `Pause game`, `Resume game`, `Set time scale`.
- **Queries:** `Is touching`, `Raycast`, `Raycast all`, `Get velocity`, `Animation playing`, `Animation length`, `Current state`, `Is in state`, `Is game paused`, `Time scale`.
- **Physics:** `Apply force`, `Apply impulse`, `Set velocity`, `Set angular velocity`, `Set gravity`, `Ignore one-way`.
Two nodes carry most graphs and deserve care:
- **`Variables`** has a list of variables picked from the declarations. Each is an output readable at any time, read at the moment it is asked, so a read after a write sees the new value. A variable switched to *Set* is also an input, written when the flow arrives; all inputs are read first and then written together, so one node can swap two variables. `count = count + 1` is one `Variables` node feeding its own output through `Add` into its input. Its *Variables* setting selects whose declarations it uses: empty means the behavior's own, otherwise a Variables asset. Renaming a variable carries the ports and their edges along in every graph.
- **`Properties`** does the same for the properties of an object it is handed. Its *Type* setting (Sprite, Text, Collider, Camera, Container, Clip area, Behavior, Any) decides which properties `+ Property` offers. For Sprite and Text the list includes the parameters of every shader in the project. This is how a graph changes a text, tints or resizes a sprite, switches an object on or off, or turns it.
### 8.5 Calling a behavior
`Run behavior` runs another behavior as one step. The inputs declared on the callee's `Start` node become input ports of the call, the outputs declared on its `End` node become output ports. A call is synchronous and isolated: the callee runs with a fresh copy of its variable defaults and its own data cache, and values travel only through the ports. A variable of the callee marked **Keep** instead resumes from where the last call from the same node on the same object left it. A behavior calling itself raises an error. Double-clicking a call node opens the callee, with a breadcrumb back. Dragging a behavior asset onto a graph creates a call. Renaming or removing a declared port updates every caller. A port declared on `Start` or `End` may be of the kind **Flow**: on `Start` it is a further flow input of the call, and a call arriving there begins at the `Start` output of that name only; on `End` it is a further flow output, and the call continues first through the one its `End` was reached by last, then through its plain `exec`, which always fires.
`On behavior` is the same behavior used as an event. The node has no flow input; the behavior it names runs at the node for as long as the surrounding behavior does, with all of its own events firing and its own variables, timers and local state machines, one set per node. Whenever one of its flows reaches its `End`, the node fires through the output named like the flow input reached last (the plain `exec` only when `End` was reached by `exec`), carrying the values `End` was handed. The value inputs of `Start` are inputs of the node, read anew for every inner flow. Behaviors may listen to each other to any depth; a behavior listening to itself, directly or through others, is reported on the node and not started again. Inside a behavior that is run as a call, `On behavior` stays silent.
### 8.6 Lifecycle and event order
- An instance is created when the behavior is effectively enabled (itself and all its parents): at play start, when spawned, or when a disabled parent is switched on. The order is `On start` (once), `On enable`, then updates, `On disable`, `On destroy`. Disabled, an instance is frozen, not gone: variables, timers and states survive, and switching it on again fires `On enable`, not `On start`. `On destroy` fires when the object leaves the world, including when the run stops or restarts.
- A frame goes event kind by event kind across all behaviors: disable, enable, key down, key up, key held, timer, collision enter, stay, exit, animation finished, click, mouse wheel, and last update. State events caused by a kind follow right after it. Every event node has an integer setting *Order* (default 0): within one kind, lower runs first; ties run in hierarchy order, then in node creation order.
- An object destroyed during a frame receives no further events of that frame except its farewell events at the end of it.
- `On click` fires for the behavior whose owner was hit: its colliders decide if it has any, otherwise the sprites and texts of the owner and everything below it. A behavior without an owner receives every click inside the camera frame.
## 9. Shared data: objects as values, Variables assets, structs, typed lists
### 9.1 Objects as values
A variable, struct field or list item of kind object holds a live object during a run (the result of `Spawn`, a lookup, a collision). At rest it holds either nothing or an object of the same scene picked on the placed behavior from a dropdown listing the scene's objects in hierarchy order. Inside a container template the dropdown lists the objects of that container, nested content included. A destroyed object reads as null. Live objects never leave a run: whatever is written back to the model or saved holds the empty object in their place.
### 9.2 Variables assets
A Variables asset is a list of declarations and nothing else. A `Variables` node naming it reads and writes one value per key for the whole run, however many graphs use it. Values start at the declared ones on every Play and are never written back to the project. While a run is on, the asset's inspector shows the live values, and editing one there reaches the run at once. Its **Scope** is *Global* (survives `Load scene`) or *Scene* (rebuilt from the declarations on every `Load scene`). A global one may be **Persistent**: its values are saved in the browser's `localStorage` per project as they change, and the next Play starts from them. That is the savegame. *Clear saved values* on the asset and *Clear saved data* in the menu forget them.
### 9.3 Struct assets and typed lists
- A struct asset declares named fields with kinds and defaults. A struct variable or struct field may take such an asset as its **Type** instead of declaring its own fields; `Pack` and `Expand` have the same setting and then take their ports from the asset.
- A list variable or list field may be typed with a struct asset through its **Items** setting. `+ Item` then adds an item of that type at its defaults and the field names are fixed. A list holding anything but structs refuses to be typed, with a line in the console.
- A change to the struct asset's fields reaches every place of that type in one undo step: a new field appears everywhere at its default, a removed one leaves every value and takes its port and edge, a renamed one keeps its value and its edge. This holds through nested types, per-object overrides and typed lists.
- A struct type may not contain itself, directly or through another type; the type picker does not offer such a choice.
- Deleting a struct asset leaves every user with the fields it last had, now as its own.
- At run time values are plain data and edges do not check struct types. What checks is writing: a typed list variable keeps only written items whose field names match the type exactly and reports each rejected item in the console; a typed struct variable accepts a struct whose fields are a subset of the type's (the missing ones keep their value) and refuses anything else; `Add item` and `Set item` with a *Type* setting accept exact matches only.
- Right-clicking an output pin that carries a typed struct offers **Split into fields**: the output is replaced by one pin per field. **Recombine** restores it.
## 10. State machines
A state machine asset is a graph of `State` nodes joined by **transitions**. It uses the shared graph editor with these differences: the palette offers only `State`; states show no ports; a transition is made by right-click → *Make transition* and then clicking the target state; it is drawn as a straight arrow from border to border; a pair of opposite transitions lies side by side; a transition from a state to itself is drawn as a loop above the node. Transitions are named and removed in the inspector of a selected state, which lists them as *out* and *in*. Several transitions may join the same two states if their names differ. Right-click a state → *Set as start state*.
- **Scope:** *Local* (default) gives every behavior instance its own current state, *Global* keeps one for the whole run, *Scene* keeps one per loaded scene.
- **Nodes in a behavior**, each with a `machine` input picked on the node: `State branch` (one exec output per state plus `else`), `Set state` (one exec input per state; the input the flow arrives through names the new state), `Set state by name` (text; with a `not found` output), `State transition` (takes the transition of the chosen name out of the current state; with none or two of that name the machine stays), `Current state` (name and id), `Is in state` (one boolean per state), `On state entered` and `On state exited` (one exec output per state). Ports are keyed by state id and labelled by name, so renaming a state keeps every edge.
- **Rules:** setting the state a machine is already in is no move and fires nothing; a self-transition is a move and fires exited then entered. The start state counts as entered. Events fire after the flow that caused the move has finished, exited before entered. A local machine's move is heard by the instance that made it, a shared one by every instance with a matching event node. Events that keep moving the machine endlessly stop the run with an error.
- While playing, the State machine tab marks the current states, strongly for the instance of the selected object.
## 11. Colliders and physics
- **Collider** shapes: box (`width`, `height`), circle (`radius`), capsule, polygon (a list of points edited with handles in the scene view; concave polygons are decomposed into convex pieces; a self-crossing one is drawn red and collides with nothing). A collider is placed as a child of the object it belongs to and takes its transform. *Add collider…* creates a box sized like the sprite or places an existing collider asset. Properties: `isTrigger` (reports but never blocks), `layer`, `collidesWith`, `oneWay` with a blocking side and angle, and the material `friction` (0.5), `bounciness` (0), `density` (1).
- **Collision layers:** up to 16 named layers in the project settings. Two colliders meet only when each names the other's layer. A new project has one layer that meets everything.
- **Events:** every frame, overlapping collider pairs are sorted into enter, stay and exit and delivered to the behaviors whose owner the colliders belong to. Colliders of the same body never collide with each other.
- **`Move` with *Collide with solids*:** the object's solid colliders are swept along the move; at the first solid they stop flush and the rest of the move continues along the surface, reduced by friction. The node outputs `blocked x`, `blocked y`, `hit` and the contact `normal x/y`. A fast move is split into sub-steps so nothing tunnels through a thin wall. An overlap that already exists is never resolved and never deepened. This is how a platformer character is built without a rigidbody.
- **One-way colliders** block only contacts whose normal lies within the angle of their blocking side; `Ignore one-way` lets an object fall through for a given time.
- **Rigidbody:** placed as a child, at most one per object. Types Dynamic, Static, Kinematic; `mass` (0 = from density), `gravityScale`, `linearDamping`, `angularDamping`, `fixedRotation`, `ccd`. It carries every collider below its object. Rapier simulates at a fixed 60 steps per second regardless of frame rate (at most 8 catch-up steps), with 100 world units to one metre; the scene's `gravityX` / `gravityY` default to (0, 980). Rendering interpolates between the last two steps. The engine's result is written back as the object's local position and rotation.
- **Raycast** returns the first solid met along a ray (object, collider, point, normal, distance), with settings *Ignore owner*, *Hit triggers* and a layer filter. **Raycast all** returns every hit as parallel lists sorted by distance.
- Collision geometry is your own double-precision code shared by the editor and the runtime; Rapier is used for rigidbody simulation and for proposing contact pairs.
## 12. Textures and sprites
- **Import:** dropping image files on the window, pasting an image, or `+ → Texture` (a picker showing every image the server already holds, plus *Upload…*). The bytes are stored by content hash, so the project document holds only a file name.
- A texture has `filter`: Linear or Nearest.
- **Frames:** the Texture tab shows the image with its frames, named rectangles in image pixels. Drag on empty picture to cut one, drag it or its handles to adjust, **Slice…** cuts a regular grid from columns and rows with optional offset and spacing. A sprite's `texture` property holds either a texture id or a frame id, through the same picker ("Sheet / Frame", searchable).
- **Nine-slice:** a frame carries four borders and a fill mode (Repeat or Stretch). A sprite wearing it draws corners at their own size, edges along one axis and the middle along both. All borders at zero means not sliced.
- Drawing a frame must never show pixels of the neighbouring cell at its edges.
- Sprite `width` / `height` rows offer *Set to picture size*. Right-click a texture → *Create sprite from texture* and *Create animation from texture* (a sprite plus an animation keying its frames one per frame).
- All images, fonts and sounds a project names are loaded as soon as it opens.
## 13. Text and fonts
A font asset is an uploaded `.ttf`, `.otf`, `.woff` or `.woff2`, stored by content hash and registered with the browser under a family name derived from the hash. A text's `font` row picks one; none or a missing one falls back to the editor's default face. A font has `filter` Linear or Nearest; Nearest renders pixel fonts with hard edges at any zoom. Text is rasterised into a texture of its own and drawn like a sprite, which is what lets a text wear a shader. Until the face has loaded, the text is drawn in the fallback and redraws when it arrives.
## 14. Sound
A sound asset is an uploaded `.mp3`, `.ogg`, `.wav` or `.m4a`, stored by content hash, with `volume` (0 to 1) and `loop`, and a preview button in its inspector. Only a graph plays it:
- `Play sound` starts a new voice each time (two plays overlap). Its `volume` multiplies the asset's, its `loop` is *From the asset*, *On* or *Off*, and `detached` decides ownership.
- A voice belongs to the behavior that started it and ends when that behavior is destroyed, unless it is detached. `Stop sound` ends every voice of the named sound, or all voices when none is named.
- Pausing the run holds every voice, stopping it ends them all. Time scale does not change pitch. Decoding happens on first use and `Play sound` never blocks; a stop issued while the bytes are still loading wins.
## 15. Animations
An animation asset is a timeline over one **root asset** (a sprite, text, container or scene). It has **rows**, each aiming at one property of the root or of an object inside it (to any depth of nested containers), and each row has **keys**: a frame number and a value. Position rows `x`, `y`, `z` are offered for placed objects; behavior variables (non-private) and shader parameters can be keyed too.
- Properties: `fps` (default 10) and `playback`: Loop, Once (hold), Once (reset), Ping-pong.
- Numbers and colours blend between keys; textures, booleans, texts and choices hold until the next key. The playhead moves every tick, not only on whole frames. A row may be **relative**: its numbers are added to what the target held when the animation started.
- Each key of a blending row has an **easing** to the next key: Linear, Step, the curves Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce each as In, Out or In-out, or a curve asset.
- **Animation tab:** a header (root picker, *+ Row*, Play/Pause, Fit), a preview that shows the root at the playhead without changing the document, and a timeline with a frame ruler: double-click a cell to set a key, click or band-select keys, drag to move them, `Ctrl`+drag to duplicate, `Delete` removes them, copy and paste at the playhead, click the ruler to scrub. The selected key's value and easing are edited in the inspector.
- **Runtime:** `Play animation` (object, animation, `blend` seconds to fade in from the current values, `blend out` for Once (reset), `restart`), `Stop animation` (with optional blend out), `Pause animation`. Playing an animation on a placed container plays it on that instance alone. Several animations may run on one object; where two write the same property the one started later wins, and a behavior writing it in the same frame wins over both. `On animation finished` fires exactly once in the frame a Once animation ends. `Animation playing` returns `playing`, `progress` (0 to 1) and `time left`; `Animation length` returns the seconds of one pass.
- Animations run in play only; the scene view shows objects as stored.
## 16. Shaders
A shader asset is a node graph compiled to a GLSL ES 3.00 fragment shader on every change. A sprite, a text or a particle source wears it through its `shader` property. A graph that does not compile says why on the offending node, and whatever wears it is drawn plain.
- Values are numbers (`float`) or colours (`vec3`). `any` ports take both, and a number fed to a colour is broadcast.
- **Nodes:** `Texture color`, `Texture alpha`, `UV` (`u`, `v` from 0 to 1), `Sample texture` (at `u`, `v`), `Texture size`, `Time`, `Property` (declares a named number with default, min, max), `Color`, `Number`, `Add`, `Subtract`, `Multiply`, `Divide` (by 0 gives 0), `Mix`, `Luminance`, `Sin`, `Floor`, `Fract`, `Abs`, `Step`, `Pow`, `Sqrt`, `Clamp`, and one `Output` with inputs `color` and `alpha` and the setting *Edge* (Clamp, Mirror, Repeat) for sampling past the border. Unconnected Output inputs keep the texture's own colour and alpha.
- Every `Property` becomes a uniform and a row below the wearer's `shader` dropdown: an ordinary property that can be overridden per object, animated, and written by a graph through `Properties`. A property name may not collide with a property the sprite already has.
- A shader runs in the sprite's own draw (no intermediate render target), and sampling respects the texture's filter so pixel art stays sharp. A frame is sampled as a texture of its own: sampling past its border follows *Edge* and never shows the neighbouring frame.
- Compilation is cached per asset by a fingerprint of what the fragment depends on; moving a node recompiles nothing.
- **Shader tab:** the graph on the left, a preview on the right showing the last selected sprite or texture (or a built-in test image) wearing the shader, with a slider per property that affects only the preview, and Hold / Restart for the clock when a `Time` node is used.
## 17. Particles
A particle asset is a node graph that defines one particle as a pure function of its age and of random numbers drawn once per particle. Nothing is simulated step by step, so the same second always draws the same picture and a preview can jump to any time. A placed particle asset is a **source**.
- **Nodes** (all values are numbers; a colour is a packed `0xRRGGBB` number): `Age` (0 to 1), `Age in seconds`, `Time`, `Index`, `Random` (min, max; each Random node draws independently), `Gravity` (strength → x, y offset), `Start velocity` (angle, speed → x, y offset), `Property`, `Number`, `Color`, `Add`, `Subtract`, `Multiply`, `Divide`, `Min`, `Max`, `Clamp`, `Mix`, `Mix color`, `Sin`, `Cos` (degrees), `Sqrt`, `Abs`, `Floor`, `Fract`, `Step`, `Pow`, and one `Output` with `x`, `y`, `scaleX`, `scaleY`, `rotation`, `color`, `alpha`.
- **Source properties:** `emitting`, `mode` (Continuous or Burst), `rate`, `burstCount`, `lifetime`, `timeScale`, `prewarm`, `maxParticles`, `size`, `texture`, `color`, `moveWithSource`, `shader`, then one row per `Property` node of the graph.
- A source's whole field is drawn in one draw call. The `Emit` node restarts a source.
- **Particles tab:** the graph on the left, a preview on the right with Hold / Run, Restart and a time slider over three lifetimes.
## 18. Curves
A curve asset is an easing drawn by hand: a chain of points from t = 0 to t = 1 with optional bezier handles, whose value may leave the range 0 to 1 (overshoot). The first and last point stay at t = 0 and t = 1, points never overtake each other in t, and handles are limited so the curve has exactly one value for every t. The Curve tab edits points and handles by dragging, offers presets for the built-in eases, and previews the motion. A curve is used as the easing of an animation key and as the ease of a `Lerp` node; a missing curve falls back to linear. The tab, the animation player and `Lerp` evaluate the same function.
## 19. Server and storage
One Node process, no authentication.
- `GET /api/projects` lists projects (id, name, modified). `POST /api/projects` creates one. `GET`, `PUT`, `DELETE /api/projects/:id` load, save and delete. A save carries the revision it was based on; a stale one is rejected with 409 and the editor tells the user the project changed elsewhere instead of overwriting.
- `POST /api/files/:kind` (`textures`, `fonts`, `sounds`) takes raw bytes and stores them as `data/<kind>/<sha256>.<ext>`, with the extension derived from the content's signature, never from what the client claims, and the original file name kept beside it. `GET /api/files/:kind` lists what is stored. Stored files are served statically and are never deleted automatically, which is what makes deleting an asset undoable.
- Projects are stored as `data/projects/<id>.json`, written atomically (temp file, then rename).
- Images over 8192 px per side and unknown formats are refused with a clear message.
## 20. Three example games
Build these three games with the editor's own features and ship them as seeded projects. They are the proof that the parts work together, and each one must be playable from start to end. Build them the way a user would: as assets, scenes and graphs in project files produced by a seed script that uses your `core` commands, not as special runtime code.
1. **Platformer.** A character that runs and jumps (`Move` with *Collide with solids*, gravity in a variable), one-way platforms, coins that count up in a persistent Variables asset and play a sound, a hazard that restarts the level, a moving kinematic platform or a dynamic crate that can be pushed, a camera that follows the player, a walk animation from a sprite sheet, a particle burst on pickup, a title screen and a win screen as containers switched on and off, and a second scene reached with `Load scene`.
2. **Tetris.** A 10 x 20 board held in a list variable, pieces as structs, rotation, line clearing, score and level as text in a custom font, increasing speed, auto-repeat on the arrow keys, game over and restart. The board cells are sprites addressed by name from a loop.
3. **Top-down arena.** A player moved with `Axis input`, enemies spawned from a container prefab on a timer, each with a local state machine (patrol, chase, attack) and a raycast for line of sight, shots that hit enemies on their own collision layer, a hit flash through a shader property written from a graph, a pause menu that works through `Pause game` and *Run while paused*, and a health bar built from a nine-sliced sprite inside a clip area.
Each game gets an end-to-end smoke test that opens it, presses Play, drives it with real key presses for a scripted sequence and asserts on game state (score increased, scene changed, enemy destroyed).
## 21. Milestones
Work through these in order. Each ends with `npm run check` and the end-to-end tests so far green, a commit, and an updated `PROGRESS.md`.
1. Skeleton: repository, scripts, server with project storage, an empty editor shell with the panel layout, a Pixi scene view, and whatever your end-to-end tests need to read state and find elements.
2. Model core: assets, schemas, overrides, scene objects, hierarchy, transform, commands and undo, autosave, inspector rows with reset and apply.
3. Scene editing: placing, selecting, dragging, gizmo, hierarchy panel, copy and paste, symbols, multiple scenes.
4. Textures, frames, nine-slice, sprites, text and fonts, clip areas, camera and the Game tab preview.
5. Containers: template resolution, instance edits, nested containers, create and unpack, the Container tab.
6. Runtime: Play and Stop, the world, the clock, pause, editing while playing.
7. Graph editor foundation and behaviors: interpreter, variables, the node catalogue, lifecycle and event order, calls, live view, console.
8. Lists, structs, object values, Variables assets, struct assets, typed lists.
9. Colliders, `Move` with solids, collision events, layers, one-way, raycasts, rigidbodies with Rapier.
10. State machines.
11. Animations and curves.
12. Sound.
13. Shaders.
14. Particles.
15. The three example games and their smoke tests.
16. A final pass: run everything, look at every tab in screenshots, fix what is off, write `README.md` and `ACCEPTANCE.md`.
## 22. Deliverables and how the result is judged
**Files in the repository root:**
- `README.md`: what this is, how to install, run and test, and the layout of the code.
- `PROGRESS.md` and `DECISIONS.md` as described in section 1.
- `ACCEPTANCE.md`: one line per numbered criterion below with its state (done, partly, not done), the test that proves it, and an honest note where something falls short.
**Acceptance criteria.** These are checked by running your commands and your tests, by reading the tests to see whether they prove what they claim, and by using the editor and playing the three games by hand.
1. `npm install`, `npm run build`, `npm run check` and `npm run e2e` succeed on a clean checkout with Node 22.
2. A new project can be created, edited, reloaded and deleted; a reload shows exactly what was saved; a stale save is rejected.
3. Every user edit is undoable and redoable, one gesture per step, across every tab.
4. A property resolves override → asset → default; reset and apply work; an override equal to its base disappears.
5. Parenting cascades position, z, rotation, scale and mirror; re-parenting keeps the world position and look.
6. A template edit reaches every placement of a container; a placement's value edits survive it; nested containers resolve to any depth; apply and reset to container work; create, unpack and the cycle refusal work.
7. Play runs the chosen scene through the active camera, Stop restores the saved state exactly, and the editor can inspect and edit the run.
8. Every node of section 8.4 exists, is found in the palette, and behaves as specified; the lifecycle and event order of section 8.6 hold.
9. Lists, structs and object values nest, display and edit as specified; Variables assets share, scope and persist; struct assets and typed lists follow their type and check writes.
10. State machines edit, run, scope and fire events as specified.
11. Colliders of all four shapes detect, block and slide; layers, triggers, one-way and raycasts work; rigidbodies fall, rest, bounce and are pushed.
12. Textures import, slice, nine-slice and filter correctly; text renders in uploaded fonts.
13. Sounds play, overlap, loop, stop with their owner and with the run.
14. Animations play in all four playback modes with easing, blending, relative rows and nested targets; the timeline edits keys.
15. A shader graph compiles, shows on sprites, texts and particles, exposes its properties as overridable rows, and matches the expected pixels for a known graph.
16. A particle graph renders deterministically: the same time gives the same picture.
17. Curve assets edit under their constraints and drive animation keys and `Lerp`.
18. The three example games are seeded by `npm run seed`, open without console errors and are playable to their end.
19. No console errors or unhandled rejections occur during the end-to-end suite.
20. `ACCEPTANCE.md` matches reality.
## 23. Out of scope
Leave these out entirely; time spent on them is taken from what is judged: user accounts and permissions, publishing or exporting a game outside the editor, plugins, performance overlays, behaviors that run in the editor while the project is not playing, project snapshots or version history, tilemaps and tile assets, a pixel art editor, a vector drawing editor, hierarchy folders, an eyedropper for picking objects in the view, per-frame collision outlines on textures, mobile or touch support, localisation, and theming.