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): P168042ceconfig foundation · P23e7dd18room-warren generator · P3c6a2fffaction input/dash · P4fb3c308binary detection + peace/war + noise waves · P523031daenemy AI parity · P65e00204nest runtime · P7b361910timber/salve-cap/crack-channel · P833be398corruption + no-clock flow · P9b8656dbextraction 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-arrowarrowsFromStepgating, cameraleadSteer/leadFight/leadSmoothvalue 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.jsat HEAD. All line numbers cite HEAD. Supersedes: the feature-inventory/status sections (§0, §4, §5, §10) ofweb-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(nocorridor.*shape keys, norival.*/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/gatekeeperEnemyDefassets (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_Hmutable,MAP_CAP = 120×120 = 14400buffer ceiling;setMapSizeshrinks only (game.js:13-23). Tile idsFLOOR 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 helperstRange/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.buildBushClusters→bushId/sameBush(game.js:3030-3056) — 4-connected flood fill of BUSH, run innewRunfor 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:trueis only the marker that this is the defense chamber pad (defEx()game.js:6539) — rename toisDefenseChamberon port; the old siege system is gone.mouths = {L,R,T,S}: three mob doorways (spawn,entry2 tiles inside,door+aprontile rects) +S, the player's single entrance from below (door rect only).corridorkey = 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 onlycorridor.chamber(world-px box, game.js:479-482); HUD hint gating (game.js:9185).gateis a dead stub{x:-9999,y:-9999,tx:-1,ty:-1,open:false}(no vault on live generators).zonesalways[].- Post-gen passes in
newRunfor every style: 3×3 BUSH→FLOOR clearing around everynestDefenemy (game.js:395-403);buildBushClusters;map.legendsnapshot (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 thancorridorWretries 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 atcw+2and cap atlinkLen.max— long 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; heightchainRoomH [5,9], orlongRoomH [10,14]atlongRoomChance 0.2. Top door re-aimed inside the room span (the room's diagonal). Dress rollchainDress 0.75→pick(loot,loot,pillars,grove,guard)else plain; dressed rooms becomeways(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+nNestfrom 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 withMARGIN 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 widthmouthWFor(area) = area>=roomBigArea(90) ? 3 : 2. Flank alternation viasideFlip. Run rooms: stub offset 2..maxMouth 3,rw ≥ 6or skip, heightclamp(area/rw, 5, roomMaxH 14); mouth center keepsroomSpawnGap 900px from spawn. Elbow rooms (roomElbow 0.5preference roll) sit in jog pockets, vertical mouth into the landing;roomTwinDoor 0.35rolls a 2-row second door into the adjacent run (twin attempts first 9 tries, then plain elbow). Sizes:roomArea [60,110]× aspectpick(0.6,0.75,0.9,1.0,1.1,1.3,1.6), clamped toroomSize [6,14]. - Depth-band scheduling: slots (runs + jogs) sorted spawn-end first, partitioned into
wantRoomsbands, ≤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 viasafeAnchor(first row ≥spawnSafe+TILEfrom spawn — spawn-safe can't delete the mini-boss); elite = tier-2 pick,eliteHp 1.6(damage NOT scaled), escorteliteEscort [1,2]trash, one sharedpackId. Per run:packs [1,1]×packSize [2,3]trash; anchors keeppackGap 620px 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 withgearBias = loot.caseGearChance 0.7, garrison always (roomGuard 1.0) on budgetroomGuardBudget 3 + step×0.30viarollRoster; guards leashed pad-0 (never leave the room until triggered), rejected only withinspawnSafe×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.045seeds ×1-3; side rooms singles atmap.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 leavingpinchGap 2walkable, shoulder bush opposite. Saturation, links only: everysaturateEvery 5rows 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 ofrooms.total [7,9]roomsroomW [7,13] × roomH [5,9]; direction up, or sideways atsideChance 0.35— never two sideways in a row; up-steps overlap ≥door+2with one wall row between and arooms.door 2door 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 toplan.rectsand 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 validated —
genRoomsMapruns NO pathfind check;tools/gen-smoke.jsis 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 budgetroomGuardBudget 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 ifbud≥3). Nest room: §3.5. All scatter goes throughinRoomclamp thennearestFloor. 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,
nearestFloorback 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.nestrow (hp 900, atk/speed/visR/visA 0, r 18, stationary), themedspider|goblin|emberball∩ available roster; carriesnestDef {kind},nestRarity(room depth rarity),nestRoom(world-px box), the room'spackId, hard leash.MOB_TIER.nest = 3; deliberately absent frommobIntro— 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 withinlootSafeRof 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 bygame.js— portgame.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 → defPortalOpen →
open = 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.35 → 1.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 stabReady — melee 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 56px,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 500peak /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 withinfightLinger 2OR def-mobs alive OR any non-nest enemy locked with eyes/target on you.G.atWar= hunted OR a woken nest withwarZonewithinnest.range 460with 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), thenaimTime 0.9aim 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 160gates openers. StickinessswitchMargin 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 viainBlindSpot(mobs: outside the drawn cone —backstabFromFov; elsebackstabArc 0.55); ×(critBase 1.5+ crit/100).meleeToFireLockinert. - Backstab is alive and primary: passive
autoBackstabPlayer(nearestbackstabReady) — 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, hitstopbackstabHitstop 0.13, silence windowsilenceWindow 3→ tinynoise.backstab 60ring on the kill instead ofdeathMob 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,wallsBlockrequires LOS, stop atrecruitCap 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 2per 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/STALE — EnemyController 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 endrand(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 =
chargeAngwhile 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).combatMoveweave: 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 (
alertPackgame.js:6199, throttled 1.2 s per spotter, full-spot only): authored packs rally only their ownpackIdwithin 520 px; un-packed mobs use radius 380 (pack:true) / 250. AnestDefpack-mate gets_woken— the guardians are the nest's eyes. - Charged shots (game.js:5310-5365): commit gate =
fireCd ≤ 0AND 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 lastchargeLockLead 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.fireCdinitrand(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 webmelee.*values over it; note web'sgame.js:5745omitsatkCdMulif 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
packIdor_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, theninterval [3.5,6];liveCap 4counts live brood + pending tells; birth point sampled ≤12 times insidenestRoom(inset 12 px) rejecting solid tiles (births can't land in rock — rooms roll pillars/outcrops), fallback nest position; tellnest.tell 0.9via 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 = falseon corridor-family floors — no clock death, no exit closings;G.timefrozen;map.roundTime 300survives only as the corruption pacer.- Corruption (game.js:6459-6490): after
delay 5s the front climbs fromWORLD_HtowardstopY = chamber.y1(stopAtChamber true) overtravelFrac 0.5 × roundTime 300 = 150 s(speed derived each frame — live-retunable). Inside: ×slowMult 0.6,dps 5intick 1.0bites throughdamage()(re-entry restarts the tick); one-shot warn banner atwarnDist 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 iskind:"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),
autoEquipEmptyfills 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:
#defPopupbottom-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-worldpoiLabels false. Purple screen wash while inside corruption. Threat arrows fromarrowsFromStep 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). UnityCameraRigowns 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¶
ex.siegeis a tag, not a system — rename (isDefenseChamber) to avoid resurrecting the dead siege mentally.DEF_ZONESids are meta-save keys — append-only forever, in Unity too.- 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.
- Config beats code fallbacks — §3.3 divergence list (chainRoomH/maxMouth/roomMaxH/ saturateEvery).
stagesPerBiome = 5— the "3-sub-stage" prose in config.js:17/749-750 is stale.- No boss on live floors — DECIDED: fine as-is; bosses revisited later. Boss/gatekeeper
EnemyDefassets stay in Unity, unused (§1.4). - Warren reachability validation — DECIDED: add in Unity (§3.6b).
- Warren shape progression — DECIDED: no
byBiomeport; future lever =BiomeDef/floor-def assets (§3.6, §18). - Multi-touch legacy leak (§6) — DECIDED: it is a bug; do not port.
- Dodge i-frames ≠ stealth window — acquisition blocked only ~0.06 s (§7.2); projectile
immunity is the full
invuln. - Spikes endRun mid-update — the web
returns out of the frame; structure the Unity update accordingly. defense.durationis next-call class — changing mid-hold-out desyncs elapsed vs timer.baseMove 110/stickDead 0.15/ stick-radius space — the pre-existing flagged parity debts; resolve while porting §6.- Charge-facing contract: cone, telegraph line, and shot must all read
chargeAng— any drift breaks the honest-telegraph promise. - Loot noise stays muted — keep
chestCrack/chestPop/gateOpenas 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.
- Config foundation: everything per the §18 landing map — new
GameConfiggroups (Rooms, Defense, Corruption, ActionInput+ new combat/stealth/fog/noise keys; NO groups for excluded systems, §1.4), value sync of §13,EnemyDefupdates (visR/chargeDur sync,meleeCdfield,nestflag + NestSettings +Enemy_nest, four defense-mob assets,DefenseWaveSet),MapDefinition/BiomeDefreshape (§18.4) + write the.assets. - Map contract + room warren: REMOVE the zones
MapGenerator/MapPopulatorinternals 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. - Input: action config group, dash button, deadzone/radius parity.
- Detection/stealth/noise rewrite: binary DetectionModel, eyes memory, peace/war draw, flank crit, wave-based NoiseSystem, quiet looting.
- Enemy AI parity: dwell, hunted/atWar, weave, charged shots, swat commit, pack wake. (Lunge/raider/boss code untouched-parked per §1.4.)
- Nests (entity, wake, births, payout, exemptions).
- Crackables + timber + loot pipeline deltas (salve cap, channel arbitration).
- Corruption + no-clock run flow + endRun/ladder deltas.
- Extraction defense (arena, blockers, buildings, waves, spikes auto-extract, persistence, UI) — biggest single system, but everything above it is its substrate.
- 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 againstconfig.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/defCountsmath 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*Settingsstruct behind a behavior flag (the existing lunge/fire/web/rocket/bomb pattern).rankAndFile=false+introStepis the existing gate that keeps special creatures out of the normal spawn pool.BiomeDef= per-biome art, prefabs, density, difficulty band; ownsfloors[].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. (mutualSightFireand 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(+ zerohpRegen,potionChannelTimehandling — Unity keeps its existing fields, values synced).Move/MapGen/Loot/Extraction/EnemyScale/Mob: value sync only, per §13.nestis NOT a GameConfig group — it lands onEnemyDefas a newnestbehavior flag +NestSettingsstruct (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. OneEnemy_nestasset 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'sNestSettingsfor seating/brood/lootSafeR; what the nest births is itsnestDef.kindtheme (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): keepsseed; gains optionaldefenseWaves(§18.3) and an optional rooms-shape override block (nullable per-key, the companion spec'sOptional<T>pattern — empty at parity). This is the "separate config per level" slot the design asked for.BiomeDefstays the biome unit:floors[](5MapDefinitionassets per biome —stagesPerBiome 5), tile art/tileSet, difficulty band (dangerBase/dangerStep/lootMul— already the webbDangermath), bush wind/shake; gains optionaldefenseWavesand an optional rooms-shape override (biome-level, floor-def wins). New biome-swappable prefab slots following the existingchestPrefabpattern: 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/ThemeDefassets,BiomeDef's zones-era bestiary fields (tierRoster,keykeeper*,descentGuardKey/bossGuardKey,rivalDiv/rivalCap,vault*), boss/gatekeeper/raider EnemyDef assets,Weaponsarchetypes,WorldEvents/Shakegroups. 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 |