Map-Gen + Vault Faithfulness Audit — web vs Unity¶
Date: 2026-06-26 · Covers the zone-based map slice + vault slice (Unity port).
Method: side-by-side of the Unity implementation (SOs + code) against game.js/config.js, verified
by Unity-MCP generation/play checks throughout the build.
Legend: ✅ exact match · ≈ faithful (tuned equivalent) · ⏸ deferred (separate slice) · ⚠ minor noted delta.
1. Config values (config.js → Unity)¶
| Value | Web | Unity | |
|---|---|---|---|
| px→world divisor | map.tile 40 |
GameConfig.map.tile 40 |
✅ |
| cover wall clusters | 0.0138 |
MapDefinition.wallClusters 0.0138 |
✅ |
| cover bush lines | 0.0055 |
MapDefinition.bushLines 0.0055 |
✅ |
| cover bush clusters | 0.0075 |
MapDefinition.bushClusters 0.0075 |
✅ |
| roamer density | prog.roamDiv 450 |
MapDefinition.roamDiv 450 |
✅ |
| rival density / cap | prog.botDiv 1500 / map.bots 5 |
rivalDiv 1500 / rivalCap 5 |
✅ |
| leash pad / gray leash | 2.5 / 6 |
prog.leashPad 2.5 / grayLeash 6 |
✅ |
| spawn-safe | map.spawnSafe 760 |
map.spawnSafe 760 |
✅ |
| gate time / reach | 5.0 / 56 |
map.gateTime 5.0 / gateReach 56 |
✅ |
| vault from stage | prog.vaultFromStage 2 |
MapDefinition.hasVault (per-map; biome ladder deferred) |
≈ |
| zone tint alpha | zones.fillAlpha 0.20 |
zones.fillAlpha 0.20 (tint render deferred — no minimap) |
✅ |
| per-map size | prog.stageSizes[][] |
MapDefinition.width/height (moved per-map) |
≈ |
Verdict: every numeric map-gen tunable matches. The two ≈ are intentional architecture moves (per-map size on the def; vault gated by a per-map flag until the biome ladder lands).
2. Zones (ZONE_TIERS + placement)¶
| Aspect | Web | Unity | |
|---|---|---|---|
| rare roster / count | goblin·hound·spider·emberball [3,4] |
Zone_rare same |
✅ |
| epic roster / count | warden·skeleton·emberball·rocketeer [3,4] |
Zone_epic same |
✅ |
| legendary roster / count | rocketeer·bombardier·sentinel·warden [2,3] |
Zone_legendary same |
✅ |
| tier → rarity → tint → case chest | one language | ZoneTierDef.rarity drives chest + (later) tint |
✅ |
| placement | placeCenter 240 candidates max-min |
PlaceCenter 240 max-min |
✅ |
| footprint | hw=clamp(W·.10,4,6) hh=clamp(H·.085,3,5) |
identical | ✅ |
| guard area-scale | clamp((hw·2+1)(hh·2+1)/143,.4,1) |
identical | ✅ |
| mini-boss "Elder" | hp×1.6 atk×1.25 | bonusHp 1.6 / bonusAtk 1.25 |
✅ |
| zone leash | box ± leashPad; home = guard spawn |
identical | ✅ |
3. Themes (ZONE_THEMES carves)¶
| Theme | Web carve | Unity (generic ThemeDef params) | |
|---|---|---|---|
| Graveyard | 18 gravestone walls (2-tall @0.4) + 14 bush | featureCount 18, fillChance 0.4; bush dots reuse featureCount (18) |
⚠ bush 18 vs 14 |
| Witch's Swamp | bush fill 0.5 + 5×5 cleared core | fillChance 0.5 + ±2 core |
✅ |
| Abandoned Church | wallRing + 3 doors + 5 interior pillars | doorCount 3, featureCount 5 |
✅ |
| Old Ruins | 10 rubble segments, fill 0.75 | featureCount 10, fillChance 0.75 |
✅ |
| Burned Village | hut-stub grid step 3, 0.7 | grid step 3, fillChance 0.7 |
✅ |
ThemeDef was generalized (per-kind-named fields → featureCount/fillChance/doorCount), each
kind reads the ones it uses — same results, cleaner authoring.
4. Generator results (verified output)¶
- 70×70 map → 3 zones, well-spread (max-min), in-bounds, 13×11 footprints, distinct themes. ✅
- Each carve produces its signature structure (Graveyard 22w/12b · Church 46w · Village 18w · Ruins 12w · Swamp 57b). ✅
- Populate: case chest per zone (rarity = tier), area-scaled leashed guards, gray roamers (area/roamDiv), rivals (clamp(area/rivalDiv,1,cap)), portals, spawn-safe player. ✅
5. Vault (genMap:553-585 + updateInteractions:1845-1869)¶
| Aspect | Web | Unity | |
|---|---|---|---|
| room | 7×7 walled (vhw=vhh=3), one gate on a random side | identical (PlaceVault) |
✅ |
| no-spawn bubble | vaultClear over the room | all spawns skip RoomTileBox |
✅ |
| prize chest | centre, vault rarity, guaranteed gear, on top of cap | isVault+guaranteeGear → RollChestLoot(guaranteeGear) (40/40 gear) |
✅ |
| Keykeeper | warden, FLAT +120hp/+8atk AFTER mobMult, dropsKey, leashed outside the door | bonusHpFlat 120/bonusAtkFlat 8 added after mobHpMult/mobDmgMult — verified 540hp in-game (210×2+120), exactly web |
✅ |
| seal | closed gate = wall in findPath + losTiles | one check in MapData.IsWall → seals A*, LOS, projectiles, vision |
✅ |
| force-open | idle + key, dt/gateTime, chestCrack pulse, decay ×0.6, gateOpen boom |
identical (Gate) |
✅ |
| key | drop on keeper death, pickup on contact → hasKey |
identical (KeyPickup) |
✅ |
5b. Spawning + map population (web genMap:588-708) — fixed this pass¶
The MapGenerator/MapPopulator split had dropped web's accumulating-avoid ordering. Fixed by moving
spawn + portal placement into the generator (before the mesh bakes, so pad/pocket clears render) and
restoring the avoid geometry. All verified in-game on Map_vault (70×70).
| Aspect | Web | Unity (fixed) | |
|---|---|---|---|
| Keykeeper name | km.name = "Keykeeper" (582) |
EnemyController.nameOverride → label shows "Keykeeper", not "Forest Warden" |
✅ |
| Elder name | "Elder " + name (547) |
nameOverride = "Elder " + displayName |
✅ |
| player spawn | FLOOR tile, avoid placed ×0.7 sepPx, reachable, 5×5 pocket (588-601) | ComputePlayerSpawn (BFS reachability, pocket clear) — verified 23 tiles to nearest enemy at spawn |
✅ |
| spawn-safe cap | min(spawnSafe, MAP_W·TILE·0.30) (607) |
was 0.35 → 0.30 | ✅ |
| zone guard spread | within ±hw·0.7 of centre (542-544) | was full footprint → ±hw·0.7 (keeps guards off the spawn) | ✅ |
| guards/roamers/rivals | spawn on FLOOR (freeTile) | RandomFloor now floor-only (was any non-wall, allowed bush) |
✅ |
| portal spread | max-min vs spawn(exGap) + POIs(220px) + portals(exSpread) (657-671) | PlacePortals same — verified 3 portals ≥25 tiles apart |
✅ |
| portal px→world | config px ÷ TILE | ÷ map.tile (px constants 220/52 + spawnGap/spread were wrongly used as world units → 0 portals) | ✅ |
| portal pad clear | bushes off pad, r = z.r+TILE (675-682) | bushes and walls (vault-room-guarded) — verified 0 non-floor in 5×5 pad | ✅ |
| gray field chests | area/fieldChestDiv, common→rare (614-618) | MapPopulator step 6 — verified 7 on a 70×70 |
✅ |
| scattered ground loot | area/groundLootDiv, common→rare (702-705) | step 7 via LootService.Spill — verified 9 |
✅ |
| zone case gearBias | caseChest.gearBias = caseGearChance (534) |
Chest.gearBias = 0.70 |
✅ |
| projDmg buff | neither Elder nor keeper buffs projDmg | projDmg takes mobDmgMult alone (bonusAtk removed) | ✅ |
Config wiring confirmed (tooltips updated DEFERRED→USED): prog.zoneSeparation, extraction.spawnGap,
extraction.spread, loot.caseGearChance, loot.fieldChestDiv, loot.groundLootDiv.
5c. keyRacer rival (web updateBot phases, game.js:2861-2940 + onKill:2377) — DONE¶
A rival contests the vault. Verified in-game (claim→race→open + both key-drop paths).
| Aspect | Web | Unity | |
|---|---|---|---|
| flag | rival #0 keyRacer when hasVault (639) |
MapPopulator sets it on rival 0 when m.Vault != null |
✅ |
| warm-up | loots until keyHuntDelay, then goal "key" (2864) |
KeyMission same; extract suppressed during warm-up/mission |
✅ |
| phase 1 hunt | chase+attack keeper, aggressor canFire (2868) | same; injured (hpR ≤ gateMinHp) bails to flee |
✅ |
| earn the key | killer takes it, else drops (onKill 2380) | keeper ClaimVaultKey → OnDied gives to live claimant else drops a pickup — verified no-drop on claim |
✅ |
| phase 2 race | chase gate APPROACH (sealed tile unreachable), channel dt/(gateTime·gateTimeMult), close-range pressure denies, mob/low-hp bails (2887) |
same (GateApproach, danger check, decay ×0.6) |
✅ |
| open | gate.open, boom gateOpen, goal loot, wantVault, key consumed (2910) |
OpenVaultByRival same; Gate.cs hides the door on any open — verified Vault.Open=True |
✅ |
| abandon/death | gateGiveUp or death/extract drops key (2932/2384/3121) |
DropKey on giveUp/OnDied/Escape — verified +1 pickup on keybearer death |
✅ |
| nametag | name + " 🔑" while carried (4132) | UpdateNametag appends 🔑 |
✅ |
Divergences: the "ambush the looters" banner is skipped (no banner system — the loud gateOpen ping
still draws enemies). Kill-attribution uses a keyClaimant-stamp (the rival hammering the keeper when it
falls claims the key) since Unity's Health carries no damage source — faithful to the web outcome.
5d. Stage boss + Descent Portal (web genMap:643-700 / onKill:2339 / progLocked:3196) — DONE¶
A boss-gated progression exit. Verified in-game (seal → kill guard → descend; safe exit banks).
| Aspect | Web | Unity | |
|---|---|---|---|
| one progress portal | portalDefs[0] kind "progress" + safe exits (649) |
generator marks portal 0 "progress" when MapDefinition.descentPortal; rest "safe" |
✅ |
| guard | Gatekeeper (floors 1-4) / Boss (last), on the pad, tight 5-tile leash (688-697) | populator posts descentGuardKey on the pad, SetLeash 5·tile |
✅ |
| no HP-sponge | guardScale.noHpSponge (691) |
EnemyController.noHpSponge skips mobHpMult — verified gatekeeper 520, not 1040 |
✅ |
| unseal | stageBoss death → G.bossDown (2339) |
stageBoss → OnDied sets RaidState.BossDown |
✅ |
| seal | progLocked = progress && !bossDown (3196) |
Portal.cs refuses to board while IsProgress && !BossDown — verified sat-on-pad no-board |
✅ |
| extract kind | progress = descend, safe = bank (3280) | EndRaid(descended) → TesterHud advances only on descend (verified Stage 0→1), safe replays |
✅ |
Divergences: the "Gatekeeper down!" banner and "defeat the guardian first" denial floater are skipped
(no banner/floater system — the gating still works silently). The boss's bonus gold/blueprint haul
(web 2329-2335) is skipped (meta-currency, biome-ladder territory); the guard still drops tier-4 loot.
Floor-danger scaling of the guard (mobScale) is deferred with the biome ladder — it uses base def
stats. Guard name uses the def's displayName ("Thornwood Gatekeeper"/"Thornback Brute"), not web's "Gatekeeper".
5e. Feedback floaters + banners (web floater() / showBanner()) — DONE¶
Presentation/Floater (world-space rising/fading pop-text) + Banner (lazy screen-space announcement) +
the static Floaters entry point. Wired the recent features' missing feedback — key acquired/dropped/
rival-grabbed, "rival cracked the Vault — ambush the looters" (floater + banner), Gatekeeper/Boss down
(floater + banner), "need a key" (gate), "defeat the guardian first" (sealed portal). Verified in-game.
6. Deferred (not deltas — scoped to later slices)¶
- Biome ladder driving
vaultFromStage+stageSizes+ per-floor danger scaling (incl. the guard'smobScale) + lootlootMul— slice 4 (reserved by design decision). - Zone map-tint render — blocked on a minimap / tactical-map system (doesn't exist yet).
- Graveyard bush count (18 vs 14) is a ⚠ negligible cosmetic delta (would need a per-theme second-count field).
Conclusion: the themes, zones, config values, and generator/vault results are faithful to the web prototype, with the only non-deferred delta a negligible graveyard bush count. Everything deferred is a clearly-bounded separate feature.