DOMINATION

Map authoring by blueprint image + schematic inspection

A hands-on alternative to writing per-map Python generators. You paint a top-down blueprint (or generate one as art); the toolkit converts it to a playable, validated map and lets you see the layout instantly without a 2-minute Unity capture.

The three tools (unity/tools/maptools/)

Tool What it does Speed
map_schematic.py <map.json> Renders any map's terrain grid as a colour PNG with spawn/resource/prop markers — the fast inspection loop <1s, no Unity
image_to_map.py <blueprint.png> --id … --name … --biome … Converts a painted blueprint → validated map JSON (BFS connectivity, symmetry, in-bounds) <1s
terrain_tiers.py <map.json> Builds a coherent elevation heightfield — lower plains → rolling hills → high ground → towering mountains, biased toward the rim, with graded slopes. BFS-proven walkable network <1s
terrain_surface.py <map.json> Paints realistic surface materials onto that elevation — grass plains → dirt/scree hills → rock cliffs → biome caps (snow / scorched / bare rock). Connectivity-safe by construction <1s
dress_map.py <map.json> Scatters biome-appropriate props/decor (trees, rocks, tufts, wrecks) with symmetry <1s
stamps.py Pre-QA'd region stamps (forest, settlement) placed by a single seed pixel
map_palette.py The shared terrain↔colour key (below)

Blueprint colour key (paint terrain with these)

Paint 1 pixel = 1 map cell. Nearest-colour match on import, so approximate hues are fine.

Paint Terrain Notes
green #568C3E grass
tan #D6C08A sand desert base
brown #966E42 dirt
dark grey #5A544E rock IMPASSABLE — chokepoint walls
blue #305C9E water IMPASSABLE deep water
light blue #6EA0C8 shallow ford passable water crossing
plank brown #967846 bridge over water
dark asphalt #46464A paved_road
mid grey #78746C road dirt track
charcoal #3C3630 scorched battle scar
pale cyan #C8E0EC ice snow maps

Pin dots (drop these bright colours as markers)

Reserved saturated hues NOT used by terrain — drop a small dot; the cell under it keeps the base terrain. Multi-pixel dots are auto-clustered to one pin (centroid).

Pin Meaning
pure red #FF0000 player spawn (team = paint order)
magenta #FF00FF ore node
orange #FF8000 oil derrick (contested economy)
yellow #FFFF00 strategic point (tech)
cyan #00FFFF strategic point (radar)
bright green #00FF00 forest region stamp seed
violet #8000FF settlement region stamp seed

Workflow

# 1. paint/generate blueprint.png (left half only if using --mirror)
# 2. convert -> validated map
python3 unity/tools/maptools/image_to_map.py blueprint.png \
    --id my_map --name "My Map" --biome desert --mirror
# 3. inspect the layout instantly
python3 unity/tools/maptools/map_schematic.py \
    unity/Assets/StreamingAssets/data/maps/my_map.json
# 4. redraw blueprint, repeat — or capture in-engine when the layout reads right

--mirror paints only the left half and generates the right by 180° rotation, so the map is guaranteed competitively symmetric — the painter can't accidentally make it unfair.

Terrain relief & realism pipeline (mountains · hills · plains · snow · lava)

A flat blueprint gives you the layout; this pass gives it realistic 3D landscape. Run the two elevation tools after the layout reads right — they are deterministic (seeded from the map id), symmetry-aware, and each ends with a BFS reachability gate that hard-fails rather than ship a map with a severed spawn.

M=unity/Assets/StreamingAssets/data/maps/my_map.json
# 1. RELIEF — raise coherent plains→hills→mountains (rim-biased so bases sit in the valley).
#    --clamp 3.2 keeps ridges high-but-passable for open / many-spawn maps (steppe, 32-base basins).
python3 unity/tools/maptools/terrain_tiers.py   "$M"            # add --clamp 3.2 for open maps
# 2. SURFACE — repaint the ground by elevation+slope so mountains stop rendering as green grass.
#    Biome is auto-read from the map's `biome` field (snow→snow caps, volcanic→scorched, else rock).
python3 unity/tools/maptools/terrain_surface.py "$M"            # or --biome snow|volcanic|desert
# 3. DECOR — scatter trees/rocks/tufts on the new terrain, then inspect.
python3 unity/tools/maptools/dress_map.py       "$M" --mirror auto
python3 unity/tools/maptools/map_schematic.py   "$M"

Why two passes. terrain_tiers.py only moves the ground up; it never re-textures it, so a peak it raised still renders as green grass. Across the shipped set this left tens of thousands of impassable mountain cells painted grass (a glacier map with green peaks, a volcano with green slopes). terrain_surface.py closes that gap by banding the surface:

Elevation Temperate Snow biome Volcanic
plains (low) (left as authored) (left as authored) (left as authored)
rolling hills / high walkable grass + dirt scree on steep swells same same
mountain cliffs (> 3.5 walk cap) rock rock on steep faces rock
summits snow on tall peaks only (≥ 6.5) snow blankets the high ground scorched charcoal

Connectivity is provably preserved: an impassable char (rock) is only ever written to a cell whose elevation is already above the 3.5 walk cap, and every walkable cell only receives passable ground. Roads / water / rail / bridges / concrete / ore are never touched. The pass prints a SURFACE {…} line with the change histogram and an errors list that must be empty.

Why this beats a blind generator

  • You author with your eyes. Layout is a picture you paint, not terrain math you can't see.
  • The schematic is the inner loop. A dark wadi, a broken lane, a resource on water — all visible in <1s, not hidden behind a far 3D overview.
  • Validation is kept as a gate. BFS connectivity, symmetry, in-bounds, determinism still run on every import — the fairness/lockstep guarantees the generators gave, unchanged.
  • Stamps compose from verified pieces. A forest/settlement is designed + QA'd once, then reused by dropping a seed pixel.

Proven end-to-end: a painted blueprint → validated JSON → loads in-engine (connectivity 0 warnings, deterministic checksum match).

Terrain contracts (verified against the engine)

These are the rules the blueprint tools enforce, each confirmed against terrain.json, GridMap.PassableFor and image_to_map. Every one of them cost a rejected draft or a shipped bug to learn, so check them before authoring rather than after.

Rock does NOT block — it blocks only as a CLIFF

terrain.json marks rock 'k' passable: true (buildable: false). GridMap.PassableFor falls through to terrain[tid].passable, so rock stops a unit only when its elevation exceeds MaxWalkElev (3.5). Painting rock on flat ground produces rocky ground you can walk over, not a wall. rattlesnake_gulch shipped a 43,184-cell "canyon labyrinth" that units walked straight through until this was found (#2860).

  • Want decorative/unbuildable rocky ground? Flat rock is correct.
  • Want ground that can be crossed but never fortified? Flat rock is the idiom — passable:true + buildable:false is exactly "you may walk it, you may not dig in." no_mans_land uses a flat rock belt between the two trench lines for precisely this: crossing it is the drama, and neither side can build in it. Do not "fix" that by elevating it; it would turn a contested crossing into a wall.
  • Want a barrier? Also raise elevOffsets above 3.5 (a heightmap, terrain_tiers, or a direct elevation pass). validate_maps warns when a large or long-and-thin flat-rock mass looks like an intended wall.

Move domains — the same tile is not passable to everyone

terrain ground / vehicles infantry notes
water 'w' blocked blocked amphibious only (Navy SEAL)
shallow 'h' blocked fords it at 0.6x speed passable:false + the #1997 ford gate
mud 'm' passable, 0.65x passable, 0.65x not buildable — no fortifying
rock 'k' passable unless elev > 3.5 same unbuildable
road/paved/concrete 1.5x / 1.5x / 1.25x same the fast arteries

A causeway crossing water must be bridge 'b', never shallow 'h' — a ford silently strands every vehicle. lakeside_wheel lost its whole hub to that (#2856). validate_maps now BFSes the ground domain separately and errors on a spawn reachable only by fording.

Bridge contract (image_to_map.bridge_errors)

A bridge must be one filled axis-aligned rectangle with:

  • length >= 3 and width 3-6 — a span, not a slab;
  • a full-deck-width approach of real road chars at BOTH endsROAD_CHARS = {"r","a","q"}; dirt 'd' does not count;
  • a genuine obstacle (w/h/k/l) along its sides.

Working recipe: a 5-wide deck with a 5-wide 'r' strip abutting each end. Under --mirror x a deck touching the seam merges with its reflection, so author the approach only on your own half.

Objectives must sit on passable terrain

image_to_map rejects a spawn/resource/strategic point placed on impassable ground. On three_fronts that rejection is what produced the inter-lane passages — treat these failures as design feedback.