Skip to content

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 — a Func<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)

  1. 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.
  2. 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.
  3. Key item — dropped by the Keykeeper on death; auto-picked-up by walking over it → RaidState.PlayerHasKey.
  4. Keykeeper — a buffed warden (+hp/+atk, dropsKey), leashed near the vault, posted outside the door.
  5. 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.
  6. Vault prize chest — vault-tier rarity (best open zone +1, capped legendary), a guaranteed gear piece.
  7. 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, web genMap:636, updateBot) — complex rival behaviour; its own slice (3b).
  • Stage boss + progression/descent portal (stageBoss/bossDown gating 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 a MapDefinition.hasVault flag stands in.

3. Data + state

  • MapDefinition gains: bool hasVault, ZoneTierDef vaultTier (the prize tier; explicit per-map since the biome-ladder "best zone +1" rule is deferred), string keykeeperKey = "warden".
  • MapData gains a VaultGate record: { Tx, Ty, Vector3 World, Rect RoomBox, bool Open } (null when no vault) + the vault chest reference is just a normal Chest flagged IsVault.
  • RaidState gains bool PlayerHasKey and MapData.VaultGate access for the seal predicate.
  • Chest gains bool 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 Chest at the room centre: rarity = def.vaultTier.rarity, isVault = guaranteeGear = true.
  • Spawn the Keykeeper (def.keykeeperKey, buffed via bonusHp/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. Wire PathAgent.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 through Los/MapData.IsWall-equivalent (a closed gate reads as wall).
  • Force-open (web 1845-1869): in Update, if the player is within gateReach of the gate, idle (no path), and RaidState.PlayerHasKey: advance Progress += dt/gateTime, stamp the player channel (gold ring on the vitals, reuse PlayerChannelFrame/Progress/IsExtract=false), pulse chestCrack noise on chestCd; at Progress>=1Open=true, emit gateOpen (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 = true and despawns the key (web key drop damage:2377 + pickup).
  • Keykeeper death → drop hooked from EnemyController.OnDied when def.dropsKey (new EnemyDef.dropsKey flag 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.Update reading RaidState.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 hasVault MapDefinition → assert MapData.VaultGate exists, 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)

  1. Data: MapDefinition.hasVault/vaultTier/keykeeper*, MapData.VaultGate, RaidState.PlayerHasKey, Chest.isVault/guaranteeGear, EnemyDef.dropsKey.
  2. MapGenerator: vault room gen + VaultGate record.
  3. MapPopulator: prize chest + Keykeeper + room no-spawn.
  4. Gate component: pathing + LOS seal (wire PathAgent.SealedTile + LOS).
  5. Gate force-open channel (key-gated, noise, decay, boom) + closed-gate visual.
  6. KeyPickup + Keykeeper key-drop on death + PlayerHasKey.
  7. Vault prize chest guaranteed-gear roll (LootRoller).
  8. Faithfulness audit + fixes.

9. Risks

  • Map vault asset: author one Map_vault definition (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).