# GDD: Eye HUD

**System ID:** SYS-27  
**Design Order:** 14 of 33 (FINAL MVP GDD)  
**Priority:** MVP — Presentation Layer  
**Dependencies:** SYS-06 (Eye Laser — charge state, cooldown progress), SYS-05 (Geometric LoS — per-runner visibility state), SYS-09 (Loot & Extraction — extraction alert when Runner channels), SYS-22 (Match State Machine — phase, match timer)  
**Dependents:** None (leaf node — presentation only)  
**Status:** Designed  

---

## 1. Purpose

The Eye HUD is the Eye player's information layer. It must communicate:
- **Weapon state** — is the laser charged, charging, or cooling down?
- **Who can I see?** — which Runners are currently visible to the Eye's LoS
- **Is someone at the altar?** — extraction alert (the Eye's most urgent signal)
- **Match context** — phase and time remaining

The Eye's HUD philosophy is the opposite of the Runner's. Runners are paranoid and information-starved. The Eye is an all-seeing entity that needs **clarity about threats** — specifically, where Runners are and whether any are extracting. The Eye's horror is NOT uncertainty about Runner locations (they CAN see them) but the challenge of ACTING on that information before Runners slip away.

**HUD design principle:** The Eye is a god looking down at insects. The HUD should feel like surveillance readouts, not combat indicators. Cold, clinical, omniscient in tone — until an extraction alert fires and everything becomes urgent.

**Anti-patterns to avoid:**
- Health bar — the Eye has no HP
- Stamina bar — the Eye hovers, no stamina
- Ability cooldown icons (like Runner's Air Dash) — the Eye only has the laser and Incarnation (Alpha)
- Minimap — the Eye's entire view IS the map. A minimap would be redundant.

---

## 2. HUD Architecture

### 2.1 Owner-Only, Eye Role

The Eye HUD only renders for the `EyeController` owner (the player assigned the Eye role). Initialized by `EyeController.OnStartClient()` when `IsOwner`.

```csharp
public class EyeHUD : MonoBehaviour
{
    private EyeController       _eye;
    private EyeLaserController  _laser;
    private LoSManager          _losManager;
    private GameManager         _gameManager;
    private ExtractionManager   _extractManager;

    // Called by EyeController.OnStartClient() when IsOwner
    public void Initialize(EyeController eye)
    {
        _eye          = eye;
        _laser        = eye.GetComponent<EyeLaserController>();
        _losManager   = LoSManager.Instance;
        _gameManager  = GameManager.Instance;
        _extractManager = ExtractionManager.Instance;
        gameObject.SetActive(true);
    }
}
```

Same Canvas setup as SYS-26: Screen Space Overlay, 1920×1080 reference, uGUI.

### 2.2 Screen Layout

```
┌───────────────────────────────────────────────────────┐
│  [RUNNER STATUS BAR]              [PHASE / TIMER]     │  ← Top strip
│                                                       │
│                                                       │
│                                                       │
│                   [LASER CHARGE RING]                 │
│                          +  ← reticle                 │  ← Center
│                   (cursor-space UI)                   │
│                                                       │
│                                                       │
│  [DREAD METER stub]       [INCARNATION LIST stub]     │  ← Bottom (stubs in MVP)
│            [EXTRACTION ALERT — fullscreen flash]      │  ← Overlay (contextual)
└───────────────────────────────────────────────────────┘
```

---

## 3. HUD Elements

### 3.1 Laser Charge Ring (Center — Around Cursor)

**This is the Eye's primary feedback element.** The charge ring sits in screen space, centered on the cursor position each frame. It shows laser state at a glance without the Eye taking their view off the dollhouse.

**Why cursor-space, not a fixed UI element?**
The Eye is always aiming. Their attention is on the cursor and the Runners near it. A fixed-corner laser meter would require constant eye movement away from the action. Cursor-adjacent feedback keeps the Eye's gaze where it belongs.

```csharp
// In Update() — runs on IsOwner EyeController only
private void UpdateLaserChargeRing()
{
    // Position ring at cursor screen position (cursor moves with mouse, stays on-screen)
    _chargeRing.rectTransform.position = Input.mousePosition;

    switch (_laser.CurrentState)
    {
        case LaserState.Idle:
            _chargeRing.gameObject.SetActive(false);
            break;

        case LaserState.Charging:
            _chargeRing.gameObject.SetActive(true);
            float t = _laser.ChargeProgress;  // 0–1
            _chargeRingFill.fillAmount = t;
            _chargeRingFill.color = Color.Lerp(_chargeColorDim, _chargeColorFull, t);
            _chargeRing.localScale = Vector3.one * Mathf.Lerp(1.0f, 1.15f, t);  // grows slightly
            break;

        case LaserState.FullCharge:
            _chargeRing.gameObject.SetActive(true);
            _chargeRingFill.fillAmount = 1f;
            // Pulse: scale oscillates to signal "ready, release now"
            float pulse = 1f + 0.08f * Mathf.Sin(Time.time * 12f);
            _chargeRing.localScale = Vector3.one * pulse;
            _chargeRingFill.color = _chargeColorFull;
            break;

        case LaserState.Cooldown:
            _chargeRing.gameObject.SetActive(true);
            _chargeRingFill.fillAmount = _laser.CooldownProgress;  // drains 1→0 during cooldown
            _chargeRingFill.color = _cooldownColor;  // grey-blue "recharging" color
            _chargeRing.localScale = Vector3.one;
            break;

        case LaserState.EarlyRelease:
            _chargeRing.gameObject.SetActive(true);
            _chargeRingFill.fillAmount = 0f;
            _chargeRingFill.color = _errorColor;  // red flash
            break;
    }
}
```

**Charge ring visual spec:**
- Outer diameter: 48px at 1080p (small enough not to obscure the scene)
- Fill direction: clockwise, starting at 12 o'clock
- Colors: Dim teal → bright gold at full charge; grey during cooldown; red flash on early release
- Ring has a subtle glow shader (not a hard edge)

**Cursor reticle** (center dot): always visible regardless of charge state — 6px dot. The charge ring draws AROUND it.

---

### 3.2 Runner Status Bar (Top Left)

Shows one row per Runner in the match. Tells the Eye at a glance: who's alive, who they can currently see, and who has triggered extraction.

```
[ ● Runner 1 ]  👁 VISIBLE    carrying: 3.4 kg
[ ○ Runner 2 ]    hidden       carrying: ?
[ ● Runner 3 ]  ⚡ EXTRACTING  carrying: 5.1 kg
```

Per-runner row data:
- **Alive/Dead indicator**: filled circle (alive) or X (dead/extracted)
- **Visibility badge**: `VISIBLE` (eye icon, lit) or `hidden` (eye icon, dim) — driven by `LoSManager._runnerVisibility[id]` SyncVar
- **Carried weight**: visible only when Runner is currently visible to LoS. When hidden: "?"
- **EXTRACTING badge**: shown when `RunnerController._isChanneling == true`

```csharp
private void UpdateRunnerStatusBar()
{
    for (int i = 0; i < _runnerRows.Count; i++)
    {
        var runner = _gameManager.GetRunner(i);
        if (runner == null) continue;

        var row = _runnerRows[i];
        row.aliveIndicator.color   = runner.IsAlive ? _aliveColor : _deadColor;
        bool visible               = _losManager.IsRunnerVisible(runner);
        row.visibilityLabel.text   = visible ? "VISIBLE" : "hidden";
        row.visibilityLabel.color  = visible ? _visibleColor : _hiddenColor;
        row.weightLabel.text       = visible
            ? $"{runner.GetComponent<PlayerInventory>().CarriedWeight:F1} kg"
            : "?";
        row.extractingBadge.gameObject.SetActive(runner.IsChanneling);
    }
}
```

**Why show carried weight when visible?**
The Eye's strategy depends on knowing if a Runner is "worth killing right now." A Runner carrying 8kg who reaches the altar is a major loss. A Runner carrying 0.1kg is not an immediate priority. Showing weight when visible lets the Eye make tactical priority decisions.

**The Eye CANNOT see carried weight when a Runner is hidden.** The Eye only knows what they can see. A Runner who was carrying 5kg and ran behind a pillar — the Eye doesn't get to keep that information updated. The weight label snaps to "?" when visibility drops.

---

### 3.3 Extraction Alert (Overlay)

The extraction alert is the Eye's most urgent feedback. When ANY Runner's `_isChanneling` becomes true, the screen flashes and a persistent alert banner appears.

**This is the Eye's "GET THERE NOW" signal.** Design intent: extraction is a race. The Eye has at minimum 3 seconds (bare minimum channel time) and up to 11 seconds (maximum load) to reach the altar with a charged laser. The alert gives the Eye the information they need to act.

```csharp
// Subscribe to SyncVar changes on ALL RunnerControllers
// (EyeHUD subscribes after all Runners are spawned)
private void OnAnyRunnerChannelingChanged(RunnerController runner, bool wasChanneling, bool isChanneling)
{
    if (isChanneling && !wasChanneling)
        StartCoroutine(ShowExtractionAlert(runner));
    else if (!isChanneling && wasChanneling)
        HideExtractionAlert(runner);
}
```

**Alert visual states:**

| State | Visual | Duration |
|-------|--------|----------|
| Alert fires | Screen edge flash (red-gold) + banner slides in | 0.4s flash, banner persists |
| Alert active | Persistent banner: "EXTRACTING — Runner X — 8.2s" with countdown | Until channel ends |
| Runner extracted | Banner: "EXTRACTED — Runner X escaped" (red) | 2.0s then fades |
| Channel cancelled | Banner fades out silently | 0.3s fade |

```csharp
private IEnumerator ShowExtractionAlert(RunnerController runner)
{
    // Screen edge flash
    _screenFlash.DOFade(0.4f, 0.1f).Then(() => _screenFlash.DOFade(0f, 0.3f));

    // Alert banner slide in from top
    _alertBanner.gameObject.SetActive(true);
    _alertBanner.DOAnchorPosY(_alertBannerShowY, 0.2f).SetEase(Ease.OutBack);

    // Update countdown each frame while runner is channeling
    while (runner.IsAlive && runner.IsChanneling)
    {
        float remaining = _extractManager.GetChannelTimeRemaining(runner.Owner.ClientId);
        _alertTimeLabel.text = $"EXTRACTING — {remaining:F1}s remaining";

        // Urgency color: gold → red as time runs out
        float urgency = 1f - (remaining / _extractManager.GetChannelTotalTime(runner.Owner.ClientId));
        _alertBanner.GetComponent<Image>().color = Color.Lerp(_alertGold, _alertRed, urgency);

        yield return null;
    }
}
```

**Multiple simultaneous extractions:** In a 4-Runner match, multiple Runners could start channeling at once. The alert banner supports a stack: up to 4 concurrent extraction rows. Each row has its own countdown. Uncommon in practice but must be handled.

---

### 3.4 Phase Banner and Match Timer (Top Right)

Same logic as SYS-26 Runner HUD, mirrored to the Eye perspective:

| Phase | Banner text | Eye context |
|-------|------------|-------------|
| Grace | "GRACE PERIOD" | Eye cannot fire — waiting begins |
| Hunt | *(hidden)* | Normal play |
| ExtractionCrisis | "EXTRACTION CRISIS" | At least one Runner is at the altar |
| Overtime | "OVERTIME" | Pulsing red — kill remaining Runners |
| PostMatch | "MATCH OVER" | Win/lose result |

Timer: countdown from match duration. During Overtime: count UP (pressure mounts).

The Eye's timer context is different from the Runner's — the Eye wants Runners to run out of time. During Hunt phase, the Eye feels the pressure of the countdown working in their favor. The timer color shifts: green (early match, Eye has time) → yellow → orange → red as the match approaches Overtime.

```csharp
private void UpdateMatchTimer()
{
    float remaining = _gameManager.MatchTimeRemaining;
    _timerLabel.text = $"{Mathf.FloorToInt(remaining / 60f):00}:{Mathf.FloorToInt(remaining % 60f):00}";

    // Color shifts from green → red as time runs out
    float urgency = 1f - (remaining / _gameManager.TotalMatchDuration);
    _timerLabel.color = Color.Lerp(Color.green, Color.red, Mathf.Clamp01(urgency * 2f));
}
```

---

### 3.5 Dread Meter (Bottom Left — MVP Stub)

Dread is the Eye's resource for Alpha (SYS-08). In MVP, the Dread system doesn't exist — but the Eye HUD reserves space for it as a stub.

**MVP display:** A horizontal bar, greyed out, labeled "DREAD — ALPHA." It doesn't fill or deplete. Its presence signals to playtesters that this is a future mechanic without hiding it.

Post-MVP when SYS-08 is implemented: this stub activates by reading `DreadManager.CurrentDread` and `DreadManager.MaxDread`, and the grey color becomes a dynamic red-gold fill.

```csharp
// In MVP: stub display
_dreadBar.fillAmount = 0f;
_dreadBar.color      = new Color(0.4f, 0.4f, 0.4f, 0.6f);  // grey, semi-transparent
_dreadLabel.text     = "DREAD";
```

---

### 3.6 Incarnation List (Bottom Right — MVP Stub)

Incarnation (SYS-07) is Alpha. MVP stub shows available IncarnationProp slots greyed out, labeled "POSSESS — ALPHA."

Post-Alpha: activates to show available IncarnationProps with proximity and blinded state indicators, and highlights the currently-possessed prop when incarnated.

---

## 4. Noise Directional Indicator (Bottom Center — Alpha Stub)

SYS-19 Sound Radius is Alpha. When implemented, the Eye HUD shows a compass-rose-style directional indicator that flashes a wedge in the direction of recent noise events (prop breaks, Runner footsteps).

MVP: stub element, hidden (`gameObject.SetActive(false)`). Space reserved in layout.

---

## 5. Grace Phase Lockout Display

During `MatchPhase.Grace`, the laser is disabled (SYS-06 contract). The Eye HUD communicates this:
- Charge ring is hidden
- A "GRACE PERIOD — LASER OFFLINE" text overlay appears in screen center
- Screen has a subtle blue-grey vignette tint

```csharp
private void OnPhaseChanged(MatchPhase prev, MatchPhase next)
{
    bool isGrace = next == MatchPhase.Grace;
    _graceLockoutOverlay.gameObject.SetActive(isGrace);

    // Charge ring controlled by laser state directly — no extra logic needed here
    // (LaserState.Idle during Grace means ring is already hidden)
}
```

---

## 6. Update Strategy

| Element | Update trigger | Reason |
|---------|---------------|--------|
| Laser Charge Ring | Per-frame | Needs to track cursor position every frame; charge animation must be smooth |
| Runner Status Bar | Per-frame | Visibility state can change at 30Hz; weight changes on pickup |
| Extraction Alert countdown | Per-frame (coroutine) | Countdown must be smooth and urgent |
| Extraction Alert appear/hide | SyncVar OnChange event | Only fires on channel state transitions |
| Phase Banner | OnPhaseChanged event | Rare transition |
| Match Timer | Once per second | Second precision sufficient |
| Dread Meter | Per-frame (post-Alpha) | Resource UI needs smooth fill |

---

## 7. Visibility vs. Paranoia: The Eye's Counterpart

The Runner HUD deliberately withholds LoS information (paranoia design). The Eye HUD is the counterpart — the Eye GETS the information the Runner doesn't have. This asymmetry is intentional and central to the design.

However, the Eye's information has **latency and gaps**:
- LoS runs at 30Hz (33ms lag) — a Runner who ducks behind cover takes up to 33ms to disappear from "VISIBLE"
- Carried weight of hidden Runners is unknown — the Eye can only see what the LoS system confirms
- The extraction alert fires when `_isChanneling` becomes true, but the Eye may be zoomed in to the wrong part of the map

The Eye's HUD communicates **what the Eye knows**, not omniscience. When the Runner status bar shows "hidden" for all 3 Runners, the Eye has lost all threads. That's the Eye's version of paranoia.

---

## 8. Visual Style Notes

The Eye HUD should feel distinct from the Runner HUD — same Gothic theme, but **clinical and cold** vs. the Runner's **tense and hunted**:

- **Color scheme:** Dark viridian and cold grey as base. Accent colors: gold for laser charge, red for alerts and kills, cool teal for neutral info.
- **Typography:** Same HUD font (monospace/slab-serif from `GlobalRules_SO.HudFont`), but the Eye HUD uses UPPERCASE more aggressively — clinical readout aesthetic.
- **The charge ring should feel like a scope reticle**, not a fighting game power meter.
- **Extraction alert should feel like a proximity alarm** — not a friendly notification. The color shift from gold to red as time runs out is a tension ramp.
- **Visibility badges** should feel like surveillance system readouts: `[TRACKED]` and `[LOST]` would also work as alternative labels. For MVP: `VISIBLE` / `hidden`.

---

## 9. Prototype Test Checklist

| Test | Pass Condition |
|------|---------------|
| HUD only visible on Eye client | Runner clients don't see Eye HUD |
| Charge ring tracks cursor | Ring moves with mouse position every frame |
| Charge ring fills during charge | Hold LMB → ring fills clockwise |
| Charge ring pulses at full | At full charge: ring pulses in scale |
| Charge ring drains during cooldown | After fire: ring drains grey over cooldown duration |
| Charge ring red flash on early release | Release too early → red flash briefly |
| Charge ring hidden at idle | No LMB held → ring not visible |
| Runner status: alive indicator | 3 Runners → 3 rows with alive dots |
| Runner status: visible badge | Runner in LoS → "VISIBLE" badge lights |
| Runner status: hidden badge | Runner behind prop → "hidden" badge |
| Runner weight shows when visible | Visible Runner carrying 3kg → "3.0 kg" shown |
| Runner weight "?" when hidden | Same Runner ducks → label snaps to "?" |
| Extraction alert fires | Runner presses F at altar → screen flash + banner |
| Alert countdown accurate | Banner shows decreasing time matching channel progress |
| Alert hides on success | Runner extracts → "EXTRACTED" banner then fades |
| Alert hides on cancel | Runner moves from altar → alert fades |
| Multiple alerts stack | 2 Runners channel → 2 rows in alert banner |
| Grace overlay shows | Match start → "LASER OFFLINE" overlay appears |
| Grace overlay hides on Hunt | Phase transitions to Hunt → overlay disappears |
| Phase banner "OVERTIME" | Overtime phase → pulsing orange-red banner |
| Timer countdown correct | Match timer decrements; turns red near end |
| Dread stub visible but inactive | Grey bar labeled "DREAD" shown but doesn't fill |

---

## 10. MVP Scope

- [x] Canvas setup (Screen Space Overlay, 1920×1080, same as SYS-26)
- [x] EyeHUD initialized by EyeController.OnStartClient() IsOwner check
- [x] Laser Charge Ring — cursor-space, fills/pulses/drains by LaserState
- [x] Laser Charge Ring — color states: dim teal → gold (charge), grey (cooldown), red (error)
- [x] Runner Status Bar — per-Runner rows: alive, visibility badge, weight or "?"
- [x] Carried weight shown only when Runner is LoS-visible
- [x] Extraction Alert — screen flash + slide-in banner on _isChanneling → true
- [x] Extraction Alert — countdown timer to channel completion, gold→red urgency color
- [x] Extraction Alert — "EXTRACTED" message on success, silent fade on cancel
- [x] Multiple simultaneous extraction alert rows (up to 4)
- [x] Phase Banner — same event-driven pattern as SYS-26, Eye-specific text/colors
- [x] Match Timer — countdown, color shifts green→red, Overtime counts UP
- [x] Grace Phase overlay — "LASER OFFLINE" overlay during Grace
- [x] Dread Meter — greyed stub labeled "DREAD — ALPHA"
- [x] Incarnation List — greyed stub labeled "POSSESS — ALPHA"
- [x] Noise directional indicator — hidden stub (SYS-19 Alpha)
- [x] Paranoia counterpart design documented (Eye has information; Runner doesn't)
- [x] 21-test prototype checklist
- [ ] Actual art for Runner portrait icons (art dependency)
- [ ] Directional noise compass rose (SYS-19 Alpha dependency)
- [ ] Dread meter activation (SYS-08 Alpha dependency)
- [ ] Incarnation list activation (SYS-07 Alpha dependency)
- [ ] Minion status indicators (SYS-18 Beta dependency — Minions as Eye's remote scouts)

---

*System: SYS-27 — Eye HUD*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Depends on: [sys-06-eye-laser.md](sys-06-eye-laser.md), [sys-05-geometric-los.md](sys-05-geometric-los.md), [sys-09-loot-extraction.md](sys-09-loot-extraction.md), [sys-22-match-state-machine.md](sys-22-match-state-machine.md)*  
*Status: Complete — ready for implementation*
