Files
feuerwerk/benchmark/engine-prompt.md
T
adminandClaude Opus 5.5 defc34bd09 Add a single prompt that builds the engine from scratch
The prompt is meant as a benchmark: one agent, one prompt, and the goal
is a finished engine rather than a measure of how far it gets.

- The scope follows the decisions made on 2026-10-01. In: projects,
  scenes with a hierarchy, camera, containers with nesting and instance
  edits, behaviors, shaders and particle effects as graphs, colliders,
  animations, text with custom fonts, textures and sprites, state
  machines, variables assets, structs, typed lists, sound, the console
  and custom curves.
- Out: accounts, plugins, the performance panel, export and player,
  tilemaps, the pixel art and vector art editors, run in editor,
  snapshots and hierarchy groups.
- The stack is fixed except for the UI framework, which the agent
  picks itself. The interface is DOM, Pixi only draws the views.
- "Done" is checkable: three sample games with smoke tests, numbered
  acceptance criteria and an ACCEPTANCE.md the agent has to fill in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:44:52 +02:00

57 KiB

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), 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, 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.

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.