# Map standard (standards_maps)

What a map for JCOM and Conquer must be, so a designer (human or model) can build one the engine draws, paths and plays at the right scale.

Status: draft 1, written 2026-10-20 from the live game tree. Nothing here is decided by the owner yet. Rules marked **RULE** quote what the engine or rules data does today. Rules marked **PROPOSED** are the recommended standard and have no source yet. Conflicts are numbered `OD-n` and listed in `OPEN-DECISIONS.md`. The machine-readable form is `map-standard.json`.

Source tree abbreviations used in citations:

- `game:` = the live game tree, `M:\z4yin-work-overflow\hermes-share\codex-lanes\asset-split\sol-maps\work` (read on 2026-10-20).
- `share:` = `M:\z4yin-work-overflow\hermes-share`.

The game repo docs `ADR-010`, `ADR-011` and `docs/guides/MAP-PIPELINE.md` are cited by the rules files but were not found in the live tree or the share (routes tried: live-tree find, share find to depth 8, `tools/recall/ask.mjs` (index stale, no match), estate_find (read failed)). Their content here is only what the rules files quote from them.

---

## 1. The numbers in one table

| Quantity | Value | Source |
|---|---|---|
| World units per real metre (`metreToWorld`) | 0.4 | game:data/rules/world-scale.json:9 |
| Legacy tile size (`metresPerTile`) | 5 m | game:data/rules/world-scale.json:10 |
| Legacy tile in world units (`tileWorldSize`) | 2 (= 5 x 0.4) | game:data/rules/world-scale.json:11, :28 |
| Map cell size | per map, `metresPerCell` (see 2.2) | game:skirmish/src/layers/scale.ts:21-31 |
| Fallback cell size when a map names none | 5 m | game:data/rules/map-layers.json:10 |
| Default height unit | metre | game:data/rules/world-scale.json:12 |
| Raw 0-255 height sample | 0.2 m | game:data/rules/world-scale.json:18-19 |
| Mesh cap | 512 segments per axis | game:data/rules/world-scale.json:21 |
| Cliff slope | 32 degrees | game:data/rules/world-scale.json:23 |
| Snow line | 180 m | game:data/rules/world-scale.json:22 |
| Water-plane lift | 0.125 m | game:data/rules/world-scale.json:25 |
| Shallow water | depth 0 to 3 m | game:data/rules/nav-domains.json:10 |
| Amphibious max depth | 5 m | game:data/rules/nav-domains.json:14 |
| Ship under-keel clearance | 1 m | game:data/rules/nav-domains.json:12 |
| Naval / forge map cells | 20 m | game:data/rules/naval-maps.json:10, game:data/maps/specs/forge_jslc00_greenland.json |
| Gameplay weapon/sight range | fraction of the map's shorter axis, in cells | game:data/rules/game-ranges.json:2-7, game:skirmish/src/gameRanges.ts:33-35 |

The HQ map, `greenland_naval_bunker.json`, is 320 x 200 cells at 20 m, so 6.4 x 4.0 km. Its heights run from -18 m to 140 m in metres with `heightScale` 1. In world units it is 2560 x 1600.

---

## 2. Scale

### 2.1 One metre transform

**RULE.** Terrain, water and unit models share one transform: world = metres x `metreToWorld` (0.4). The old terrain-only vertical scale of 0.25 is retired. (game:data/rules/world-scale.json:4, :27)

**RULE.** `tileWorldSize` must equal `metresPerTile x metreToWorld`. Code derives it. Do not author it. (game:data/rules/world-scale.json:28; game:skirmish/src/terrain.ts:42, game:skirmish/src/unitDimensions.ts:22)

These values are marked "assumed default, tunable (ADR-010)" (game:data/rules/world-scale.json:4-8). Until the owner fixes them (OD-1), a map must not hard-code world units. Author in metres and cells only.

### 2.2 Cell size (metres per cell)

**RULE.** The engine resolves a map's cell size in this order (game:skirmish/src/layers/scale.ts:16-31):

1. the map's top-level `metresPerCell`;
2. `meta.cellSizeM`;
3. the per-file table `metresPerCellByMapFile` in game:data/rules/map-layers.json:12-16;
4. `fallbackMetresPerCell`, 5 m (game:data/rules/map-layers.json:10).

The renderer multiplies by `metreToWorld` to get `cellWorldSize` (game:skirmish/src/layers/scale.ts:58; game:skirmish/src/main.ts:606-608). The sim reads the same value and converts legacy 5 m-tile values with `rangeCellScale = 5 / metresPerCell` (game:skirmish/src/sim.ts:1476-1477).

**PROPOSED.** Every map sets top-level `metresPerCell` and the same value in `meta.cellSizeM`. A missing value is a validation error, not a fallback. Today 30 of the 58 playable sheets set neither and silently draw at 5 m (audit in section 15; OD-2).

Cell sizes in use today:

| Cell | Used by | Ground extent at that cell |
|---|---|---|
| 5 m | legacy and retail-derived sheets, AoI campaign sheets | 128 cells = 640 m |
| 20 m | naval sheets, forge Greenland/Gaza, HQ, undersea sheets, kharg_approach | 320 cells = 6.4 km |
| 30 m | forge_p01_queenmaud | 128 cells = 3.84 km |
| 80 m | forge_bandar_abbas pair | 320 cells = 25.6 km |
| 500 m | redsea_surface, camp-07-silicon-oasis | 200 x 512 cells = 100 x 256 km |

**PROPOSED.** Standard cell sizes are 5 m (infantry/base scale, legacy), 20 m (default for new real-world tactical maps) and 80 m (operational coast). Theatre overview sheets may use 500 m but are not tactical. Any other value needs a note in `meta.notes` (OD-3).

### 2.3 Gameplay ranges scale with cells, not metres

**RULE.** With the default range policy `generals-map-v1` (game:data/rules/sim-performance.json:5), weapon, sight and sensor ranges are fractions of the map's shorter axis measured in cells. The reference span is 4000 original Generals world units (game:data/rules/game-ranges.json:2-7; game:skirmish/src/gameRanges.ts:33-35; game:skirmish/SIM-PERFORMANCE.md:39-41). A rifle is 0.025 of the short axis: 3.2 cells on a 128-cell map, 5 cells on the 200-row HQ.

Consequence for designers: the cell count of the short axis, not the metre size, decides how far units shoot and see. A 128 x 128 map at 20 m has the same engagement geometry, in cells, as one at 5 m. A map can pin `rangePolicy: "legacy"` for fixed-metre balance (game:skirmish/src/sim.ts:425, :1478-1479). Whether ranges should become real metres is OD-4.

### 2.4 Height units and heightScale

**RULE.** World Y = sample x `heightUnitToMetre[heightmapUnits]` x `heightScale` x `metreToWorld` (game:skirmish/src/main.ts:677-690; game:skirmish/src/terrain.ts:466-469). Allowed units: `metre`/`metres`/`meters`/`m` (x1) and `raw0_255`/`0-255` (x0.2) (game:data/rules/world-scale.json:13-20).

**RULE (data).** "A map must set heightmapUnits. Map size and corner values are not used to guess the unit." (game:data/rules/world-scale.json:29). The renderer defaults a missing unit to metre (game:skirmish/src/main.ts:677-679). The map checker guesses instead: a sheet with no unit, cells under 10 m and integer samples at or below 255 is read as raw 0.2 m (game:tools/map-check/engine.ts:521-535; game:data/rules/map-check.json:89-90). So `starter.json` and `kharg_island.json` are 5x taller in the game than in the checker (OD-5).

**RULE.** `heightScale` (alias `elevationScale`) multiplies samples and also `waterLevel` (game:skirmish/src/terrain.ts:108-116, :469; game:skirmish/src/main.ts:853). Nav depth uses raw samples and raw `waterLevel` with no unit or scale (game:skirmish/src/nav/grid.ts:26-47).

**RULE (forge).** `heightScale` = the spec's `verticalExaggeration` when XZ scales with cell size (game:data/rules/terrain-forge.json:61-67). Default exaggeration is 1.0.

**PROPOSED.** Heights are stored in true metres above the map datum, `heightmapUnits: "metre"`, `heightScale: 1`. Any relief exaggeration is an art pass recorded in `meta` and kept out of `heightmap`, so the sim, nav and map-check all see true metres. Today `forge_jslc00_greenland` (1.5), `forge_aoi01_gaza` (1.5) and `forge_p01_queenmaud` (1.2) exaggerate through `heightScale`, and `forge_bandar_abbas` bakes a 4x exaggeration above 25 m into the stored heights (game:skirmish/MECHANICS-coastal-art.md:5). The HQ (`greenland_naval_bunker`) uses 1 (OD-6).

---

## 3. Grid

**RULE.** A map is a raster of `width` columns by `height` rows. `tiles[y][x]` and `heightmap[y][x]` are row-major, one entry per cell, both `height` rows of `width` entries (game:skirmish/src/sim.ts:420-437; game:skirmish/src/nav/grid.ts:31-38). Heights are sampled at cell centres: cell (x, y) centre is at ((x + 0.5) x cell, (y + 0.5) x cell) metres (game:skirmish/src/layers/scale.ts:77-86).

**RULE.** The terrain mesh and height query share the 512-segment cap per axis (game:data/rules/world-scale.json:21, :30). A side longer than 512 cells is downsampled when drawn.

**RULE (forge).** `textureGrid` may be finer than the cell grid: k texels per cell, k = round(metresPerCell / 4 m), capped at 1.1 M texels; `textureGridScale` records k (game:data/rules/terrain-forge.json:129-133; `forge_jslc00_greenland.json` has `textureGridScale: 4`).

Sizes in use: 64x64, 104x128, 128x73, 128x85, 128x128, 120x120, 160x160, 240x160, 320x200, 320x320, 360x240, 100x180, 200x512.

**PROPOSED.** Width and height are each 64 to 512. Skirmish 1v1 short axis at least 128 cells; 4-player at least 160. (The 128 floor follows the Generals-derived maps, which are 128 cells on the long side; the 160 figure has no source.)

---

## 4. Coordinate frame and north

**RULE.** Column `x` increases east. World X = (x + 0.5) x cellWorldSize - worldWidth / 2; world Z likewise from row `y`. The map is centred on the world origin (game:skirmish/src/layers/scale.ts:81-93; game:skirmish/src/main.ts tileToWorld after line 645). Y is up. Models are glTF Y-up, heading -Z, starboard +X (share:ARCHITECTURE-BRIEFING.md:43-44).

**RULE.** Positions in `starts`, `objects`, `units`, mission `startingForces` and `areas` are tile-space (cell) coordinates, the sim's Unit x/y (game:data/missions/schema.json description). Metre twins `xM`, `yM` are optional and win when present in layer code (game:skirmish/src/layers/world.ts:84-92).

**CONFLICT on north (OD-7).** The default camera sits on +Z looking toward -Z with yaw 0 called "north-up" (game:skirmish/src/render/cameraEye.ts:16-18; game:skirmish/src/data/keyscamera-rules.json:6; game:skirmish/src/main.ts:2279-2280), so row 0 is at the top of the screen. Mission scripting agrees: the `north` edge is `y = 0` (game:skirmish/src/script/engine.ts:246-247). But the layer and nav code and the naval generator say row 0 is south (game:skirmish/src/layers/scale.ts:72; game:skirmish/src/nav/types.ts:43; game:map-import/naval/README.md:24), and glacier dressing treats `y + 1` as north (game:skirmish/src/render/glacierDressing.ts:76). Forge maps are also rotated: `forge_jslc00_greenland` "map up points NNW" (`rotationDeg` 335), Gaza 34.5 degrees (game:data/maps/specs/*.json:19).

**PROPOSED.** Row 0 is the top of the screen at default camera. Store true bearing of map-up as `meta.northBearingDeg` (degrees clockwise from true north to map-up; 0 when map-up is north). Real-world sheets record `meta.source.center` and `rotationDeg` as the forge specs do.

---

## 5. Layers

### 5.1 Tile layer (`tiles`)

**RULE.** Codes: 0 open, 1 water, 2 road, 3 cliff. Water and cliff block ground movement (game:skirmish/src/sim.ts:410-413; game:skirmish/src/terrain.ts:33-37; legend repeated in game:data/rules/naval-maps.json `tiles`). No other value is read by the skirmish engine.

The older `jslcnc` `beta/` engine used a 0-8 legend with `HARBOR_DOCK` = 8 (share:DOLPHIN-ANSWERS.md:102). That format is not read by the live skirmish engine (OD-12).

### 5.2 Height layer (`heightmap`) and water level

**RULE.** `waterLevel` is the sea surface in height-sample units. Depth = waterLevel - height (game:skirmish/src/nav/grid.ts:26-47). Undersea sheets put the datum at -820 m with `datumM` and `waterLevel` -820 (e.g. `camp-aoi-05-tunnel-network.json`).

### 5.3 Navigation domains (derived, not authored)

**RULE.** Nav builds a domain per cell from tiles, heights and water level (game:skirmish/src/nav/grid.ts:23-56; game:data/rules/nav-domains.json):

| Domain | Condition |
|---|---|
| cliff | tile 3 (always blocked to surface movers) |
| deep | depth > 3 m |
| shallow | 0 < depth <= 3 m |
| land | depth <= 0 |

Movers:

| Profile | Passes |
|---|---|
| ground | land (open and road), not water tiles, not cliff |
| amphibious | land plus water to 5 m depth (game:data/rules/nav-domains.json:14) |
| hover | land and water (nav/grid.ts header) |
| ship | water with depth >= class draft + 1 m, plus beam clearance (game:data/rules/nav-domains.json:12, `shipClasses`) |
| air | everywhere (game:data/rules/map-check.json `reach.airAlwaysReaches`) |

Ship drafts in use: capital 11.9 m, destroyer 10.4 m, LHD 8.1 m, replenishment 11.6 m, submarine 6 m, corvette 5 m, small craft 2 m (game:data/rules/nav-domains.json `shipClasses`). A channel for a carrier therefore needs at least 12.9 m of water and 41 m of beam (about 3 cells at 20 m).

If a water tile has no height sample, nav reads it as 4 m deep, so it counts as deep (game:skirmish/src/nav/grid.ts:40-42).

A* steps cost 10 orthogonal and 14 diagonal. A blocked start or goal snaps up to 8 cells (game:data/rules/nav-domains.json:15-19).

### 5.4 Roads

**RULE.** Roads are tile 2. Civilian traffic runs on tile 2 roads and treats tile 1 as water (game:skirmish/src/civilians/roadGraph.ts:3-6; `premade.civilianTraffic` in premade maps). Forge routes roads by slope cost and avoids water and cliffs (game:data/rules/terrain-forge.json `roads`).

### 5.5 Structures, footprints and aprons

**RULE.** Placed objects are `objects[] {type, x, y, faction, id?}`. Building footprints come from unit dimensions in metres. The collision radius is the circumradius of length x width (game:data/rules/building-footprints.json metadata). Spacing and starting offsets are authored in legacy 5 m tiles and scaled to the map cell (game:skirmish/src/sim.ts:1582-1587).

**RULE (base pads).** Forge flattens a base pad of 4 to 14 cells with a 10-cell blend, a 3 x 10-cell factory exit lane and a 2 m freeboard (game:data/rules/terrain-forge.json `pads`). map-check wants a 40 m buildable radius with no more than 25 m of relief (tan 32 degrees x 40 m) and a factory exit of at least 2 clear cells (game:data/rules/map-check.json `base`, `layout`).

**RULE (naval aprons).** Ship goals are dock marker objects: `CapitalDrydock`, `CapitalBerth`, `SubmarinePen`, `SeaDragonDock`, `GlacierDrydock`, `ArcticSubPen`, `CaissonGate`, `Dock` (game:data/rules/nav-domains.json `dockMarkerTypes`). Drydock, berth and pen sizes are in game:data/rules/naval-maps.json `counts`.

### 5.6 Zones, layers stack, links

**RULE.** `zones[]` are rectangles `{id, shape: "rect", x0M, y0M, x1M, y1M, role|kind}` in metres (HQ), or `{x0, y0, x1, y1}` in cells (premade maps). A multi-sheet world uses `layers {activeLayerId, stack[], verticalDatums[], elevator, portal}` (game:data/rules/map-layers.json; HQ `layers`). A map with no `layers` block becomes one layer, `single` (game:data/rules/map-layers.json `singleSheetLayerId`).

**PROPOSED.** All zones and links use metres (`x0M` etc.) with cells derived. Mixed cell/metre zones are a validation warning.

---

## 6. Water rendering requirements

The owner caught maps drawn with no visible sea. This is how the renderer decides whether to draw water.

**RULE.** The terrain module draws water only when one of these is true (game:skirmish/src/terrain.ts:333-358, :480):

1. `waterLevel` (after unit and scale) is above 0: a full-map water plane;
2. the sheet is a real-surface or coastal sheet (`meta.realSurface`, coastal terrain): a continuous depth-clipped plane;
3. otherwise, quads only over cells whose tile is 1.

So a sheet with `waterLevel` 0 whose sea cells are not tile 1 and which is not a real-surface sheet draws **no water**, even where heights are below 0. The fallback path (no terrain module) draws water only when the water level is above 0 or the layer datum is below 0 (game:skirmish/src/main.ts:815-826, :853-865). E02 (the HQ playable) owns its own basin and gets no raster water (game:skirmish/src/terrain.ts:480).

**PROPOSED (water rules every map must pass):**

1. Every cell with height < `waterLevel` that is meant to be sea is tile 1. Every tile-1 cell has height < `waterLevel` (a dry "water" tile is an error).
2. A map whose `waterLevel` is 0 or below has tile-1 cells or `meta.realSurface`; otherwise it must declare `meta.dryInterior: true`.
3. Sea floor is at least 3 m deep near shore and deepens by 1.5 m per cell to 60 m (forge defaults, game:data/rules/terrain-forge.json `height`), so nav sees shallow then deep, not a cliff.
4. Shipping lanes meet the draft rule in 5.3 for every ship class the map spawns.
5. Water share: below 15 % water is an obstacle, not a naval domain (game:data/rules/map-check.json:148). A naval map is at least 15 % water.
6. The theatre sets `waterColour` (game:data/rules/theatres/disko_bay.json).
7. A screenshot at default camera shows water wherever the minimap shows water. This is a manual check until a tool does it.

---

## 7. Real-world sourcing

**RULE (sources in use).**

| Layer | Source | Where named |
|---|---|---|
| Land elevation | Copernicus DEM GLO-30 (DSM), AWS open data; Terrarium tiles for Bandar Abbas | game:data/rules/terrain-forge.json `sources.copernicus30`; game:data/maps/specs/forge_bandar_abbas.json `source.note` |
| Bathymetry and coarse topography | GMRT Synthesis, ocean gaps filled with GEBCO | game:data/rules/redsea-map.json `sources.elevation` |
| Land cover | ESA WorldCover 10 m 2021 v200 | game:data/rules/terrain-forge.json `sources.worldcover` |
| Roads | OpenStreetMap highways via Overpass (ODbL) | game:data/rules/terrain-forge.json `sources.osm`; game:data/rules/redsea-map.json `sources.vectors` |
| Ground textures | ambientCG CC0 | game:data/rules/terrain-forge.json `sources.ambientcg` |

**RULE (projection).** Sheets use a local equal-metre grid: equirectangular at a reference latitude (`redsea-map.json` `projection`) or an "80 m square local tangent grid" (Bandar Abbas spec). The spec records `source.center {lat, lon}` and `rotationDeg`.

**RULE (honesty).** Authored edits that are not survey data are labelled as such in `meta.realSurface` or the spec (`gameplayEdits`, "CONCEPT ... not surveyed"; game:map-import/real_terrain.py:227-261). Keep attribution strings in `meta.attribution`.

**RULE (licence).** Licence is not a filter for this personal project (owner memory, 2026-10-19). Attribution is still recorded.

New maps come from `tools/terrain-forge` with specs in `game:data/maps/specs/` and theatre looks in `game:data/rules/theatres/`. The naval generators are superseded (game:map-import/naval/README.md:3). The forge tool itself is not in the live tree (OD-11).

---

## 8. Map and theatre metadata

**RULE (fields in use).**

| Field | Meaning | Example |
|---|---|---|
| `id` | file stem, unique | `greenland_naval_bunker` |
| `name` | display name | "The Forge in the Ice (Eqip Sermia, Disko Bay)" |
| `theatreId` | key into the curated theatre list | `disko_bay` |
| `metocTag` | weather profile; HQ is `hq_ideal` | HQ |
| `datumM` | vertical datum of the sheet | 0, -820 |
| `meta.cellSizeM` | twin of `metresPerCell` | 20 |
| `meta.extentM {widthM, heightM}` | width x cell, height x cell | 6400 x 4000 |
| `meta.axis` | free-text local frame | HQ |
| `meta.tileLegend` | copy of the tile codes | HQ |
| `meta.rules` | rules file that generated the sheet | `data/rules/naval-maps.json` |
| `meta.realSurface` | art source classes and honesty notes | many |
| `meta.notes[]` | design notes | many |
| `premade {sourceMap, sourceKind, kept, edit}` | provenance of a sheet derived from another | premade_* |
| `campaign`, `missionId` | campaign links | camp-aoi-05 |

Theatre ids in the player list (game:data/maps/curated.json `theatres`): persian_gulf, red_sea_gaza_aqaba, greenland, diego_garcia, desert, disko_bay, gaza, litani, gulf_of_aden, queen_maud_land, weddell_sea, mount_erebus, wilkes_land, south_georgia, mid_atlantic_ridge, svalbard. Theatre look files exist for bandar_abbas, disko_bay, gaza, greenland and queen_maud_land (game:data/rules/theatres/). A theatre may `extend` another (disko_bay extends greenland).

**RULE (curated list entry).** `{id, label, file, players, theatre, preview, kind}`, previews under `/maps/previews/` (game:data/maps/curated.json).

**PROPOSED (required metadata).** `id`, `name`, `theatreId`, `metresPerCell`, `heightmapUnits`, `heightScale`, `waterLevel`, `meta.cellSizeM`, `meta.extentM`, `meta.northBearingDeg`, and `meta.source` (`{kind: "dem" | "synthetic" | "retail-derived" | "hand", center?, rotationDeg?, attribution?}`).

---

## 9. Missions and spawn data

**RULE.** Skirmish starts are `starts[] {player, x, y, faction?}` in cells. Map forces are `units[] {type, x, y, player | faction, rosterId?}` (game:skirmish/src/sim.ts:427-436).

**RULE.** A mission is a separate file in `game:data/missions/` and names its map: `map {file, theatreId?, variant?: "linked3" | "continuous"}`. It carries `players`, `startingForces`, `areas {id, xMin, yMin, xMax, yMax}` in tile space, `objectives`, `triggers` and times in seconds (game:data/missions/schema.json). It may set `replaceMapForces`. HQ missions may opt into weather with `environment.metocTag`; otherwise conditions are ideal (owner ruling 2026-10-19 in the schema).

**RULE.** Fresh skirmish uses `Sim.spawnStartForces`; starts on isolated beaches are relocated by `game:data/rules/skirmish-starts.json`, which wants at least eight clear cells around a start.

**RULE (map-check spawn gates).** Skirmish start pairs at least 15 SAGE cells apart; no enemy defence within 15 SAGE cells at t=0; nearest oil within 60 SAGE cells and not beyond the enemy; at least one oil per base-building start (game:data/rules/map-check.json `spawn`, `resources`). SAGE cells convert with 10 SAGE world units per cell over the map's metres per cell (`units`).

---

## 10. File format

One JSON file per sheet in `game:skirmish/maps/`. Optional sidecars: `<id>.blend.json` (terrain blend from a World Builder `.map`), previews in `skirmish/public/maps/previews/`. A generator spec lives in `game:data/maps/specs/<id>.json`.

| Field | Type | Required today | Required by this standard |
|---|---|---|---|
| `width`, `height` | integer | yes | yes |
| `tiles` | int[height][width], 0-3 | yes | yes |
| `heightmap` | number[height][width] | no (flat if missing) | yes |
| `waterLevel` | number | yes | yes |
| `starts` | array | yes | yes |
| `objects`, `units` | array | yes | yes |
| `metresPerCell` | number > 0 | no (5 m fallback) | yes |
| `heightmapUnits` | enum | no (metre in game, guessed in check) | yes |
| `heightScale` | number | no (1) | yes, 1 |
| `id`, `name`, `theatreId` | string | no | yes |
| `meta.cellSizeM`, `meta.extentM` | | no | yes |
| `textures`, `textureGrid`, `textureGridScale` | | no | optional |
| `zones`, `layers`, `datumM`, `metocTag`, `rangePolicy` | | no | optional |

`map-standard.json` is the checkable form. Byte cap: naval generator 5 MB (game:data/rules/naval-maps.json:13). Largest sheet today: forge_bandar_abbas, 4.0 MB.

---

## 11. World Builder (C&C3 / SAGE) compatibility

- Retail Generals maps were imported as 128-cell sheets with `<id>.blend.json` blend sidecars that name the `.map` source (`alpine_assault`, `barstow_badlands`, `blue_zone_dam`, `tournament_desert` from `TD_CnC3`, `twilight_flame`). They have no `metresPerCell` and draw at the 5 m fallback. They are blocked from the player list (game:data/maps/curated.json `blockedIds`).
- Premade maps reuse them (`premade_redsea_sar` from `tournament_desert`, recorded in `premade.sourceMap`).
- map-check measures against SAGE: 1 heightmap cell = 10 SAGE world units, and uses the Generals column of the retail measurements (game:data/rules/map-check.json:18-19, `eraColumn`).
- The client has a World Builder binary reader for presentation data (game:skirmish/src/mechanics/presentation/worldbuilder.ts).
- A Zero Hour `.map` importer (terrain, water, objects, waypoints, triggers, scripts to our JSON) was in progress on lane anl9/ans9 and was not merged as of the briefing (share:ARCHITECTURE-BRIEFING.md:55; share:DOLPHIN-ANSWERS.md:104). Its tooling is under `M:\z4yin-work-overflow\worldbuilder`.
- The metre size of a retail SAGE cell is not recorded in any source read, so the true scale of retail-derived maps is undetermined (OD-8).

---

## 12. Performance budgets

| Budget | Value | Source |
|---|---|---|
| Desktop smoke gate | 30 fps target, p95 frame <= 50 ms, at most 5 stalls over 100 ms per 10 s sample | game:data/rules/runtime-budgets.json:8-11 |
| JS heap / GPU memory | 1024 MB / 2048 MB | game:data/rules/runtime-budgets.json:12-13 |
| Reference GPU | NVIDIA T1000, 1920x1080 | game:data/rules/runtime-budgets.json; game:data/rules/client-performance.json:2 |
| Desktop display | 60 Hz minimum adaptive target | game:data/rules/client-performance.json:18 |
| Terrain mesh | 512 segments per axis | game:data/rules/world-scale.json:21 |
| Texture grid | 1.1 M texels | game:data/rules/terrain-forge.json:130 |
| Props per sheet | 3500 max; client scatter off at 80+ authored props | game:data/rules/terrain-forge.json:138-141 |
| Map file | 5 MB (naval generator cap) | game:data/rules/naval-maps.json:13 |
| Model triangles lod0 | structure 6000, vehicle 8000, aircraft/ship 15000, infantry 3000 | game:data/rules/scale-overrides.json `desktopTriCaps` |

---

## 13. Map creation checklist

1. Pick the theatre (`theatreId` from section 8). Add a theatre look file if the theatre has none.
2. Pick the cell size (section 2.2) and the grid so the short axis gives the engagement size you want (section 2.3).
3. Write a spec in `data/maps/specs/<id>.json`: size, `metresPerCell`, `verticalExaggeration` 1, `waterLevelM`, `source {kind, center, rotationDeg}`, starts, areas, roads.
4. Source heights in metres from the DEM and bathymetry in section 7. Record attribution.
5. Set `heightmapUnits: "metre"`, `heightScale: 1`, `metresPerCell`, `meta.cellSizeM`, `meta.extentM`, `meta.northBearingDeg`.
6. Paint tiles: every sea cell tile 1 and below `waterLevel`; cliffs tile 3 where slope exceeds 32 degrees; roads tile 2.
7. Check water (section 6) for each ship class the map spawns.
8. Place starts, oil and supply per section 9; flatten base pads per 5.5.
9. Put zones and links in metres.
10. Run the validators in section 14. Fix every error.
11. Load the map in the game and take a screenshot at default camera. Confirm water, scale of a known unit next to a known structure, and north.
12. Add a curated entry and preview if it is player-facing.

---

## 14. How a map is validated

| Tool | Command (from the game root) | What it checks |
|---|---|---|
| map-check | `node tools/map-check/check.mjs <map.json> [--mission <mission.json>]`, `--all`, `--curated`, `--gate` | buildable base pad and relief, oil/supply/tech travel, ground reach (unless naval-only), chokes and wall-offs, start distance and spawn safety, fairness, passable share, water share, base layout score, optional AI play balance. Uses the real nav stack (game:tools/map-check/cli.ts:1-8; game:data/rules/map-check.json) |
| scale print | `node <helper>/mapscale.mjs <map.json>...` (dolphin4 helper, `C:\Users\MaxGlasser\.claude\jobs\44b57189\tmp\mapscale.mjs`) | prints every scale field per map |
| naval load check | `vite-node map-import/naval/check_naval_maps.ts` | naval sheets load (superseded generator) |
| curated validation | `data/maps/curated.json` `validation` | 10-minute hard-AI matches for faction pairs plus map-check |

Naval-only sheets are listed in `navalOnlyIds` and skip the ground-reach gate (game:data/rules/map-check.json:71).

**PROPOSED.** Add a schema check that reads `map-standard.json`. It would fail a map that has no `metresPerCell`, no `heightmapUnits`, a `heightScale` other than 1, a height range out of bounds for its theatre, mismatched grid sizes, a dry water tile, or a sea with no drawable water.

---

## 15. Audit of current sheets (2026-10-20)

Measured from `game:skirmish/maps/*.json`. "mpc" is the cell size the engine resolves. "-" means not set.

| Map | Grid | mpc (source) | units | hScale | height m | water % | Flags |
|---|---|---|---|---|---|---|---|
| greenland_naval_bunker (HQ) | 320x200 | 20 | metre | 1 | -18..140 | 12 | none |
| forge_jslc00_greenland | 320x200 | 20 | metre | 1.5 | -60..530 | 15 | heightScale drift |
| camp-p04-disko-bay, camp-06-apollo-chronocast | 320x200 | 20 | metre | 1 | -80..2000 | 25 | none |
| diego_garcia_surface / abyss | 120x120 / 360x240 | 20 | metre | 1 | | 68 / 65 | none |
| kharg_approach | 160x160 | 20 (meta only) | - | - | 12..116 | 64 | no units |
| kharg_island | 160x160 | 5 fallback | - | - | 12..116 | 64 | same data as kharg_approach at 1/4 size; checker reads heights as 0.2 m |
| kharg_shiraz, ep1-3, theatre, camp-14-iron-ledger | 160-320 | 5 fallback | - | - | up to 2997 | 8-52 | 3 km relief on a 0.8-1.6 km sheet |
| camp-aoi-05-tunnel-network | 240x160 | 5 | metre | 1 | -842..-720 | 43 | built from 20 m redsea_undersea, `meta.meshNote` says 20 m |
| premade_redsea_sar | 104x128 | 5 fallback | - | 1 | 0..43 | 54 | retail-derived, scale undetermined |
| premade_* others, camp-p01/02/05/06/09, camp-02/04/09/11b | 104-128 | 5 fallback | - | 1 | | | no cell size |
| forge_aoi01_gaza | 128x85 | 20 | metre | 1.5 | | 13 | heightScale drift |
| forge_p01_queenmaud | 128x128 | 30 | metre | 1.2 | 1114..1846 | 0 | heightScale drift; 30 m nonstandard |
| forge_bandar_abbas (+invasion) | 320x320 | 80 | metre | 1 | -35..1289 | 24 | 4x exaggeration baked into heights |
| redsea_surface | 200x512 | 500 | metre | 1 | -860..1338 | 6 | theatre overview scale |
| camp-07-silicon-oasis | 100x180 | 500 | metre | 1 | 29..999 | 0 | 500 m tactical sheet |
| starter | 64x64 | 5 fallback | - | - | 8..200 | 7 | checker reads heights as 0.2 m |
