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+guaranteeGearRollChestLoot(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 ClaimVaultKeyOnDied 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) stageBossOnDied 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.