Docs Design

Contents

Paint a level: the Studio's level editor

Designed 2026-10-06. Part of M11 ("Documents and assets … every document browsable and editable"; "play while you make it … from any level or place"), after the second layout (design/studio-layout.md) and previews (design/studio-previews.md).

Why

A platformer's rooms and a grid puzzle's levels are grids of characters with a legend (Room.rows, Level.rows, run-length coded: "16#/#14.#/…"). In the Studio today they are a text field. Nobody makes a level by typing #7.*6.#; every game editor (Mario Maker, Tiled, Godot's TileMap, PuzzleScript's editor) lets a person paint it and play it at once. This is the single change that most makes the Studio feel like a place to make games, not a place to edit files.

What

Opening a Room or a Level shows it as a grid in the middle (a document tab over the game), drawn as the game draws it:

  • The canvas. Each cell drawn by the game's own look: a platformer room's tiles and things by kit.platformer's view and the Style's palette; a puzzle level's objects by kit.grid's view and their sprites. Pan with space-drag or the middle button, zoom with the wheel or Ctrl+=/−, a grid line on hover, the cell under the pointer named under the canvas ("Spikes at 5, 10"; once in the window's status bar too, which said it twice).
  • The palette. The legend's entries as swatches drawn the same way, named in words ("Solid ground", "Gem", "Crate on a plank"), with a key each (1–9); New entry adds one through the legend's own fields (a tile kind or a thing's archetype, picked by name).
  • Tools. Paint (B), Erase (E: the legend's empty entry), Rectangle (R), Fill (G), Pick (I: the cell's entry becomes the brush), Select and move (M), with Shift for straight lines. A stroke is one edit: mouse down to mouse up writes /rows once through docs.edit, so Ctrl+Z takes back a stroke, and a proposal over the same document shows as Now and Proposed canvases.
  • Size. A room's or a level's size changes from the Inspector (sizeTiles, sizeCells); the canvas grows or crops from its bottom-right, said before it crops.
  • Checks as you paint, a moment after the last stroke, under the canvas, on it and on its tab:
    • a puzzle level: solvable or not (gamewright solve --level, the core's solver), and in how many moves; unsolvable marks the level's tab red and says why in words ("No way to bring the crate to a berth");
    • a platformer room: its open sides lead somewhere (a side named open has a walkable cell on its edge), the player's start and the exit exist where a level needs them, and nothing places a thing inside solid ground; a room a level's chain uses keeps its role.
  • Play this. F5 on a level's tab plays from that level (--level), on a room's from a level that holds it (or a one-room level built for the try); the game shows in the middle, the header's start chooser names that level, and the level's tab stays beside the game's, a click away (Esc stays the game's: its own pause needs it). The play is a try: nothing about the game changes.
  • The Director, on the canvas. "Ask the Director about this level" sends the level, its solver's verdict and the selection, so "make this harder" or "add a second crate here" lands as a proposal shown on the canvas.

How

  • The model. GwGridRows (pure, RefCounted): run-length rows to a 2D array of legend chars and back, canonical on the way out (the core's encoding: the same text for the same grid, so a stroke that changes nothing writes nothing and diffs stay small). Tested against every Room and Level in the samples and starters (decode, encode, byte-equal).
  • The canvas. GwStudioGridCanvas (a Control drawing into a SubViewport of the game's own view nodes for the cells, cached by legend entry; or, where a kit's view cannot draw a lone cell, a swatch from the Style's palette and the entry's sprite). One file per tool.
  • The core. No new kind: the editor writes rows, legend, sizeTiles, sizeCells through docs.edit. A docs.check_level session method answers the solver's verdict and the room checks above, cached by the document's hash, so painting never waits on it; it checks the canvas's own rows (rows), so a stroke is judged before, or without, its write.
  • Strokes are drafts. A level being painted is unfinished most of the way: no way to win yet, a room with no way through. docs.edit refuses an edit that breaks the game's rules, so the editor sends each stroke with draft: true (design/m11-studio.md S6). A draft lands when the documents it writes fit their schemas, even if it breaks their game rules; what it keeps shows as problems (the red tab, the checks, the problems list) and bake and play refuse the game until they are fixed, saying why. It is still refused for a schema error in a file it writes, or a problem it newly causes in another document. Only the person's edits are drafts; the Director's proposals keep the whole rule. Undo and redo work across drafts as for any edit.

Not now

Placing things in a top-down world (Worldgen's scatter is generated; fixed placements and a painted region come later, on the same canvas), a Dialogue's graph (its own editor), and Screens drawn as the game draws them.

Proof

  • GwGridRows round-trips every Room and Level of every sample and starter byte-for-byte.
  • Paint, erase, rectangle, fill and pick each change exactly the cells they should (gdUnit4, red first), and one stroke is one docs.edit (undone by one Ctrl+Z).
  • A scenario: open Pier Pressure's first level, paint a crate onto a berth's row so it is unsolvable, see the red tab and the words, undo, see it solvable again in the same moves as before; open a Burrow Run room, paint spikes, play from its level, come back; frames at 1280×720 and at scale 2, Read; plain words on every frame.
  • The canvas draws a 64×48 room in under 16 ms a frame while painting (recorded, failing above).

From docs/design/studio-level-editor.md in the Gamewright repository.