Map & zones
The map describes a game’s spatial layout: regions (zones), single spots
(points), and decorations (annotations). Everything is authored in a
local meters frame and projected onto real coordinates when the gamemaster
places it.
interface MapDefinition { zones: MapZone[]; points?: MapPoint[]; annotations?: MapAnnotation[];}Coordinate frame
Section titled “Coordinate frame”Zone and point geometry use MetersXY:
interface MetersXY { x: number; y: number } // x = east, y = north, in metersSo from: { x: -150, y: -100 }, to: { x: 150, y: 100 } is a 300 m × 200 m
rectangle centered on the layout origin. The GM chooses the real-world center at
placement; the engine projects meters → latitude/longitude there.
interface MapZone { id: string; name?: string; color?: string; // hex; outline + fill on the GM canvas shapes?: MapShape[]; placeable?: boolean; required?: boolean; exclusion?: boolean; navigable?: boolean; // default true minArea?: number; // square meters maxArea?: number; teamId?: string; blockedBy?: { key: string; value: unknown }; confinedBy?: { key: string; value: unknown }; zones?: MapZone[]; // nested children}id— unique; how rules reference the zone (zoneId: play_area).placeable— top-level zones with this flag get a Place/Re-place control in the GM console. Children inherit their parent’s placement.required— the game won’t start until this zone is placed and has shapes. Same gate applies to required points.navigable— defaulttrue. Whenfalse, player position updates that would land inside the zone are rejected (an unconditional wall).blockedBy/confinedBy— conditional movement gates keyed on player state.blockedBykeeps matching players out;confinedBykeeps matching players in. Shape{ key, value }(state key orteamId).teamId— tags the zone as belonging to a team (drives visuals and GM affordances; gameplay enforcement is still up to your rules).minArea/maxArea— square-meter constraints checked at start.zones— nested child zones (see Hierarchy).- A zone with no shapes and no children is a placeholder the GM fills at runtime.
Shapes
Section titled “Shapes”A zone’s geometry is one or more MapShape — a Shape with an id and optional
exclusion:
type Shape = | { kind: "polygon"; vertices: MetersXY[] } | { kind: "rectangle"; from: MetersXY; to: MetersXY } | { kind: "rectangle"; center: MetersXY; halfWidth: number; halfHeight: number } | { kind: "circle"; center: MetersXY; radius: number } | { kind: "circle"; through: [MetersXY, MetersXY, MetersXY] };
type MapShape = Shape & { id: string; exclusion?: boolean };Multiple shapes in one zone union together into its effective area.
shapes: - { id: a, kind: rectangle, from: { x: -35, y: -35 }, to: { x: 35, y: 35 } } - { id: b, kind: circle, center: { x: 0, y: 0 }, radius: 20 }Rectangle — two forms
Section titled “Rectangle — two forms”from/to (opposite corners) or center/halfWidth/halfHeight.
Circle — two forms
Section titled “Circle — two forms”center/radius or through (three points the circle passes through).
Polygon
Section titled “Polygon”An array of vertices.
Exclusions
Section titled “Exclusions”A shape with exclusion: true subtracts from its containing zone — a cutout.
Players standing inside a cutout count as outside the zone, and random item
spawns avoid it.
# Pirate's Booty: keep loot off the ships by punching them out of the island.shapes: - { id: treasure_island-shape-0, kind: rectangle, from: { x: -35, y: -35 }, to: { x: 35, y: 35 } } - { id: treasure_island-exclude-red, kind: circle, center: { x: -35, y: 35 }, radius: 15, exclusion: true }Source: apps/wage-engine/src/games/pirates_booty/game.yaml.
Hierarchy
Section titled “Hierarchy”Zones nest via zones. A child is positioned relative to its parent’s frame, so
placing (or moving/scaling) the parent moves the whole subtree rigidly. This is
how most multi-zone games are a single GM drag:
# King of the Hill: one placeable play area with the hill nested in the middle.zones: - id: play_area placeable: true required: true shapes: [{ id: play_area-shape-0, kind: rectangle, from: { x: -150, y: -100 }, to: { x: 150, y: 100 } }] zones: - id: hill name: The Hill shapes: [{ id: hill-shape-0, kind: circle, center: { x: 0, y: 0 }, radius: 20 }]Source: apps/wage-engine/src/games/king_of_the_hill/game.yaml.
Points
Section titled “Points”A MapPoint is a single named coordinate — a flag spawn, a goal.
interface MapPoint { id: string; name?: string; required?: boolean; parentZoneId?: string; // auto-places with that zone shape?: { center: MetersXY }; // default position (omit to have the GM drop it)}- With
parentZoneId+shape.center, the point auto-places when its parent zone is placed (CTF’s flag points work this way). - With no
shape.center, the GM drops the point at runtime. required: truegates the start like a required zone.
points: - { id: blue_flag, name: Blue Flag, parentZoneId: blue_base, shape: { center: { x: -170, y: 0 } } }Source: apps/wage-engine/src/games/capture_the_flag/game.yaml.
Annotations
Section titled “Annotations”Purely visual map decorations with no gameplay effect — brief your players, mark a no-go area, draw an arrow.
interface MapAnnotation { id?: string; kind: "line" | "arrow" | "rectangle" | "circle" | "text"; name?: string; // GM-facing label from?: MetersXY; to?: MetersXY; // line / arrow center?: MetersXY; radius?: number; // circle at?: MetersXY; text?: string; // text color?: string; style?: "solid" | "dashed";}Rules never reference annotations; clients just render them in a decoration layer. The GM can also add annotations live from the console’s drawing tools.