Skip to content

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's mobScale) + loot lootMul — 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.