Skip to content

Web → Unity Migration — Gameplay-Pivot Parity (2026-08-07)

Status: authoritative migration inventory for everything landed in the web prototype between commit 6d92290 (inclusive) and ebd6c39 (HEAD, 2026-08-07) — 107 commits.

IMPLEMENTATION: ALL TEN PHASES LANDED (branch bza-unity-pivot-parity, 2026-08-07): P1 68042ce config foundation · P2 3e7dd18 room-warren generator · P3 c6a2fff action input/dash · P4 fb3c308 binary detection + peace/war + noise waves · P5 23031da enemy AI parity · P6 5e00204 nest runtime · P7 b361910 timber/salve-cap/crack-channel · P8 33be398 corruption + no-clock flow · P9 b8656db extraction defense (both exits validated live) · P10 HUD polish + audit. Every phase compile-validated + play-tested; the warren smoke harness (menu: Shadow Raiders → Warren Gen Smoke Test) is green at 60+ floors. Remaining known polish (non-blocking): minimap/tactical corruption band, threat-arrow arrowsFromStep gating, camera leadSteer/leadFight/leadSmooth value sync on CameraRig, per-level building pips/HP bars, real art for defense mobs/units/tells (placeholder primitives), DropIconSet timber icon. Source of truth: config.js + game.js at HEAD. All line numbers cite HEAD. Supersedes: the feature-inventory/status sections (§0, §4, §5, §10) of web-to-unity-migration.md. That doc's §1–§3 (principles, toolset, architecture/folder contract) and §9 (tuning/authoring tools) remain valid and are assumed here. Companion spec: per-biome-level-shape-spec.md (level-shape overrides — current, port pending).

How to use this doc — every port task follows the standing 3-step workflow: 1. Full web context first — read the cited game.js function before writing Unity code; the goal is to mirror, not approximate. 2. Everything through configs/defs — every value in the key tables below goes to GameConfig (new groups listed per section) or the relevant EnemyDef/SO. No magic numbers at use sites. When a new GameConfig field lands, write the .asset, not just the C# default. 3. Audit against web afterwards — line-by-line faithfulness pass vs the cited web logic: values, ordering, edge cases.


1. Scope

1.1 In scope — the game as it actually plays at HEAD

  • Level generation: the corridor-family system — the room warren (map.style:"rooms", the boot default) and the chain corridor ("corridor", selectable, same contract) — plus the shared map contract, nest room seating, per-biome shape overrides, and the gen-smoke acceptance harness. §3.
  • Extraction-defense finale: fixed chamber, 17 build slots, 4 building kinds, timber economy, threat-budget wave scheduler with session scaling, spikes auto-extract, persistent building levels. §4.
  • Timber + fast crackable containers + the ground-loot pipeline (spill/magnet/auto-grab, salve cap, crack channel rules). §5.
  • Input: the action model — floating joystick + Action Button dash (+WASD/Space). §6.
  • Detection/stealth: binary aggro-only detection, sight fairness, eyes memory, honest cones, the peace/war plant-and-draw ranged grammar, flank crit, quiet looting. §7.
  • Noise: travelling sound waves, recruit cap, the muted-ring table. §8.
  • Enemy AI/combat: patrol dwell, hunted/atWar, Unity-parity lastSeen search, pack wake, combat weave, charged shots, melee swat commit. §9.
  • Nest runtime. §10.
  • Run flow / meta: corruption wave, single-exit floors, no round clock, 5-floor ladder, endRun banking. §11.
  • HUD/UI (gameplay): timber chip, defense popup/finale button, timer-as-phase-line, corruption on maps, camera look-ahead. §12.
  • Re-tuned values shared with already-ported systems. §13.

1.2 Out of scope — parked / removed / superseded / unreachable

Full evidence table in §14. Headlines: the Dive (code deleted), the free-placement fort + siege (deleted, replaced by §4), rival raiders + the whole Tier-0 layer (disabled AND structurally absent from the live generators), tutorial/FTUE (load-time graduate stamp), tunnel + zones generators, vault/Keykeeper/boss/gatekeeper/safe-exits (structurally absent from live generators), classic tap-to-move input layer + sprint/stamina, gradual detection, shield, weak-point armor, in-run inventory/Loot Station, Safe Pocket, valuables, storm finale, dev tuning panel.

1.3 Level-generator scope — DECIDED 2026-08-07

Port the room warren ONLY (map.style:"rooms", roomsPlan/genRoomsMap) — exactly as it works on web. The chain corridor, tunnel, and zones generators are all excluded:

Generator map.style Decision
Room warren "rooms" PORT — the one level layout
Chain corridor "corridor" NOT ported. §3.3 kept as reference only; CFG.corridor group NOT mirrored to Unity (except the two crackable keys — see §3.4). The byBiome companion spec loses its target (see §3.6).
Tunnel, Zones "tunnel", "zones" NOT ported (legacy)

Unity's existing open-world (zones) map generation is removed and replaced: MapGenerator.cs / MapPopulator.cs are structurally coupled to the zones model (MapData.Zones) and get rewritten around the warren plan + the shared map contract (§3.2).

1.4 Unity-side keep-parked rule — DECIDED 2026-08-07

Parked/unused web systems are not ported — and where Unity already has them, they are not deleted either: park in place, untouched, for possible later revival.

  • Do NOT add config keys/groups for parked systems to GameConfig (no corridor.* shape keys, no rival.*/bot.*, no Safe-Pocket/shield/weak-point/stealth-fill keys, no dive anything). A parked system's tunables travel with the system, later, if it ever revives.
  • Do NOT delete from Unity: the melee-lunge system (EnemyDef.LungeSettings, EnemyController.LungerChase/MeleeSequence — used by existing Unity mobs; with the HEAD roster and tier gate it simply never triggers, which IS web parity), RaiderController + rival assets (disable spawns only — zero logic edits; their web-parity update is a separate future task), boss/gatekeeper EnemyDef assets (keep, unused), the stamina model (parked, no consumers), ClickToMove/tap-to-move (already parked).

2. The run at HEAD — one-page narrative

Player spawns at the bottom of a portrait-oriented floor built as a chain of door-joined rooms (warren) or a jogging corridor with side rooms. No round clock (timerOn is false on corridor-family floors). After a 5 s grace, a purple corruption front climbs from below spawn toward the extraction chamber over 0.5 × 300 s and stops below the chamber — a soft wall that slows (×0.6) and ticks (5 dps) but never executes. The player steers with a floating joystick; the one button is a free 0.9 s-cooldown dash. Detection is binary and cone-honest; at peace the bow is down — standing still for 0.25 s + 0.9 s draws it; being hunted means return-fire only against enemies with eyes on you; flanks crit. Loot is quiet: chests (3 s) and fast crackables (1.05 s) crack on proximity via one sticky channel, spill Diablo-style, and vacuum up; crates and piles deliver exactly 60 timber per floor. Rooms hold caches, camps, or nests (corner-seated optional encounters that wake only via direct hit or their pack's alarm). At the top: a fixed 22×18 extraction chamber with 17 persistent build slots (Base/Towers/Gates/Barracks, bought with timber). Standing on the pad 2.5 s starts a 30 s hold-out: threat-budget waves pour in through three doorways. At time-up with mobs alive → ⚡ SPIKES: a radial sweep kills everything and auto-extracts — the sweep's end is the run's end. Extraction banks gold/blueprints/gear, advances the floor (5 per biome), and bumps the defense session (harder next wave). Death forfeits the entire run haul but never gear or ladder progress.


3. Level generation

Unity status: ABSENT. MapGenerator.cs/MapPopulator.cs implement only the retired zones model and are structurally coupled to it (MapData.Zones) — both are removed and rewritten around the warren (decision §1.3). New config groups: rooms (10 keys) + the two shared crackable keys (§3.4) + nest (gen-side keys). The corridor group is NOT ported; no map.style switch is needed in Unity — the warren is the only generator.

3.1 Shared constants and helpers (port first)

  • TILE = map.tile = 40; MAP_W/MAP_H mutable, MAP_CAP = 120×120 = 14400 buffer ceiling; setMapSize shrinks only (game.js:13-23). Tile ids FLOOR 0, WALL 1, BUSH 2, FENCE 3 (game.js:24). FENCE = built defense gate: solid to movement/pathing, transparent to sight and shots.
  • Defense geometry constants (also used by generation): DEF_CHAMBER {w:22,h:18,door:4}, DEF_APRON 4, DEF_PAD {fx:0.5, fy:0.548}, DEF_ZONES (17 slots — array index = save id, APPEND-ONLY forever) — game.js:33-60.
  • RNG helpers with exact distributions: rand(a,b) uniform float, randi(a,b) inclusive both ends, pick, and range helpers tRange/tRandi/tRand (game.js:114-119, 599-601). The web shuffles via .sort(() => Math.random()-0.5) — not uniform; use Fisher-Yates in Unity unless bit-parity is needed.
  • nearestFloor(tiles,x,y,ox,oy) (game.js:3082) — walks a blocked point back toward an origin at t = 0.8/0.6/0.4/0.2/0. Used everywhere placement could land in rock.
  • buildBushClustersbushId/sameBush (game.js:3030-3056) — 4-connected flood fill of BUSH, run in newRun for every style; drives shared-bush stealth.

3.2 The shared map contract

Both generators return (corridor game.js:1869-1870, rooms game.js:2333-2334):

{ tiles, at, spawn, chests, enemies, extracts, items, gate, pois, zones, corridor }
  • extracts = exactly one pad: {x,y, r:60, kind:"progress", unguarded, phase:"idle", prog:0, siege:true, chamberBox, mouths}. siege:true is only the marker that this is the defense chamber pad (defEx() game.js:6539) — rename to isDefenseChamber on port; the old siege system is gone.
  • mouths = {L,R,T,S}: three mob doorways (spawn, entry 2 tiles inside, door + apron tile rects) + S, the player's single entrance from below (door rect only).
  • corridor key = the family marker. Warren emits it with empty arrays + chain: plan.rects. Consumers: timerOn = sys.timer !== false && !map.corridor (game.js:467 — presence of the key disables the round clock); corruption init reads only corridor.chamber (world-px box, game.js:479-482); HUD hint gating (game.js:9185).
  • gate is a dead stub {x:-9999,y:-9999,tx:-1,ty:-1,open:false} (no vault on live generators). zones always [].
  • Post-gen passes in newRun for every style: 3×3 BUSH→FLOOR clearing around every nestDef enemy (game.js:395-403); buildBushClusters; map.legend snapshot (game.js:407-415).
  • Neither live generator spawns rivals, roaming gray mobs, a vault, a boss/gatekeeper, or safe exits.

3.3 Chain corridor — NOT PORTED (reference only, decision §1.3)

Kept below solely as a design/code reference; do not implement, do not mirror CFG.corridor to GameConfig (exception: the two crackable keys the warren shares, §3.4).

(corridorPlan game.js:1042-1096, genCorridorMap game.js:1098-1871)

Grammar: spawn stub → through-room → link (≤ linkLen.max, jog chance) → … → chamber stub → fixed chamber. runs.length = 2×nChain + 1. Map height derived from the rolled plan; plan is rolled before setMapSize. Full step-ordered detail is in the code cites; the load-bearing rules:

  • Plan rolls: corridorW [3,4], chainRooms [4,6] (min 2), roomsLoot [3,3], roomsNest [3,3], width [42,56] (clamped ≥36 for chamber+aprons), linkLen [3,7], jogChance 0.8, jog [4,7]. A jog shorter than corridorW retries the other direction and is dropped if still short (not a visible turn). Jog band shares rows with the segment above — corners are structurally connected. Links floor at cw+2 and cap at linkLen.maxlong bare corridor is structurally impossible. Last link never jogs.
  • Through-rooms: width chainRoomW [6,10] shrunk until it contains the incoming door column with side walls; height chainRoomH [5,9], or longRoomH [10,14] at longRoomChance 0.2. Top door re-aimed inside the room span (the room's diagonal). Dress roll chainDress 0.75pick(loot,loot,pillars,grove,guard) else plain; dressed rooms become ways (Waystation POIs).
  • Junction landings (junctionRooms true): 2 landing boxes per jog (both elbow ends), the corridor cross-section widened 1 tile each side (game.js:1169-1179).
  • Chamber: interior 20×16 carved above the last run; mouths built in order L, R, T (padTy = cY0+8); S = the passage row below the interior (game.js:1182-1227).
  • Side rooms (nLoot+nNest from a shuffled type pool; pool order in placement order decides each room's type): the island rule — reachable only through their own mouth; footprint rejected vs runs/jog bands/rooms/mouths/chamber+aprons with MARGIN 2 (landings check with 1-tile margin only; mouths deliberately don't check landings — a door into the corner plaza is the sanctioned exception). Mouth width mouthWFor(area) = area>=roomBigArea(90) ? 3 : 2. Flank alternation via sideFlip. Run rooms: stub offset 2..maxMouth 3, rw ≥ 6 or skip, height clamp(area/rw, 5, roomMaxH 14); mouth center keeps roomSpawnGap 900 px from spawn. Elbow rooms (roomElbow 0.5 preference roll) sit in jog pockets, vertical mouth into the landing; roomTwinDoor 0.35 rolls a 2-row second door into the adjacent run (twin attempts first 9 tries, then plain elbow). Sizes: roomArea [60,110] × aspect pick(0.6,0.75,0.9,1.0,1.1,1.3,1.6), clamped to roomSize [6,14].
  • Depth-band scheduling: slots (runs + jogs) sorted spawn-end first, partitioned into wantRooms bands, ≤1 room per band; chain rooms preferred as hosts over links. Top-up passes with shrink [1, 0.8, 0.62], then ≤3 fill-recovery rounds at areas 54/46/40 — authored counts are a contract, geometry gets squeezed before the count gets shorted (game.js:1424-1477).
  • Encounters: elites [1,2] seated in the upper half via safeAnchor (first row ≥ spawnSafe+TILE from spawn — spawn-safe can't delete the mini-boss); elite = tier-2 pick, eliteHp 1.6 (damage NOT scaled), escort eliteEscort [1,2] trash, one shared packId. Per run: packs [1,1] × packSize [2,3] trash; anchors keep packGap 620 px vs all anchors (seeded with dressed-way centers); no anchor → pack dropped, never stacked. Members spawn-safe-culled at 760 px; leashTiles(±2 rows, leashPad 3), leashHard. 50%/run one ground drop (80% common / 20% rare) (game.js:1489-1597).
  • Side-room contents: rarity by climb fraction (>0.66 epic, >0.33 rare, else common). Loot room: chest with gearBias = loot.caseGearChance 0.7, garrison always (roomGuard 1.0) on budget roomGuardBudget 3 + step×0.30 via rollRoster; guards leashed pad-0 (never leave the room until triggered), rejected only within spawnSafe×0.5; plus one floor item (60% blueprint / 40% coin). Nest room: see §3.5 (game.js:1599-1673).
  • Cover: per-run bush clumps at bushDensity 0.045 seeds ×1-3; side rooms singles at map.coverBushClusters×2; chamber never (game.js:1677-1695).
  • Content pass (last): every solid placement validated by pathOK() = path spawn→chamber AND to every room center, reverted on failure. Way dressing (loot chest 30% rare; wayGuardChance 0.6 ≤2 leashed guards; pillarCount [2,3] free-standing islands; plain 70% crackable; grove bushes). Landing beats 40% crackable. Pinch pass, links only: pinchChance 0.3, 1-2 alternating wall stubs leaving pinchGap 2 walkable, shoulder bush opposite. Saturation, links only: every saturateEvery 5 rows with nothing near, place 55% crackable / 45% bush patch — coverage is guaranteed, not rolled (game.js:1697-1838).
  • Timber: §5.4 (game.js:1840-1866).

⚠ Config-vs-code-fallback divergences — port the config values: chainRoomH config [5,9] (code fallback [5,8]), maxMouth 3 (fallback 4), roomMaxH 14 (fallback 12), saturateEvery 5 (fallback 7).

3.4 Room warren (roomsPlan game.js:1884-1991, genRoomsMap game.js:1993-2335) — the default

  • Plan: width rooms.width [40,54]; fixed 7×5 spawn room bottom-center; chain of rooms.total [7,9] rooms roomW [7,13] × roomH [5,9]; direction up, or sideways at sideChance 0.35never two sideways in a row; up-steps overlap ≥ door+2 with one wall row between and a rooms.door 2 door punched with a corner tile guaranteed each side; side-steps sit one wall column away, bottoms near-aligned (0-2 jitter). Chamber (20×16 interior) seated to clear every room (minTop), player door is a 3-wide shaft spanning the wall rows between; mob doorways L/R/T as corridor (aprons added to plan.rects and carved in step 1). Height derived; everything shifted so segment 0 sits 4 tiles above the bottom.
  • Silhouettes (de-ghosting): rooms only (never spawn/chamber/aprons — build-slot layout depends on exact chamber geometry): 1-2 corner bites (2-3 × 2-3, whole bite rejected near doors ±2), 1-3 single wall-wobble tiles. Safety is argued geometrically, not validatedgenRoomsMap runs NO pathfind check; tools/gen-smoke.js is the only reachability net. Port decision: add a post-gen validation pass in Unity (cheap, and the web argument is easy to break by future edits) (game.js:2035-2063).
  • Seating: nests [2,2] claim the largest rooms (area jittered ×rand(0.8,1.3)), loot [2,3] next tier, smallest stay plain; no same-type chain-adjacency (pass 1), count is a contract (pass 2 ignores adjacency) (game.js:2148-2170).
  • Room density knob: bud = clamp(round(interiorArea / densityArea 16), 1, 5) drives guard count, second chest, structure roll, camp beats — no room is empty, no closet is crowded (game.js:2099).
  • Structure roll (every room, bud ≥ 3): 35% pillars (2-3, all-8-neighbours-FLOOR islands, skip center±1); 25% outcrop (2-3 tile lump needing a clear 4×4); 20% heavy grove (bush ×2.2); 20% nothing (game.js:2117-2147).
  • Contents: rarity by climb fraction (same thresholds). Loot room: chest at center (+2nd common chest if bud≥4), garrison on budget roomGuardBudget 3 + floor(bud/3) mixing tier-2 picks (cost 2) at 50%, guards leashed to the full room, spawn-safe×0.5 abort; 40% one crackable; POI "Cache". Plain room: bud≥2 → 2-4 trash guards; 35% a camp (two adjacent crackables) else 1 crackable (+50% second if bud≥3). Nest room: §3.5. All scatter goes through inRoom clamp then nearestFloor. Every room ends with base bush clumps (rooms.bushDensity 0.03) (game.js:2194-2292).
  • Chamber empty; door framing: non-defense doors get a 35% jamb bush (game.js:2294-2304).
  • Timber: §5.4 (game.js:2306-2330).
  • Crackable keys: the warren reads CFG.corridor.barrelTime/crateChance (game.js:2018-2030) only because the corridor generator authored them first. With corridor not ported, Unity homes these two keys in the rooms/crackables config (see §18) — do not create a corridor group for them.

3.5 Nest rooms — generation side (both generators)

  • Corner seat: score the 4 interior corners by min-distance to the room's own doors/mouths, take the max, nearestFloor back toward center. Never the door-to-door walkline — crossing is free, engaging is a detour (corridor game.js:1637-1649, warren game.js:2210-2224).
  • Nest mob: enemies.nest row (hp 900, atk/speed/visR/visA 0, r 18, stationary), themed spider|goblin|emberball ∩ available roster; carries nestDef {kind}, nestRarity (room depth rarity), nestRoom (world-px box), the room's packId, hard leash. MOB_TIER.nest = 3; deliberately absent from mobIntro — only generators place it.
  • Brood: preSpawn [2,3] (warren: min(roll, bud)) clustered at the nest (±120 px / ±3 tiles), leash still room-wide.
  • No containers in nest rooms (hard rule — a container is a stand-still channel). Warren: 35% one ground pickup mirrored across the room center, rejected within lootSafeR 160. Timber piles also reject within lootSafeR of any nest (both generators).

3.6 Per-biome shape progression — DECIDED 2026-08-07

The web byBiome mechanism is corridor-only and goes away with the corridor (§1.3). The warren generates shape-identical floors across biomes at HEAD — that is the state to port. The future lever is Unity-native, not a web port: per-biome / per-floor shape overrides ride the existing BiomeDef / floor-def data architecture (each biome and each floor already an asset) — see the landing map §18. When shape progression is wanted, author it there (and mirror to web only if the web prototype still matters then). The companion per-biome-level-shape-spec.md §3 resolution semantics (resolve once, hand the same object to plan AND carve) remain the normative pattern for whatever override lands.

3.6b Reachability validation — DECIDED 2026-08-07 (approved)

genRoomsMap has no runtime reachability check (safety is a geometric argument; gen-smoke is an offline net). Unity adds a post-generation validation pass: path spawn→chamber pad exists AND every room's center is reachable; on failure, regenerate the floor (log it — repeated failures mean a generator bug, not bad luck). Cheap (one A* sweep per floor), zero effect on floor look.

3.7 Acceptance harness

tools/gen-smoke.js (139 lines): stubs DOM, splices a test hatch into the IIFE, runs floor sweeps. Unity mirrors the warren sweep only (corridor sweeps are out of scope with §1.3): spawn→chamber path + every chain-rect center reachable, zero failures over ≥60 floors; room count within rooms.total; nest/loot counts match authored; chestInWall = 0 (decoration never buries interactables); typeRuns = 0 (no same-type adjacency); timber totals exactly 60. The §3.6b runtime validation covers reachability in production; the harness proves distributions.


4. Extraction-defense finale

Unity status: ABSENT (no defense config, no prefabs, no wave code). The web's Portal.cs-era call→incoming→board flow survives only as the rare hand-cleared path. New config group: defense (46 keys, config.js:219-261).

Do NOT port: the free-placement fort (CFG.fort), fences/chaining/drag-drop/drag-remove/ per-kind-caps/seal-guard, the fort bar, the siege (CFG.siege/updateSiege), the Fort Sandbox — all deleted by cd31f46. features/extraction-defense/ is a standalone design prototype, not loaded by the game; its SPEC.md numbers are superseded by game.js — port game.js.

4.1 Arena

Fixed chamber every floor (persistent buildings require identical geometry): interior 20×16 tiles, DEF_PAD pad (r 60), 3 mob doorways (L/R/T, 4 wide) each with a 4-deep spawn apron and an entry point 2 tiles inside; S = player entrance. Invisible player-only, entry-only blocker rects on L/R/T always, on S while phase ∈ {incoming,finale,spikes} (mobs pass freely; a player inside a rect when it arms can walk out). defResolvePlayer runs after movement every frame and ejects toward the pad — the never-entombed guarantee (game.js:6586-6617). Seal drawn as a pulsing red dashed line (game.js:7176). Chamber is empty by generation. Inside the chamber, fog lerps fog.radius 200 → defense.fogRadius 500 over 0.8 s.

4.2 Phases (updateExtraction game.js:6307-6416 + updateDefense game.js:6849+)

idle (build window; pad fills at activate 2.5 s standing, drains deactivate 1.4/s off-pad; fill >0.02 locks building) → callExtraction: timer = defense.duration 30 (NOT extraction.incoming), noise extractCall, banner → incoming (hold-out; S sealed; waves run) → at timer ≤ 0: mobs alive → finale ("TIME'S UP — finish them all!", ⚡ SPIKES button, mobs keep fighting; hand-clearing the field → defPortalOpen) | clean field → defPortalOpenopen = classic boarding (boardPassive 3 / boardForce 3, boardStall 0, window -1 never closes).

Spikes (defStartSpikes game.js:6827-6841, tick 6862-6884): grid over chamber+apron+2 tiles, step spikeStep 80, per-spike delay = dist(pad)/spikeSpeed 900 — a radial sweep. Each spike kills def-mobs within 58 px (credited to the player). Envelope rise 0.16/hold 0.9/fall 0.3 s, height 42 px. When no spike remains active: kill-sweep any survivor, G._defWon = true, G.extracted = true, endRun(true, …) immediately — no boarding, no portal beat; the sweep's end IS the run's end. Mid-update return after endRun.

Session: save.defense.session++ only on win && G._defWon && !sandbox (game.js:9288) — hand-cleared boarding does NOT advance the session.

4.3 Waves

Threat budget startThreat 260 × growth 1.16^(session-1), denominated in swarm-equivalents (cost = hp×dmg/atkCd normalized to swarm). Split trashShare 0.17 / heavyShare 0.34 / mid 0.49 (runnerOfMid 0.29 rolled per-mob). Three credit streams over windows (fractions of duration): trash [0,0.45] flat, mid [0.30,0.95] and heavy [0.47,0.95] shaped by 1+(ramp 2.4−1)·u, normalized count-neutral. Packet size ≈ heads/duration/packDiv 1.5. Doorway rotation: 1 door until sides1 0.25, 2 until sides2 0.58, then 3; groups rotate L/R/T with a per-group counter. Spawn in the apron's outer half; each mob carries its doorway's entry point. No telegraphs — readability = fixed doorways + widened fog + red edge arrows + countdown (game.js:6694-6763).

Defense-only mobs (defense.enemies, constant stats, config.js:242-247): swarm 12hp/72spd/3dmg/ 0.8cd, grunt 46/52/8/1.0, runner 26/88/5/0.7, brute 190/34/18/1.5. Type "defmob", separate lists (G.defMobs/G.defUnits) — never touch stealth/noise/loot/minimap/onKill; drop nothing. In Unity these become EnemyDef assets + a data-driven wave lineup — see §18.3. Targeting: entry-walk → chew gates blocking them; switch to player within engagePlayer 130 or an infantry unit within engageUnit 70. Player auto-fire vs def-mobs bypasses stealth grading, gated by playerEngage 320 (passive ring drawn all hold-out).

4.4 Buildings (DEF_BUILD game.js:6525-6538, stats config.js:249-253)

17 fixed slots (DEF_ZONES fractional coords of chamberBox; ids = array indices, persisted, append-only). Per-kind capacity = slot count: 1 Base, 9 Archer Towers, 3 Barracks, 3 Gates.

Kind Cost/level HP/level Behavior
Base (maxL 5) 30/40/50/55/60 500/800/1150/1550/2000 Unlock ladder: L1 pad towers (ids 13-16) · L2 barracks (3,9,10) · L3 doorway towers (1,2,4,5,11,12) · L4 gates (6,7,8). Base destroyed mid-fight → defBaseLvl = 0 (ladder collapses for the raid; built things keep working).
Archer Tower (3) 25/35/50 180/270/380 dmg 14/22/34, rate 0.8/0.65/0.5 s, range 280/320/360, projSpd 640; nearest def-mob in range; fires whenever mobs exist.
Gate (3) 20/30/45 420/780/1250 armor ×1/0.85/0.7 (damage-taken mult). Writes FENCE tiles into its doorway (one tile inside, one tile longer each end): solid to mob/unit movement & pathing, transparent to sight and projectiles, player passes freely. Mobs pushed out of a gate tile chew it (atkCd-paced). Death reopens the doorway.
Barracks (3) 40/55/75 240/360/500 interval 5/4.2/3.4 s, cap 2/3/4 units; unit hp 80/120/175, dmg 10/16/23, spd 74, seek 520, atk cd 0.75. Musters only while the fight is on; infantry pass friendly gates.

UX: unbuilt unlocked slots draw dashed contours + cost 🪵 labels while defCanBuild() (phase idle, fill ≤0.02, alive). Walk within 46 px → bottom-center popup (#defPopup, the 15vh lane) with stat chips previewing the level being bought; one delegated pointerdown on #defBuyBtn; KeyE builds nearest. defBuyZone order (deny floaters → restore branch at restoreCostMult 0.6× same-raid, full HP, no save write → cost/afford checks → lvl++, gate solidify, base unlock banner + pulse newly-unlocked slots → defWriteSave() → HUD refresh) (game.js:6648-6679, 7097-7140).

Persistence: save.defense = {session:1, levels:{}} (+ migration for old saves). Levels persist account-wide; every raid buildings stand back up at saved level, full HP, free (only same-raid rebuilds cost 0.6×). Timber, damage, dead flags never persist (game.js:162, 6549-6582, 6627-6633).

Rendering: iso cylinder + level pips + conditional HP bar, y-sorted with actors; gates render in the wall pass as orientation-aware rail runs with posts that visibly disappear as HP drops, single HP sliver on the middle tile (game.js:7190-7250).

Defense Sandbox (lobby, #btnDefSandbox): spawn in chamber, full 60 timber, corruption off, ladder/session untouched. Dev tool — optional in Unity.


5. Timber, crackables, loot pipeline

Unity status: LootRoller/spill/magnet/chest-channel exist and are close; crackables, timber, salve cap, quiet-looting, channel arbitration are absent/stale.

5.1 Containers

One record (mkChest game.js:3092) + flags. Chest 3.0 s (loot.chestTime, was 4); crackable = timeMult = corridor.barrelTime 0.351.05 s, 50% crate (fixedLoot: 1 no-gear common roll → blueprint; may carry _timber) / 50% barrel keg (50% coin 50-100g / 50% blueprint). Crackables bypass the whole chest loot table (fixedLoot short-circuit, game.js:4369). Rendering: crate/keg sprites, no rarity glow; real chests keep the glow.

5.2 Crack channel rules (the "loot intent" system)

updateInteractions (game.js:4249-4365): proximity-activated at ≤43 px, one sticky channel — tap > remembered channel > nearest; non-channel chests decay 0.6/s; channel memory survives combat suppression (cleared only on open or leaving reach). Eligibility gates: not playerInFight() (fire-lock/melee stance/hit within fightLinger 2), and not stabReadymelee outranks loot: while any enemy is backstab-ready the proximity crack waits so the stab fires the same frame; an explicit tap still takes the channel. Movement does NOT cancel cracking. Vault-gate code defers by decaying while a chest cracks (inert on live generators — no vault). Progress reads as the player's channel halo, not per-chest rings. Quiet looting: crack/pop/gate/equip/sheet noise is muted outside the (parked) tutorial — looting never recruits AI, ever (§8). Scavenging also suppresses the peace-draw (!p.interacting in the draw gate) — one stillness, one intent (game.js:4012-4021).

5.3 Spill → magnet → grab

spillLoot (game.js:4451): ring of items at spillRadius 64×(0.6-1.1), landings through nearestFloor; arc spillTime 0.4 (ungrabbable while arcing), spillHop 34; magnet grace magnetDelay 0.6 after landing; armed flag prevents re-grab until you step off. Magnet (magnetRadius 56, magnetPull 320) only pulls collectable() items — the salve-cap fix: a capped salve is never dragged. collect() (game.js:4423): timber/coin/blueprint always; salve refused at player.potionsMax 3 ("SALVES FULL" floater, stays on ground); gear vs bagFull() (bagCap −1 ⇒ never full). With inRunInventory false everything auto-grabs on contact and kills spill their whole loot list (no lootable bodies).

5.4 Timber (the defense economy)

Run-scoped G.timber, never banks. Exactly defense.timberBudget 60 per floor: crates take 2-3 each first, remainder as loose piles (random run/room floor tiles; reject < spawnSafe×0.4 of spawn, inside the chamber (corridor), or within nest.lootSafeR 160 of a nest; ≤300 tries), any remainder dumped beside spawn (exactness rail). Pickup = instant auto-grab + magnet; +n 🪵 floater; HUD chip appears at first plank. No kill/nest drops. Rivals wouldn't loot it (moot) (game.js:1840-1866, 2306-2330, 3269, 4398, 4425).

5.5 Loot tables & caps (changed values)

rollChestLoot: chestItems 3 + legendary bonus 1 + randi(0,1) + bonusItems; rarity ladder 62/28/10 capped at chest tier; maxGearPerChest 1; common chests blueprint-only (t1Valuables false); coinChance 0.7; consumableChance 0.6 (potions are the only heal — hpRegen 0). rollLootItem: gearChance 0.06 (caseGearChance 0.7 on case chests; firstFloorGearBoost 2.5 on global step 0 only); valuableChance 0 (parked). Kill drops table: nest = guaranteed gear at room tier + 5-8 valuables/blueprints + 1-3 bp (the vault-grade payout); t1 trash t1MaterialChance 0.65 blueprint; brood nothing (game.js:4368-4405, 5070-5136, 3273).

5.6 Economy re-slope

CUR_MULT {1, 1.25, 1.5, 2.0} for coins/blueprints/valuables vs RAR_MULT {1, 1.8, 3, 5} gear scoring only (rarity is depth-driven — wide currency spread paid depth twice). Upgrade cost unchanged (level×2 bp). Defense costs are the new timber sink (§4.4) (game.js:81-86).


6. Input — the action model

Unity status: PARTIAL/diverged. JoystickInput.cs exists (tuning on the component, not config; deadzone 0.12 vs web 0.15; radius in different space — needs an explicit mapping decision). The Action Button dash is absent. ClickToMove/sprint/dodge/hunt already parked in Unity — matches web. New config group: action (5 keys) — move the component tuning into it.

  • Stick: anchor-on-touch, stickRadius 56 px, stickDead 0.15; drag on window-level move (finger off-canvas must still steer); no anchor re-centering; mag saturates at 1; steer direction unprojected through the iso tilt into world space (game.js:3422-3479, 3774).
  • Steering wipes tap-order state every frame; speed multipliers stack: bushSpeed × webbed 0.5 × corruption 0.6; axis-separated wall-slide vs rockBlocked (WALL-only — player passes FENCE) at r×0.8 + bodyBlocked (solid enemy bodies, combat.bodyBlock true) at r; facing instant (game.js:3933-3947).
  • Action Button (#btnSkill, pointerdown; Space): dash in steer direction (facing when idle), dashCd 0.9, dashFree true (no stamina), dodgeTime 0.35 / dodgeSpeed 500 peak / dodgeEase 1 ⇒ ~87 px; i-frames 0.40 s; respects walls AND enemy bodies; button pulses when off-cooldown and a nearby enemy (≤380 px) is winding up (game.js:3631-3727, 3918-3930).
  • Surviving taps (short still press < 260 ms, < 12 px travel): rival MARK (no-op — rivals off), minimap→big map, map tap, close loot sheet (game.js:3480-3495).
  • WASD/arrows steer (digital mag 1), Space dash, E build.
  • ⚠ Known web quirk — decide, don't inherit blindly: a second simultaneous pointer falls through to the full legacy path (swipe-dodge + onTap). Recommend NOT porting the leak.
  • Stamina: no consumers at HEAD — bar hidden, sprint unreachable, dash free. Port the stamina model as dormant data or skip until a consumer returns.

7. Detection & stealth

Unity status: STALE — DetectionModel.cs is the old gradual alert-fill the web dropped. Replace with the binary model. New keys: combat.aggroOnly/flankCrit/peaceWar/plantTime/ aimTime/engageRange/switchMargin/warReturnFire/mutualSightFire(false)/bodyBlock, fog.sightFairness, stealth.eyesMemory/reacquireCone/hideLockedCones.

7.1 Sight bands (all re-tuned)

combat.engageRange 160 (player commitment) < fog.radius 200 (player sight/fog; was 300) < fog.senseRadius 300 (echo blips; was 660). Enemy visR all ≤200 (§13) — trash 150-170 sees less than the player: the sneak window is structural.

7.2 Binary detection (aggroOnly true)

en.alert is 0 or 1 — no fill, no suspect state, no detection vignette (forced 0). UI: ❗ locked / ? searching (decayed alert 0.06-1) / nothing. perceive (game.js:5223-5266) exact order: bush shrink (bushDetectMult 0.30, only at alert 0, sameBush = no concealment) → setStealth/ raiderVsMob mults → sight fairness: nothing acquires the player beyond fog.radius while alert<1 (chase memory unclamped after lock) → cone gate: 360° viewers or lock-with-eyes bypass; reacquireCone true: a searcher that lost eyes is cone-gated again (honest cones) → dodge i-frames block acquisition only while invuln ≥ invulnWindow 0.34 (first ~0.06 s of the 0.40 s dash — NOT the whole roll) and never affect an already-locked enemy → range+cone+LOS.

7.3 Eyes memory

stealth.eyesMemory 3 s: _eyesHold topped up while seen, decays after. While >0: locked mob tracks 360°, its cone is not drawn (hideLockedCones true — the cone returns exactly when facing is information again), auto-fire/backstab treat it as "has eyes on you". Alert decay on lost sight: chase-commitment floor 0.55 in the open; bushBreakDecay 0.9/s if contact broke in foliage (earned stealth reset); mobAlertDecay 0.15/s × heavyTenacity 0.35 for tier≥3.

7.4 Peace/war ranged grammar (the "ranged stealth" system)

  • G.hunted = hit within fightLinger 2 OR def-mobs alive OR any non-nest enemy locked with eyes/target on you. G.atWar = hunted OR a woken nest with warZone within nest.range 460 with LOS (game.js:4047-4058).
  • At peace, moving = weapon down (global gate). The draw: stand still plantTime 0.25 (deadzone transits don't count — steerHeld), then aimTime 0.9 aim at the peeked target (retarget restarts; scavenging suppresses; ring shows blue → gold-filling → red).
  • Target selection nearestEngaged (game.js:4658-4743) grade ladder: woken nest = attacker at ANY range; locked-with-eyes enemy = attacker; warReturnFire true: while hunted, only attackers are valid — anything blind to you is a held-fire silent-kill opportunity; at peace a blind creature needs the full draw; engageRange 160 gates openers. Stickiness switchMargin 0.7: lock holds unless strictly higher grade or 30% closer. Position is intent (press into the nest = keep the nest; back off = the wave takes the arrows).
  • Flank crit (flankCrit true) replaces the old fire-lock: ranged crit judged at impact on the shot's incoming ray via inBlindSpot (mobs: outside the drawn cone — backstabFromFov; else backstabArc 0.55); ×(critBase 1.5 + crit/100). meleeToFireLock inert.
  • Backstab is alive and primary: passive autoBackstabPlayer (nearest backstabReady) — opener-only (!seesPlayer && _eyesHold 0; re-arms when a searcher loses you), ≤ reach (meleeReach 25), autoBackstabArc 0.72 (tell at 72 px), crit bypasses power damping, hitstop backstabHitstop 0.13, silence window silenceWindow 3 → tiny noise.backstab 60 ring on the kill instead of deathMob 300.

8. Noise

Unity status: STALE (NoiseModel predates waves/mute). New keys: noise.waveSpeed 560, wallsBlock true, recruitCap 2, hearRadius 1.5, hunterStepCd 0.5, retuned radii (§13).

  • makeNoise (game.js:7284-7323): player-facing sonar ring always (heard through walls at radius×1.5 — the player's threat radar; anti-stack 0.22 s/70 px; ring duration = radius/ waveSpeed and the drawn ring IS the trigger surface); recruit path pushes a travelling wave (front += 560·dt), each enemy touched once in distance order, wallsBlock requires LOS, stop at recruitCap 2.
  • noiseTouch (game.js:7263): engaged enemies immune; searcher (alert>0.5) redirects its hunt to the fuzzed point (±24); calm mob investigates unless posted (leashed and the point is outside its box) or already heading somewhere closer; stationary never.
  • Muted forever (do not wire): chest crack/pop, gate tick/boom, equip, loot-sheet actions, nest/brood deaths, melee-clash vs a nest. Recruiting rings: gunshot 300 (gunCd 2 per shooter), meleeClash 280 (non-crit), bombLob 280, explosion 320, deathMob 300, deathRaider 320, backstab 60, extractCall 1160, portalOpen 600. Non-recruiting: hunterStep 130 (roused, moving, unlit enemies only — the through-wall "something is hunting you" channel), portalIncoming 880, botChest 240.

9. Enemy AI & combat

Unity status: PARTIAL/STALEEnemyController FSM exists on the old detection ladder; charged shots are a simple wind-up; no dwell/hunted-split/weave parity. New keys: combat.patrolHold 1.2, chestDriftRange 340, turnMelee 6 / turnRanged 5 / turnHeavy 3 / turnClose 4.5 / turnCloseRange 70, mobMeleeArc 1.75, chargeArc 0.7 / chargeAimMargin 0.6 / chargeTurn 1.8 / chargeLockLead 0.25. Retired: combat.mobSearchRadius (Unity's route-completion arrival is already the correct model — keep it).

  • updateMob order (game.js:5631-5804): nest → melee sequence → target scan (player + bots) → _engaged → lock/lastSeen/pack-broadcast → lost-decay → stationary branch → chase → lastSeen search → investigate (44 px / 9 s) → patrol. Hard leash clamps only when NOT chasing, only when near the box (a returning mob walks home, never teleports).
  • Patrol: half speed, cone turn patrolTurn 3.5; guaranteed dwell at every leg end rand(0.7,1.4)×patrolHold 1.2 (no surprise 180° sweeps); leg picks: portal-guard ring / 35% chest-drift within own leash ∩ chestDriftRange 340 / home if outside zone / uniform point in leash / free 150-380 px.
  • Chase: facing = chargeAng while charging, else lerpAngle at per-regime turn rates (heavy 3, pure-melee 6, ranged 5, whipped to 4.5 inside 70 px — exponential smoothing, only the charge aim uses true rate). combatMove weave: radial hold (melee reach ±22; ranged standoff min(visR×0.55, 210) −28/+44) + perpendicular juke (sign flips 0.45-1.1 s), weave damped 0.35 while charging (braced, not frozen), speed ×0.45 charging; wall-wedged → A* fallback.
  • LastSeen search (Unity-parity): arrival = route completed at the cell or within 1 TILE (never a straight-line radius); stale-goal re-route guard (moveGoal vs lastSeen < 0.5 tile); unreachable → scan in place; spin ~0.9 rad/s (random sign per search), give up after mobSearchPause 4 → patrol, clear everything. Re-acquisition during search is cone-gated.
  • Pack wake (alertPack game.js:6199, throttled 1.2 s per spotter, full-spot only): authored packs rally only their own packId within 520 px; un-packed mobs use radius 380 (pack:true) / 250. A nestDef pack-mate gets _woken — the guardians are the nest's eyes.
  • Charged shots (game.js:5310-5365): commit gate = fireCd ≤ 0 AND bearing error ≤ min(chargeArc 0.7, chargeTurn 1.8 × (chargeDur − lockLead 0.25) × aimMargin 0.6) — per-mob (emberball 9° … boss capped 40°): commit only to a shot you can still aim. Aim seeds from current facing (no snap), tracks at a true rate cap 1.8 rad/s until the last chargeLockLead 0.25 s (freeze = the dodge cue), then fires along the line (400 px projection; bombs ignore aimPt — the ground fuse circle is their telegraph). Facing = chargeAng throughout (cone/line/shot agree). Lost sight/range aborts. Dodge i-frames are acquisition-only — a locked charger keeps eyes through the roll; the counterplay is movement. fireCd init rand(0,1) desyncs pack volleys; effective cadence ×atkCdMul 1.5.
  • Melee swat commit (game.js:5358-5364): reach r+r+8, mobMeleeCd 0.7×1.5, blocked while charging (no parallel melee+ranged) and cone-gated ±mobMeleeArc 1.75 (no hitting behind its back) — the flank behind a committed enemy is genuinely safe.
  • Lunge system is DEAD CODE at HEAD — no mob satisfies isLunger (lungeMinTier 2, and every tier≥2 mob is ranged/boss/nest). Unity keeps its existing lunge implementation untouched (EnemyDef.LungeSettings, EnemyController.LungerChase/MeleeSequence — decision §1.4); with the ported roster + tier gate it never triggers, which IS web parity. Do not port web melee.* values over it; note web's game.js:5745 omits atkCdMul if ever revived.
  • Player melee: stance arbiter unchanged (enter reach+meleeBand 5, exit +bandHysteresis 5, stanceCommit 0.15); meleeCd 0.9; no arc for the player; nests never flip you to melee.
  • NPC facing is gradual everywhere (7b13190 — snapshot crutch removed); player facing instant.

10. Nest runtime

Unity status: ABSENT. New config group: nest (9 keys) + enemies.nest row.

  • Wake = exactly two paths (no senses of its own): a direct hit (also rallies the whole room: same packId or _nestOf → alert 1, chase, target=attacker — never touches outsiders), or the pack alarm (§9). Undetected pass or silent executions leave it sleeping — the stealth route is real. Waking is sticky; one-shot "stirs!" floater; HP bar drawn only once woken (the bar IS the tell) (game.js:5586-5630, 4970-4985).
  • Births: paused (not reset) while player dead or beyond range 460. First interval ×0.5, then interval [3.5,6]; liveCap 4 counts live brood + pending tells; birth point sampled ≤12 times inside nestRoom (inset 12 px) rejecting solid tiles (births can't land in rock — rooms roll pillars/outcrops), fallback nest position; tell nest.tell 0.9 via the spawn-tell rig; nest death mid-tell cancels the birth; brood born calm (patrol, alert 0), inherits packId + hard room leash + noDrop (spawnDrops false — no farming faucet).
  • Death payout: guaranteed gear at nestRarity + valuables [5,8] rolls (55% valuable / 45% blueprint) + 1-3 bp; "NEST DESTROYED!" floater; no ring, no recruit.
  • Combat-grammar exemptions (each is a rule): woken nest = any-range attacker; never melee stance; exempt from warReturnFire + draw requirement; no backstab/tell/cone; not bush-concealed; engage ring suppressed when woken; boss-width HP bar; swings on it are silent.
  • warZone (LOS-gated, §7.4) makes standing in a woken nest's zone "at war" (draw-free fire) without being "hunted" (return-fire-only doesn't apply).

11. Run flow / meta

Unity status: PARTIAL — ladder/endRun exist for the old flow; corruption absent; no-clock floors absent; auto-extract absent. New config group: corruption (9 keys).

  • timerOn = false on corridor-family floors — no clock death, no exit closings; G.time frozen; map.roundTime 300 survives only as the corruption pacer.
  • Corruption (game.js:6459-6490): after delay 5 s the front climbs from WORLD_H toward stopY = chamber.y1 (stopAtChamber true) over travelFrac 0.5 × roundTime 300 = 150 s (speed derived each frame — live-retunable). Inside: ×slowMult 0.6, dps 5 in tick 1.0 bites through damage() (re-entry restarts the tick); one-shot warn banner at warnDist 420; swallowed creatures die silently a body-length behind the front (killMobs true) — no loot, no credit. Soft wall by design — never executes the player.
  • Ladder: prog.stagesPerBiome = 5 (the "3-sub-stage" comments in config are stale prose — port 5), biomeCount 3. Every rooms/corridor extraction is kind:"progress" + unguarded ⇒ every win advances a floor; floor 5 win clears the biome and unlocks the next; death changes nothing. ⚠ Design gap to sign off: no boss/gatekeeper exists on live floors — "the boss floor" is currently just floor 5 by index; the defense session (§4.2) is the actual difficulty ratchet.
  • endRun (game.js:9278-9434): win → bank bag (gear→stash, valuables→gold), gold/blueprints, persist run loadout (field-equips become permanent), autoEquipEmpty fills empty slots from the banked haul; timber never banks. Death → everything forfeits (safeSlots 0), gear/stash/ladder untouched; death card leads with "Raid Again" (same ctx) + "To the Lobby". deathDelay 1.0.
  • Dive: REMOVED from code entirely (0ab609b) — zero residue. Its replacement flow is the above: one floor per run, extraction = the end.

12. HUD / UI (gameplay)

Unity status: MVC screens exist and are current-ish; defense UI, timber chip, phase-line timer, corruption overlays, camera keys absent.

  • In-world vitals at the character (ui.vitalsScale 1.2): HP, ammo pips, reload ring, channel halo (crack progress). Stamina bar hidden (pivot).
  • HUD chips: power, gold, timber 🪵 (appears at first plank), potion count (cap 3). Hidden: bag 🎒, shield 🔋. Objectives panel retired — hardwired empty; the floor teaches itself.
  • Timer block hidden on corridor-family floors unless a portal phase is live; sub-line narrates: "Hold out · Ns" / "Portal open — board!" / "Time's up — ⚡ finish them!" / "⚡ SPIKES!" / boarding lines.
  • Defense UI: #defPopup bottom-center 15vh lane; #defFinaleBtn ⚡ SPIKES; ground contours + cost labels; red seal line; passive 320 px engage ring during the hold-out; red edge arrows for off-screen def-mobs; chamber fog widening.
  • Maps: minimap no enemy blips (minimapEnemies false); corruption band on minimap (rgba(150,70,220,.45)) and tactical map (+ front line); poiLabelsMap true / in-world poiLabels false. Purple screen wash while inside corruption. Threat arrows from arrowsFromStep 1 (none on B1F1); alerted cones render hot (brighter red/alpha/edge).
  • Engage ring (ui.engageRing true): blue peace / gold-filling draw / red war; suppressed for woken nests.
  • Camera: look-ahead re-homed to stick steering; fight target wins, movement otherwise; camera.leadSteer 225 / leadFight 150 / leadSmooth 0.02; lean toward the aim target while the bow draws; fitMin 1 (small-screen zoom-out reverted). Unity CameraRig owns these — sync values.
  • Death card: Safe-Pocket-aware rendering ports but degrades to "everything lost" at safeSlots 0.
  • Result buttons: win → Continue; death → "⚔ Raid Again" (same run ctx) + "🏕 To the Lobby".

13. Re-tuned values (parity table for existing Unity systems)

Key Web HEAD Unity now Note
move.baseMove 110 100 (asset) flagged in-config; sync
move.speedGain 0.075 0.075 ok
move.dodgeTime 0.35 0.25-era dash ~87 px, i-frames 0.40
action.stickDead 0.15 0.12 (component) sync; move to config
action.stickRadius 56 (canvas px) 160 (UI ref px) different spaces — define the mapping
map.roundTime 300 600 corruption pacer only
fog.radius / senseRadius 200 / 300 300 / 660-era
combat.mobHpMult / mobDmgMult 1 / 1 2 / 0.5 sponge retired
loot.chestTime 3.0 4.0-era
loot.consumableChance 0.6 0.45-era
loot.t1MaterialChance 0.65 0.5-era
player.hpRegen 0 0.005-era potions only
player.potionChannelTime → swig 0 / potionSwig 0.35 rooted channel un-rooted; hit interrupts, potion kept
player.potionsMax 3 new
player.baseHp 190 verify
extraction.incoming/window 30 / −1 −1 / −1 plain-portal path only
extraction.boardPassive/Force/Stall 3 / 3 / 0 9-era stall 0: commit, eat it, leave
enemyScale.atkCdMul 1.5 1.5 applies to charge cadence too
enemies.*.visR spider/goblin 150 · skeleton 140 · hound/emberball 170 · bombardier 180 · warden/rocketeer/gatekeeper 190 · sentinel/boss 200 6d92290-era + boss.chargeDur 1.0, gatekeeper.chargeDur 0.7, weapons.sniper.mag 3
enemy speed all non-boss = baseMove × enemySpeedMul 1.0 = 110 (authored speed column inert); boss 70 verify patrol ×0.5
mobIntro hound 2 · emberball 3 · skeleton 4 · warden 6 · rocketeer 7 · sentinel 8 · bombardier 10 older one new mob per floor max
melee.recover 0.35 0.15-era (dead path, see §9)
noise radii §8 table older
camera.fitMin 1 0.9-era

14. Exclusion list — do NOT port

Standing rule (decision §1.4): an excluded system's config values are excluded with it — no dormant keys added to GameConfig/defs. Where Unity already contains an excluded system (raiders, lunge, stamina, tap-to-move, boss/gatekeeper defs, tutorial), it is parked in place, not deleted, not modified.

Feature State Mechanism / evidence
Chain corridor generator (corridorPlan/genCorridorMap, CFG.corridor except the two crackable keys) NOT PORTED (decision §1.3) rooms is the only level layout; §3.3 kept as reference
The Dive (descend-carry, depth mult, cards) REMOVED 0ab609b; zero grep residue
Fort (free placement, fences/chaining/drag, fort bar) + Siege + Fort Sandbox REMOVED cd31f46; ex.siege survives only as the chamber-pad tag — rename
mobReact startle · frozen-cone highlight · chargeLock flag · stalk-hold ladder · per-chest rings · tasks panel · rival glyphs · "First hit shielded" perk · mobSearchRadius REMOVED 621425b, 561f6f9, 7a2f76f, c4682f8, game.js:9164, 4509, 3226
Tutorial / FTUE (3-stage pivot AND legacy 7-stage) PARKED (load stamp) game.js:171-175 stamps every save graduated; machinery intact for the planned new FTUE — do not port either curriculum
Rival raiders + entire Tier-0 layer (neutral, MARK, safe crossfire, Opportunists, rival colors, engage-ring-near-rivals) DOUBLE-DEAD prog.botsFromBiome 99 AND no mkBot call in live generators; excludes rival.*, bot.*, botDiv, raiderVsMob, tryDeclareTap consumers
Vault / key / Keykeeper / keybearer race STRUCTURALLY ABSENT live generators emit the dead gate stub; excludes gateTime/gateReach/vaultFromStage/bot.gate*
Boss (Thornback Brute) + Gatekeeper on floors STRUCTURALLY ABSENT no guard placement in live generators; unguarded:true; excludes gatekeeperRamp, extraction.guardsFromStep (see §11 design gap)
Safe exits + closing schedule + _finalSafe STRUCTURALLY ABSENT one progress pad; tilesPerExit/minSafe/maxSafe/closing* inert
Colored zones + leashes + stageSizes sizing zones-generator only port only if zones ever ports
Tunnel generator (tunnel.*, 23 keys) + zones generator selectable, not default do not port
Classic input: tap-to-move, double-tap sprint, swipe-dodge, tap-to-hunt, stealth-chase sprint, sprint & stamina UNREACHABLE action.enabled true consumes the pointer; stamina has no consumers
Gradual detection (fill, suspect, vignette, detectTime/detectProx*/bushDetectFill) UNREACHABLE aggroOnly true + tutorial parked
Mutual-sight fire / cover-drops-fire FLAG OFF mutualSightFire false
meleeToFireLock INERT requires !flankCrit
Melee lunge system (melee.*, meleeSequence) DEAD CODE no mob passes isLunger — authoring-only
Shield/overshield/cells · weak-point armor · valuables (valuableChance 0) · t1Valuables · floorGear · RTS mode · in-run inventory/Loot Station/bag cap · Safe Pocket (safeSlots 0) · manual Protect FLAG OFF config-gated on web; neither the systems nor their config values are ported (§1.4); Unity-side existing counterparts stay parked untouched
Storm finale + cataclysm + #stormOverlay · "Wildmagic stirs" FLAG OFF stormFinale/stirEvent false; corruption is a different system, not a renamed storm
Classic extraction ritual as primary RARE PATH live only on hand-cleared fields; port as the open-phase fallback, not the main flow
FTUE extraction tunables, coach, tutorial AI patterns (tutHold/tutScan/tutPatrol) INERT tutorial parked
Minimap enemy blips · in-world POI labels · small-screen zoom-out (fitMin) · board stall FLAG OFF / ZEROED
Dev tuning panel + balance preview + Defense/plain Sandbox benches dev-only per brief
Stale copy debt — do not port strings: 4 of 15 TIPS lines + all 4 RAID_INTRO lines + how-to "Rival raiders stay hidden" describe dead systems (rivals, closing exits, vault, safe-exit banking) live-but-wrong game.js:9218-9241, index.html:258 — rewrite for the pivot, don't translate verbatim

15. Parity traps & open items

  1. ex.siege is a tag, not a system — rename (isDefenseChamber) to avoid resurrecting the dead siege mentally.
  2. DEF_ZONES ids are meta-save keys — append-only forever, in Unity too.
  3. FENCE is a tile-map mutation with split semantics (solid to feet/pathing, transparent to sight/shots, player-passable) — in Unity: a movement-blocking layer the visibility/projectile raycasts ignore; gate build/death must invalidate the fog polygon.
  4. Config beats code fallbacks — §3.3 divergence list (chainRoomH/maxMouth/roomMaxH/ saturateEvery).
  5. stagesPerBiome = 5 — the "3-sub-stage" prose in config.js:17/749-750 is stale.
  6. No boss on live floors — DECIDED: fine as-is; bosses revisited later. Boss/gatekeeper EnemyDef assets stay in Unity, unused (§1.4).
  7. Warren reachability validation — DECIDED: add in Unity (§3.6b).
  8. Warren shape progression — DECIDED: no byBiome port; future lever = BiomeDef/floor-def assets (§3.6, §18).
  9. Multi-touch legacy leak (§6) — DECIDED: it is a bug; do not port.
  10. Dodge i-frames ≠ stealth window — acquisition blocked only ~0.06 s (§7.2); projectile immunity is the full invuln.
  11. Spikes endRun mid-update — the web returns out of the frame; structure the Unity update accordingly.
  12. defense.duration is next-call class — changing mid-hold-out desyncs elapsed vs timer.
  13. baseMove 110 / stickDead 0.15 / stick-radius space — the pre-existing flagged parity debts; resolve while porting §6.
  14. Charge-facing contract: cone, telegraph line, and shot must all read chargeAng — any drift breaks the honest-telegraph promise.
  15. Loot noise stays muted — keep chestCrack/chestPop/gateOpen as data if you like, but never wire them to recruitment.

16. Suggested implementation order

Dependency-driven; each phase ends with the §17 checks that apply. Phase 0 is §18 — the config landing map must be settled (it is, below) before phase 1 writes any field.

  1. Config foundation: everything per the §18 landing map — new GameConfig groups (Rooms, Defense, Corruption, ActionInput + new combat/stealth/fog/noise keys; NO groups for excluded systems, §1.4), value sync of §13, EnemyDef updates (visR/chargeDur sync, meleeCd field, nest flag + NestSettings + Enemy_nest, four defense-mob assets, DefenseWaveSet), MapDefinition/BiomeDef reshape (§18.4) + write the .assets.
  2. Map contract + room warren: REMOVE the zones MapGenerator/MapPopulator internals and rebuild on the warren plan (§3.2/§3.4), + the §3.6b validation pass + the Unity gen-smoke harness (§3.7). Authored counts replace zone budgets.
  3. Input: action config group, dash button, deadzone/radius parity.
  4. Detection/stealth/noise rewrite: binary DetectionModel, eyes memory, peace/war draw, flank crit, wave-based NoiseSystem, quiet looting.
  5. Enemy AI parity: dwell, hunted/atWar, weave, charged shots, swat commit, pack wake. (Lunge/raider/boss code untouched-parked per §1.4.)
  6. Nests (entity, wake, births, payout, exemptions).
  7. Crackables + timber + loot pipeline deltas (salve cap, channel arbitration).
  8. Corruption + no-clock run flow + endRun/ladder deltas.
  9. Extraction defense (arena, blockers, buildings, waves, spikes auto-extract, persistence, UI) — biggest single system, but everything above it is its substrate.
  10. HUD/camera polish (§12) + stale-copy rewrite.

17. Acceptance / validation

  • Generator parity (warren): Unity harness sweeps per §3.7 — zero reachability failures over ≥60 floors; room/nest/loot counts within authored bounds; chestInWall = 0; typeRuns = 0; timber totals exactly 60; nest rooms contain zero containers.
  • Config parity: every key named in this doc exists in GameConfig/defs with the web default; no magic numbers at use sites; diff the asset against config.js (the §13 table is the regression list).
  • Behavioral spot-audits (step 3 of the workflow): binary detection edges (bush at alert 0 only, sight-fairness 200 cap, reacquire cone, eyes-hold 3 s); draw timing 0.25+0.9 with restart rules; charge arcs per mob vs the derived table in §9; lastSeen arrival = route completion; wave budget/count preview vs defThreat/defCounts math for sessions 1-5; spikes sweep timing (spike at distance d fires at d/900); corruption front position at t = 5 s + k·dt vs the derived speed; endRun banking matrix (win/death × gear/currency/timber).
  • Commit coverage: Appendix A maps all 107 commits — any future re-audit starts there.

18. Unity config landing map — where every value lives (DECIDED before implementation)

Derived from the actual Unity data architecture (Config/GameConfig.cs 22 groups, EnemyDef.cs/EnemyDatabase.cs, BiomeDef.cs, MapDefinition.cs, ZoneTierDef.cs). Placement principles, matching the codebase's own conventions:

  • GameConfig = global rules & feel that never vary per floor/biome/creature.
  • EnemyDef = everything per-creature; per-special tuning as a *Settings struct behind a behavior flag (the existing lunge/fire/web/rocket/bomb pattern). rankAndFile=false + introStep is the existing gate that keeps special creatures out of the normal spawn pool.
  • BiomeDef = per-biome art, prefabs, density, difficulty band; owns floors[].
  • MapDefinition (floor/stage def) = the lean per-floor recipe — only what genuinely differs floor to floor.

18.1 New GameConfig groups

Group Keys Notes
Rooms width [40,54], door 2, total [7,9], roomW [7,13], roomH [5,9], sideChance 0.35, nests [2,2], loot [2,3], densityArea 16, bushDensity 0.03 + crateChance 0.5, barrelTime 0.35 (re-homed from web corridor.*, §3.4) Base warren shape. Per-floor/per-biome overrides come later via floor/biome defs (§18.4), not via a byBiome array.
Defense activation (activate 2.5, deactivate 1.4), timberBudget 60, duration 30, scheduler (startThreat 260, growth 1.16, trashShare 0.17, heavyShare 0.34, runnerOfMid 0.29, ramp 2.4, trashWin/midWin/heavyWin, sides1 0.25, sides2 0.58, packDiv 1.5), building stat blocks (base/tower/gate/barracks cost/hp/… arrays §4.4), restoreCostMult 0.6, engagePlayer 130, engageUnit 70, playerEngage 320, fogRadius 500, spikeSpeed 900, spikeStep 80, + the DEF_ZONES slot table (serialized list — append-only, ids are save keys) and DEF_CHAMBER/DEF_APRON/DEF_PAD constants Buildings stay in GameConfig (4 fixed kinds, no extensibility ask). Defense mob stats do NOT live here — they become EnemyDef assets (§18.2); the scheduler's role lineup lives on defs (§18.3).
Corruption enabled, delay 5, travelFrac 0.5, slowMult 0.6, dps 5, tick 1.0, warnDist 420, stopAtChamber true, killMobs true Global; reads only the chamber box from the map contract.
ActionInput stickRadius, stickDead 0.15, dashCd 0.9, dashFree true Moves JoystickInput component tuning into config (resolves the 0.12-vs-0.15 and radius-space debts — define the px mapping here once).

18.2 New keys into existing GameConfig groups

  • Combat: aggroOnly, flankCrit, peaceWar, plantTime 0.25, aimTime 0.9, engageRange 160, switchMargin 0.7, warReturnFire, bodyBlock, fightLinger 2, mobMeleeArc 1.75, patrolHold 1.2, chestDriftRange 340, turnMelee/Ranged/Heavy/Close/ CloseRange, chargeArc/chargeAimMargin/chargeTurn/chargeLockLead. (mutualSightFire and the gradual-detection keys are NOT added — excluded systems, §1.4.)
  • Stealth: eyesMemory 3, reacquireCone, hideLockedCones.
  • Fog: sightFairness true + value sync (radius 200, senseRadius 300).
  • Noise: waveSpeed 560, wallsBlock, recruitCap 2, hearRadius 1.5, hunterStepCd 0.5
  • retuned radii (§8). Muted loot-noise keys are NOT added.
  • Player: potionsMax 3, potionSwig 0.35 (+ zero hpRegen, potionChannelTime handling — Unity keeps its existing fields, values synced).
  • Move/MapGen/Loot/Extraction/EnemyScale/Mob: value sync only, per §13.
  • nest is NOT a GameConfig group — it lands on EnemyDef as a new nest behavior flag + NestSettings struct (the existing lunge/fire/web/rocket/bomb pattern): interval [3.5,6], liveCap 4, range 460, warZone true, tell 0.9, preSpawn [2,3], spawnDrops false, valuables [5,8], lootSafeR 160. One Enemy_nest asset at parity (hp 900, r 18, stationary, visR/visA 0, tier 3, rankAndFile false); themed nest variants later = more assets. The generator reads the def's NestSettings for seating/brood/lootSafeR; what the nest births is its nestDef.kind theme (asset reference on the nest def or the generator's theme pick).

18.3 Defense mobs → EnemyDef assets (per user decision)

The four wave creatures (swarm/grunt/runner/brute, web defense.enemies config.js:242-247) become normal EnemyDef assets in the existing EnemyDatabase — nothing blocks it:

web field EnemyDef field
hp hp
dmg atk
spd speed
r radius
atkCd new field: meleeCd (0 = fall back to global combat.mobMeleeCd) — the one addition; harmless for existing defs at 0

Guardrails, all via existing mechanisms: rankAndFile = false (never enters the normal roster/threat budget — same gate as boss/gatekeeper), melee = true, ranged = false, no introStep. They are driven by the defense controller, not EnemyController (separate lists, no stealth/noise/loot/onKill — §4.3); the EnemyDef is the data container only. Wave cost is computed from the def (hp × atk / meleeCd normalized to the trash entry) — defThreatCost parity for free, and a new mob type is just a new asset.

Wave lineup = data, not hardcoded keys: a small DefenseWaveSet SO — entries { enemyDef, stream: trash|mid|heavy, midWeight } — replaces the web's hardcoded swarm/grunt/runner/brute names. Resolution: MapDefinition.defenseWaves → (null) → BiomeDef.defenseWaves → (null) → the default set asset referenced from GameConfig.defense. Parity state: one shared default asset with the four web mobs (midWeight encodes runnerOfMid 0.29); shares/windows stay scheduler-level in GameConfig.defense. Adding enemy types per floor later = author a set asset on that floor's def.

18.4 MapDefinition / BiomeDef reshape

  • MapDefinition (floor def) is rebuilt for warren floors (its zones-era fields — size, zones[], hasVault — go with the removed zones generator, §1.3): keeps seed; gains optional defenseWaves (§18.3) and an optional rooms-shape override block (nullable per-key, the companion spec's Optional<T> pattern — empty at parity). This is the "separate config per level" slot the design asked for.
  • BiomeDef stays the biome unit: floors[] (5 MapDefinition assets per biome — stagesPerBiome 5), tile art/tileSet, difficulty band (dangerBase/dangerStep/lootMul — already the web bDanger math), bush wind/shake; gains optional defenseWaves and an optional rooms-shape override (biome-level, floor-def wins). New biome-swappable prefab slots following the existing chestPrefab pattern: crate, keg, timber pile, nest.
  • Resolution rule (per companion spec §3): resolve base + biome + floor overrides ONCE per floor generation and hand the same resolved object to plan AND populate.
  • Left parked untouched (§1.4): ZoneTierDef/ThemeDef assets, BiomeDef's zones-era bestiary fields (tierRoster, keykeeper*, descentGuardKey/bossGuardKey, rivalDiv/ rivalCap, vault*), boss/gatekeeper/raider EnemyDef assets, Weapons archetypes, WorldEvents/Shake groups. Unused ≠ deleted.

Appendix A — commit coverage map (6d92290^..ebd6c39, 107 commits)

Disposition: §n = ported via that section · EXCL = excluded (§14) · SUP = superseded by a later landed commit (port the successor) · DOCS = documentation-only · MERGE.

Commits Disposition
ebd6c39 0484262 MERGE
c66ee8e nests corner-seat / tutorial parked / melee outranks loot §3.5 §10 §5.2 + EXCL (tutorial)
2564223 spikes auto-extract §4.2
7dac6d0 corruption 0.5 roundTime / nest births rock §11 §10
0ab609b cut dive / tuning reorg EXCL (dive removed; panel dev-only)
cd31f46 defense replaces fort+siege §4 (+ EXCL fort/siege)
3a614f1 lastSeen parity / visR retune §9 §13
df3824c nest wake/targeting/intents §10 §7.4 §5.2
16ec2cc muted rings / hunted-atWar / one crack channel §8 §7.4 §5.2
24ba678 warren de-ghosted §3.4
e24e266 rooms generator (default) §3.4
3ed486d gen-smoke harness §3.7 §17
ee393c6 room-chain generator §3.3
95a59f6 corridor content pass §3.3
644e3fd fort-bar lane / safe pocket off / baseMove 120 §12 (lane) + EXCL (safe pocket) + SUP (baseMove→110)
6910b34 53c2c0d f2f7106 a0d4617 a0b7546 200fd77 a91b9b6 7935885 fort bar / fort UX / fort sandbox / fences SUP by cd31f46 (gate rendering descends into §4.4)
b732009 fences solid-to-body transparent-to-sight §4.4 (FENCE semantics survive in the Gate)
d1576aa Last Stand P0: timber + fort §5.4 (timber) + SUP (fort)
5e24ce1 stealth/noise/nest overhaul §7 §8 §10
be06253 corridor master level / corruption / siege groundwork §3 §11 + SUP (siege)
c977075 disable rivals EXCL
718b731 b257e0b d5abc67 tunnel generator EXCL
b3e5608 tunnel audit · 2639fb3 experimental review · 914ac04 e91d688 7e2cb9d dive docs · ce4f6f0 6d92290 pivot docs · b30dd38 e846842 G0 checklist DOCS
4cfe0da death card safe pocket §12 (ports; dormant at safeSlots 0)
ffbefa4 d554ff3 d148321 7a2f76f 231d3f5 d1a8644 charged shots §9
68a56fa 17aec3d chargeLock comment scrubs DOCS
cc0e932 salve-cap magnet §5.3
a8279bb salve cap / auto safe pocket / economy re-slope §5.3 §5.6 + EXCL (safe pocket off)
89acceb b6b186b a22774f 43468e2 bec6226 dive implementation EXCL (removed)
5a90b5d G0+ combat tuning §13
9a4ee99 shared bush = mutual sight §7.2 (sameBush)
bcfd223 melee swat commits §9
df9dafa keybearer fix / rival ring EXCL (rivals/vault)
8b45505 patrol dwell §9
ef2400e alerted cone reads hot §12
6c51b88 df69235 fea8f69 3692ab1 607febe rival Tier-0 / MARK / fair looting EXCL (rivals)
d1310f8 0225caf fc5a6b5 dde0de7 2187430 FTUE restructure EXCL (tutorial parked)
ff3fbe1 engage ring peace-only §7.4 §12
cd8f08b camera lean to aim · dc57090 stick look-ahead · c1cb05c f0e9260 lead tunables §12
dd8fe32 peace opener wind-up · c4682f8 Peace/War §7.4 (+ SUP stalk-hold)
7621130 ad54ddb stalk-hold iterations SUP by c4682f8 (plant-dwell rule survives via steerHeld)
90f7fc6 G0 verdict (stalk-hold, rival colors, Opportunist, swig) §5/§13 (swig) + EXCL (rest)
865edd9 fog 300 / echo 420 SUP by 3a614f1 (now 200/300)
dd0da3f G0 feel (dash, zoom floor, camera) §6 §12
9f2b948 closing pressure floors 1-3 EXCL (closings inert on live floors)
621425b remove mobReact · 561f6f9 remove frozen cone · bd9a749 frozen cone toned EXCL (removed)
360e2ea travelling sound waves (+startle) §8 (waves) + EXCL (startle removed)
6268bab stance force-board (Portal.cs parity) §4.2 (open fallback path)
ae076c8 Phase-0 plan audit §6 §7 misc
79aaa42 raid 600→330 SUP (now 300) §13
4d13a86 review batch (closures, point-blank whip, wind-up cues) §9 + EXCL (closures)
7621255 stealth-lite binary bushes · 3567f45 aggro-only · 82b1bde engageRange · 5616a9b engage ring · 7b13190 gradual facing · 4121985 solid-body dash + stab opener · 25a77ca solid bodies (+ EXCL crossfire/flee) · d2cfe38 flank-crit windows (+ dead lunge) §7 §9
bcaf62c guarded extraction ramp EXCL (guards off on defense floors)
4f18501 probes 0.7-0.9+0.13 (floor gating, Raid Again, loot density, instant potion) §5 §11 §12 §13
416cc5f probes 0.4-0.5 (crack ring, vault door) §5.2 + EXCL (vault)
b47d36b probes 0.3+0.11 (parity input, proximity interactions) §5.2 §6
2df3802 probes 0.1-0.2 (joystick + Action Button) §6