Skip to content

Zone-Based Map Generation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the Unity tester build's throwaway random scatter with the web prototype's zone-based map generation — tiered colored zones (rare/epic/legendary) with themed carving, leashed guards, and case chests — driven by designer-authored ScriptableObjects.

Architecture: A two-stage generate → populate pipeline. MapGenerator.Generate(MapDefinition) builds tiles + a MapData.Zones list (pure structure, no entities); MapPopulator.Populate(MapData, MapDefinition) spawns everything from that data. Three SOs (MapDefinition, ZoneTierDef, ThemeDef) hold all tunables. Ported faithfully from game.js:genMap (444–709), ZONE_TIERS (406), ZONE_THEMES (416), stageZoneSpecs/placeCenter.

Tech Stack: Unity 6000.3 / URP, C#, Zenject DI. Game code lives in Assembly-CSharp (no asmdef). Shapes (component mode) for any vector visuals.

Global Constraints

  • No magic numbers. Every tunable lives on a SO (MapDefinition/ZoneTierDef/ThemeDef) or GameConfig; reference it at the use site. (Project rule + unity-migration-workflow memory.)
  • Faithful to the web. Port values/formulas verbatim from the cited game.js lines; a final audit task diffs the result. The web is the source of truth — when porting verbatim internals (cover scatter, theme carves, placeCenter constants), copy the numbers from game.js, do not invent.
  • Verification = Unity MCP, not a test framework (this project has no NUnit/EditMode harness). Each task compiles via mcp__unity-mcp__Unity_RunCommand (AssetDatabase.Refresh()), checks mcp__unity-mcp__Unity_GetConsoleLogs (logTypes "Error") is empty, then runs a RunCommand that constructs/loads the new code and asserts via result.Log(...).
  • MCP RunCommand gotchas (see mcp-runcommand-gotchas memory): CommandScript : IRunCommand; no System.Reflection; fully-qualify UnityEditor.Editor; can't edit scripts during play; a compile error anywhere keeps the last-good assembly loaded (so a new type reads as "namespace does not exist" → check GetConsoleLogs for the real error). Don't be in play mode when editing scripts.
  • map.tile (px→world divisor) stays a global GameConfig constant; per-map size/density moves onto MapDefinition.
  • Commit after each task. End commit messages with the Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> trailer. Branch: migrate-unity.
  • Namespaces: SOs in ShadowRaiders.Config; runtime in ShadowRaiders.World.
  • Px→world: world units = px / GameConfig.map.tile (tile = 40). Web works in px; convert at the boundary.

File Structure

File Responsibility
Assets/Scripts/Config/ZoneTierDef.cs (new) SO: one tier (rare/epic/legendary) → mob roster, guard count, rarity
Assets/Scripts/Config/ThemeDef.cs (new) SO: one theme → name/emoji + a data-driven carve descriptor (CarveKind + params)
Assets/Scripts/Config/MapDefinition.cs (new) SO: the per-map recipe — size, cover densities, zone list, theme pool, roamer/rival/portal counts, seed
Assets/Scripts/World/MapGenerator.cs (modify) Generate(MapDefinition): tiles + cover + zone placement + theme carve; exposes MapData.Zones
Assets/Scripts/World/MapPopulator.cs (new) Populate stage: zone guards (leashed) + case chests, gray roamers, rivals, portals, player spawn
Assets/Scripts/World/EnemyController.cs (modify) Leash box + Home; patrol-inside / chase-out / return-home
Assets/Scripts/Tester/RaidBootstrap.cs (modify) Slim orchestrator: pick MapDefinition → Generate → Populate
Assets/Scripts/World/EnemySpawner.cs, RaiderSpawner.cs (delete) Absorbed into MapPopulator
Assets/Scripts/Config/GameConfig.cs (modify) Remove fields moved to MapDefinition (size table, cover, roamDiv, botDiv/bots)
Authored assets Assets/Config/Zones/*.asset (3 tiers), Assets/Config/Themes/*.asset (2), Assets/Config/Maps/*.asset (≥2)

Dependency order: ZoneTierDef → ThemeDef → MapDefinition → MapGenerator → (Theme carve) → Leashing → MapPopulator → RaidBootstrap/cleanup → audit.


Task 1: ZoneTierDef ScriptableObject + tier assets

Files: - Create: Assets/Scripts/Config/ZoneTierDef.cs - Create (via MCP): Assets/Config/Zones/Zone_rare.asset, Zone_epic.asset, Zone_legendary.asset

Interfaces: - Produces: ShadowRaiders.Config.ZoneTierDef : ScriptableObject with public fields LootRarity rarity, string[] mobKeys, Vector2Int guardCount, bool allowMiniBoss.

  • [ ] Step 1: Create the SO
using UnityEngine;
using ShadowRaiders.Loot;   // LootRarity

namespace ShadowRaiders.Config
{
    /// <summary>
    /// One colored-zone tier (web ZONE_TIERS[tier], game.js:406). The tier IS an item rarity, so it drives
    /// the zone's tint, its case-chest rarity, and its loot. Designer-authored: add a tier = add an asset.
    /// </summary>
    [CreateAssetMenu(fileName = "Zone_", menuName = "Shadow Raiders/Zone Tier Def")]
    public class ZoneTierDef : ScriptableObject
    {
        [Tooltip("The item rarity this zone yields — also its colour (rare=blue/epic=purple/legendary=gold).")]
        public LootRarity rarity = LootRarity.Rare;
        [Tooltip("Guard roster (EnemyDef keys) spawned inside the zone, picked at random per guard.")]
        public string[] mobKeys;
        [Tooltip("Guard count [min,max] BEFORE map-area scaling (web ZONE_TIERS count).")]
        public Vector2Int guardCount = new Vector2Int(3, 4);
        [Tooltip("May host an 'Elder' mini-boss (a buffed guard) when the map definition requests one.")]
        public bool allowMiniBoss = true;
    }
}
  • [ ] Step 2: Compile + check no errors

Run mcp__unity-mcp__Unity_RunCommand: AssetDatabase.Refresh(); then mcp__unity-mcp__Unity_GetConsoleLogs (logTypes "Error"). Expected: empty.

Note: LootRarity must have Common/Rare/Epic/Legendary. If Legendary is missing, add it to Assets/Scripts/Loot/LootRarity.cs in this step and re-compile.

  • [ ] Step 3: Author the 3 tier assets via MCP

RunCommand (web ZONE_TIERS, game.js:407-409):

using UnityEngine; using UnityEditor; using ShadowRaiders.Config; using ShadowRaiders.Loot;
internal class CommandScript : IRunCommand {
  public void Execute(ExecutionResult r) {
    System.IO.Directory.CreateDirectory("Assets/Config/Zones");
    void Make(string name, LootRarity rar, string[] mobs, int lo, int hi) {
      var z = ScriptableObject.CreateInstance<ZoneTierDef>();
      z.rarity = rar; z.mobKeys = mobs; z.guardCount = new Vector2Int(lo, hi); z.allowMiniBoss = true;
      AssetDatabase.CreateAsset(z, $"Assets/Config/Zones/Zone_{name}.asset");
    }
    Make("rare", LootRarity.Rare, new[]{"goblin","hound","spider","emberball"}, 3, 4);
    Make("epic", LootRarity.Epic, new[]{"warden","skeleton","emberball","rocketeer"}, 3, 4);
    Make("legendary", LootRarity.Legendary, new[]{"rocketeer","bombardier","sentinel","warden"}, 2, 3);
    AssetDatabase.SaveAssets();
    r.Log("created 3 ZoneTierDef assets");
  }
}
  • [ ] Step 4: Verify assets load with correct data

RunCommand: load Assets/Config/Zones/Zone_rare.asset as ZoneTierDef, result.Log its rarity, mobKeys.Length, guardCount. Expected: Rare, 4, (3,4).

  • [ ] Step 5: Commit
git add unity/raiders/Assets/Scripts/Config/ZoneTierDef.cs unity/raiders/Assets/Config/Zones
git commit -m "Map gen: ZoneTierDef SO + rare/epic/legendary tier assets (web ZONE_TIERS)"

Task 2: ThemeDef ScriptableObject + 2 theme assets

Files: - Create: Assets/Scripts/Config/ThemeDef.cs - Create (via MCP): Assets/Config/Themes/Theme_graveyard.asset, Theme_swamp.asset

Interfaces: - Produces: ShadowRaiders.Config.ThemeDef : ScriptableObject with string displayName, string emoji, CarveKind kind, and carve params int gravestoneCount, float bushChance, int doorCount, int wallSegments. Public enum ShadowRaiders.Config.CarveKind { Graveyard, Swamp, Church, Ruins, Village }.

  • [ ] Step 1: Create the SO + enum
using UnityEngine;

namespace ShadowRaiders.Config
{
    /// <summary>How a theme shapes its zone rect (web ZONE_THEMES carve, game.js:416). Data-driven so themes are
    /// authorable; MapGenerator runs the matching carve. Graveyard + Swamp ship now; the rest are authored later.</summary>
    public enum CarveKind { Graveyard, Swamp, Church, Ruins, Village }

    /// <summary>One zone theme — its label + the carve descriptor that gives the zone its signature structure.</summary>
    [CreateAssetMenu(fileName = "Theme_", menuName = "Shadow Raiders/Theme Def")]
    public class ThemeDef : ScriptableObject
    {
        public string displayName = "Theme";
        [Tooltip("Label glyph shown with the zone name.")] public string emoji = "";
        public CarveKind kind = CarveKind.Graveyard;

        [Header("Carve params (only the ones the kind uses apply)")]
        [Tooltip("Graveyard: gravestone wall dots scattered in the zone (web 18).")] public int gravestoneCount = 18;
        [Tooltip("Graveyard/Swamp: chance a floor tile becomes bush.")] [Range(0,1)] public float bushChance = 0.4f;
        [Tooltip("Church: breach doors in the wall ring (web 3).")] public int doorCount = 3;
        [Tooltip("Ruins/Village: rubble/hut wall segments (web ~10).")] public int wallSegments = 10;
    }
}
  • [ ] Step 2: Compile + check no errors (as Task 1 Step 2).

  • [ ] Step 3: Author Graveyard + Swamp assets via MCP

RunCommand (web ZONE_THEMES Graveyard game.js:417, Witch's Swamp game.js:427):

using UnityEngine; using UnityEditor; using ShadowRaiders.Config;
internal class CommandScript : IRunCommand {
  public void Execute(ExecutionResult r) {
    System.IO.Directory.CreateDirectory("Assets/Config/Themes");
    var g = ScriptableObject.CreateInstance<ThemeDef>();
    g.displayName="Graveyard"; g.emoji="🪦"; g.kind=CarveKind.Graveyard; g.gravestoneCount=18; g.bushChance=0.4f;
    AssetDatabase.CreateAsset(g, "Assets/Config/Themes/Theme_graveyard.asset");
    var s = ScriptableObject.CreateInstance<ThemeDef>();
    s.displayName="Witch's Swamp"; s.emoji="🌿"; s.kind=CarveKind.Swamp; s.bushChance=0.5f;
    AssetDatabase.CreateAsset(s, "Assets/Config/Themes/Theme_swamp.asset");
    AssetDatabase.SaveAssets(); r.Log("created 2 ThemeDef assets");
  }
}
  • [ ] Step 4: Verify — load both, result.Log displayName+kind. Expected: Graveyard/Graveyard, Witch's Swamp/Swamp.

  • [ ] Step 5: Commit

git add unity/raiders/Assets/Scripts/Config/ThemeDef.cs unity/raiders/Assets/Config/Themes
git commit -m "Map gen: ThemeDef SO + CarveKind + Graveyard/Swamp assets (web ZONE_THEMES)"

Task 3: MapDefinition ScriptableObject + map assets

Files: - Create: Assets/Scripts/Config/MapDefinition.cs - Create (via MCP): Assets/Config/Maps/Map_small.asset, Map_large.asset

Interfaces: - Produces: ShadowRaiders.Config.MapDefinition : ScriptableObject with fields: int width, height; float wallClusters, bushLines, bushClusters; ZoneEntry[] zones (serializable { ZoneTierDef tier; bool miniBoss; }); ThemeDef[] themePool; string[] roamerKeys; int roamDiv; int rivalDiv, rivalCap; int portalCount; int seed.

  • [ ] Step 1: Create the SO
using UnityEngine;

namespace ShadowRaiders.Config
{
    /// <summary>The per-map recipe (the "stagedef") — procedural params for one map. Replaces RaidBootstrap's
    /// inline values + the old prog.stageSizes table. Designer-authored: add a map = add an asset.</summary>
    [CreateAssetMenu(fileName = "Map_", menuName = "Shadow Raiders/Map Definition")]
    public class MapDefinition : ScriptableObject
    {
        [System.Serializable] public class ZoneEntry { public ZoneTierDef tier; public bool miniBoss; }

        [Header("Grid")]
        public int width = 40;
        public int height = 40;

        [Header("Connective cover (× map area; web map.cover*)")]
        public float wallClusters = 0.0138f;
        public float bushLines = 0.0055f;
        public float bushClusters = 0.0075f;

        [Header("Zones")]
        public ZoneEntry[] zones;
        [Tooltip("Distinct themes assigned per zone (shuffled). Needs >= zones.Length to avoid repeats.")]
        public ThemeDef[] themePool;

        [Header("Gray roamers")]
        public string[] roamerKeys = { "goblin", "spider" };
        [Tooltip("1 gray roamer per N tiles (web prog.roamDiv).")] public int roamDiv = 450;

        [Header("Rivals + exits")]
        [Tooltip("1 rival per N tiles (web prog.botDiv).")] public int rivalDiv = 1500;
        [Tooltip("Rival upper clamp (web map.bots).")] public int rivalCap = 5;
        public int portalCount = 2;

        [Tooltip("-1 = fresh random seed per run.")] public int seed = -1;
    }
}
  • [ ] Step 2: Compile + check no errors.

  • [ ] Step 3: Author Map_small + Map_large via MCP

RunCommand: load the Zone/Theme assets, build a small map (36×36, 1 rare zone, mini-boss) and a large (70×70, rare+epic+epic). Example for small:

var def = ScriptableObject.CreateInstance<MapDefinition>();
def.width=36; def.height=36; def.wallClusters=0.0138f; def.bushLines=0.0055f; def.bushClusters=0.0075f;
var rare = AssetDatabase.LoadAssetAtPath<ZoneTierDef>("Assets/Config/Zones/Zone_rare.asset");
def.zones = new[]{ new MapDefinition.ZoneEntry{ tier=rare, miniBoss=true } };
def.themePool = new[]{ AssetDatabase.LoadAssetAtPath<ThemeDef>("Assets/Config/Themes/Theme_graveyard.asset"),
                       AssetDatabase.LoadAssetAtPath<ThemeDef>("Assets/Config/Themes/Theme_swamp.asset") };
def.roamerKeys = new[]{"goblin","spider"}; def.roamDiv=450; def.rivalDiv=1500; def.rivalCap=5; def.portalCount=2; def.seed=-1;
AssetDatabase.CreateAsset(def, "Assets/Config/Maps/Map_small.asset");
(Map_large: 70×70, zones = rare + epic + epic, portalCount=3.)

  • [ ] Step 4: Verify — load Map_small, result.Log width, zones.Length, zones[0].tier.rarity, themePool.Length. Expected: 36, 1, Rare, 2.

  • [ ] Step 5: Commit

git add unity/raiders/Assets/Scripts/Config/MapDefinition.cs unity/raiders/Assets/Config/Maps
git commit -m "Map gen: MapDefinition SO + small/large map assets (per-map recipe)"

Task 4: MapData.Zones + MapGenerator.Generate(MapDefinition)

Files: - Modify: Assets/Scripts/World/MapGenerator.cs

Interfaces: - Produces: MapData.Zones (public IReadOnlyList<Zone>); MapGenerator.Generate(MapDefinition def). public class Zone { public int Tx0,Ty0,Tx1,Ty1; public Vector3 CenterWorld; public ZoneTierDef Tier; public ThemeDef Theme; public bool MiniBoss; }. Consumes: MapDefinition (Task 3).

  • [ ] Step 1: Add the Zone type + Zones list to MapData

In MapGenerator.cs, add to MapData a readonly List<Zone> ZonesMutable exposed as public IReadOnlyList<Zone> Zones. Define Zone (fields above) in the ShadowRaiders.World namespace.

  • [ ] Step 2: Add Generate(MapDefinition) overload

Add a public void Generate(MapDefinition def) that sets mapW=def.width; mapH=def.height; wallClusterDensity=def.wallClusters; bushChance=def.bushLines+def.bushClusters; then runs BuildGrid() (existing cover scatter, faithful to game.js:494-496 — keep current behavior) and the zone-placement below, then BuildChunks. Keep the existing param-less Generate() for back-compat or route it through a default.

Zone placement (port game.js:507-551, no entities yet):

// max-spread placeCenter (web game.js:507): over 240 candidates pick the one whose nearest placed
// center is farthest. Footprint scales with map, capped at the classic 13x11 (web hw/hh clamps).
// For each MapDefinition.ZoneEntry: PlaceCenter -> ClearArea -> assign a distinct theme -> Carve (Task 5
// stubs to ClearArea for now) -> re-open a 3x3 core pocket -> record a Zone.
Add private helpers PlaceCenter(int hw,int hh, List<Vector2> placed), ClearArea, WallRing mirroring game.js:468-479, 507-515. Theme assignment: shuffle def.themePool, index per zone (web themePool/themeIdx).

  • [ ] Step 3: Compile + check no errors.

  • [ ] Step 4: Verify zone placement via MCP

RunCommand: find the scene MapGenerator (or new GameObject), set tileSet, call Generate(Map_large). result.Log Map.Zones.Count (expect 3), each zone's box (assert Tx0>0 && Tx1<width-1 in bounds), and min pairwise center distance (assert > a few tiles → max-spread working). Assert each zone's Tier/Theme non-null.

  • [ ] Step 5: Commit
git add unity/raiders/Assets/Scripts/World/MapGenerator.cs
git commit -m "Map gen: MapGenerator.Generate(MapDefinition) — tiles + max-spread zone placement (web genMap)"

Task 5: Theme carving (Graveyard + Swamp)

Files: - Modify: Assets/Scripts/World/MapGenerator.cs

Interfaces: - Consumes: Zone.Theme (ThemeDef), the tile set helpers (Set/Get/InBounds). - Produces: void Carve(Zone z, ThemeDef theme) called during zone placement (Task 4 Step 2 hook).

  • [ ] Step 1: Implement Carve for Graveyard + Swamp

Port game.js:417-419 (Graveyard) + 427-429 (Swamp):

// Graveyard (web 417): scatter `gravestoneCount` wall dots (40% get a 2nd wall below), then ~14 bush dots.
// Swamp (web 427): fill the zone rect with bush at `bushChance`, then re-clear a 5x5 core.
// Church/Ruins/Village: switch-default no-op for now (assets authored in the fast-follow).
Use a System.Random seeded from the map seed for determinism. Call Carve after ClearArea, before the core-pocket re-open.

  • [ ] Step 2: Compile + check no errors.

  • [ ] Step 3: Verify carve output via MCP

RunCommand: Generate a map whose single zone uses Graveyard; count WALL tiles inside the zone box (assert > 0, roughly gravestoneCount-ish). Then a Swamp zone; count BUSH tiles inside (assert > 0) and assert the 5×5 core is FLOOR.

  • [ ] Step 4: Commit
git add unity/raiders/Assets/Scripts/World/MapGenerator.cs
git commit -m "Map gen: Graveyard + Swamp theme carves (web ZONE_THEMES)"

Task 6: Leashing on EnemyController

Files: - Modify: Assets/Scripts/World/EnemyController.cs

Interfaces: - Produces: public void SetLeash(Rect worldBox, Vector3 home) (null/empty box = free roam); patrol clamps wander targets to the box; lost-trail returns to home/inside the box. - Consumes: nothing new (uses existing patrol/wander + _agent).

  • [ ] Step 1: Add leash state + setter

Add Rect _leash; bool _hasLeash; Vector3 _home; and public void SetLeash(Rect box, Vector3 home){ _leash=box; _home=home; _hasLeash = box.width>0; }. World-space Rect on XZ (x→x, y→z).

  • [ ] Step 2: Clamp patrol wander + return-home

In the patrol/wander pick (where it chooses a wander destination) clamp the target inside _leash when _hasLeash (web updateMob leash). In the lost-trail/give-up branch (after a search times out), if _hasLeash and the agent is outside the box with no target, set destination toward _home (web homeX/homeY). Chasing is unchanged (may leave the box).

  • [ ] Step 3: Compile + check no errors.

  • [ ] Step 4: Verify in play

RunCommand (enter play): spawn one enemy, SetLeash(new Rect(...), home) a small box away from the player, wait (next command) and assert its position stays within the box (+ a small leashPad) over time. (Use the spawn-safe trick — keep the player from dying isn't needed; just read the leashed mob's position across two commands.)

  • [ ] Step 5: Commit
git add unity/raiders/Assets/Scripts/World/EnemyController.cs
git commit -m "Map gen: mob leashing (patrol-inside / chase-out / return-home, web en.leash)"

Task 7: MapPopulator — spawn everything from map data

Files: - Create: Assets/Scripts/World/MapPopulator.cs

Interfaces: - Produces: MapPopulator.Populate(MapData map, MapDefinition def, Transform player). Spawns zone guards (leashed) + case chests (rarity = zone tier), gray roamers (loose leash), rivals, portals, player spawn. - Consumes: MapData.Zones (Task 4), EnemyController.SetLeash (Task 6), the Enemy/Raider/Chest/Portal prefabs + EnemyDatabase (Zenject), GameConfig (tile, spawnSafe, leashPad, grayLeash).

  • [ ] Step 1: Create MapPopulator

A Zenject-injected MonoBehaviour (or plain class taking DiContainer). Port the placement from game.js:530-544 (zone guards + case chest), 535-536 (rare cluster optional), gray roamers (game.js roamer loop), plus the current RaiderSpawner rival math and RaidBootstrap portal/player placement. For each zone: instantiate guardCount area-scaled guards (web areaScale game.js:540) inside the footprint via _container.InstantiatePrefab (mirror current EnemySpawner), assign def + gaze, and SetLeash(zoneBoxWorld + leashPad, zoneCenter). Place a Chest prefab at the zone center with rarity = zone.Tier.rarity. Gray roamers: area/roamDiv mobs from def.roamerKeys, each SetLeash a grayLeash-sized box around its spawn. Rivals + portals + player spawn: lift from the current RaiderSpawner/RaidBootstrap (spawn-safe).

  • [ ] Step 2: Compile + check no errors.

  • [ ] Step 3: Verify counts + leashes via MCP (play)

RunCommand (play): after Populate, result.Log total Chest count == zones with a case (1 per zone), each chest's rarity matches its zone tier; total enemies ≈ Σ guard counts + roamers; sample a zone guard and assert its leash keeps it near its zone (read position across two commands). Assert portals == def.portalCount, player placed on floor.

  • [ ] Step 4: Commit
git add unity/raiders/Assets/Scripts/World/MapPopulator.cs
git commit -m "Map gen: MapPopulator — leashed zone guards + case chests, roamers, rivals, portals (web genMap populate)"

Task 8: Wire RaidBootstrap, remove old spawners, migrate config

Files: - Modify: Assets/Scripts/Tester/RaidBootstrap.cs - Delete: Assets/Scripts/World/EnemySpawner.cs, Assets/Scripts/World/RaiderSpawner.cs (+ .meta) - Modify: Assets/Scripts/Config/GameConfig.cs (remove moved fields) - Modify (via MCP scene edit): Assets/Scenes/Raid_Slice.unity

Interfaces: - Consumes: MapGenerator.Generate(MapDefinition), MapPopulator.Populate, the stage→MapDefinition map list.

  • [ ] Step 1: Slim RaidBootstrap

Replace its inline-param generation with: a serialized MapDefinition[] stageMaps; pick stageMaps[clamp(TesterRunState.Stage, 0, len-1)]; map.Generate(mapDef); populator.Populate(map.Map, mapDef, player). Drop the player/chest/portal/spawner wiring that MapPopulator now owns. Keep the round-timer-death + restart flow (unchanged, lives in RaidController/TesterHud).

  • [ ] Step 2: Remove the old spawners + migrate GameConfig

Delete EnemySpawner.cs/RaiderSpawner.cs (+ metas). In GameConfig.cs remove the now-per-map fields: prog.stageSizesB1/B2/B3, map.coverWallClusters/coverBushLines/coverBushClusters, map.roamDiv, prog.botDiv, map.bots. Keep map.tile/roundTime/spawnSafe, prog.leashPad/grayLeash/zoneSeparation, zones.fillAlpha, and the biome-ladder fields. Update GameConfigEditor section arrays if any removed field was referenced.

  • [ ] Step 3: Compile + check no errors (watch for references to the deleted fields/spawners — fix each).

  • [ ] Step 4: Scene edit via MCP

RunCommand (edit mode, not play): on Raid_Slice, remove the EnemySpawner/RaiderSpawner GameObjects; add a MapPopulator (and assign prefabs/refs); assign RaidBootstrap.stageMaps = [Map_small, Map_large]; set MapGenerator.generateOnStart=false; EditorSceneManager.MarkSceneDirty + SaveScene. (Mirror the wiring approach from the original tester-build scene setup.)

  • [ ] Step 5: Verify a full raid in play

RunCommand (enter play): assert map.Map.Zones.Count matches the stage's def, chests/enemies/rivals/portals spawned, player on floor spawn-safe, no console errors. Visually confirm (Camera capture or scene-view) zones render tinted/themed.

  • [ ] Step 6: Commit
git add -A unity/raiders/Assets
git commit -m "Map gen: wire RaidBootstrap to MapDefinition pipeline; remove tester spawners; migrate config to MapDefinition"

Task 9: Faithfulness audit vs genMap + fixes

Files: - Modify: whichever files have deltas (likely MapGenerator.cs, MapPopulator.cs).

  • [ ] Step 1: Diff against the web (migration workflow step 3). Compare, line-by-line, citing game.js:
  • PlaceCenter (240 candidates, max-min) + footprint clamps (hw=clamp(W*0.10,4,6), hh=clamp(H*0.085,3,5)) — game.js:507-522.
  • Cover scatter densities + shapes — game.js:494-496.
  • areaScale guard scaling clamp(((hw*2+1)*(hh*2+1))/(13*11),0.4,1) — game.js:540.
  • ZONE_TIERS rosters/counts; tier→rarity→tint — game.js:406-413.
  • Graveyard/Swamp carves (counts/chances) — game.js:417-419, 427-429.
  • Leashing patrol-inside/chase-out/return-home — game.js updateMob leash branch + 528-530.
  • Gray-roamer loose leash (prog.grayLeash) + case-chest tier.

  • [ ] Step 2: Fix any deltas inline. For deltas blocked on deferred systems (progression danger scaling, the boss/vault), note them in the spec's "deferred" list rather than fixing.

  • [ ] Step 3: Verify — re-run the Task 8 play check; confirm a couple of regenerations produce web-plausible layouts (zone count/spread/theme structure). Optional: a Camera/scene-view capture for eyeballing.

  • [ ] Step 4: Commit

git add -A unity/raiders/Assets
git commit -m "Map gen: faithfulness audit vs web genMap — fix placement/scaling/carve deltas"

Self-Review

Spec coverage: ZoneTierDef (T1) · ThemeDef + 2 carves (T2,T5) · MapDefinition + config migration (T3,T8) · generate→populate option A (T4,T7) · max-spread placement + footprint scaling (T4) · leashing (T6) · case chests + tier rarity + guards + roamers + rivals + portals + player (T7) · spawner removal (T8) · faithfulness audit (T9) · verification via MCP throughout. Deferred items (vault, biome ladder, 3 themes, MapLayout) are explicitly out of scope in the spec and not tasked. ✓

Placeholder scan: Carve/placement internals cite exact game.js lines to port verbatim (the web is the canonical source — transcribing it fully into the plan risks divergence); SO schemas + interfaces + verification commands are concrete. No TBD/TODO. ✓

Type consistency: ZoneTierDef.rarity/mobKeys/guardCount/allowMiniBoss, ThemeDef.kind/CarveKind, MapDefinition.ZoneEntry{tier,miniBoss}, MapData.Zones/Zone, MapGenerator.Generate(MapDefinition)/Carve, EnemyController.SetLeash(Rect,Vector3), MapPopulator.Populate(MapData,MapDefinition,Transform) — used consistently across tasks. ✓