# IStalk — Master Architecture

## Document Status

- Version: 1.0
- Last Updated: 2026-06-23
- Engine: Unity 6 (URP 17.3) + FishNet v4 + Synapse UDP
- GDDs Covered: sys-01-item-database, sys-01-runner-movement, sys-02-eye-camera, sys-04-weight-burden, sys-05-geometric-los, sys-06-eye-laser, sys-09-loot-extraction, sys-10-procedural-map-generator, sys-11-prop-system, sys-12-room-prefab-framework, sys-20-relay-transport, sys-21-lobby-matchmaking, sys-22-match-state-machine, sys-26-runner-hud, sys-27-eye-hud, sys-33-fishnet-replication
- ADRs Referenced: ADR-0001 (Relay Transport), ADR-0002 (Scene Architecture), ADR-0003 (SO Loading), ADR-0004 (Authority Model), ADR-0005 (Prediction Pattern), ADR-0006 (Physics Layers), ADR-0007 (HUD uGUI)
- Technical Director Sign-Off: Pending
- Lead Programmer Feasibility: Pending

---

## Engine Stack

**Engine:** Unity 6 (URP 17.3) + FishNet v4 + Synapse UDP  
**Cinemachine:** 3.1.7 (installed via Package Manager)  
**FishNet:** v4 source at `Assets/FishNet/` — all APIs directly readable  
**Input System:** Unity Input System (installed)  

All APIs are available in the project source or via packages. No unknowns.

---

## System Layer Map

```
┌──────────────────────────────────────────────────────────────────┐
│  PRESENTATION LAYER                                               │
│  SYS-26 Runner HUD · SYS-27 Eye HUD                             │
│  AudioManager (service) · VFXManager (service)                   │
├──────────────────────────────────────────────────────────────────┤
│  FEATURE LAYER                                                    │
│  SYS-06 Eye Laser · SYS-09 Loot & Extraction                    │
│  SYS-11 Prop System · SYS-12 Room Prefab Framework              │
│  WorldItemSpawner                                                 │
├──────────────────────────────────────────────────────────────────┤
│  CORE LAYER                                                       │
│  SYS-22 Match State Machine · SYS-21 Lobby/Matchmaking          │
│  SYS-01 Runner Movement · SYS-04 Weight/Burden                  │
│  SYS-02 Eye Camera · SYS-05 Geometric LoS                       │
│  SYS-10 Procedural Map Generator · PlayerSpawner                 │
├──────────────────────────────────────────────────────────────────┤
│  FOUNDATION LAYER                                                 │
│  SYS-14 Item Database · SYS-20 RelayClient                      │
│  SYS-33/20 RelayTransport · GameStartData · GlobalRules_SO      │
│  FishNet NetworkManager (singleton)                               │
├──────────────────────────────────────────────────────────────────┤
│  PLATFORM LAYER                                                   │
│  Unity 6 (URP 17.3) · FishNet v4 · Synapse UDP                 │
│  WebSocket library · DOTween · Cinemachine 3 · Input System     │
└──────────────────────────────────────────────────────────────────┘
```

**Layer rules:**
- Each layer may only depend on layers below it — no upward references.
- Foundation → Core → Feature → Presentation is the only allowed direction.
- Cross-layer communication uses events (`OnPhaseChanged`, `OnMatchEnded`) not direct method calls.
- Exception: Presentation reads Core/Feature state via per-frame polling — acceptable since Presentation is a leaf layer.

---

## Module Ownership

### Foundation Layer

| Module | Owns | Exposes | Consumes |
|--------|------|---------|---------|
| **ItemDatabase** | SO dictionaries (loot, shrine, traps, rooms) | `GetLootItem(id)`, `GetShrineItem(id)`, `GetTrap(id)`, `GetRoom(id)`, `GetRoomsWithLootSlots()` | `Resources.LoadAll<T>()` — Unity platform |
| **GlobalRules_SO** | All tunable match constants, `WeightTierConfig`, `MovementConfig`, `ExtractionConfig`, `EyeLaserConfig`, `EyeCameraConfig` | Public fields/properties; `GetTier(kg)`, `GetSpeedMultiplier(tier)`, `GetDreadMultiplierForTimer()` | Nothing |
| **RelayClient** | WebSocket connection state, JSON lobby protocol, binary game packet routing, `LocalPlayerId`, `HostPlayerId`, relay→FishNet ID map | Events: `OnLobbyState`, `OnGameStart`, `OnCountdown`, `OnGamePacket`, `OnError`, `OnDisconnected`; Methods: `ConnectAsync()`, `CreateRoom()`, `JoinRoom()`, `SetReady()`, `StartGame()`, `SendGamePacket()` | WebSocket library, `GlobalRules_SO` relay URL |
| **RelayTransport** | Synapse→relay bridge (Option A: Synapse WebSocket mode / Option B: custom `RelayTransport : Transport` class — see ADR-0001) | FishNet `Transport` API surface | `RelayClient.SendGamePacket()`, `RelayClient.OnGamePacket` |
| **GameStartData** | `game_start` message cross-scene data | `Current` (static property), `Set()`, `Clear()` | Nothing (pure static class) |

### Core Layer

| Module | Owns | Exposes | Consumes |
|--------|------|---------|---------|
| **GameManager** | `MatchPhase` SyncVar, match timer, alive/extracted runner counts, win condition logic | `OnPhaseChanged`, `OnMatchEnded`, `OnAltarsActivated`; `NotifyRunnerKilled(conn)`, `NotifyRunnerExtracted(conn)`, `NotifyAllSpawned(count)`, `NotifyExtractionAttemptStarted()`; Properties: `Phase`, `MatchTimer`, `IsGrace`, `CurrentDreadRegenMultiplier` | `GlobalRules_SO`, FishNet lifecycle |
| **LobbyManager** | Pre-game relay flow, scene load sequencing, FishNet start orchestration | `OnDisplayStateChanged`; all relay action wrappers | `RelayClient`, `GameStartData`, Unity `SceneManager` |
| **PlayerSpawner** | Server-side `NetworkObject` spawning, relay→FishNet ID registration | Internal (called automatically on all-clients-connected) | `GameStartData`, `RelayClient`, FishNet `ServerManager`, Runner/Eye prefabs |
| **RunnerController** | Movement state machine, input processing, FishNet prediction, `CharacterController` | `IsAlive` SyncVar, `IsChanneling` SyncVar, `IsInvulnerable` property, `AirDashAvailable`, `DoubleJumpAvailable`; Events: `OnLanded`, `OnJumped`, `OnDashed` | `WeightComponent` (tier), `GameManager.Phase`, FishNet `TimeManager` |
| **WeightComponent** | Current weight (float), current tier (`WeightTier`), server-authoritative mutation | `CurrentWeight` SyncVar, `CurrentTier` SyncVar; `AddWeight(kg)`, `RemoveWeight(kg)`, `ApplyWeightDelta(kg)` | `GlobalRules_SO` (tier thresholds) |
| **EyeCameraController** | Eye pan position, FOV zoom, Grace overlay alpha, cursor world-space position | `CursorWorldPosition` property; `ApplyLaserFireShake()`; `BeginIncarnationTransition(prop)`, `EndIncarnationTransition()` | `GameManager.OnPhaseChanged`, Eye input reader, Cinemachine 3 |
| **LoSManager** | Per-Runner visibility state at 30Hz, pre-cull by distance + frustum | `IsRunnerVisible(runner)` (synchronous, server-side, re-performs raycast); `_isVisible` SyncVar on each `RunnerController` | Eye camera mount `NetworkTransform`, `RunnerController` positions, `Physics.Raycast` |
| **MapGenerator** | Seed-based procedural room layout, room instantiation, content population (loot, altars, spawn points) | `GenerateMap(seed)`, `GetRoomAtGrid(pos)`, `GetAllPlacedRooms()`; runs server-only | `ItemDatabase.GetRoomsWithLootSlots()`, `GlobalRules_SO` (room count, floor count, cell dimensions), FishNet `ServerManager.Spawn()` |

### Feature Layer

| Module | Owns | Exposes | Consumes |
|--------|------|---------|---------|
| **EyeLaserController** | Charge state machine (`LaserState`), `SphereCastAll` fire validation, telegraph sync at 10Hz | `CurrentState`, `ChargeProgress`, `CooldownProgress`; `ObserversLaserFired(origin, hit, isKill)` | `EyeCameraController.CursorWorldPosition`, `LoSManager.IsRunnerVisible()`, `DynamicProp.ServerTakeLaserHit()`, `GameManager.NotifyRunnerKilled()`, `GameManager.Phase` |
| **PlayerInventory** | `SyncList<InventoryEntry>` of carried items, personal loot value tally (never decrements on drop) | `CarriedItems`, `PersonalLootValue`, `CarriedWeight`; `ServerAddItem(itemId, weight, value)`, `ServerRemoveItem(itemId)`, `ServerClearInventory()`, `GetCarriedValue()` | `ItemDatabase`, `WeightComponent` |
| **ExtractionManager** | Channel timer coroutines, channel progress, extraction success flow | `GetChannelProgress(clientId)`, `GetChannelTimeRemaining(clientId)`, `CalculateChannelTime(weight)`; `ServerBeginExtraction(conn)` | `PlayerInventory`, `GameManager.NotifyRunnerExtracted()`, `GameManager.NotifyExtractionAttemptStarted()` |
| **WorldItemSpawner** | Item `WorldItem` spawn at loot slots populated by MapGenerator | Internal only | `MapGenerator.GetAllPlacedRooms()`, `ItemDatabase`, FishNet `ServerManager` |
| **RoomPrefabFramework** | Editor-only tools: ConnectorMarker, SpawnSlot, RoomBounds, RoomValidator, custom inspectors, gizmos | Editor menu items, gizmo rendering, validation API | `RoomNode_SO`, room prefabs (editor time only — no runtime presence) |
| **PropBase / DynamicProp / IncarnationProp** | Prop health, LoS blocking geometry, incarnation state, blinded state | `ServerTakeLaserHit()`; `ServerRunnerImpact(force, dir)`; `ServerApplyOcularPaint()`; SyncVars: `_health`, `_isIncarnated`, `_isBlinded` | FishNet `ServerManager.Despawn()` |

### Presentation Layer

| Module | Owns | Exposes | Consumes (reads from) |
|--------|------|---------|---------|
| **RunnerHUD** | All local Runner UI elements (weight meter, ability icons, inventory strip, pickup prompt, extraction bar, match timer) | None (leaf) | `RunnerController`, `WeightComponent`, `PlayerInventory.CarriedItems` (SyncList), `RunnerInteractor.NearestInteractable`, `ExtractionManager`, `GameManager.Phase` |
| **EyeHUD** | All Eye UI elements (cursor charge ring, runner status bars, extraction alert banner, phase banner, dread/incarnation alpha stubs) | None (leaf) | `EyeLaserController.CurrentState/ChargeProgress`, `LoSManager` visibility per Runner, `RunnerController._isChanneling` (all Runners), `ExtractionManager`, `GameManager.Phase` |
| **AudioManager** | Sound playback | `PlayAt(sfx, position)`, `PlayGlobal(sfx)` | Unity `AudioSource`, FishNet `ObserversRpc` (called from feature layer) |
| **VFXManager** | Particle and visual effect playback | `SpawnAt(vfx, position)`, `SpawnLaserBeam(start, end, isKill)` | Unity `ParticleSystem`, FishNet `ObserversRpc` |

---

## Data Flow

### Scenario 1 — Match Start

```
LobbyScene: game_start received by RelayClient
    → LobbyManager.HandleGameStart(msg)
    → GameStartData.Set(msg)
    → SceneManager.LoadSceneAsync("GameScene")  [await]
    → StartFishNet(msg)
        → if host: NetworkManager.ServerManager.StartConnection()
                   NetworkManager.ClientManager.StartConnection("localhost")
        → else:    NetworkManager.ClientManager.StartConnection(relayAddress)
    → FishNet server: OnStartServer → PlayerSpawner.WaitForAllClients()
    → All clients connected → PlayerSpawner.SpawnAllPlayers()
        → ServerManager.Spawn(eyeObj, eyeConn)
        → ServerManager.Spawn(runnerObj, runnerConn) ×N
    → GameManager.OnStartServer() → _aliveRunnerCount = runnerCount
    → GameManager._matchPhase = Grace → ObserversPhaseTransition()
    → All clients: OnPhaseChanged(_, Grace) fires
        → EyeCameraController: enable grace overlay
        → EyeLaserController: disable
        → RunnerHUD / EyeHUD: show "GRACE PERIOD"
```

Communication type: Synchronous call chain through scene load → FishNet SyncVar replication for phase state.

---

### Scenario 1b — Map Generation (during Match Start)

```
[Server — after FishNet starts, before PlayerSpawner]
    seed = GenerateSeed() (from System time or relay)
    MapGenerator.GenerateMap(seed):
        roomPool = ItemDatabase.GetAllRooms()
        grid = new Grid(GlobalRules_SO.maxFloors, maxWidth)
        PLACE starting room at center
        GROW rooms via constrained frontier algorithm
        SEAL unused connectors
        VALIDATE (connectivity, altar distance, min rooms)
        
    FOR each placed room:
        obj = Instantiate(room.SO.roomPrefab, worldPos, Quaternion.identity)
        ServerManager.Spawn(obj)  → clients receive via FishNet observer spawn
    
    POPULATE content:
        Loot items → weighted random at loot slots → ServerManager.Spawn(lootObj)
        Altars → at altar slots ≥ 3 rooms from spawn → ServerManager.Spawn(altarObj)
        Incarnation props → at incarnation slots → ServerManager.Spawn(propObj)
    
    ObserversReceiveMapSeed(seed)  → stored for replay/debug only
    
    THEN → PlayerSpawner.SpawnAllPlayers()
        Runner spawn at starting room's RunnerSpawn slot
        Eye spawn at camera mount position
```

Communication type: Server-only generation. Clients receive spawned NetworkObjects through FishNet's standard observer pipeline. No client-side generation.

---

### Scenario 2 — Runner Movement Tick (30Hz)

```
[Owner Client — RunnerController.TimeManager_OnTick()]
    Reconcile(default)        ← apply last server reconcile
    input = BuildReplicateData()
    Move(input)               ← predict locally (deterministic)

[Server — RunnerController.TimeManager_OnTick()]
    Move(default)             ← replay owner inputs
    state = BuildReconcileData()
    Reconcile(state)          ← authoritative state → owner
                              ← FishNet checks divergence > 0.1m → rewind+replay

[NetworkTransform — 30Hz]
    Position synced → all non-owner clients (interpolated)
```

Communication type: FishNet Unreliable channel (fast, loss-tolerant). Prediction runs owner + server. Remote clients only receive position via `NetworkTransform`.

---

### Scenario 3 — Eye Laser Fire

```
[Eye Owner Client]
    Hold LMB → LaserState.Charging
    [ServerRpc, 10Hz] ServerTelegraphUpdate(start, end, progress)
        → Server → [ObserversRpc] → all clients → LineRenderer update

    Release LMB (full charge) → [ServerRpc] ServerRequestFire(cursorWorldPos)

[Server — EyeLaserController]
    Validate: _serverLaserState == FullCharge
    Physics.SphereCastAll(origin, 0.35m, direction, 80m, "Runner|Prop" mask)
    Sort hits by distance
    For each hit:
        if DynamicProp → prop.ServerTakeLaserHit() → break
        if RunnerController:
            LoSManager.IsRunnerVisible(runner)  ← fresh raycast, not stale SyncVar
            runner.IsInvulnerable check
            → ServerEliminateRunner(runner)
                → runner._isAlive = false (SyncVar)
                → ObserversRunnerEliminated()
                → GameManager.NotifyRunnerKilled(conn)
    → [ObserversRpc] ObserversLaserFired(origin, hitPoint, isKill)
        → VFXManager.SpawnLaserBeam()
        → AudioManager.PlayAt(LaserFire / LaserKill)
        → EyeCameraController.ApplyLaserFireShake() (Eye client only)
```

Communication type: `ServerRpc` (fire request); `ObserversRpc` (VFX/SFX/state). SyncVar for `_isAlive` replicates immediately to all clients.

---

### Scenario 4 — Runner Extraction Channel

```
[Runner Owner Client — near altar]
    RunnerInteractor detects ExtractionAltar within 2.5m
    RunnerHUD: "F to Extract" prompt (per-frame polling)
    Player presses F → [ServerRpc] ServerBeginExtraction()

[Server — ExtractionManager]
    Validate: phase ∈ {Hunt, ExtractionCrisis}, has items, within 2.5m
    runner._isChanneling = true (SyncVar) → all clients
        → EyeHUD: screen flash + extraction alert banner
    GameManager.NotifyExtractionAttemptStarted() → may trigger ExtractionCrisis
    WatchChannelProgress coroutine (tick every 0.1s):
        Check: still within 2.5m?
        Check: still alive?
        If fail: cancel → runner._isChanneling = false
        If elapsed >= channelTime (= 3.0 + carriedWeight × 0.4s):
            [ObserversRpc] ObserversRunnerExtracted(clientId)
            PlayerInventory.ServerClearInventory()
            GameManager.NotifyRunnerExtracted(conn)
                → _extractedRunnerCount++
                → CheckWinConditions()
            ServerManager.Despawn(runnerObj)

[Eye Client — receives _isChanneling SyncVar change]
    EyeHUD: screen flash + banner slide from top, per-frame countdown
```

Communication type: `ServerRpc` (channel start); SyncVar (`_isChanneling` for Eye alert); `ObserversRpc` (extraction VFX). Server coroutine owns the timer — authoritative.

---

## API Boundaries

### GameManager public contract

```csharp
// All systems call only these — no internal state access
public interface IGameManagerContract
{
    // State reads (all clients safe)
    MatchPhase Phase { get; }
    float MatchTimer { get; }
    bool IsGrace { get; }
    float CurrentDreadRegenMultiplier { get; }

    // Events (subscribe in OnStartNetwork, not Awake/Start)
    event Action<MatchPhase, MatchPhase> OnPhaseChanged;   // (previous, next)
    event Action<MatchResult> OnMatchEnded;
    event Action OnAltarsActivated;

    // Server-only notifications (called by feature systems)
    void NotifyRunnerKilled(NetworkConnection conn);           // [Server]
    void NotifyRunnerExtracted(NetworkConnection conn);        // [Server]
    void NotifyAllSpawned(int runnerCount);                    // [Server]
    void NotifyExtractionAttemptStarted();                     // [Server]
}
```

---

### RelayClient public contract

```csharp
public interface IRelayClientContract
{
    int LocalPlayerId { get; }
    int HostPlayerId { get; }
    bool IsHost { get; }

    // Lobby actions
    Task ConnectAsync();
    void CreateRoom(string playerName);
    void JoinRoom(string roomCode, string playerName);
    void SetReady(bool ready);
    void StartGame();
    void ReturnToLobby();

    // Game phase
    void SendGamePacket(byte targetByte, byte[] payload);

    // ID mapping
    int RelayIdToFishNetId(int relayPlayerId);
    void RegisterMapping(int relayId, int fishNetId);

    // Events
    event Action<LobbyStateMsg> OnLobbyState;
    event Action<GameStartMsg> OnGameStart;
    event Action<byte[]> OnGamePacket;
    event Action<string> OnError;
    event Action OnDisconnected;
}
```

---

### WeightComponent public contract

```csharp
public interface IWeightComponentContract
{
    float CurrentWeight { get; }      // SyncVar — readable on all clients
    WeightTier CurrentTier { get; }   // SyncVar — readable on all clients

    // Server-only mutations
    void AddWeight(float kg);         // [Server]
    void RemoveWeight(float kg);      // [Server]
    void ApplyWeightDelta(float kg);  // [Server] — negative = reduce
}
```

---

### EyeCameraController public contract

```csharp
public interface IEyeCameraContract
{
    Vector3 CursorWorldPosition { get; }                      // per-frame, EyeLaserController reads this
    void ApplyLaserFireShake();                                // EyeLaserController calls after confirmed kill
    void BeginIncarnationTransition(IncarnationProp prop);     // Alpha — IncarnationSystem calls
    void EndIncarnationTransition();                           // Alpha — IncarnationSystem calls
}
```

---

### DynamicProp public contract (Feature → Feature boundary)

```csharp
public interface IDynamicPropContract
{
    void ServerTakeLaserHit();                               // [Server] — EyeLaserController calls
    void ServerRunnerImpact(float force, Vector3 direction); // [Server] — OnControllerColliderHit
}
```

---

## Technical Requirements Baseline

Extracted from 16 GDDs — cross-cutting concerns only.

| Req ID | Domain | Requirement | Source GDD |
|--------|--------|-------------|-----------|
| TR-NET-001 | Network | WebSocket client (wss prod, ws dev) — `#if DEVELOPMENT_BUILD` URL switching | SYS-20 |
| TR-NET-002 | Network | JSON lobby protocol: 8 message types, text frames, UTF-8 | SYS-20 |
| TR-NET-003 | Network | Binary game packet: 1-byte target routing + Synapse payload | SYS-20 |
| TR-NET-004 | Network | Inbound packets have 2-byte uint16LE sender ID prepended by relay | SYS-20 |
| TR-NET-005 | Network | Host election from `game_start.hostId` — relay-assigned; client cannot self-promote | SYS-20/21 |
| TR-NET-006 | Network | Client-hosted FishNet: host = room creator = FishNet server+client; others = clients only | SYS-33 |
| TR-NET-007 | Network | Synapse UDP → relay WebSocket bridge strategy — **RESOLVED: ADR-0001 (Option B: custom RelayTransport)** | SYS-33 |
| TR-NET-008 | Network | RelayPlayerId → FishNetConnectionId mapping via `ServerRpc` self-registration on connect | SYS-21/33 |
| TR-NET-009 | Network | 30Hz server tick rate minimum | SYS-33 |
| TR-NET-010 | Network | No host migration in MVP — host disconnect ends match | SYS-20/21 |
| TR-MOV-001 | Movement | CharacterController + custom velocity (not Rigidbody) — determinism required for FishNet prediction | SYS-01 |
| TR-MOV-002 | Movement | FishNet prediction: `IReplicateData` + `IReconcileData`, `TimeManager_OnTick()` hook | SYS-01/33 |
| TR-MOV-003 | Movement | Separate `_hVelocity` (Vector3) / `_vVelocity` (float) — horizontal and vertical never mixed | SYS-01 |
| TR-MOV-004 | Movement | Coyote time 0.15s + jump buffer 0.10s — mandatory for parkour feel | SYS-01 |
| TR-MOV-005 | Movement | 0.1m reconciliation divergence tolerance before position snap | SYS-01/33 |
| TR-DAT-001 | Data | All game data in ScriptableObjects: `ItemData_SO`, `ShrineItemData_SO`, `TrapData_SO`, `RoomNode_SO`, `GlobalRules_SO` | SYS-14 |
| TR-DAT-002 | Data | String `itemId` synced over FishNet — never SO asset references across network | SYS-14/33 |
| TR-DAT-003 | Data | `Resources.LoadAll<T>()` for SO discovery at runtime (Loot/, Shrine/, Traps/, Rooms/ resource folders) | SYS-14 |
| TR-DAT-004 | Data | `GlobalRules_SO` is single source of all tuning constants — zero hardcoded values in gameplay code | SYS-14/01/04/02 |
| TR-PHY-001 | Physics | LoS via `Physics.Raycast` at 30Hz (not lighting, not shadow maps) — 5 anatomical sockets, pre-culled by distance + frustum | SYS-05 |
| TR-PHY-002 | Physics | Laser hit via `Physics.SphereCastAll` (0.35m radius, 80m range) — stops at first prop hit | SYS-06 |
| TR-PHY-003 | Physics | Wall detection for Runner via `Physics.SphereCast` (0.4m radius, 0.6m distance) | SYS-01 |
| TR-PHY-004 | Physics | `OnControllerColliderHit` for Runner-prop collision detection — server-side only | SYS-01/11 |
| TR-PHY-005 | Physics | 8 named layers: Wall, Prop, Runner, Eye, SmokeVolume, GrappleAnchor, Trigger, EyeBeam | SYS-05 |
| TR-UI-001 | UI | uGUI Canvas (Screen Space Overlay) for all HUDs — not UI Toolkit | SYS-26/27 |
| TR-UI-002 | UI | DOTween for HUD animations (extraction bar slide, phase banner) | SYS-26/27 |
| TR-UI-003 | UI | Cinemachine 3 for camera impulse/shake — verify `CinemachineImpulseSource` API in Unity 6 | SYS-01/02 |
| TR-UI-004 | UI | Perspective camera for Eye; FOV-zoom not dolly; uGUI CanvasGroup noise overlay for Grace | SYS-02 |
| TR-SCN-001 | Scene | Two scenes: `LobbyScene` + `GameScene` — loaded sequentially | SYS-21 |
| TR-SCN-002 | Scene | `RelayClient` + `LobbyManager` are `DontDestroyOnLoad` — persist through scene transition | SYS-21 |
| TR-SCN-003 | Scene | `GameStartData` static class — cross-scene data without networking | SYS-21/33 |
| TR-SCN-004 | Scene | FishNet starts AFTER `GameScene` finishes loading via `SceneManager.LoadSceneAsync` | SYS-21 |
| TR-SCN-005 | Scene | `PlayerSpawner` waits for all expected clients before spawning (10s timeout) | SYS-21/33 |
| TR-MAP-001 | Map | Seed-based deterministic generation — same seed = same layout | SYS-10 |
| TR-MAP-002 | Map | Server-only generation — clients do NOT run the algorithm; receive spawned NetworkObjects | SYS-10 |
| TR-MAP-003 | Map | Constrained growth: 2-4 floors, ≥20% corridors, stairwell per floor connection | SYS-10 |
| TR-MAP-004 | Map | Connector matching: direction-opposite, same size, grid-aligned | SYS-10 |
| TR-MAP-005 | Map | Validation pass: connectivity flood-fill, altar distance ≥3 from spawn, min room count | SYS-10 |
| TR-MAP-006 | Map | Content population: loot at slots, altars ≥3 rooms from spawn, total loot ≥ threshold × 2.5 | SYS-10 |
| TR-MAP-007 | Map | Adding new RoomNode_SO to Resources/ auto-includes in generation pool — no code change | SYS-10/12 |

---

## ADR Audit

All 7 required ADRs have been written to `docs/architecture/decisions/`.

| ADR | Title | Status | GDD Requirements Covered |
|-----|-------|--------|--------------------------|
| ADR-0001 | Relay Transport Integration Strategy | Accepted | TR-NET-003, TR-NET-004, TR-NET-007, TR-NET-008 |
| ADR-0002 | Scene Architecture and DontDestroyOnLoad Boundaries | Accepted | TR-SCN-001, TR-SCN-002, TR-SCN-003, TR-SCN-004 |
| ADR-0003 | ScriptableObject Asset Loading Strategy | Accepted | TR-DAT-003 |
| ADR-0004 | FishNet Client-Hosted Authority Model | Accepted | TR-NET-005, TR-NET-006, TR-NET-010 |
| ADR-0005 | FishNet v4 Prediction Implementation Pattern | Accepted | TR-MOV-001, TR-MOV-002, TR-MOV-003, TR-MOV-005, TR-NET-009 |
| ADR-0006 | Named Physics Layer Architecture | Accepted | TR-PHY-001, TR-PHY-002, TR-PHY-003, TR-PHY-004, TR-PHY-005 |
| ADR-0007 | HUD Implementation — uGUI over UI Toolkit | Accepted | TR-UI-001, TR-UI-002 |

### Uncovered Technical Requirements

These TRs are not addressed by any ADR but are straightforward implementation concerns (no architectural decision needed):

| Req ID | Requirement | Resolution |
|--------|-------------|-----------|
| TR-NET-001 | WebSocket URL switching (prod/dev) | Config in `GlobalRules_SO` + `#if` — no ADR needed |
| TR-NET-002 | JSON lobby protocol | Defined in SYS-20 GDD — implementation spec, not decision |
| TR-MOV-004 | Coyote time + jump buffer | Tuning values in `GlobalRules_SO` — no ADR needed |
| TR-DAT-001 | SO class definitions | Schema defined in SYS-14 GDD — no ADR needed |
| TR-DAT-002 | String itemId contract | Enforced by ItemDatabase API — no ADR needed |
| TR-DAT-004 | GlobalRules_SO single source | Architecture Principle #5 — no ADR needed |
| TR-UI-003 | Cinemachine 3 impulse API | — requires doc verification, not an ADR |
| TR-UI-004 | Eye perspective camera + FOV zoom | Implementation detail in SYS-02 — no ADR needed |
| TR-SCN-005 | PlayerSpawner wait-for-all | Implementation detail in SYS-21 — no ADR needed |

---

## ADR Index

All decisions are in `docs/architecture/decisions/`:

| ADR | File | Summary |
|-----|------|---------|
| 0001 | `ADR-0001-relay-transport-integration-strategy.md` | Custom `RelayTransport : Transport` class bridges FishNet↔relay WebSocket |
| 0002 | `ADR-0002-scene-architecture-and-ddol-boundaries.md` | Two scenes (Lobby+Game), 4 DDOL objects, clean teardown on match end |
| 0003 | `ADR-0003-scriptableobject-asset-loading-strategy.md` | `Resources.LoadAll<T>()` for MVP; revisit Addressables at Alpha |
| 0004 | `ADR-0004-fishnet-client-hosted-authority-model.md` | Eye = host = FishNet server; all state server-authoritative; no host migration |
| 0005 | `ADR-0005-fishnet-v4-prediction-implementation-pattern.md` | `IReplicateData`/`IReconcileData` at 30Hz; 0.1m snap tolerance |
| 0006 | `ADR-0006-named-physics-layer-architecture.md` | 8 named layers (8-15); per-system raycast masks defined |
| 0007 | `ADR-0007-hud-implementation-ugui-over-ui-toolkit.md` | uGUI + DOTween; owner-only initialization pattern |

---

## Architecture Principles

1. **Server is truth, clients predict.** All gameplay state changes (kills, weight, extraction, phase transitions) are server-authoritative. Clients predict their own movement (Runner only) but never self-report state changes. The host (Eye = server) validates everything.

2. **Events over polling for state changes.** Systems that gate behavior on match phase subscribe to `GameManager.OnPhaseChanged`. Systems that react to discrete events (pickup, death, extraction) subscribe to their respective events. Per-frame polling is reserved for continuous HUD state (weight meter fill, ability icon cooldowns) where it is unavoidable.

3. **No SO references across the wire.** String `itemId` is synced over FishNet; SO resolution happens locally via `ItemDatabase`. This contract is enforced at the `PlayerInventory` and `WorldItem` layer and must never be violated in any `NetworkBehaviour`.

4. **Layers respect the stack.** Feature systems call Foundation/Core contracts. Presentation reads Feature/Core state passively. Nothing in Foundation references Feature or Core. Circular references are architectural failures, not tradeoffs.

5. **All tuning lives in GlobalRules_SO.** No magic numbers in gameplay code. Balance changes are an asset save + Play Mode, not a code recompile. Any constant that affects feel, balance, or timing belongs in `GlobalRules_SO`.

---

## Open Questions

None. All architectural decisions resolved via ADRs 0001–0007.
