Builder's Manual · Chapter 6

Projecting the Map

Maps are app-managed metadata. They never appear in the working tree, the GM cannot write them, and they are replaced whole by craft map set. Everything here is builder work.

What a room becomes

With a grid cols wide and rows tall, a room at gridX/gridY becomes an area with four vertices:

x0 = gridX / cols        x1 = x0 + 1/cols
y0 = gridY / rows        y1 = y0 + 1/rows

Set aspectRatio to { width: cols, height: rows } so the cells come out square.

The two fields that decide everything

The room's tile attaches as fillImage, and its offset and scale are in map coordinate space, not relative to the area:

  • offset = the cell's top-left corner, { x: x0, y: y0 }
  • scale = the cell's width, 1/cols

Height is never specified. It follows from the image's own aspect ratio, which is why square tiles fill square cells exactly.

The trap: offset {0,0} with scale 1 looks like a sensible default meaning "fit the area." It does not. It draws the image at full MAP size anchored to the map's top-left corner, and the area polygon then clips out whatever fraction falls inside it — so every room displays a different window onto one enormous image. The symptom is unmistakable once you have seen it: each room shows a corner or an edge of a room rather than a whole one.

The outline is black

Every area carries a style of fillColor #000000, fillOpacity 0 — the tile does the filling, never the polygon — and strokeColor #000000.

The stroke used to be a pale #a3a390, and it was wrong for exactly the reason a bright floor is wrong. Every tile already draws its own dark masonry border, so a light outline sitting on top of that border reads as a UI grid laid over the dungeon rather than as part of it. On a black page it was the first thing the eye found, and what it found was the shape of the data.

Black lets the polygon keep doing its real job — holding one room apart from the next — without announcing itself.

If you ever change this, change every floor at once. A pool where some maps outline in one colour and some in another looks like a bug, because it is one. Recolouring in place is a craft map get, a field swap, and a craft map set per floor; there is no need to re-derive tiles to do it.

Choosing the tile

Derive the room's door set; never store it.

  • Its exit is its own doorDirection.
  • Its entry is the wall opposite the doorDirection of whichever room points at it.

Sort those by n, e, s, w, join, and use the result as the key into the template's tiles. Interior rooms give two-letter keys; the entrance and the final dead end give one.

A consequence worth stating: a wrong doorDirection now corrupts the art of two rooms, not one, because its neighbour derives an entry from it.

Fog of war

discovered decides whether a room is drawn at all. Project only discovered rooms for a play-facing map; project everything with undiscovered rooms dimmed for a builder's view of the whole floor.

There is no per-element hidden flag in the map schema, so fog is achieved by what you choose to emit, not by a property.

The replacement trap

craft map set replaces the whole map. A projection script that emits only aspectRatio, areas and points will silently clear the background every time it runs. Re-emit background on every projection.

Backgrounds cost nothing

The background here is a plain black PNG generated locally and pushed with craft image upload. Uploads cost no energy, while generation costs 2,000 per image. For any flat or procedurally-producible asset, make it locally and upload it.

Verify against the app

A local composite of the tiles is a good check of geometry and a poor check of rendering — it will happily show you what you intended rather than what the stored data means. The app is the only authority on how a map actually renders. When something looks wrong there, read the stored map back with craft map get and compare fields; note that it returns maps in authoring format, so stored file references come back as link: "<path>" rather than as fileReferenceId.

The root map stays dark

The Sealed Descent's own map is deliberately empty: a black background, no areas, no pins. It is the game start's startingLocation, so it is the first thing a player sees, before the GM has chosen anything.

A pin there would name a specific floor before the run has picked one. That spoils the destination, and on most runs it is simply wrong - a pin reading "The Ashen Vaults" is a lie on any run that does not include it. Navigation between floors happens when the GM sets the scene, never by clicking a pin.

So when you add a floor to the pool, add it to the dungeon's floors array and give it its own projected map. Do not add a pin for it.

Fog of war is not implemented

Every floor's map is projected fully visible, and the GM never touches it.

An earlier design had the GM hide undiscovered rooms by setting each area's opacity and raising it on discovery. It was removed after playtesting: the GM spent many turns attempting the adjustment and ultimately failed at it, which cost far more than the effect was worth. The -OnlyDiscovered switch on the projection script still exists for a builder-side view, but nothing at runtime uses it.

The consequence is that a player sees the floor's SHAPE from the moment the scene is set - room count, connections, where it ends. They still cannot see what is IN any room. So the game's secret is contents, not layout, and the GM instructions are written that way.

If a cheaper reveal mechanism appears later, this is the one place to change: give the GM a single call that flips a room's visibility, and re-project with -OnlyDiscovered as the starting state.

Manual updated Aug 17, 2026.