Web → Unity Migration — Balance 1.0 · Container Rarity · 10 Biomes · Floor 0 FTUE (2026-08-12)¶
Status: IMPLEMENTED (all six sections, 2026-08-12, compile-clean) — pending play-testing per §9's
audit step. Decisions taken during the port: 10-biome campaign = 7 new BiomeDef assets cloning
Biome_3 (named from the first seven endlessNames, preserving the virtual-rung name sequence; the
endless pool keeps the remaining five); Floor 0 selection = floorSel = -1 sentinel +
SaveService.SelectedRaidFloor; LootMul = computed but dormant (faithful — the web ROOMS generator
never consumed it). Covers the four web commits merged from main into bza-unity-pivot-parity
(merge 06409953):
| Commit | Title | Theme |
|---|---|---|
e953ae59 |
new balance 1.0 | onboarding difficulty, mob retune, loot economy rework, nest rewards |
efd022e5 |
rarity fix | per-biome container rarity tables (chests + nests share one roll) |
2462d639 |
10 bioms | 10-biome campaign, monotonic global-floor curves, save ladder migration |
f07cc04b |
tutorial | fixed onboarding Floor 0 raid + event-driven coach |
Source of truth: config.js + game.js at merge HEAD (06409953). Unity anchors cite the
current bza-unity-pivot-parity state. Follow the standing 3-step port workflow (read the cited web
function first · everything through GameConfig/SO assets — write the .asset, not just the C#
default · line-by-line audit after).
Architecture deltas that shape this port (verified against the Unity tree):
- Unity has no prog.biomeCount/stageSizes/danger* — danger is per-BiomeDef bands
(dangerBase/dangerStep/lootMul, BiomeDef.cs:107–110) resolved by Domain/Progression.Danger()
and stamped once per raid (RaidBootstrap.cs:139–140 → _raid.Danger/_raid.LootMul), with an
endless virtual-biome ladder (extraRungs, BiomeSet.Biome() clamps to last authored).
- Unity's only procedural generator is the room warren (WarrenGen); map size is rolled from the
rooms shape (24..64 W, derived H) — the web's stageSizes table has no Unity consumer.
- The warren populator does not use spawn.threatBudget* (rooms use R.roomGuardBudget
+ per-room bud); MobRoster.RollRoster exists but is uncalled.
- The mob intro schedule is per-EnemyDef: rankAndFile + introStep (EnemyDef.cs:31–34),
stepped in MapPopulator.cs:62.
- Warren raid floors spawn no gatekeeper/boss (the defense finale is the floor gate,
MapPopulator.cs:354–356); gatekeeperRamp is only touched by the parked FTUE path at step 0.
- The lobby is a single-biome pager (arrows + floor pips), not a tile grid
(LobbyController.cs:126–138, LobbyView.RenderStages 99–123); Unity floorSel is 0-based
(SaveData.floorSel), unlike the web's 1-based + literal 0 = Floor 0.
1. Global progression curves (replaces per-biome danger bands)¶
1.1 Web rule¶
The old bDanger = 1 + biome*dangerPerBiome + (stage-1)*stageDanger is gone (both knobs
deleted). One monotonic global-floor curve drives rank-and-file HP/damage — no difficulty dip at
biome boundaries. step = biome*stagesPerBiome + (stage-1).
// game.js progressDanger(biome, stage)
bDanger = prog.dangerBase // 0.50 — B1F1 = HALF authored
+ min(step, prog.dangerSoftFromStep) * prog.dangerPerStep // 0.11/step through step 15 (B4F1)
+ max(0, step - prog.dangerSoftFromStep) * prog.dangerLatePerStep; // 0.02/step after
// anchors: B1F1 ×0.50 · B1F5 ×0.94 · B3F5 ×2.04 · B10F5 ×2.83 (before player-Power scaling)
Companion curves, same soft-knee shape:
// progressLootMul(biome) — gray loot/chest QUANTITY
lootMul = 1 + min(b, prog.lootSoftFromBiome /*2*/) * prog.lootPerBiome /*0.30*/
+ max(0, b - 2) * prog.lootLatePerBiome /*0.08*/; // B10 ×2.16
// progressThreatBudget(step, base) = base + growth(step) — mob-count budget
growth = min(step, spawn.threatSoftFromStep /*15*/) * spawn.threatBudgetPerStep /*0.30*/
+ max(0, step - 15) * spawn.threatLatePerStep /*0.08*/;
// web consumers: zone garrisons, tunnel arenas, corridor room guards
// (progressThreatBudget(step, C.roomGuardBudget)), roamer budget (growth × roamBudgetFrac)
// gatekeeperRampValue(gr, "hp"|"dmg", step)
val = base + min(step, gr.softFromStep /*15*/) * perStep
+ max(0, step - 15) * latePerStep; // hpLate 0.015 · dmgLate 0.008
1.2 Unity current state¶
Domain/Progression.cs:19–25—Danger(BiomeDef, floorIndex, extraRungs) = biome.dangerBase + (floorIndex + extraRungs*floors) * biome.dangerStep; bands authored on Biome_1/2/3 assets (1/0.18/1 · 1.4/0.18/1.3 · 1.8/0.18/1.6)._raid.Danger/_raid.LootMulstamped atRaidBootstrap.cs:139–140; consumed byEnemyController.Start(EnemyController.cs:204–240).- Room-guard sizing:
MapPopulator.cs:265–309—tb = R.roomGuardBudget + bud/3, no global step term at all. GameConfig.spawnhasthreatBudgetBase/PerStepbut nothing consumes them in the warren path.
1.3 Port tasks¶
Progression.Danger→ the web global curve. AdddangerBase 0.50 / dangerPerStep 0.11 / dangerSoftFromStep 15 / dangerLatePerStep 0.02to a GameConfig group (extendprog) and rewriteProgression.Danger(cfg, globalStep)whereglobalStep = virtualBiome * prog.stagesPerBiome + floorIndex(the virtual index keeps the endless ladder monotonic — same spirit as today'sextraRungs). Retire/ignoreBiomeDef.dangerBase/dangerStep(leave fields or delete; if left, comment them dead).- Loot quantity —
Progression.LootMul(cfg, virtualBiome)withlootPerBiome 0.30 / lootSoftFromBiome 2 / lootLatePerBiome 0.08; replace_raid.LootMul = biome.lootMul. (Verify what consumesRaidState.LootMul— the free-scatter rig is gone, so if nothing reads it, wire it into the per-room content budgetbud(MapPopulator.cs:193) so quantity actually grows again.) - Threat growth — add
spawn.threatSoftFromStep 15 / threatLatePerStep 0.08and aProgression.ThreatGrowth(cfg, step)helper; apply the web's corridor rule to the warren's room guards:tb = R.roomGuardBudget + bud/3 + ThreatGrowth(step)(the warren room guard is the corridor room-guard analogue — that's the faithful mapping given no zones/roamers exist). - Gatekeeper late curve — add
softFromStep 15 / hpLatePerStep 0.015 / dmgLatePerStep 0.008toGameConfig.gatekeeperRampfor parity, and fix the existing gap while there: the FTUE boss assignment (MapPopulator.cs:411–418) applies onlyhpBase/dmgBase— implementGatekeeperRampValue(gr, step)sohpPerStep/dmgPerStep(and the late slope) are honored whenever a gatekeeper next spawns at step > 0. Low priority: no warren floor spawns one today. GameConfig.asset: write all new fields.
2. Enemy retune + intro schedule + speed¶
2.1 Web rule¶
| Enemy | Field | Old → New |
|---|---|---|
| spider | hp / atk | 90 → 50 / 24 → 14 |
| emberball | hp / atk / projDmg | 80 → 40 / 14 → 7 / 26 → 6 |
applyBurnburn DoT:burnDmg16 → 1 (burn becomes a tell, not a killer).enemyScale.enemySpeedMul1.0 → 0.5 (non-boss roster mobs + rivals at HALF player pace; defense enemies keep their ownspd; bosses authored).mobIntro(global step):spider 0 · emberball 1 · goblin 5 · hound 10 · warden 15 · skeleton 20 · sentinel 25 · rocketeer 30 · bombardier 35. B1F1 = spider ONLY; emberball is the one within-biome intro (B1F2); after that exactly one new type per biome start. Boss/Gatekeeper/Nest stay outside.
2.2 Unity current state¶
Assets/Config/Enemies/*: spider 90/24 · emberball 80/14/26 (projDamage). Intro steps: spider 0, goblin 0, hound 1, emberball 2, skeleton 3, warden 5, rocketeer 7, bombardier 10; sentinel introStep 8 butrankAndFile = false(unused).enemyScale.enemySpeedMul = 1.0inGameConfig.asset(EnemyController.csspeed-parity line).- Burn: web-only value so far — locate the Unity burn tick (StatusEffects / projectile fire flag)
and confirm where
burnDmglives before changing.
2.3 Port tasks¶
Enemy_spider.asset: hp 50, atk 14.Enemy_emberball.asset: hp 40, atk 7, projDamage 6.introStepupdates: spider 0 · emberball 1 · goblin 5 · hound 10 · warden 15 · skeleton 20 · sentinel 25 · rocketeer 30 · bombardier 35 — and setEnemy_sentinel.rankAndFile = true(the web schedule includes it; keepkeykeeperand defense mobs as-is).GameConfig.assetenemyScale.enemySpeedMul→ 0.5.- Burn DoT → 1 (find the Unity site; if burn isn't ported yet, note it in the burn port).
3. Loot economy rework (guaranteed chest gear · one-tier upgrade · weapon gating · biome rarity tables)¶
3.1 Web rule — chest composition¶
Every real chest guarantees exactly ONE gear piece at its own tier; per-chest gear probability
knobs are deleted (caseGearChance, maxGearPerChest, chest gearBias, vault guaranteeGear, the
legendary-chest "first item legendary" rule).
rollGuaranteedGear(base): rarity = (base < legendary && rand < loot.gearUpgradeChance /*0.20*/)
? base+1 : base; return randomLoot(rarity);
randomLoot(rarity): // slot pick is now WEAPON-GATED (loot.weaponEpicOnly = true):
// weaponMelee/weaponRanged may roll ONLY at epic/legendary; common/rare picks from the six
// NON-weapon slots. Authored mkItem calls (tutorial/starter) unaffected.
rollChestLoot(c):
1. loot[0] = rollGuaranteedGear(c.rarity) // always
2. fill to n = chestItems + legBonus + rand(0,1) + bonus: // filler is STRICTLY non-gear
rollLootItem(rollChestItemRarity(c.rarity), { noGear: true,
noValuable: c.rarity=="common" && !t1Valuables }) // common filler = blueprints
3. coin / consumable bonus rolls — unchanged
4. fixedLoot chests: if !crate && !barrel && !resourceOnly and the authored haul has NO gear,
unshift(rollGuaranteedGear(c.rarity))
rollLootItem is now the non-chest path only (mob/bot drops; gearChance 0.06 ×
firstFloorGearBoost 2.5 on global step 0 — values unchanged).
Nest power-check bypass: damage() skips the powCheck power-ratio damping when
target.nestDef && source === player — the player's real weapon damage applies to nests (§4).
Rival ranking (botLoot): chests ranked by RAR_LADDER.indexOf(rarity) (vault 5; keyRacer −1)
instead of gearBias. (Rivals are parked in Unity — carry as a note.)
3.2 Web rule — per-biome container rarity tables¶
loot.chestRarityBiome1..10 = weights [Common, Rare, Epic, Legendary]:
B1 [.80 .20 0 0] B2 [.40 .60 0 0] B3 [.20 .70 .10 0] B4 [.15 .65 .20 0] B5 [.10 .60 .30 0]
B6 [.08 .52 .40 0] B7 [.06 .44 .50 0] B8 [.04 .36 .60 0] B9 [.03 .27 .70 0] B10 [.02 .18 .80 0]
applyBiomeChestRarities(chests, enemies, biome) runs once post-generation (skipped for scripted
FTUE + Floor 0): every real chest (!crate && !barrel) re-rolls rarity from the biome table
(rollBiomeChestRarity: weighted, total-normalized, clamp-to-last-table, fallback common) and
every Nest re-rolls nestRarity from the same table. Generator depth-tier rarity becomes a
fallback only.
3.3 Unity current state¶
Loot/LootRoller.csis the complete roller port:RandomLoot(uniform slot pick, :52–88),RollLootItem(:122–127),RollChestItemRarity(:130–136),RollChestLoot(:139–160 — the OLD composition:maxGearPerChestquota,guaranteeGearvault flag, legendary first-item rule,gearBiasparam),RollMobDrop(:165–178).World/Chest.cs:22–26,186–188carriesisVault/guaranteeGear/gearBias— all dead: nothing assigns them (grep-verified); warren cache chests roll plaingearChance, the FTUE vault chest isFixedLoot.- Chest rarity today = climb depth (
MapPopulator.cs:194for nest rooms,:265for loot rooms). GameConfig.loothascaseGearChance(dead) +maxGearPerChest; nogearUpgradeChance,weaponEpicOnly, or rarity tables.- No crate
resourceOnlyconcept (crates/kegs areSpawnCrackable,MapPopulator.cs:150–164, with fixed one-blueprint / coin-or-blueprint hauls + timber fill).
3.4 Port tasks¶
GameConfig.Loot: deletecaseGearChance+maxGearPerChest; addgearUpgradeChance = 0.20,weaponEpicOnly = true, and the rarity tables — author as one serializable array of 10 rows (e.g.ChestRarityRow { float common, rare, epic, legendary; }[]) with clamp-to-last semantics, not ten named fields. Asset!LootRoller: addRollGuaranteedGear(baseRarity); gate the slot pick inRandomLoot(weaponEpicOnly && rarity < Epic→ pick from the six non-weapon slots — add aGearSlotEx.NonWeaponOrder); rewriteRollChestLootto the §3.1 composition (guaranteed piece first, fillernoGear: true, common filler blueprint-only, drop the legendary-first and quota logic, drop theguaranteeGear/gearBiasparams).Chest.cs: remove the deadguaranteeGear/gearBiasfields; addresourceOnly(used by Floor 0's timber crate, §6) and apply the fixed-loot gear-backfill rule in the open path: authoredFixedLooton a real chest (not crate/keg/resourceOnly) with no gear item getsRollGuaranteedGear(rarity)prepended.- Biome rarity pass: at the end of
MapPopulator.Populate, re-roll every spawned real chest'srarityand every nest'sNestRarityfrom the biome table (use the raid's virtual biome index, clamped by the table). The depth-basedClimbFracrarity stays as the pre-pass fallback, mirroring the web. Skip on Floor 0 / tutorial. - Nest power-check bypass: in the damage damping site (the Unity
powCheckport — combat/Health pipeline), skip the ratio clamp when the targetIsNestand the attacker is the player. - Note only (parked): rival chest ranking by rarity.
4. Nest rework (fixed per-biome HP · 3-gear payout)¶
4.1 Web rule¶
nestSpawnScale(biome): hp =nest.hpBiome1/2/3(900/1500/2400) +hpLatePerBiome(400) × (biome−2) for B4+ (B10 = 5200); passed as{hp: fixed/authored, dmg: 1, noHpSponge: true}and — critically — replaces the whole mobScale, so the nest ignores floor danger AND the player-PoweroverRefterm. A pure DPS/time gate (only gear damage growth speeds it up; combined with the §3.5 power-check bypass).- Payout:
nest.valuablesdeleted →nest.gearCount = 3; death drops exactly threerollGuaranteedGear(nestRarity)pieces — NO coin/blueprint shower, NO flatblueprints += 1..3. nestRarityfrom the shared biome table (§3.2).
4.2 Unity current state¶
- No nest config group: tuning is
EnemyDef.NestSettings(EnemyDef.cs:171–184;Enemy_nest.assethasvaluablesMin 5 / valuablesMax 8, no gearCount, no hp curve). - Nest HP goes through the generic path:
900 × mobHpMult × (1+overRef·hpPerPow) × _raid.Danger(EnemyController.Start). - Death loot:
EnemyController.cs:1450–1463— 1 guaranteed gear (RollLootItem(rarity, gearChance: 1)) + 5–8 coin/blueprint shower +BlueprintsThisRaid += 1..3.
4.3 Port tasks¶
NestSettings: removevaluablesMin/Max; addgearCount = 3,hpBiome1 = 900,hpBiome2 = 1500,hpBiome3 = 2400,hpLatePerBiome = 400(updateEnemy_nest.asset).- Spawn scaling: when
MapPopulatorseeds a nest, compute the fixed HP for the (virtual) biome and pin it — the clean Unity mechanism is the existingrampHpoverride (rampHp = fixedHp / def.hp,noHpSponge = true) plus suppressing theoverRefpower term for nests: extendEnemyController.Startso a nest (or a newflatScaleflag) skips(1 + overRef*hpPerPow)exactly like the web's replaced mobScale.rampDmg = 1. - Death loot (
EnemyController.cs:1450–1463):gearCount × RollGuaranteedGear(NestRarity); delete the shower + the direct blueprint grant.
5. Ten-biome campaign¶
5.1 Web rule¶
prog.biomeCount3 → 10;stageSizesgets 7 new rows easing to the 120-tile cap;prog.bonusZonesMax = 4caps the per-biome bonus zones.ensureBiomeLadder(s)inload(): never truncate; APPEND{unlocked: i==0 || prev.cleared, cleared: false, stage: 1}untilbiomes.length == biomeCount; clampsave.biome.- Lobby becomes scrollable (web CSS — no direct port).
5.2 Unity current state¶
- Biome count =
BiomeSet.BiomeCount(3 authored: Thornwood/Sunken Mire/Ashen Deep, 5 floors each);BiomeSet.Biome(i)clamps to last; the endless virtual ladder already extends past authored content withendlessNames(12 entries) +extraRungsdanger. SaveService.Migrate(:85–104) growsbiomes[]to a hardcodedconst biomeCount = 3;MetaLoopController.EnsureLadder(~:205–213) appends at runtime.stageSizeshas no Unity analogue (warren rolls its own W/H;MapDefinition.sizeretired).- The lobby pager already handles any biome count (arrows), gated on
unlocked.
5.3 Port tasks¶
- Campaign length: introduce the authored campaign target = 10 — since Unity derives count
from
BiomeSet, either (a) author 7 moreBiomeDefentries that REUSE Biome_3's art/waves (content fills in later), or (b) addprog.biomeCount = 10and teach the ladder/lobby to run onmax(BiomeSet.BiomeCount, prog.biomeCount)withBiomeSet.Biome(i)clamp handling art. (a) is closer to the existing architecture (everything already keys off BiomeSet + clamps); pick one and keepMigrate/EnsureLadderconsistent with it. - Save ladder: replace the hardcoded
const biomeCount = 3inSaveService.Migratewith the resolved campaign count and add the web's unlock-next-if-previous-cleared rule to the append loop (checkMetaLoopController.EnsureLadder— merge the two grow paths into one helper so load and runtime agree). - Map size:
stageSizesdoes NOT port (warren architecture). If late-biome maps should grow, express it as authoredWarrenShapeOverrideperBiomeDef/MapDefinition(widthMin/Max etc.) — flag as a design/content task, not a code port. bonusZonesMax: no port — zones were removed with the open-world cut.- Lobby: verify the pager +
StageIndicatorrow andMetaLoopControllerprogression handle biome 4+ (they should — endless already does); update any hardcoded 3s.
6. Floor 0 — the fixed onboarding raid (FTUE)¶
6.1 Web rule — map (genZeroFloorMap, 40×64 tiles)¶
A fresh save's first raid is Biome 1 · Floor 0, ctx = {kind:"raid", biome:0, stage:0} — fully
authored, deterministic:
bottom → top:
spawn room (15,54)-(24,61) — EMPTY; spawn at tile (19,59)
loot room (14,45)-(25,52) — ONE Common chest (19,48) POI "First Cache"
nest room (13,35)-(26,43) — ONE Spider Nest (16,38), no brood POI "Spider Nest"
timber room (14,26)-(25,33) — ONE wooden crate (19,29): crate=true, resourceOnly=true,
fixedLoot = [60 timber] POI "Timber Crate · 60"
chamber (10,7)-(29,22) — the STANDARD defense-chamber contract (pad at padF fractions,
T/L/R doorways + aprons mirroring the generator, 3-wide S player door)
3-tile doors connect the rooms in a straight line; zero randomness/props. fixedLayout tag.
Floor rules: no round timer; no biome chest-rarity pass; standard fog; nest = production system
at fixed B1 HP (900), nestRarity "common", leashed to its room; chest = production economy
(⇒ guaranteed gear piece); crate = production crackable holding the exact 60-timber defense budget;
defense finale = the SHARED system. mkDefMob first-floor softening covers Floor 0:
firstFloor = biome==0 && (stage==1 || zeroFloor) × defense.firstFloorMobMul = 0.25 on hp+dmg
(counts/speed/cadence untouched). The defense session does NOT advance on a Floor 0 win.
6.2 Web rule — safe corruption demo¶
At raid start Floor 0 spawns a demo front: {y: bottom, demo: true, warned: true,
stopY: just short of the player (1.4 tiles), speed: 320 px/s}. updateCorruption early-returns on
demo before every production effect (no slow, no DPS, no mob cleanup); drawCorruption renders a
demo front even with corruption disabled; arrived latches at stopY (drives coach step 0).
6.3 Web rule — the coach (zeroGuide)¶
One objective at a time; run-state {step, t, pathT, path, target}. Seven Russian lines:
0 Остерегайся порчи — она наступает. (corruption demo)
1 Открой сундук и забери лут. (the gear chest)
2 Уничтожь гнездо. (the Spider Nest)
3 Открой ящик и получи ресурс для строительства. (the 60-timber crate)
4 Построй базовое здание. (the Base slot)
5 Построй защитную башню. (nearest UNLOCKED tower slot)
6 Запусти эвакуацию и защищайся от волн противников. (the extraction pad)
Completion predicates observe REAL state (zeroGuideStepDone):
- 0: demo arrived AND line on screen ≥ 2.5 s.
- 1 & 3: container opened AND emptied AND all its spilled items collected — spilled loot carries
_source (the chest) via spillLoot(..., source); the check rejects unpicked ground items with
that source (the player can't leave the intro gear/timber behind).
- 2: nest dead · 4: Base lvl>0 && !dead · 5: ANY tower lvl>0 && !dead · 6: pad phase ≠ idle.
- On completion: step++, screenshake 0.18, timers reset; after step 6 the coach clears.
Route + pointer: every 0.35 s re-run A player → target (append the literal target point if the path ends short); render as an animated dashed gold polyline (dash [7,11], crawling offset, glow, informational only — never writes the player's path). Above the target a pulsing gold pointer*: down-triangle + ellipse ring, ±5 px sine float, lift 48/58/70 px by target type, drawn over actors. Steps 1–6 only. Coach text uses the standard prompt; entry banner "Floor 0 · Training Route"; the rotating tip is suppressed.
6.4 Web rule — save/lobby flow¶
- Save:
zeroFloorCleared(fresh = false; legacy saves migrate to true),floorSeldefault 0. selectedRaidFloor: fresh-save fallback = Floor 0 whenbiome==0 && !zeroFloorCleared, else the frontier; explicitfloorSel==0= Floor 0 (B1 only); else clamp 1..frontier.- Lobby: Biome 1's floor row gets a Floor 0 tile (always unlocked, replayable); B1 floors 1–5
LOCKED until
zeroFloorCleared; biome label "Floor 0 pending"; Play label "▶ Biome 1 · Floor 0 · Tutorial". All launch paths route throughselectedRaidFloor. endRunFloor 0 branch: progress-portal win →zeroFloorCleared = true; biome = 0; floorSel = 1("Floor 1 unlocked."); safe-exit win → banks only; death → standard rules.
6.5 Unity current state¶
- No floor-0 concept anywhere (grep-verified); no
zeroFloorCleared;SaveData.floorSelexists but is 0-based (0 = Floor 1) — the web'sfloorSel==0sentinel does NOT map 1:1. - Authored-map machinery exists but is FTUE-shaped:
MapGenerator.BuildFromLayout(:468–516, border+rects+vault, web-frameFlipY) +MapPopulator.PopulateTutorial(:371–464). Neither builds a defense chamber/mouths — onlyWarrenGen.Carvedoes. - Corruption:
CorruptionControllerhas no demo mode; spawned only on chamber floors outside tutorial (RaidBootstrap.cs:152–162). - Coach:
CoachViewis live in the HUD (panel_tutorial, fed fromRaidState.CoachText/HeaderbyInGameViewController.cs:192–194). - Pointer/route:
TutorialArrowView/TutorialArrow(billboard arrow, bob, fog rules) +PathVisualizer(dashed Shapes route for the PLAYER's own path — component-mode Shapes, pooled segments) are the closest building blocks; neither draws an arbitrary informational route yet. - Ground loot has no source-tracking (
LootService.Spill). - Defense session advance:
DefenseController.TickSpikesend →_save.Data.defense.session++.
6.6 Port tasks¶
- RaidContext/floor identity: introduce
zeroFloor(RaidState flag) selected as "biome 0 + floor −1"? No — keep it explicit: addSaveData.zeroFloorClearedand represent the selection as a dedicated flag (e.g.SaveData.floorSel = -1sentinel or a separatebool zeroFloorSelected) rather than overloading the 0-basedfloorSel. Recommendation: usefloorSel = -1+ aSelectedRaidFloor()resolver inMetaLoopControllermirroring the web's (fresh-save fallback → Floor 0; legacy migratezeroFloorCleared = trueinSaveService.Migrate). - Map: new
ZeroFloorGen(static, besideWarrenGen) emitting the standardMapDatacontract (tiles, spawn,ChamberWarrenRoom,MouthsT/L/R + S door, room list). ⚠ The Unity chamber is 20×17 interior / 22×19 outer (branch change) vs the web Floor 0's literal 20×16 tiles — do NOT copy the web tile coords for the chamber; derive chamber+mouths fromGameConfig.Defenseexactly likeWarrenGenand stack the four authored route rooms below the S door (preserve room sizes/gaps; total map ≈ 40×65).RaidBootstrap: routezeroFloorto this generator; skip the biome rarity pass (§3.4) and the round timer; populate via a small dedicated path (one chest, one nest via the §4 fixed-HP seeding withNestRarity = Commonand no pre-spawn brood, one timber cratecrate + resourceOnly + FixedLoot[60 timber], no other content). - Defense softening + session: add
GameConfig.defense.firstFloorMobMul = 0.25(asset!) and apply in the Unity defense-mob spawn (hp + atk only) whenbiome==0 && (floor==1 || zeroFloor); guarddefense.session++(DefenseControllerspikes-end) with!zeroFloor. - Corruption demo:
CorruptionController.InitDemo(stopZ, speed)— front surges at 320 px/s (÷ tile → world units) from the map's S edge and freezes ~1.4 tiles short of the spawn (Unity's front travels along +Z from below the fringe; the demo stop is behind the player, mirroring "rises from below the spawn"); skip slow/damage/cleanup and the stop-at-chamber logic; exposeArrived; render normally even ifcorruption.enabledis false.RaidBootstrapspawns the demo onzeroFloor(bypassing the!TutorialActiveguard). - Guide controller: new
ZeroFloorGuideController(shape ofTutorialController): the 7 lines (Russian, web parity — localization later) published throughRaidState.CoachText; step targets resolved live (chest, nest, crate, Base slot, nearest unlocked tower slot viaDefenseController, the pad) and the §6.3 predicates. Ground-item source tracking: add aSourcereference to the spill path (LootService.Spill/GroundLoot) so steps 1/3 wait for full pickup; smallAddShake(0.18)per completion. - Route + pointer: a
GuideRouteView— re-path every 0.35 s throughPathfinder(player → target, append target point), rendered as pooled dashed gold Shapes lines (PathVisualizeris the template: same component-mode pooling, ground-decal render queue, marching dash offset); pointer = extendTutorialArrow(it already floats/bobs/billboards) with the gold triangle+ring look, lift per target type, drawn above sprites. - Lobby:
LobbyView.RenderStages— prepend a Floor 0 pip on Biome 1 (its own template or the existingStageIndicatorwith a "T"/0 label); lock B1 floors 1–5 untilzeroFloorCleared; pass-through inLobbyController.OnStage/OnDescendfor the sentinel; stage label text ("Floor 0 · Tutorial" — the static "Raid" button label can stay, the floor readout carries it). - Raid end: in the progression banking path (
MetaLoopController/result flow): Floor 0 + progress-portal win →zeroFloorCleared = true; biome = 0; floorSel = 0 (floor 1); note text; safe-exit banks only; no ladder advance either way. - Entry banner "Floor 0 · Training Route" (
Floaters.ShowBannerat raid start).
7. Save migrations¶
Web ships three one-shot localStorage TUNING-snapshot migrations (thornwood_balance_new_v1,
_poi_gate_v1, _ten_biomes_v1) releasing stale config overrides — no Unity analogue (tuning
lives in the .asset). The SAVE migrations that do port (all into SaveService.Migrate):
| Web | Unity task |
|---|---|
ensureBiomeLadder — append to campaign count, unlocked = i==0 || prev.cleared, clamp biome |
replace the hardcoded const biomeCount = 3; unify with MetaLoopController.EnsureLadder |
legacy saves: zeroFloorCleared === undefined → true |
same rule when the field lands |
fresh defaults zeroFloorCleared:false, Floor 0 selection |
SaveData defaults + SelectedRaidFloor fallback |
8. Explicitly NOT ported¶
prog.stageSizesrows /bonusZonesMax— no Unity consumers (warren sizing / zones removed). Late-biome map growth = optionalWarrenShapeOverrideauthoring (§5.3).- Web
#menuscroll CSS — the Unity pager lobby doesn't need it. - Tuning-snapshot migrations (§7).
- Rival chest-ranking change — rivals parked.
PROJECT_CONTEXT.mdprose — web-repo docs.
9. Suggested port order¶
- §1 curves + §2 retune (config/assets +
Progressionhelpers +EnemyControllerseams) — smallest, unblocks balance testing. - §3 loot economy + rarity tables (
LootRoller/Chest/MapPopulatorpass). - §4 nest (needs
RollGuaranteedGear+ the flat-scale seam). - §5 ten biomes (BiomeSet entries or count override + save ladder unification).
- §6 Floor 0 (needs §3 crate flags, §4 nest seeding, §5 floor selection, the defense
firstFloorMobMul+ session guard). - Audit: line-by-line vs the cited web functions; play Floor 0 → B1F1 → B2 boundary (no danger dip) → nest payout → chest rarity distribution sanity across biomes.