Vault System — Design (Unity port, slice 3)¶
Date: 2026-06-26 Status: Approved-by-author (faithful port; web is the design authority). Proceeding autonomously per standing instruction. Migration workflow: web context → configs/defs (no magic numbers) → audit vs web.
1. Goal & context¶
Port the web prototype's Vault — the map's high-risk apex objective. A locked walled room holds a
guaranteed top-tier prize; the only way in is to hunt the roaming Keykeeper for the 🔑 key, then
force the locked gate open (a loud, exposed channel). Faithful to game.js (vault gen genMap:553-585,
gate interaction updateInteractions:1845-1869, pathfinding seal findPath:1176/losTiles:1238, key drop
damage:2377).
This builds on the slice-1 zone generator (zones placed via PlaceCenter, populated by MapPopulator).
What already exists (hooks)¶
GameConfig.map.gateTime(5s force-open) +gateReach(56px) — deferred, ready to use.GameConfig.noise.gateOpen+chestCrack+chestCd— gate noise.GameConfig.prog.vaultFromStage(2) — the stage the vault first appears.PathAgent.SealedTile— aFunc<int,int,bool>predicate already stubbed for "the closed-vault-gate seal".Chest(rarity + crack/spill),LootRoller.RollChestLoot,EnemyDef/leashing,MapData.Zones.
2. Scope¶
In scope (the core vault loop)¶
- Vault room generation — a walled room with a single gate tile, placed on a vault-enabled map; a no-spawn bubble over the interior; the prize chest at centre; the Keykeeper posted at the gate approach.
- Gate — a tile that, while closed, blocks pathing + LOS (the seal); a proximity/tap force-open channel (needs the key) that pulses noise and booms open; renders a closed-gate visual.
- Key item — dropped by the Keykeeper on death; auto-picked-up by walking over it →
RaidState.PlayerHasKey. - Keykeeper — a buffed warden (
+hp/+atk,dropsKey), leashed near the vault, posted outside the door. - Force-gate interaction — player near the closed gate + idle + has key → channel over
gateTime(drawn on the player's vitals like a chest crack); no key → a "need a key" nudge. - Vault prize chest — vault-tier rarity (best open zone +1, capped legendary), a guaranteed gear piece.
- MapDefinition vault flag — a per-map
hasVault(+ vault tier rule) so the generator knows to build it.
Out of scope (separate follow-ups)¶
- keyRacer rival AI (a rival that hunts the Keykeeper + races the gate,
bot.keyHuntDelay/gateTimeMult/dropKeyAt, webgenMap:636,updateBot) — complex rival behaviour; its own slice (3b). - Stage boss + progression/descent portal (
stageBoss/bossDowngating a descent exit) — a separate web mechanic from the vault; defer (slice 4 territory). - The "key can never leave the map" rule (rival drops key on extract) — depends on keyRacer.
- Biome ladder driving
vaultFromStage— slice 4; here aMapDefinition.hasVaultflag stands in.
3. Data + state¶
MapDefinitiongains:bool hasVault,ZoneTierDef vaultTier(the prize tier; explicit per-map since the biome-ladder "best zone +1" rule is deferred),string keykeeperKey = "warden".MapDatagains aVaultGaterecord:{ Tx, Ty, Vector3 World, Rect RoomBox, bool Open }(null when no vault) + the vault chest reference is just a normalChestflaggedIsVault.RaidStategainsbool PlayerHasKeyandMapData.VaultGateaccess for the seal predicate.Chestgainsbool isVault+bool guaranteeGear(the prize is a guaranteed gear roll).
4. Components / flow¶
MapGenerator — vault room (web genMap:553-585)¶
When def.hasVault: PlaceCenter(vhw+1, vhh+1) (vhw=vhh=3 → 7×7 room), clearArea, draw a wall ring, punch a
single FLOOR gate tile on a random side; record MapData.VaultGate{tx,ty,world,roomBox,open:false}. The
room interior is recorded so the populator leaves it empty (no-spawn bubble) except the prize chest.
MapPopulator — vault contents¶
- Spawn the prize
Chestat the room centre:rarity = def.vaultTier.rarity,isVault = guaranteeGear = true. - Spawn the Keykeeper (
def.keykeeperKey, buffed viabonusHp/a new key-keeper buff) outside the gate on the approach side; leash it to a large box around the vault;dropsKey = true. - Skip all other spawns inside the room box (extend the existing zone/spawn-safe avoidance with a room check).
Gate (new World/Gate.cs) — the seal + force-open¶
- Seal: while
!Open, the gate tile is solid to pathing + LOS. WirePathAgent.SealedTile(player + every enemy/raider agent) to(tx,tz) => gate != null && !gate.Open && tx==gate.Tx && tz==gate.Ty. Also gate the player's LOS/auto-fire throughLos/MapData.IsWall-equivalent (a closed gate reads as wall). - Force-open (web 1845-1869): in Update, if the player is within
gateReachof the gate, idle (no path), andRaidState.PlayerHasKey: advanceProgress += dt/gateTime, stamp the player channel (gold ring on the vitals, reusePlayerChannelFrame/Progress/IsExtract=false), pulsechestCracknoise onchestCd; atProgress>=1→Open=true, emitgateOpen(loud), un-seal pathing. No key → a throttled "need a key" cue. Step away → progress decays (web ×0.6). - Visual: a closed-gate marker (a barred/locked look) that clears when open. Minimal: a Shapes/dim quad on the gate tile while closed (mirrors a wall); on open, hide it. (Full art later.)
KeyPickup (new World/KeyPickup.cs) — the key item¶
- The Keykeeper, on death, drops a key (a small world object) at its position. Walking within pickup range
sets
RaidState.PlayerHasKey = trueand despawns the key (web key dropdamage:2377+ pickup). - Keykeeper death → drop hooked from
EnemyController.OnDiedwhendef.dropsKey(newEnemyDef.dropsKeyflag set by the populator, or a controller flag set at spawn).
Player interaction¶
- The player already routes taps to interactables (chests/portals). The gate force-open is proximity-driven
(web: idle near the gate auto-channels), so it lives in
Gate.UpdatereadingRaidState.Player— no new input wiring (matches the chest model).
5. Config (no magic numbers)¶
Un-defer + use map.gateTime, map.gateReach, noise.gateOpen. The vault rarity rule (best-zone+1) is
deferred → MapDefinition.vaultTier is authored explicitly. Keykeeper buff (+120hp, +8atk in web) →
expressed via the existing EnemyController.bonusHp/bonusAtk (converted to multipliers) or a small
MapDefinition.keykeeperBonusHp/Atk. Pickup range → a map.* or a small const cited from web.
6. Faithfulness audit (step 3)¶
Diff vs game.js: vault room size/wall-ring/random-side gate (553-585); gate channel (dt/gateTime, noise
pulse, decay ×0.6, open boom) (1845-1869); seal in pathing + LOS (1176/1238); Keykeeper buff + leash + key
drop (582-584, 2377). Note deferrals (keyRacer, boss, biome-ladder vault gating).
7. Verification (Unity MCP)¶
- Generate a
hasVaultMapDefinition → assertMapData.VaultGateexists, the room is walled with exactly one gate floor tile, the prize chest is at centre with vault rarity +isVault. - Pathing seal: a path from outside to inside the closed vault fails; opening the gate makes it succeed.
- Play: kill the Keykeeper → key drops → walk over it →
PlayerHasKey; stand at the gate → channel fills →Open; the prize chest cracks to vault-tier loot. Zero errors.
8. Task decomposition (for the plan)¶
- Data:
MapDefinition.hasVault/vaultTier/keykeeper*,MapData.VaultGate,RaidState.PlayerHasKey,Chest.isVault/guaranteeGear,EnemyDef.dropsKey. MapGenerator: vault room gen +VaultGaterecord.MapPopulator: prize chest + Keykeeper + room no-spawn.Gatecomponent: pathing + LOS seal (wirePathAgent.SealedTile+ LOS).Gateforce-open channel (key-gated, noise, decay, boom) + closed-gate visual.KeyPickup+ Keykeeper key-drop on death +PlayerHasKey.- Vault prize chest guaranteed-gear roll (
LootRoller). - Faithfulness audit + fixes.
9. Risks¶
- Map vault asset: author one
Map_vaultdefinition (large,hasVault, vaultTier=legendary) for the tester stage cursor. - LOS seal: the player's auto-fire + enemy perception use
Los/MapData; the gate must read as wall there too, not just A*. Confirm the seal covers both (a single "is this tile currently solid" helper). - Connectivity: the vault is meant to be unreachable until opened; ensure the player spawn + objectives are still reachable without it (the spawn already avoids zones; the vault is just one sealed room).