# GDD: Eye Laser System

**System ID:** SYS-06  
**Design Order:** 11 of 33  
**Priority:** MVP — Feature Layer  
**Dependencies:** SYS-02 (Eye Camera — cursor world position + camera shake), SYS-05 (Geometric LoS — visibility check before kill), SYS-11 (Prop System — props can be destroyed by laser), SYS-22 (Match State Machine — disabled during Grace/PostMatch)  
**Dependents:** SYS-27 (Eye HUD — charge indicator), SYS-32 (VFX & Juice — beam VFX, hit effects)  
**Status:** Designed  

---

## 1. Purpose

The laser is the Eye's execution tool. Pillar 3 states: "The Eye Orchestrates, Not Executes — the laser is a finisher, not the whole game." It must feel powerful and dangerous (Runners fear it) while requiring positioning and timing rather than mechanical aim skill.

**Feel target:** A charged laser release should feel inevitable and punishing when it lands — like a divine punishment rather than a bullet. The telegraph (visible charge line + escalating audio) gives Runners time to react but punishes hesitation. The Eye's skill is in POSITIONING the Runner for the shot (using traps, incarnation, minions), not in raw aiming.

This GDD defines:
- Charge and fire mechanics
- SphereCast parameters (generous hitbox — design intent)
- LoS validation before kill confirmation
- Prop destruction on hit
- Runner elimination sequence
- Network authority (server validates all kills)
- Input mapping

---

## 2. Laser Mechanics

### 2.1 Charge-and-Release Model

The laser operates on a hold-and-release model with a mandatory charge time:

```
Player holds Left Mouse Button
    → Charge begins
    → Telegraph: visible beam line (thin, red) from Eye toward cursor world position
    → Audio: escalating hum, rising pitch over charge duration
    → At full charge: beam glows bright, pulse VFX
    → Player releases Left Mouse Button
        → Fire: SphereCast from Eye toward cursor
        → Hit resolution: prop destroy OR runner eliminate
        → Post-fire: forced cooldown before next charge
```

**There is no "quick fire" at partial charge.** Releasing early cancels the charge with a visual puff and brief lockout. Holding too long (past max charge) fires automatically.

### 2.2 Timing Values

| Parameter | Value | Design Reason |
|-----------|-------|---------------|
| Charge time (hold to full) | 1.5 seconds | Long enough for Runners to react to the telegraph |
| Auto-fire at overcharge | 2.0 seconds | Prevents infinite hold; forces commitment |
| Cooldown after fire | 3.0 seconds | Eye cannot spam; each shot is deliberate |
| Early release lockout | 0.5 seconds | Penalty for misfiring — brief input disable |
| Telegraph visibility | On charge start | Runner sees it immediately |

All values in `GlobalRules_SO.EyeLaserConfig`.

### 2.3 Charge States

```csharp
public enum LaserState
{
    Idle,           // No input
    Charging,       // LMB held, building charge
    FullCharge,     // Charge complete, pulsing — waiting for release or auto-fire
    Firing,         // Release → SphereCast resolving (1 frame)
    Cooldown,       // Post-fire lockout
    EarlyRelease,   // Released too early — brief lockout
}
```

---

## 3. SphereCast Parameters

### 3.1 The Generous Hitbox Design Intent

The concept specifies "SphereCast for generous hitbox." This is a deliberate anti-pillar violation prevention: Pillar 3 says the Eye must NOT require FPS-god precision. A SphereCast with a meaningful radius means aiming near a Runner hits them, not pixel-perfect aiming at their center.

The tradeoff: too large a radius makes the laser trivially easy, defeating the "orchestration required" design. The radius must feel generous without feeling cheap.

```csharp
private const float _laserSphereRadius = 0.35f;  // 35cm — generous but not absurd
private const float _laserRange         = 80f;   // covers any dollhouse layout
```

**The 35cm radius means:** Aiming within 35cm of a Runner's surface connects. At the Eye's normal targeting zoom (FOV ~20°), a Runner at 15m away subtends about 3° of view — the sphere radius covers about 1.3° of that, so the Eye needs to be "close but not exact."

### 3.2 SphereCast Execution

```csharp
// Called server-side when Eye releases laser at full charge
[Server]
private void FireLaser()
{
    Vector3 origin = _eyePosition.Value;  // Eye NetworkObject world position
    Vector3 target = _cursorWorldPosition.Value;  // synced cursor pos from SYS-02
    Vector3 dir    = (target - origin).normalized;

    RaycastHit[] hits = Physics.SphereCastAll(
        origin, _laserSphereRadius, dir, _laserRange,
        LayerMask.GetMask("Runner", "Prop")
    );

    // Sort hits by distance — process closest first
    System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));

    foreach (var hit in hits)
    {
        // Did we hit a prop first?
        if (hit.collider.TryGetComponent<DynamicProp>(out var prop))
        {
            prop.ServerTakeLaserHit();
            // Laser stops at first prop hit — no penetration
            ObserversLaserFired(origin, hit.point, false);
            return;
        }

        // Did we hit a Runner?
        if (hit.collider.TryGetComponent<RunnerController>(out var runner))
        {
            // LoS validation — server checks LoS at fire moment
            if (!LoSManager.Instance.IsRunnerVisible(runner))
                continue;  // Runner is hidden — this is a miss even if sphere overlaps

            // Dash i-frame check (SYS-01 — Dash gives i-frames)
            if (runner.IsInvulnerable)
                continue;

            ServerEliminateRunner(runner);
            ObserversLaserFired(origin, hit.point, true);
            return;
        }
    }

    // Miss — beam fires into empty space
    ObserversLaserFired(origin, origin + dir * _laserRange, false);
}
```

### 3.3 LoS Validation at Fire

The laser validates LoS at the exact moment of fire using `LoSManager.IsRunnerVisible()`. This is critical:

**Why validate LoS at fire, not just use the synced SyncVar?**

The SyncVar `_isVisible` updates at 30Hz but the Eye fires asynchronously. At 30Hz, there's up to 33ms between LoS ticks. A Runner could duck behind cover, LoS tick hasn't updated yet → SyncVar says "visible" → laser would kill them behind cover. The server re-raycasts at fire time to prevent this.

```csharp
// In LoSManager:
[Server]
public bool IsRunnerVisible(RunnerController runner)
{
    // One-shot check using same logic as per-tick, but synchronous
    Vector3 eyeOrigin = _eyePosition.Value;
    foreach (var socket in runner.LoSSockets)
    {
        Vector3 dir = socket.position - eyeOrigin;
        if (!Physics.Raycast(eyeOrigin, dir.normalized, dir.magnitude, _losMask))
            return true;
    }
    return false;
}
```

This adds one additional set of LoS raycasts at fire time — acceptable cost for a once-per-shot event.

---

## 4. Runner Elimination Sequence

When the laser hits a visible, non-invulnerable Runner:

```
ServerEliminateRunner(runner)
    → runner.IsAlive = false (SyncVar — all clients know)
    → GameManager.NotifyRunnerKilled(runner.Owner)
    → ObserversRunnerEliminated(runner.Owner.ClientId)
        → VFX: Vaporize effect at runner position (all clients)
        → SFX: Vaporize sound (all clients)
        → Camera shake on Eye client (SYS-02 impulse)
    → Wait 0.5s (death VFX plays)
    → ServerManager.Despawn(runner.NetworkObject)
    → SYS-18 (Minion System) spawns Minion for that player's connection
```

The Runner client's camera cuts to black on `IsAlive` SyncVar change, then transitions to Minion view when the Minion NetworkObject spawns (SYS-18 contract).

### 4.1 Kill Confirmation Contract (No Self-Report)

Runners CANNOT report their own death. The server is the only authority. The elimination sequence is entirely server-driven. A Runner cannot manipulate `IsAlive` via any client path.

---

## 5. Telegraph Visual

The telegraph is the beam line shown during charge. It serves two purposes:
1. Gives Runners time to react (fairness)
2. Creates tension — seeing the beam track you is terrifying

### 5.1 Telegraph Implementation

```csharp
// On Eye client (IsOwner), runs during Charging and FullCharge states
private void UpdateTelegraph()
{
    if (_laserState != LaserState.Charging && _laserState != LaserState.FullCharge)
    {
        _telegraphLine.enabled = false;
        return;
    }

    _telegraphLine.enabled = true;
    _telegraphLine.SetPosition(0, _eyeOriginVisual.position);
    _telegraphLine.SetPosition(1, _eyeCameraController.CursorWorldPosition);

    // Color/width ramps from thin/dim at charge start → thick/bright at full charge
    float t = _chargeTimer / _rules.laserChargeTime;
    _telegraphLine.startWidth = Mathf.Lerp(0.02f, 0.08f, t);
    _telegraphLine.endWidth   = Mathf.Lerp(0.02f, 0.08f, t);
    _telegraphLine.material.SetColor("_EmissionColor",
        Color.Lerp(_telegraphDimColor, _telegraphBrightColor, t));
}
```

### 5.2 Telegraph Visibility to Runners

The telegraph beam is a `LineRenderer` in world space. Runners CAN see it from their first-person view — it's part of the horror design. A Runner who rounds a corner and sees the beam pointing at them has a split second to dive behind cover.

**The beam is synced to all clients via `ObserversTelegraphUpdate`**, not just rendered locally:

```csharp
// Sent when charge starts and updated every 0.1s (10Hz — no need for 30Hz on a visual)
[ObserversRpc]
private void ObserversTelegraphUpdate(Vector3 start, Vector3 end, float chargeProgress)
{
    // All clients update the telegraph line renderer
    _telegraphLine.SetPosition(0, start);
    _telegraphLine.SetPosition(1, end);
    float t = chargeProgress;
    _telegraphLine.startWidth = Mathf.Lerp(0.02f, 0.08f, t);
    // etc.
}

[ObserversRpc]
private void ObserversTelegraphHide()
{
    _telegraphLine.enabled = false;
}
```

Telegraph updates at 10Hz is sufficient — the beam tracks slowly relative to 30Hz gameplay. Saves bandwidth.

---

## 6. Prop Destruction by Laser

When the SphereCast hits a `DynamicProp` before hitting a Runner:

```csharp
// Laser stops at first prop hit
prop.ServerTakeLaserHit();  // SYS-11 handles health, VFX, despawn
ObserversLaserFired(origin, hit.point, false);  // show beam to hit point, no kill
```

The laser does NOT penetrate props. If a Runner is behind a crate, the laser hits the crate. The Eye must destroy the crate (3 hits for Solid) before being able to hit the Runner.

**Incarnation props block the laser.** They are on the `IncarnationProp` layer which is excluded from the SphereCast mask — laser passes through them. Rationale: the Eye cannot shoot their own possession host. If the Eye is not incarnated, the IncarnationProp still blocks (it's a physical object), but since it's excluded from the SphereCast it passes through. 

**Correction from above:** IncarnationProps DO exist as physical cover and should block the laser unless the Eye is currently incarnated in them. This requires a runtime mask adjustment:

```csharp
private int GetLaserMask()
{
    if (_incarnationSystem.IsCurrentlyIncarnated)
        // Eye is inside a prop — IncarnationProp layer excluded (can't shoot self)
        return LayerMask.GetMask("Runner", "Prop");
    else
        // Eye not incarnated — IncarnationProps also block (they are physical objects)
        return LayerMask.GetMask("Runner", "Prop", "IncarnationProp");
}
```

---

## 7. Cooldown and Input Model

### 7.1 Input Handling (Owner Only)

```csharp
// In EyeController.Update() — only runs on IsOwner
private void UpdateLaserInput()
{
    switch (_laserState)
    {
        case LaserState.Idle:
            if (_inputReader.LaserHeld)
                BeginCharge();
            break;

        case LaserState.Charging:
            _chargeTimer += Time.deltaTime;
            SyncTelegraphToServer();  // [ServerRpc] → server runs visual sync
            if (!_inputReader.LaserHeld)
            {
                if (_chargeTimer < _rules.laser.earlyReleaseMinCharge)
                    EarlyRelease();  // penalty
                // If charged enough, allow release (game concept doesn't mention partial shots — MVP: must reach full charge)
                // Actually: requires full charge. Early release = penalty.
                else
                    EarlyRelease();
                return;
            }
            if (_chargeTimer >= _rules.laser.chargeTime)
                SetState(LaserState.FullCharge);
            break;

        case LaserState.FullCharge:
            _chargeTimer += Time.deltaTime;
            if (!_inputReader.LaserHeld || _chargeTimer >= _rules.laser.autoFireTime)
                ServerRequestFire(_eyeCameraController.CursorWorldPosition);
            break;

        case LaserState.Cooldown:
        case LaserState.EarlyRelease:
            // No input accepted
            break;
    }
}
```

### 7.2 Server Fire Request

The owner client sends the cursor world position to the server. The server performs the actual SphereCast — clients cannot manipulate hit detection.

```csharp
// Called by Eye owner client
[ServerRpc(RequireOwnership = true)]
private void ServerRequestFire(Vector3 cursorWorldPos)
{
    // Server validates: is it actually in FullCharge state server-side?
    // (Client may be slightly desynced — ignore if server-side state disagrees)
    if (_serverLaserState != LaserState.FullCharge) return;

    _cursorWorldPosition = cursorWorldPos;
    FireLaser();
    SetServerState(LaserState.Cooldown);
    ObserversLaserStateChanged(LaserState.Cooldown);
}
```

### 7.3 Cursor World Position Sync

The cursor position sent to the server in the `ServerRpc` is the position at the MOMENT of release, captured client-side. This is acceptable — the client is the Eye owner and the data is used for a single shot, not ongoing simulation. No prediction/reconciliation needed for laser aiming.

---

## 8. Laser Beam VFX

The actual beam VFX (fired beam, not telegraph) is purely cosmetic. `ObserversLaserFired(start, end, isKill)` broadcasts the beam's start and end points:

- **Hit/miss beam:** Full `LineRenderer` from Eye origin to hit point, bright white/red glow, 0.3s lifetime, `DOTween` fade out
- **Kill beam:** Same beam + additional "vaporize" particle burst at Runner position
- **Prop hit beam:** Beam to prop surface + debris particle burst

All VFX owned by SYS-32. This GDD only defines the `ObserversLaserFired` call signature.

```csharp
[ObserversRpc]
private void ObserversLaserFired(Vector3 start, Vector3 end, bool isKill)
{
    VFXManager.Instance.SpawnLaserBeam(start, end, isKill);
    AudioManager.Instance.PlayAt(isKill ? SFX.LaserKill : SFX.LaserFire, start);
    if (isKill)
        _eyeCameraController.ApplyLaserFireShake();  // SYS-02 Cinemachine impulse
}
```

---

## 9. Grace Phase Lockout

During `MatchPhase.Grace`, the laser is fully disabled:

```csharp
// EyeController subscribes to GameManager.OnPhaseChanged:
private void OnPhaseChanged(MatchPhase prev, MatchPhase next)
{
    if (next == MatchPhase.Grace || next == MatchPhase.PostMatch)
        _laserEnabled = false;
    else if (prev == MatchPhase.Grace && next == MatchPhase.Hunt)
        _laserEnabled = true;
}

// In UpdateLaserInput():
if (!_laserEnabled) return;
```

Attempting to charge during Grace shows no VFX and accepts no input. The Eye just pans and observes.

---

## 10. Tuning Values in GlobalRules_SO

```csharp
[System.Serializable]
public struct EyeLaserConfig
{
    public float chargeTime;           // 1.5  seconds to full charge
    public float autoFireTime;         // 2.0  seconds — auto-fire if held too long
    public float cooldownTime;         // 3.0  seconds post-fire
    public float earlyReleaseLockout;  // 0.5  seconds penalty for early release
    public float sphereRadius;         // 0.35 meters — generous hitbox
    public float range;                // 80.0 meters
    public float telegraphUpdateHz;    // 10.0 Hz — how often telegraph syncs to server
}
```

---

## 11. Prototype Test Checklist

| Test | Pass Condition |
|------|---------------|
| Charge telegraph visible to Runner | Runner client sees beam pointing at them |
| Auto-fire at 2s | Beam fires without release input |
| Early release lockout | Input disabled 0.5s after premature release |
| SphereCast hits Runner in open | Kill confirmed; Minion spawns |
| SphereCast misses hidden Runner | No kill (LoS validation blocks) |
| Dashing Runner not hit | i-frame prevents kill during dash |
| Laser hits prop before Runner | Prop takes damage; Runner unharmed |
| 3 prop hits → prop destroyed | Solid prop despawns; cover gap opens |
| Fragile prop → 1 hit destroy | Fragile prop despawns instantly |
| Grace phase → no laser | Charge input rejected |
| Cooldown → no premature refire | Input locked during cooldown |
| Server authority | Client cannot trigger kill without server validation |

---

## 12. MVP Scope

- [x] Charge-and-release model with mandatory full charge
- [x] LaserState enum (Idle, Charging, FullCharge, Firing, Cooldown, EarlyRelease)
- [x] SphereCast with 35cm radius, stops at first prop hit
- [x] LoS re-validation at fire moment (not relying on stale SyncVar)
- [x] Dash i-frame check (runner.IsInvulnerable)
- [x] Runner elimination sequence (IsAlive → VFX → Despawn → Minion spawn trigger)
- [x] Telegraph LineRenderer synced to all clients at 10Hz
- [x] Prop destruction on hit (SYS-11 interface: ServerTakeLaserHit)
- [x] IncarnationProp layer excluded when Eye is incarnated
- [x] Grace/PostMatch lockout via phase change subscription
- [x] ServerRpc fire request (client sends cursor pos, server validates and fires)
- [x] ObserversLaserFired for beam VFX (SYS-32 interface)
- [x] ObserversRunnerEliminated for death VFX + camera shake
- [x] All timing values in GlobalRules_SO.EyeLaserConfig
- [x] 12-test prototype checklist
- [ ] Laser sound design (charging pitch ramp, fire boom) — SYS-19 dependency
- [ ] Beam VFX with DOTween fade — SYS-32 dependency
- [ ] Eye HUD charge indicator — SYS-27 dependency

---

*System: SYS-06 — Eye Laser System*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Depends on: [sys-02-eye-camera.md](sys-02-eye-camera.md), [sys-05-geometric-los.md](sys-05-geometric-los.md), [sys-11-prop-system.md](sys-11-prop-system.md), [sys-22-match-state-machine.md](sys-22-match-state-machine.md)*  
*Status: Complete — ready for implementation*
