Skip to content

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). stageSizes is 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]; then biome × bonusZonesPerBiome extra zones alternating epic/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 ×= bDanger on top of the player-power scaling.
  • Loot: lootMul = 1 + biome*lootPerBiome scales 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, heightprog.stageSizes[biome][floor]. - hasVaultfloor ≥ 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; Progression helpers (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. Remove def.width/height/hasVault/zones[] reads.
  • C3 bDanger + lootMul into RaidState → enemy scaling + loot density.
  • C4 Zone rarity tint (zones.fillAlpha).
  • C5 Hub biome/floor select + save-backed advance/unlock/reset; RaidBootstrap per-biome template.
  • C6 De-dup MapDefinition (strip obsolete fields) + author the 3 biome recipe assets; delete the obsolete Map_small/large/vault if 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 bc02d29a64600c) — 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 DEFERREDPARKED (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.