Phase C — Biome Ladder (web-faithful) Design + De-dup Audit¶
Date: 2026-06-29 Migration item: #4 Biome ladder Mode: autonomous (user away) — decisions documented here for later review.
Goal¶
Make raid progression mirror the web exactly: 3 biomes × 5 floors, with map size, zone count/tiers,
vault gating, and danger/loot scaling all driven by GameConfig.prog (biome, floor) — not hard-coded
into per-map assets. Eliminate the config↔asset duplication the Unity port introduced.
The web model (source of truth — game.js)¶
- Size:
setMapSize(prog.stageSizes[biome][stage-1])(square).stageSizesis 2-D[biome][floor]:[[36,40,44,48,52],[55,62,70,76,82],[82,89,95,102,108]]. - Zones (
stageZoneSpecs(stage, lastStage, biome)): boss floor →[rare, epic, epic]; floor 1 →[rare+mini]; middle floors →[rare, epic+mini]; thenbiome × bonusZonesPerBiomeextra zones alternatingepic/rare. Tier →ZONE_TIERS[tier](mob roster + guard count); tier IS the rarity. - Vault: built when
stage ≥ vaultFromStage(2). - Danger:
bDanger = 1 + biome*dangerPerBiome + (stage-1)*stageDanger;mobScale.hp/dmg ×= bDangeron top of the player-power scaling. - Loot:
lootMul = 1 + biome*lootPerBiomescales gray loot/chest density. - Progression (endRun): descend (boss-gated portal) →
biomes[b].stage++; boss floor cleared →biomes[b].cleared, next biome unlocks; safe extract holds; death keeps all progression.
Architecture decision¶
GameConfig.prog is the ladder source of truth. Map generation takes (MapDefinition recipe, biome,
floor) and derives size / zone-specs / vault / scaling from prog.*. MapDefinition becomes a
per-biome RECIPE TEMPLATE (theme pool, roamer keys, density, portal/guard keys, keykeeper params, the
tier→ZoneTierDef roster) — one asset per biome. Zone identity (which rarities, how many, mini-boss)
comes from stageZoneSpecs; the recipe only supplies the art/rosters those tiers resolve to.
Rationale: this mirrors the web's config-driven progression (the user's explicit ask) while keeping the Unity strength of per-biome art/flavor templates. One source of truth per concern; no dead duplicates.
De-dup audit — exact field moves¶
MapDefinition — REMOVE (now config-driven by biome/floor):
- width, height → prog.stageSizes[biome][floor].
- hasVault → floor ≥ prog.vaultFromStage.
- zones[] (explicit ZoneEntry list) → computed by stageZoneSpecs; replaced by a tierRoster
(rare/epic/legendary ZoneTierDef refs) the specs resolve against.
- vaultTier → the vault prize tier follows the web rule (best open zone tier of the floor, capped at
legendary); kept as an optional override only if a recipe sets it.
MapDefinition — KEEP (genuine per-biome recipe): wallClusters/bushLines/bushClusters (cover
style), themePool, roamerKeys, roamDiv, rivalDiv, rivalCap, portalCount, descentPortal,
descentGuardKey, keykeeperKey/HpAdd/AtkAdd, seed. (Web treats cover/density as global; keeping them
per-biome is a deliberate, documented flavor deviation — same numbers across biomes unless re-tuned.)
GameConfig.prog — un-defer + use (web values verbatim): biomeCount(3), stagesPerBiome(5),
stageSizes (NEW, 2-D via StageSizeRow[]), bonusZonesPerBiome(1), vaultFromStage(2),
dangerPerBiome(0.40), lootPerBiome(0.30), stageDanger(0.18). Already-used: zoneSeparation,
leashPad, grayLeash (unchanged).
GameConfig.zones.fillAlpha — un-defer + use (rarity zone tint).
No dead config remains: every prog.*/zones.* field is read after this phase; every removed
MapDefinition field has a config home. botDiv/cover/bots/roamDiv stay on the recipe (not
re-duplicated into config), so they are NOT obsolete config entries — they live in exactly one place.
Enemy scaling gap (to fix)¶
EnemyController.Init applies player-power scaling (scaleHp/scaleDmg) but not bDanger. Phase C
threads the current (biome, floor) into a RaidState.Danger/LootMul (set at generation) and multiplies
scaleHp/scaleDmg by Danger, and the loot density by LootMul.
Slices¶
- C1
prog.stageSizes+ un-defer prog/zones;Progressionhelpers (SizeFor,ZoneSpecs,Danger,LootMul,VaultAt). Headless-verified against web numbers. - C2
MapGenerator.Generate(def, biome, floor)— size + zone-specs + vault from config; resolve tiers via the recipe roster. Removedef.width/height/hasVault/zones[]reads. - C3
bDanger+lootMulintoRaidState→ enemy scaling + loot density. - C4 Zone rarity tint (
zones.fillAlpha). - C5 Hub biome/floor select + save-backed advance/unlock/reset;
RaidBootstrapper-biome template. - C6 De-dup
MapDefinition(strip obsolete fields) + author the 3 biome recipe assets; delete the obsoleteMap_small/large/vaultif superseded.
Verification¶
Each slice headless (Unity_RunCommand) + play. Key assertions: SizeFor(1,2)=62, Danger(2,3)=1+0.8+0.36,
zone counts per floor (f1=1, mid=2, boss=3, +biome*bonus), vault present iff floor≥2, tint = RAR_COLOR×alpha.
Part 2 (after C): balance + deferred sweep¶
After the ladder lands, a full pass over remaining DEFERRED/parked/skipped items: re-audit every
DEFERRED tag in GameConfig and the migration doc's skipped list, transfer the balance-related values,
and wire or correctly-park each, re-checking against the web. Tracked separately once Phase C is verified.
STATUS — COMPLETE (2026-06-29)¶
Phase C (commits bc02d29→a64600c) — all 6 slices landed + play-verified web-exact:
- C1 config ladder (sizes 36/62/108, danger 2.16, lootMul 1.6, zone specs 1[R]/2[R E]/3[R E E]+bonus).
- C2 size/zones/vault from config (floor1 36² 1 zone, floor2 40² 2 zones+vault).
- C3 bDanger + lootMul (biome1 floor3 Danger 1.76 → spider 180→317; LootMul 1.30).
- C4 zone rarity tint — already config-wired on the minimap (MapTextureBaker × zones.fillAlpha); the
in-world 3D floor tint stays a render-layer item (like the fog look), noted for the visual pass.
- C5 ladder advance + hub floor/biome select (floor1→unlock floor2; boss floor→biome2 unlocked+cleared).
Defensive Pathfinder start-clamp (latent OOB crash the bigger maps surfaced).
- C6 stripped obsolete MapDefinition fields (width/height/hasVault/zones[]); per-biome recipe; boss-floor
guard (floor5 = 52² 3 zones[R E E] vault DescentGuard_boss).
De-dup result: one home per value, no dead duplicates. prog.*/zones.fillAlpha un-deferred + used;
MapDefinition is a per-biome recipe (cover/themes/rosters/density/keys). Cover/roamDiv/rivalDiv stay on
the recipe (documented flavor deviation, single home — not re-duplicated into config).
Part 2 — balance + deferred sweep (commits d9d2dae, a111c41):
- Balance values verified web-exact: mobHpMult 2 / mobDmgMult 0.5, critBase 1.5, meleeReach 25,
autoBackstabArc 0.72, backstabTellRange 72, powerRef 1500, hpPerPow 0.7, dmgPerPow 0.5, player base
30/190/50/60, powAtk/Hp 6/0.9; enemy defs (goblin/spider/warden/skeleton) all match MOB_DEF.
- Bugs found + fixed: floorGear (ground loot rolled gear regardless → now noGear:!floorGear);
MapPopulator.PlayerPower (gearless 671 fed rival scaling → now gear-driven, matches PlayerController);
deathDelay wired (death-beat before the result card).
- Tags reconciled: stale DEFERRED cleared (prog/zones/floorGear/deathDelay); in-run-inventory items
relabelled DEFERRED→PARKED (bagCap, lootRootNoise, lootInterruptOnHit, corpseMinItems).
- Remaining DEFERRED/PARKED are genuinely un-ported features, correctly tagged: silenceWindow,
bushSightCutoff (stealth nuances), bandHysteresis/stanceCommit (stance-arbiter hysteresis),
shield/cells system, weak-point armor, the parked in-run inventory. No stale tags remain.