Skip to content

Zone-Based Map Generation — Design (Unity port, slice 1)

Date: 2026-06-26 Status: Approved (design) Migration workflow: web context → configs/defs (no magic numbers) → audit vs web (see unity-migration-workflow memory)


1. Goal & context

Replace the Unity tester build's throwaway random scatter (RaidBootstrap inline params + EnemySpawner/RaiderSpawner random-floor placement) with the real zone-based map architecture, ported faithfully from the web prototype's genMap (game.js:444).

A web map = light connective cover in gray space, with a few colored tiered zones (rare=blue / epic=purple / legendary=gold) carved into it. Each zone has a theme (its signature structure), a roster of leashed guard mobs, and a case chest of the zone's tier. The zone's tier is an item rarity, so zone tint == chest glow == loot color (one consistent language).

This is slice 1 of the larger map architecture. It delivers the map's actual identity and the data-asset foundation everything else builds on.

Faithfulness requirement

The generator must be able to reproduce the same map layouts the web offers. Generation logic (cover scatter, max-spread zone placement, zone footprint scaling, theme carving, leashing) is ported verbatim from genMap/ZONE_TIERS/ZONE_THEMES/placeCenter, and a step-3 audit diffs the result.


2. Scope

In scope

  • Three designer-authored ScriptableObjects: MapDefinition, ZoneTierDef, ThemeDef.
  • Two-stage generate → populate pipeline (approved option A).
  • Colored tiered zones: max-spread placement, footprint scaling, theme carving, case chest, leashed guards.
  • Mob leashing on EnemyController (zone box + gray loose-leash), web en.leash/leashPad.
  • Gray-space connective cover + gray roamers, rivals, portals, player spawn — all from map data.
  • Migrate per-map size/density settings from GameConfig onto MapDefinition.
  • Two theme carves ported now (Graveyard + Witch's Swamp); ThemeDef structure supports the rest.

Out of scope (later slices)

  • Vault + key/Keykeeper + force-gate (slice 3) — incl. the stage boss (it guards the vault portal).
  • Biome ladder / stageZoneSpecs / progression danger scaling (slice 4); prog.biomeCount/ stagesPerBiome/dangerPerBiome/stageDanger stay in GameConfig untouched.
  • The remaining 3 theme carves (Abandoned Church, Old Ruins, Burned Village) — fast-follow.
  • MapLayout fixed FTUE layouts (slice 5).
  • Valuable loot, in-run inventory — remain parked.

3. Data model (ScriptableObjects)

ZoneTierDef (one asset per tier: rare / epic / legendary)

Mirrors web ZONE_TIERS[tier]. - LootRarity rarity — the tier; drives chest rarity, loot, and tint (via the rarity color ladder). - string[] mobKeys — guard roster (e.g. rare → goblin/hound/spider/emberball). - Vector2Int guardCount[lo,hi] guards before area-scaling (web count). - bool allowMiniBoss — may host an "Elder" mini-boss (buffed guard) when the map asks for it.

Authored values (from web ZONE_TIERS): - rare: goblin, hound, spider, emberball — [3,4] - epic: warden, skeleton, emberball, rocketeer — [3,4] - legendary: rocketeer, bombardier, sentinel, warden — [2,3]

ThemeDef (one asset per theme)

Mirrors web ZONE_THEMES[i]. A zone gets a distinct theme from the map's pool. - string displayName, string emoji — for the zone label. - A carve descriptor: rather than a C# delegate, carve params are data (enum CarveKind + tunable counts/chances), so themes are authorable and the carve runs in MapGenerator. - CarveKind: Graveyard (gravestone wall dots + creeping bush), Swamp (dense thicket, cleared core), Church (wall-ring + breach doors), Ruins (rubble wall segments), Village (charred hut-stub grid). Each kind reads a small param set (e.g. gravestoneCount, bushChance, doorCount). - This slice ships Graveyard + Swamp; the other three CarveKind values exist but their assets are authored in the fast-follow.

MapDefinition (the per-stage map recipe — your "stagedef")

The procedural map params, replacing RaidBootstrap's inline values + the per-stage size table. - int width, height — tile grid size (replaces prog.stageSizesB1/B2/B3). - Cover densities: float wallClusters, bushLines, bushClusters (× map area; web map.cover*). - ZoneEntry[] zones — each: ZoneTierDef tier, bool miniBoss. (Boss/vault deferred.) - ThemeDef[] themePool — distinct themes assigned per zone (shuffled, web themePool). - Gray roamers: string[] roamerKeys, int roamDiv (1 roamer / N tiles; web prog.roamDiv). - Rivals: int rivalDiv (web prog.botDiv), int rivalCap (web map.bots). - int portalCount — authored per-map (the tester sets 2 on small maps, 3 on big). - int seed-1 = fresh per run.


4. Generation pipeline (option A)

MapGenerator.Generate(MapDefinition def) — tiles + zone data only

Pure map structure; spawns no entities. Mirrors genMap lines 444–551. 1. Border walls; size from def. 2. Connective cover scatter (wall clusters, bush lines, bush clusters) scaled to area — already ported in the current generator; move the densities to def. 3. For each ZoneEntry: - PlaceCenter(hw, hh)max-spread (over ~240 candidates pick the one whose nearest placed center is farthest; web placeCenter). Footprint scales with map size, capped at the classic 13×11 (web hw/hh clamps). - ClearArea the footprint, run the assigned ThemeDef carve, re-open a small core pocket. - Record a Zone { Box (tile rect + world rect), tier, themeName, center } into MapData.Zones. 4. Expose MapData.Zones (read-only list) for the populator + later the minimap/tactical map.

MapData gains a Zones list; nothing else downstream (fog/pathing/vision) changes — it still consumes MapData tiles agnostically.

MapPopulator.Populate(MapData, MapDefinition) — entities

Consumes the generated zones + def to spawn everything (mirrors the rest of genMap + the current spawner logic, now unified): - Per zone: a case chest at the center (Chest.rarity = zone.tier); guards = guardCount area-scaled (web areaScale), spawned inside the footprint, each leashed to the zone box; an optional mini-boss (buffed guard) if the entry asks and the tier allows. - Gray roamers: area / roamDiv mobs in gray space, each with a loose spawn-anchored leash (web grayLeash). - Rivals: clamp(area / rivalDiv, 1, rivalCap) (existing RaiderSpawner math), spawn-safe. - Portals + player spawn: as the current RaidBootstrap does (spawn-safe bubble, portals ≥ spawn-safe from the player).

EnemySpawner/RaiderSpawner (tester scaffolding) are absorbed into MapPopulator and removed. RaidBootstrap slims to: pick the stage's MapDefinitionMapGenerator.GenerateMapPopulator.Populate.


5. Leashing (EnemyController)

Port web en.leash (updateMob patrol/return + genMap leash boxes). - New Leash (world-space box) + Home point, set at spawn by the populator (zone box, or a loose box around a gray roamer's spawn via prog.grayLeash). - Patrol/wander: stay inside the leash box (clamp the wander target). - Chase: may leave the box to pursue a target (web: chasing overrides the leash). - Lost trail / give-up: path back inside the leash, then resume patrol (web homeX/homeY). - Null leash = free roam (current behavior; keeps non-zone spawns working).

This is the one gameplay-logic change; it's small and self-contained (a clamp in the wander target + a "return home" branch when leashed and outside the box with no target).


6. Config migration

Setting Today After
Map width/height MapGenerator.mapW/Hprog.stageSizesB1[stage] MapDefinition.width/height
prog.stageSizesB1/B2/B3 in GameConfig removed (per-map size lives on the def)
cover densities map.cover* GameConfig MapDefinition.wallClusters/bushLines/bushClusters
map.roamDiv GameConfig MapDefinition.roamDiv
prog.botDiv / map.bots GameConfig MapDefinition.rivalDiv / rivalCap
prog.leashPad / prog.grayLeash GameConfig (deferred) used by the populator/leashing
prog.zoneSeparation GameConfig (deferred) used by PlaceCenter
map.tile GameConfig stays (universal px→world divisor)
map.roundTime, map.spawnSafe GameConfig stays (raid-level rules)
prog.biomeCount/stagesPerBiome/dangerPerBiome/stageDanger, bonusZonesPerBiome, vaultFromStage GameConfig stays untouched (biome ladder, slice 4)
zones.fillAlpha GameConfig stays (zone tint look)

The tester's stage cursor (TesterRunState.Stage) selects among a few authored MapDefinition assets instead of indexing the size table.


7. File plan

New: Config/ZoneTierDef.cs, Config/ThemeDef.cs, Config/MapDefinition.cs, World/MapPopulator.cs, plus authored assets (3 ZoneTierDef, ≥2 ThemeDef, a few MapDefinition). Changed: World/MapGenerator.cs (Generate(MapDefinition) + zone carving + MapData.Zones), World/EnemyController.cs (leash), Tester/RaidBootstrap.cs (slim orchestrator), Config/GameConfig.cs (remove moved fields). Removed: World/EnemySpawner.cs, World/RaiderSpawner.cs. Editors: ZoneTierDef/ThemeDef/MapDefinition get grouped custom inspectors only if they grow non-trivial (reuse the EnemyDef editor pattern; otherwise default inspector is fine).


8. Faithfulness audit (step 3)

After it compiles, diff against game.js: - PlaceCenter max-spread, zone footprint clamps, areaScale guard scaling — values + formulas. - ZONE_TIERS rosters/counts; tier→rarity→tint mapping. - The two ported theme carves vs ZONE_THEMES Graveyard/Witch's-Swamp (counts/chances/structure). - Leashing: patrol-inside / chase-out / return-home vs updateMob. - Cover-scatter densities + case-chest tier + gray-roamer leash. Note any deltas blocked on deferred systems (e.g. progression danger scaling, the boss).


9. Verification

  • EditMode-ish checks via the Unity MCP: generate a MapDefinition → assert MapData.Zones count, boxes within bounds, non-overlapping (max-spread), case chest per zone at center with correct rarity.
  • Play smoke test: zones render tinted/themed, guards stay leashed (don't wander cross-map), gray roamers + rivals + portals + player place spawn-safe, a raid is completable.
  • Confirm no orphaned references after removing EnemySpawner/RaiderSpawner.

10. Open questions / risks

  • Connectivity: themed carves (wall-rings, gravestone fields) could wall off a chest/zone core. The web re-opens a core pocket + uses doors; we port that. If a zone still strands content, add a flood-fill reachability check from the player spawn (cheap insurance) — note as a possible follow-up.
  • Asset count: authoring several MapDefinitions by hand is fine for the tester; a custom map editor window (migration §8) is a separate, later tool.