The scene recipe format

A recipe describes a battle map the way you would say it out loud: fill this rectangle with flagstone, wall this room, run a road along these points, scatter trees here. TerrainTale expands it into grid-correct geometry on import. The format is free to use in your own tools. For a guided walk-through, see building maps with AI.

A recipe is a JSON file that describes a scene as intent — "paint this meadow with grass", "wall this perimeter", "run a dirt road along these points", "scatter trees here" — instead of a raw list of 2,000 hand-placed transforms. The app expands it into correct geometry on import: natural ground is painted onto the terrain with soft, ragged edges free of the grid, built floors tile with no gaps, walls share canonical edges so they meet, and roads pick the right junction piece and rotation from the path shape.

This is the format you author. It is expanded by src/lib/sceneBuilder.ts (pure, unit-tested) and loaded by src/lib/sceneRecipe.ts.

How the user loads it: save the recipe as a .json file, then in the app open File ▸ Import Scene JSON and pick it. Every model the recipe references is pulled from the built-in library automatically — the user does not need to add assets first. (Programmatically: window.__vtt.loadSceneJson(obj).)


Coordinate system — the one rule

Every horizontal number (X, Z) is a grid cell. Every vertical number (Y) is in world units. One cell is one world unit by default.

  • Cell [x, z] — integer column/row. A tile placed there fills the whole cell; a prop placed there lands on the cell centre ((x+0.5), y, (z+0.5)).
  • North is −Z, south +Z, east +X, west −X. (Rotations follow from this.)
  • rect: [x0, z0, x1, z1] — an inclusive rectangle of cells. A [0,0,9,9] rect is 10×10 = 100 cells.
  • level — integer vertical storey; each level is one cell-size up (for bridges, upper floors). Default 0. Levels stack on top of the terrain: a level-1 floor over a 2-unit hill sits at y = 3.
  • Rotations are in degrees. Fractional cell coordinates are allowed for fine placement (e.g. at: [5.5, 3]).

Keep the whole scene in a sensible band, e.g. cells 0..40. Negative cells are fine but keeping the origin at a corner makes the layout easier to reason about.


Top-level shape

{
  "format": "vtt-recipe",
  "name": "Gothic Castle in the Forest",
  "seed": 7,
  "grid": { "size": 1 },
  "environment": { "preset": "dusk", "fog": true },
  "view": { "target": [20, 1, 18], "position": [31, 12, 34] },
  "ops": [ /* build operations, applied in order */ ]
}
fieldrequirednotes
format✅Must be the literal "vtt-recipe". This is how the loader tells a recipe from a plain scene.
nameScene name shown in the app.
seedInteger. Makes all random ops (scatter/jitter) reproducible. Change it to reroll.
grid.sizeWorld units per cell. Leave at 1 unless you have a reason.
environmentSee below.
viewThe opening view: where the camera stands (position) and what it looks at (target), each [x, y, z] — X/Z in cells, Y in world units. It is what the maker and every player joining the table see first. Leave it out and the whole map is framed. Aim it at the heart of the scene (the gate, the altar, the party) from 15–30 cells back and 8–15 up.
ops✅Array of operations, applied top-to-bottom. Later floor/wall/road tiles replace an earlier one in the same slot (same rule as the app's brush), so you can lay a base then overwrite.

environment

Set the hour with timeOfDay (0..24, e.g. 7.5 for half past seven) and override individual fields. The hour places the sun, so it decides which way every shadow falls: it rises in the east, stands across the map (north, away from the opening camera) at noon and sets in the west, and at night a dim moon casts soft shadows instead. The sky, ambient and fog colour follow the hour. preset (dawn, day, dusk, night) still works on its own and stands for 06:45, 13:00, 17:45 and 23:00.

"environment": {
  "preset": "night",
  "timeOfDay": 23,
  "background": "#0d1220",
  "sky": false,
  "fog": true, "fogColor": "#0d1220", "fogNear": 12, "fogFar": 60,
  "ambientColor": "#5a6b96", "ambientIntensity": 0.22
}

sky: false keeps the preset's sky off the backdrop — set it for anywhere with a ceiling (a dungeon, a cave); the flat background colour shows instead.

weather is what falls and how the wind blows:

"environment": { "preset": "dusk", "weather": { "kind": "storm", "intensity": 0.9, "wind": 0.95, "heading": 240 } }

kind: clear | rain | storm | snow | ash; intensity and wind 0..1 (each kind has a sensible default); heading is where the wind comes from in degrees (0 north, 90 east). Rain, snow and ash fall over the whole map and close the horizon in; a storm darkens the sky and brings lightning; the trees sway with the wind in every weather. Leave it out for a clear, calm day.

A recipe with no lights still renders, lit and shadowed by the sun of its hour — but place torches and lamps deliberately (see the building guide). There is no sun to place: low raking light is an hour near 7 or 17, a high noon sun is 12–13.


Operations

Each op is an object with an op field. A bad op never crashes the load — it is skipped and a warning is logged to the browser console — so one typo can't sink a hundred good ops.

terrain — shape the ground

The ground is a height field the whole scene stands on. Floor tiles drape over it (a grass tile painted on a hill is the hill's surface); every wall, prop, light, effect and mini the other ops place gets the ground height under it baked into its position, so a keep on a rise sits on the rise and the party on the road is at the road's height. Every outdoor scene should start with one or more terrain ops — flat ground is the one thing nature never does. Interiors (a dungeon, a tavern) stay flat: a built floor is level by definition.

Terrain ops are applied before anything is placed, wherever they sit in the list, in the order they appear; shapes add to what is already there, so a gentle noise swell, a hill under the keep and a flatten for the keep's floor compose. Convention: write them first.

{ "op": "terrain", "shape": "noise", "amplitude": 0.6, "scale": 9 }
{ "op": "terrain", "shape": "hill", "at": [18, 14], "radius": 10, "height": 3 }
{ "op": "terrain", "shape": "flatten", "rect": [13, 10, 23, 18], "margin": 2 }
{ "op": "terrain", "shape": "ridge", "path": [[18, 31], [18, 20]], "width": 5, "height": 0, "heightEnd": 2.2 }
{ "op": "terrain", "shape": "plateau", "rect": [4, 4, 8, 8], "height": 1, "skirt": 3 }
shapefieldswhat it does
noisearea (default: the extent of every floor op and paint rect — the map), amplitude (max rise/fall, world units, default 0.8), scale (feature size in cells, default 10), octaves (1–4, default 3), feather (cells, default 2), seedRolling ground. Zero-mean, so the map stays around y = 0. amplitude 0.3–0.6 is a meadow, 1–2 is hill country.
hillat [x, z], radius (cells), height (world units)A round hill. Negative height digs a hollow, a pond bed, a sunken court.
ridgepath (corner points), width (cells, at the base), height, heightEndA bank along a path. With heightEnd the crest climbs from the first point to the last: a road graded up to a gate, a spur off a hill, a ramp.
plateaurect, height, skirt (cells the sides slope over, default 3)A raised block with sloped sides — the mound a shrine or a watchtower stands on.
flattenrect, height (default: the mean of what is there), margin (cells to blend over, default 2)Level the ground inside the rectangle. Do this under every building — a hall whose flagstones follow the hillside is a bug. Leave height out to keep the building on its hill.

Any terrain op may set skin (a library floor id, default realistic-surfaces/dirt_floor): the ground texture drawn where nothing is painted and no tile is laid, and through any crack between tiles. Past the painted map it is what the hills are made of.

Heights are world units (one cell-size is 5 ft; 1.0 is a person's shoulder, 3 is a real hill, 0.5 is a rolling field). Walls are rigid: a run across a slope steps piece by piece and its ends cut into the hill, which is how a built wall looks. Stairs, bridges and daises are placed at their level plus the ground; use y on a prop for anything that must float.

water — a pond, a stream, a river, a sea

Water is not a tile: a body of water is a set of cells under one level surface, and what the water looks like comes from the ground beneath it. The renderer reads the terrain's depth under every point of the surface — the shallows wear the body's color at its opacity, a unit down it is the deepColor and opaque, and the ground climbing out of the water foams. So water wants a bed, and the op can dig one: carve lowers the ground under the cells into a bowl that deep, dropping off over the first two cells inside the painted edge, and slopes the bank down to the water over bank cells outside it. The shoreline is then where the ground breaks the surface (inside the paint), the painted outline is hidden under the bank, and the same op makes a beach that shelves into the sea (bank 6–8) or a brook cut into a meadow (bank 2).

{ "op": "water", "path": [[0, 15], [9, 13], [18, 15], [35, 14]], "width": 5, "level": -0.35, "style": "river", "flow": 90, "carve": 1.4, "bank": 2 }
{ "op": "water", "rect": [20, 3, 27, 9], "level": -0.2, "style": "clear", "carve": 1.4, "bank": 3 }
{ "op": "water", "rect": [0, 18, 35, 29], "level": -0.5, "style": "sea", "carve": 3, "bank": 7, "name": "The Cove" }
{ "op": "water", "cells": [[4, 4], [5, 4], [4, 5]], "style": "swamp", "level": 0.05 }
fieldnotes
rect \cells \cell \path + widthWhere, like a floor op. A path is the corner points of a stream or river, painted width cells wide (default 3). Cells are one body's; a later op painting the same cell takes it.
levelSurface height, world units. Default 0.05, a hair above level ground. With carve, set it a little below the surrounding ground (−0.2 to −0.5 on a zero-mean noise map) so the banks stand above the water.
styleA look: lake (default), clear, tropical, sea, river, swamp, murky, cave. Sets the colours, the murk, the ripples, and for river a current.
color, deepColorOverride the style's colours, #rrggbb: the shallows and the deep.
opacity0..1, how opaque the shallows are (deep water is always opaque). 0.4 shows the bed, 0.9 is a swamp.
waves0..1: a still pond (0.1) to a choppy sea (0.8). Above ~0.5 the surface also swells.
flow, flowSpeedA current: a compass heading in degrees (0 north, 90 east, 180 south) and 0..1. flow alone gives a moderate current. Streams and rivers flow downhill and toward the sea; a lake is still.
softEdgeDefault true: the water fades out over the last cell of its painted edge where no ground rises to hide it. Set false for a hard edge (a stone-lined moat, a cistern).
carveDig the bed this deep below level, world units. 0 or absent leaves the ground alone (water then lies over whatever is there). Runs in the terrain pre-pass, in order with the terrain ops: after the noise, before a flatten you want to keep.
bankCells the bank slopes over, outside the painted edge. Default 3.
nameShown in the app's water bar.

carve follows the painted cells, so a path on the diagonal gets a stepped shore; for a winding stream dig the channel with a terrain ridge of negative height along the same path instead and leave carve off (see the building guide's Water).

A carve with no terrain op still makes a terrain; a terrain op after a water op edits the bed it dug (a flatten at −0.6 across a river is a ford; a ridge across it is a weir). A prop with "float": true stands on the water's surface instead of the bed — a boat, a raft, a jetty's planks, a barrel adrift.

paint — paint a surface onto the ground, free of the grid

This is how natural ground is made. A floor surface (grass, dirt, mud, sand, snow, moss, gravel, cave rock…) is painted onto the terrain like a splat: the edge is soft and ragged, it crosses cell lines freely, and one surface blends into the next. A meadow of floor tiles prints every grid line onto the map as a staircase edge; paint does not. Keep floor tiles for built floors whose seams belong on the grid — planks, flagstones, a tiled hall, a cobbled courtyard inside walls.

{ "op": "paint", "surface": "realistic-surfaces/grass_floor", "rect": [0, 0, 35, 29] }
{ "op": "paint", "surface": "realistic-surfaces/dirt_floor", "patches": 14, "patchRadius": [1.5, 3.5], "soft": 1.5 }
{ "op": "paint", "surface": "realistic-surfaces/dirt_floor", "path": [[0, 20], [9, 16], [18, 17], [35, 11]], "width": 3, "soft": 1.2 }
{ "op": "paint", "surface": "realistic-surfaces/mud_floor", "at": [22, 8], "radius": 3, "soft": 2, "strength": 0.7 }
fieldnotes
surfaceA library floor id (anything under Floors in the asset index). Not needed with erase.
rectInclusive cell rectangle — the base ground over the whole map, a field, a yard.
at + radiusA round patch: centre cell (fractions fine), radius in cells (default 3).
path + widthA trail, road or track through the corner points (cell centres), width cells wide (default 2). Curves and forks are just more ops.
(none of those)The whole map: the extent of the recipe's floor ops and paint rects, else the terrain's.
patches + patchRadiusInstead of filling, scatter this many round patches over rect (or the map): worn spots, clover, puddles, drifts. patchRadius is cells, a number or [min, max] (default [1, 2.5]). Patches stay inside the area.
softCells the edge fades over. 0 is crisp (a flagstone path's border), 1 (default) a natural edge, 2–3 a broad blend between two grounds.
rough0..1, how ragged the edge is (default 0.6). 0 is a ruler line; 0.8+ is torn and patchy. The ragged lobes only reach out from the shape (up to about a cell at 0.6), never into it, so a shape is always solid inside.
strength0..1, how much of the surface shows inside the shape (default 1). 0.5 lets the ground under it through — a dusting of snow, damp ground, a hint of moss.
erasetrue wipes paint off instead of adding it (all surfaces), with the same shapes.
seedRerolls the edge noise and the patch layout.

Ops stack like paint, in recipe order: a later op covers what is under it, so write the base first, then patches, then paths on top. Paint the base over the whole map — a soft edge fades to whatever is beneath, and on a flat map with no terrain that is empty space. Nothing is painted off the map, so a road may run out past the edge. The painted ground drapes over the terrain like a floor tile and lies just under tiles, so a tiled floor, a jetty or a bridge placed on painted ground still shows. It is not an entity: nothing to select, nothing in avoid. Users paint the same way in the app (Paint tool, a floor from the shelf, Texture mode — Sharp / Medium / Soft edges, radius, strength).

floor — paint ground tiles on the grid

Fills cells with a floor tile, for built floors — planks, flagstones, a tiled hall, a cobbled court inside walls. Correct by construction: one tile per cell, centred, pinned to the grid slot. For natural ground (grass, dirt, mud, sand, snow, a trail) use paint above: tiles give it square edges on every cell line.

{ "op": "floor", "asset": "realistic-surfaces/flagstone_floor", "rect": [0, 0, 19, 19] }
fieldnotes
assetA library id, or an array of ids to randomise per tile and kill the visible repeat (e.g. two grass variants).
rect \cells \cell \pathWhere. rect is inclusive; cells is an explicit list; cell is one; path is a polyline tiled width cells wide — a paved street or a plank walk. A dirt road or trail is a paint path.
weights, blendWith several assets: relative frequency per id, and the patch size (in cells) the mix clusters into. blend: 5 turns a grass/dirt mix into worn patches instead of noise.
levelVertical storey. Default 0.
randomYawtrue → each tile gets a random 90° turn. Great for organic ground, off for anything with a printed direction (roads, patterned tiles).
jitterPosition jitter as a fraction of a cell (0–~0.25). Subtle, breaks up regularity.
scaleExtra uniform scale on top of the automatic kit-fit.

walls — paint a perimeter or a run

Walls are edge-based: they sit on the line between two cells and auto-rotate to run along it, so a rectangle's four sides meet at the corners. This is the same canonical-edge system the app's wall brush uses.

{ "op": "walls", "asset": "realistic-surfaces/stone_wall", "rect": [0, 0, 19, 19],
  "gaps": [{ "side": "S", "at": 10 }] }
{ "op": "walls", "asset": "realistic-surfaces/stone_wall_archway", "rect": [10, 19, 10, 19], "sides": "S" }
fieldnotes
assetWall library id.
rectBuild the perimeter of this rectangle. A one-cell rect plus sides addresses a single edge — how a doorway/archway/window piece goes into a gap.
from / to…or a single straight run between two cells (must share a row or column).
path…or a chain of corner points, each leg axis-aligned (an L-shaped hall; repeat the first point to close a ring).
sidesWith rect: which sides to build, any of "NESW". Default all four.
gapsDoorways/breaks. Either `{ "side": "N"\"E"\"S"\"W", "at": <cell index along that side> }, or an explicit [x, z] for a from/to` run.
levelVertical storey (stack walls for a second storey).
cornerOptional corner-piece id, placed at the 4 rectangle corners as free props.
cornerRotationDegrees added to every corner piece — the one knob to fix a pack whose corner faces differently.
rotationOffsetDegrees added to every wall piece. Walls already auto-orient to run along their edge (see below); use this only if a pack still faces 90°/180° wrong — the same escape hatch a road has.

Walls auto-face their edge. Every shipped wall piece is authored along X and one cell long; the builder rotates each onto its edge so a run meets side-by-side. You do not set a rotation per wall, and you never hand-place wall segments. A user-imported kit modelled along Z is corrected per pack in grid.ts; if a run still faces wrong, one rotationOffset on the op fixes it. Openings are wall pieces too: <material>_wall_doorway, _archway, _window, _low, castle_wall_battlement. Leave a gaps entry in the main run, then place the opening on that edge with a one-cell walls op.

building — a whole house from a rectangle (or several)

{ "op": "building", "rect": [10, 6, 17, 11], "floor": "realistic-surfaces/wood_plank_old_floor",
  "wall": "realistic-surfaces/brick_mossy_wall", "storeys": 2, "roof": "clay_roof" }
{ "op": "building", "rects": [[4, 4, 12, 8], [4, 9, 7, 13]], "floor": "realistic-surfaces/wood_plank_floor",
  "wall": "realistic-surfaces/plaster_wall", "roof": "slate_roof", "door": "E" }
{ "op": "building", "rect": [20, 3, 25, 8], "floor": "realistic-surfaces/wood_plank_floor",
  "wall": "realistic-surfaces/brick_wall", "storeys": 3, "roof": "flat" }

Floors and walls on every storey, a doorway in the middle of the south wall, windows every other cell, and a gable roof laid from the roof material's pieces, ridge the long way, gable ends walled up to the ridge. The same thing the app's Room tool builds with its Door, Windows, Storeys and Roof switches on.

fieldnotes
rect, floor, wallThe footprint and the two materials. The wall must be a 1-cell library …_wall; its _doorway (or _archway) and _window siblings make the openings.
rectsInstead of (or after) rect: a footprint of several rectangles — an L, a T, a hall with a wing. The parts open into each other (no wall between them) and share one roof: each part gets a gable roof, and a wing whose end meets the rest of the house runs its ridge on into that roof instead of ending in a gable — the cross gable of a real L- or T-shaped house. The first rectangle is the main part.
doortrue (default): a doorway in the middle of the main part's south wall. "N", "E", "S" or "W": in that side instead — face the door onto the street. false: none.
windowsDefault true. Castle and cave walls have an archway and no window.
storeys1–4, stacked storeyHeight apart (default 1.4, the height of a room wall; castle walls are 2.8).
roofclay_roof (default, mossy medieval tiles), slate_roof, tile_roof (terracotta), flat, or false. A flat roof is a deck of roofFloor tiles (default gravel_floor, a tar-and-gravel roof) with a parapet of the wall's _low piece round it: town houses, warehouses, a city block, a rooftop to fight on.
roofShapegable (default: upright gable walls at the ends) or hip (the ends slope down too, eaves all round — a manor, a farmhouse, a pavilion). Pitched roofs hang out past the walls on their own.

Flatten the ground under it first (terrain flatten): the roof is one rigid piece set at the ground height under the building's middle, and the upper floors drape like any floor tile, so a house on a slope leans.

Furnish the inside. In Play anyone at the table can lift the roofs (the Roofs button, U) and see into every house, so a house the party may enter is a room like any other: a hearth, a table, a bed, lights, its people. Place them with the other ops inside the footprint (the roof hides them from the usual camera until the roofs come off). Keep the door clear, and put a little dressing outside it (a lantern, a barrel, a trodden patch).

Generated houses and building models together. The sketchfab-medieval buildings (timber_town_house, thatched_house, medieval_house_small, town_house_row, old_wooden_barn, stone_church, round_tower, wooden_windmill, …) are whole buildings with detailed roofs, one prop each, and solid: nobody goes inside. Use them for the town around the action, and building ops for the places the party enters. They are fitted to the grid by footprint, so most are small next to a mini: scale them (1.5–2 for the houses) until their door is about a mini's height, and flatten under them too.

road — lay a connected road from a directional tile kit

For a tile kit whose road pieces have a printed direction (straight, bend, T, cross, end), this op reads each cell's neighbours and picks the right piece and rotation. The shipped library has no such kit — its surfaces are photographed and have no direction — so paint roads with a paint op's path instead (or a floor path for a paved street whose stones follow the grid). The op remains for user-imported kits.

{ "op": "road",
  "pieces": { "straight": "my-kit/road-straight", "corner": "my-kit/road-bend",
              "tee": "my-kit/road-t", "cross": "my-kit/road-cross", "end": "my-kit/road-end" },
  "path": [[2,18],[2,10],[2,3],[8,3],[14,3]] }
fieldnotes
piecesThe junction set. straight and corner are required; tee, cross, end fall back to straight if omitted.
path \rect \cellsWhere. Junctions are inferred from adjacency, so loops, forks and grids all work.
level, layer, rotationOffsetlayer defaults to floor.

prop — place one model

{ "op": "prop", "asset": "polyhaven-props/treasure_chest", "at": [10, 4], "rotation": 180 }
fieldnotes
assetLibrary id.
atCell [x, z]; lands on that cell's centre.
yWorld height added on top (sit something on a table, float a lantern).
level, rotation (deg), scale (number or [x,y,z])
posRaw world [x, y, z], overriding at/level/y when you need exact control.
floattrue puts the prop on the water's surface where a water op covers the cell (a boat, a raft, a floating lantern) instead of on the bed.

line — a row of props

A fence of torches, a colonnade, trees along a road.

{ "op": "line", "asset": "realistic-surfaces/fir_tree",
  "from": [2, 2], "to": [18, 2], "spacing": 2, "jitterPos": 0.15, "jitterYaw": true }

The example ids above are real (see asset-index.json). Always confirm an id exists before using it.

fieldnotes
from, toCell endpoints.
countNumber of props (≥2). Overrides spacing.
spacing…or a fixed gap in cells between props.
faceLineRotate each prop to face along the line (for directional props).
rotation, scale, y, level
jitterPos, jitterYawBreak up the mechanical spacing.

scatter — fill an area randomly

The forest maker. Places count items in a rectangle, avoiding keep-out zones (the castle footprint, the road) and optionally spacing them out.

{ "op": "scatter", "asset": ["realistic-surfaces/fir_tree","realistic-surfaces/fir_tree_large","realistic-surfaces/pine_tree"],
  "area": [0, 0, 39, 39], "count": 60,
  "avoid": [[6, 6, 33, 33]], "minSpacing": 2.2,
  "scaleJitter": 0.3, "clumps": 4, "clumpRadius": 7, "sink": 0.03, "seed": 3 }
fieldnotes
assetId or array of ids (mix tree species).
areaRectangle to fill.
countTarget number. If the area can't fit it (with minSpacing), it places what it can and warns.
avoidArray of rectangles to keep clear.
minSpacingMinimum gap between items, in cells.
yaw"random" (default), "grid" (90° steps), or a fixed degree.
scale, scaleJitter (±fraction), y, level, seed
clumps, clumpRadiusGather the items into stands with clearings between, instead of an even plantation.
sinkRandom burial depth in world units, so rocks and stumps bed into the ground instead of sitting on it.

clutter — dress a room's walls

Props placed against the inside of a rectangle's walls, facing the room — shelves, crates, barrels and baskets where furniture actually lives, leaving the floor open.

{ "op": "clutter", "asset": ["polyhaven-props/wooden_crate_01", "polyhaven-props/wine_barrel_01"],
  "rect": [4, 3, 19, 15], "count": 8, "sides": "NS", "avoid": [[10, 13, 13, 15]] }

sides picks which walls; inset (cells from the wall, default 0.35), facing (in/out), yawJitter, avoid (the doorway, the hearth), minSpacing.

light — point, torch, ambient

{ "op": "light", "type": "torch", "at": [10, 1.6, 8], "intensity": 6, "distance": 7 }

type: point | torch | ambient. at is [cellX, worldY, cellZ]. Optional color, intensity, distance (falloff for point/torch), castShadow. Sensible defaults per type are filled in. A sun op is ignored with a warning: the sun is environment.timeOfDay.

effect — fire, smoke, fog, magic (particles)

{ "op": "effect", "type": "fire", "at": [10, 0.2, 8] }

type: fire | smoke | fog | magic. at is [cellX, worldY, cellZ]. Optional color, count, size, speed, radius. A fire effect is a real flame (billboard, embers, a glow on the ground); pair it with a torch light so it lights its surroundings. A library fire pit, hearth, candle, lantern or lamp placed with prop lights itself — flame, glow and light — so it needs neither.

mini — a token on the board

Turns a character/monster model into a miniature with a coloured ring, label, and optional HP.

{ "op": "mini", "asset": "dnd-monsters/ghoul", "at": [12, 9],
  "ring": "hostile", "label": "Ghoul", "hp": 22, "maxHp": 22 }

ring: a name (ally/player/friendly/hostile/enemy/neutral/amber/ purple/pink/cyan) or a #hex. Optional label, hp, maxHp, rotation, scale, and y (world units) to lift a flying creature off the floor — give it "conditions": ["flying"] too, so in Play it moves in 3D (holds its altitude, climbs and dives) instead of walking at a fixed clearance. "hidden": true keeps the mini from the players at a shared table until the host presses Show on it in Play — use it for an ambush or a monster that has not entered the room yet. "conditions": ["prone", "poisoned"] puts conditions on it from the start (the 5e conditions plus markers such as concentration, dead, dying, raging, blessed, hexed, hunters-mark, burning, flying; unknown names are reported and dropped).


Rotation & facing

prop, line, mini, and road/walls all take a rotation/rotationOffset in degrees — a yaw about the vertical axis. It turns the model clockwise seen from above: 0 → 90 → 180 → 270 are the four quarter-turns. North is −Z, east +X, south +Z, west −X.

  • Floors and roads don't need a rotation you compute — floors can take randomYaw to hide tiling; roads pick their own junction rotation from the path.
  • Walls auto-face their edge (per pack). Don't rotate them by hand; if a whole run faces wrong, use the op's rotationOffset.
  • Props you must aim yourself. A model's "front" is however the artist authored it, so which number faces a given way is model-dependent — place it, then adjust by 90° until it faces right. Aim things deliberately:
  • Chairs/stools/benches face their table (a chair west of a table faces east). Doors and gates open toward the room / face outward at an entrance. Beds head to a wall. Shelves, counters, barrels-behind-a-bar, weapon racks put their back to the wall they stand against.
  • A little variety reads as lived-in: nudge a stray chair by 10–20°, angle a cart.
  • Fractional cells (at: [11.5, 16]) place a prop on an edge or between cells — use it to centre a wide door in a 2-cell gap.

You author blind, so facing is the thing to verify in the viewport. In the app you can turn any selected object: R for a quarter turn (Shift+R back), the Inspector's numeric Rot fields, , / . for a small step, or the toolbar's Rotate gizmo.


Order & layering

Ops apply in array order. A good default order mirrors how you'd build it by hand:

  1. terrain — the lie of the land, first: a swell over the map, a rise under the keep, a flattened footprint for every building (outdoors only).
  2. water — the pond, the river, the sea, with carve for its bed, right after the swell and before anything stands on the banks.
  3. paint — the ground next: the base surface over the whole map, then patches, then roads and trails as paint paths on top (later paint covers earlier); the bed and banks of the water are paint too (gravel, sand, mud), a little wider than the water so the banks are stony. Then floor tiles for built floors — a hall, a yard, a jetty's planks — which sit on top of the paint.
  4. walls — structure, then the opening pieces in the gaps.
  5. prop / line / clutter — pillars, stairs, furniture, doors, dressing.
  6. scatter — nature filling the space around the build (with the build's footprint and the water in avoid).
  7. light / effect — mood.
  8. mini — tokens last.

See scene-building-guide.md for how to compose a believable, connected scene, and model-guide.md for which assets to reach for.

Ready to put a map on the table?

Open the builder and start, no account needed. Sixteen starter scenes are on the welcome screen; sign up free when you want to save one or play it with your group.