# 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 `~