# GDD: Prop System

**System ID:** SYS-11  
**Design Order:** 10 of 33  
**Priority:** MVP — Feature Layer  
**Dependencies:** SYS-05 (Geometric LoS — props are the primary occluders), SYS-10 (Procedural Map Generator — props populate at designated PropSlot markers in generated rooms)  
**Dependents:** SYS-06 (Eye Laser — destroys props, exposing Runners), SYS-07 (Incarnation — Eye possesses eye-decorated props), SYS-32 (VFX & Juice — destruction VFX, interaction feedback)  
**Status:** Designed  

---

## 1. Purpose

Props are the physical geometry of stealth. A Runner without cover is dead. A Runner who knows how to read prop coverage and chain from cover to cover survives. The Prop System defines:

- The three prop categories and what separates them
- How props interact with LoS raycasting (they are the blockers)
- How dynamic props react to players (pushable, destructible)
- How the Eye destroys props and collapses cover
- How Incarnation props are tagged and discovered

The concept states: "Cover is destructible — Eye laser can destroy props after a delay, exposing Runners. Runners can flip/push props to create new cover dynamically."

This system owns the prop components, interaction contracts, and destruction logic. It does NOT own the Eye laser that triggers destruction (SYS-06) or the Incarnation gameplay (SYS-07).

---

## 2. Prop Categories

### 2.1 Three Prop Types

| Type | Examples | Behavior | LoS | Eye Can Destroy? |
|------|----------|----------|-----|-----------------|
| **Static** | Stone walls, pillars, bookshelves bolted to wall, chandeliers | Immovable, indestructible | Full blocker | No |
| **Dynamic** | Crates, barrels, furniture, vases, tables | Can be pushed/tipped by Runners; fragile ones shatter on contact | Full blocker until destroyed | Yes (fragile) / No (solid) |
| **Incarnation** | Portraits with painted eyes, gargoyles, busts, grandfather clocks | Eye can possess these for interior view; emit glow to signal possession possibility | Full blocker | No (special — destroyed only by Smoke Bomb blinding, not laser) |

### 2.2 Dynamic Prop Sub-Types

Dynamic props have two sub-types:

| Sub-Type | Material | Behavior on contact | Behavior on laser | Noise on break |
|----------|----------|--------------------|--------------------|----------------|
| **Solid** | Heavy wood, stone, metal | Slides when pushed (Runner momentum transfers), does not break | Can be hit by laser — takes multiple hits (3) to destroy, each hit deals visual damage | Yes — sliding thud, impact |
| **Fragile** | Pottery, glass, thin shelves, candles | Shatters on runner contact OR laser (1 hit each) | Instant destroy on first laser hit | Large — pottery break feeds Dread spike |

**Fragile props create a stealth risk.** A Runner dashing through a room full of vases makes noise that spikes the Eye's Dread. A careful Runner walks around them. This is Pillar 5 (Paranoia is a Feature) in the prop design — every object in the room is a potential alarm.

---

## 3. Prop Component Architecture

### 3.1 PropBase (Abstract — attached to all props)

```csharp
public abstract class PropBase : NetworkBehaviour
{
    [SerializeField] public PropType  PropType;    // Static | Dynamic | Incarnation
    [SerializeField] public PropLayer PropLayer;   // set at design time for LoS mask
    // Note: PropLayer = "Prop" for all props that block LoS (set in prefab)

    // Called by server when prop should be removed from the world
    [Server]
    public virtual void ServerDestroy()
    {
        ObserversPlayDestroyFX();
        ServerManager.Despawn(gameObject);
    }

    [ObserversRpc]
    private void ObserversPlayDestroyFX()
    {
        VFXManager.Instance.SpawnAt(VFX.PropDestroy, transform.position);
        AudioManager.Instance.PlayAt(SFX.PropDestroy, transform.position);
    }
}
```

### 3.2 DynamicProp (inherits PropBase)

```csharp
public class DynamicProp : PropBase
{
    [SerializeField] public  DynamicPropSubType SubType;  // Solid | Fragile
    [SerializeField] private int                _maxHealth = 3;  // Solid: 3 hits; Fragile: 1
    [SerializeField] private float              _slideFriction = 2.0f;
    [SerializeField] private float              _noiseRadius;  // 0 = fragile has large radius

    private readonly SyncVar<int> _health = new(0);

    public override void OnStartServer()
    {
        base.OnStartServer();
        _health.Value = _maxHealth;
    }

    // Called by SYS-06 (Eye Laser) on hit
    [Server]
    public void ServerTakeLaserHit()
    {
        _health.Value--;
        if (_health.Value <= 0)
            ServerDestroy();
        else
            ObserversDamageVisual(_maxHealth - _health.Value);
    }

    // Called by Physics collision with Runner's CharacterController
    [Server]
    public void ServerRunnerImpact(float impactForce, Vector3 direction)
    {
        if (SubType == DynamicPropSubType.Fragile)
        {
            SoundRadiusSystem.Instance.EmitNoise(transform.position, _noiseRadius, NoiseType.PropBreak);
            ServerDestroy();
        }
        else
        {
            // Push the solid prop
            _rigidbody.AddForce(direction * impactForce, ForceMode.Impulse);
            SoundRadiusSystem.Instance.EmitNoise(transform.position, _noiseRadius * 0.3f, NoiseType.PropSlide);
        }
    }

    [ObserversRpc]
    private void ObserversDamageVisual(int damageLevel)
    {
        // Switch material/mesh to cracked variant
        _meshSwapper.SetDamageLevel(damageLevel);
    }
}
```

### 3.3 IncarnationProp (inherits PropBase)

```csharp
public class IncarnationProp : PropBase
{
    // Eye mini-laser socket — where the incarnated laser fires from
    [SerializeField] public Transform LaserOrigin;

    // Called by SYS-07 when Eye selects this prop for incarnation
    public void SetIncarnating(bool isActive)
    {
        // Visual state: glow on → Eye is inside (local tell for Runners)
        _glowEffect.SetActive(isActive);
        // Server updates a SyncVar so all clients see the glow
        if (IsServer) _isIncarnated.Value = isActive;
    }

    private readonly SyncVar<bool> _isIncarnated = new(false);
}
```

---

## 4. LoS Interaction Contract

Props block LoS by being on the `Prop` layer (see SYS-05 Section 11 — Layer Architecture). This is entirely passive — there is no code in PropBase that talks to LoSManager. The layer assignment IS the contract.

**Rules:**
- All props use the `Prop` layer — static, dynamic, and incarnation
- When `ServerDestroy()` is called, `ServerManager.Despawn()` removes the NetworkObject from the scene, which removes its collider, which removes it from the `Prop` layer
- LoS recalculates on the next tick (33ms) and the gap in cover is immediately real
- No explicit "notify LoS" call needed — Physics handles it automatically

**Dynamic prop push:** When a solid prop slides, its collider moves with it in the Physics simulation (replicated via `NetworkRigidbody`). LoS raycasts against the new position on the next tick.

---

## 5. Player–Prop Physics Interaction

### 5.1 Detection Method

Runner's CharacterController doesn't fire `OnCollisionEnter` — it fires `OnControllerColliderHit`. This is the hook for prop impact:

```csharp
// In RunnerController — called every frame by Unity when CC hits a collider
private void OnControllerColliderHit(ControllerColliderHit hit)
{
    if (!IsServer) return;  // only server handles prop impact
    if (!hit.gameObject.TryGetComponent<DynamicProp>(out var prop)) return;

    // Only trigger on horizontal hits (not standing on props)
    if (Mathf.Abs(hit.normal.y) > 0.7f) return;

    float speed = _hVelocity.magnitude;
    if (speed < 2f) return;  // walking → no impact

    prop.ServerRunnerImpact(speed * 0.5f, hit.moveDirection);
}
```

The server runs `OnControllerColliderHit` because the server runs the Runner's `CharacterController.Move()` via FishNet prediction (SYS-01). The server is the authority on collision events.

### 5.2 Impact Thresholds

| Runner speed | Impact type | Effect |
|-------------|------------|--------|
| < 2 m/s (walk) | None | Prop untouched |
| 2–5 m/s (light jog) | Glancing | Fragile: shatter; Solid: small push |
| > 5 m/s (sprint/dash) | Full | Fragile: shatter + loud noise; Solid: significant push |

Running through a room at full sprint will shatter every fragile prop in your path. Skilled Runners navigate around fragile props or accept the Dread spike as the cost of speed.

---

## 6. Eye Laser Destruction Sequence

When SYS-06 fires and the SphereCast hits a `DynamicProp`:

```
SYS-06 detects prop hit
    → prop.ServerTakeLaserHit()
        → _health.Value--
        → if health > 0: ObserversDamageVisual(damageLevel)  — visual crack
        → if health == 0: ServerDestroy()
            → ObserversPlayDestroyFX()  — explosion/shatter VFX on all clients
            → ServerManager.Despawn(gameObject)
                → Collider removed from physics
                → LoS next tick: Runner behind it may become visible
```

**Solid props take 3 laser hits** — the Eye must commit to destroying a prop. This prevents "laser spam everything into dust" — the Eye has to hold the beam on a prop for ~3 seconds total (charge + fire is ~1.5s per SYS-06, so two separate aim-fires). Fragile props die in one hit but they weren't major cover anyway.

**Incarnation props cannot be destroyed by laser** — they are host bodies for the Eye. Shooting through your own eye into your portrait body is undefined. Design decision: laser passes through Incarnation props (they are on a sub-layer `IncarnationProp` excluded from the laser's SphereCast mask).

---

## 7. Noise Generation

Prop events feed into SYS-19 (Sound Radius System). All noise calls go through a central `SoundRadiusSystem.EmitNoise()`:

| Event | Noise Radius | Noise Type | Dread Feed |
|-------|-------------|-----------|-----------|
| Fragile prop shattered by Runner | 12m | `PropBreak` | High spike |
| Fragile prop shattered by laser | 12m | `PropBreak` | None (Eye caused it) |
| Solid prop pushed | 4m | `PropSlide` | Low |
| Prop destroyed by laser (any) | 8m | `PropDestroy` | Medium (collateral) |

The Eye hears prop breaks as a directional UI indicator, even from outside the dollhouse. A Runner who shatters a vase three rooms away gives the Eye a clear "go there" signal.

---

## 8. Incarnation Prop Discovery

How does a Runner know which props are incarnation points? The concept says "Props glow (local tell)" and "Giant eye goes blank (global tell)."

**Incarnation prop visual state machine:**

| State | Visual | Runner sees | Eye sees |
|-------|--------|------------|---------|
| Available (Eye not incarnated) | Subtle eye motif on the object; faint pulsing outline (only visible when Runner is within 3m) | Small glow at close range | Normal |
| Active (Eye incarnated) | Strong pulsing glow on the prop | Bright warning light — "Eye is here" | Eye's view is from inside this prop |
| Blinded (Ocular Paint applied) | Dark, cracked eye motif | "This one is safe" | Grayed out in Eye's target list |

The 3m proximity glow is important — Runners only discover incarnation props when they get close. This creates a risk: checking a portrait for an Eye eye requires getting close to it.

### 8.1 Ocular Paint Interaction (SYS-16)

When a Runner applies Ocular Paint to an Incarnation prop (SYS-16 usable item), the prop is permanently blinded for the match:

```csharp
[Server]
public void ServerApplyOcularPaint()
{
    // Called by SYS-16 when Runner uses Ocular Paint on this prop
    _isBlinded.Value = true;
    // Eye's SYS-07 can no longer select this prop for incarnation
}

private readonly SyncVar<bool> _isBlinded = new(false);
```

---

## 9. Prop Prefab Specifications

### 9.1 Required Components per Prefab

All prop prefabs must have:
- `PropBase` (or subclass) script
- `Rigidbody` — for dynamic props only (solid: `isKinematic = false` when pushed; fragile: `isKinematic = true` until destroyed)
- `Collider` — on `Prop` layer
- `NetworkObject` — for networked state (health SyncVar, incarnation SyncVar)
- `NetworkRigidbody` — for solid props that slide (position/velocity sync)
- `NetworkTransform` — NOT needed if using `NetworkRigidbody`
- `MeshSwapper` script — for solid props that show damage states
- `LoSBlocker` tag — editor convenience for LoS system layer verification

### 9.2 MVP Prop Roster

Minimum prop set for generated rooms (placed at PropSlot markers by the map generator):

| Prop | Type | Count per room | Role |
|------|------|---------------|------|
| Stone pillar | Static | 2-4 | Primary cover, undestroyable |
| Heavy crate (wood) | Dynamic/Solid | 2-3 | Secondary cover, pushable, 3-hit destroy |
| Vase (ornate) | Dynamic/Fragile | 3-5 | Alarm object, shatters on Runner contact |
| Bookshelf | Static | 1-2 | Wall-adjacent cover |
| Portrait with eye motif | Incarnation | 1 | Eye possession point |
| Barrel | Dynamic/Solid | 1-2 | Rollable cover |
| Candelabra | Dynamic/Fragile | 2-3 | Alarm, easily shattered |
| Armchair | Dynamic/Solid | 1-2 | Compact cover, pushable |

This roster covers: reliable cover (pillars), tactical cover (crates), Eye ambush point (portrait), and noise hazards (vases, candelabra). Enough to test all stealth interactions.

---

## 10. Networked State Summary

| SyncVar | On Type | Default | Purpose |
|---------|---------|---------|---------|
| `_health` | DynamicProp | `_maxHealth` | Sync damage state to all clients |
| `_isIncarnated` | IncarnationProp | `false` | Sync glow state to all clients |
| `_isBlinded` | IncarnationProp | `false` | Sync Ocular Paint state |

All SyncVars are server-set. Clients react via `OnChange` callbacks to update visuals.

`NetworkRigidbody` on Solid props syncs position + velocity when the prop is sliding. Stops syncing once at rest (FishNet NRigidbody handles sleep detection).

---

## 11. Performance Notes

### 11.1 Prop Collider Count

A 20-room dollhouse with ~8 props per room = ~160 prop colliders. This is well within Unity Physics' comfortable range. No spatial partitioning needed for MVP.

### 11.2 NetworkObject Overhead

160 `NetworkObject` instances in a scene is acceptable for FishNet at 30Hz. Each `DynamicProp` SyncVar only updates when its health changes or it's destroyed — not every tick. Static props have no SyncVars and consume minimal network bandwidth.

### 11.3 Destruction Cleanup

`ServerManager.Despawn()` removes the `NetworkObject` from all clients cleanly. Prop count is bounded by the generated map (~20-30 rooms × ~5 props = ~150 max). Consider a simple free list for prop NetworkObjects if profiling shows GC spikes from mass instantiation during map generation.

---

## 12. MVP Scope

- [x] Three prop categories: Static, Dynamic, Incarnation
- [x] Two Dynamic sub-types: Solid (3 hits, pushable) and Fragile (1 hit, shatters on Runner contact)
- [x] PropBase NetworkBehaviour with ServerDestroy() and ObserversPlayDestroyFX()
- [x] DynamicProp: _health SyncVar, ServerTakeLaserHit(), ServerRunnerImpact()
- [x] IncarnationProp: _isIncarnated SyncVar, _isBlinded SyncVar, LaserOrigin socket
- [x] LoS interaction: passive via Prop layer — no explicit LoS callback
- [x] OnControllerColliderHit integration in RunnerController for prop impact
- [x] Impact thresholds: walk = no impact, jog = light push, sprint = full impact
- [x] Noise generation: fragile break (high), solid push (low), laser destroy (medium)
- [x] Eye laser destruction sequence (3 hits for solid, 1 for fragile)
- [x] Incarnation prop visibility: proximity glow (3m), active glow, blinded state
- [x] Ocular Paint: ServerApplyOcularPaint() sets _isBlinded permanently
- [x] MVP prop roster (8 prop types documented)
- [x] Required prefab components list
- [ ] Prop push physics networking (NetworkRigidbody sync — needs Unity Physics tuning in prototype)
- [ ] Prop damage visual variants (mesh swapper — art dependency)
- [ ] Perlin-noise prop clustering (variation within generated rooms)
- [ ] Prop pooling (if profiling shows GC spikes during map generation spawn burst)

---

*System: SYS-11 — Prop System*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Depends on: [sys-05-geometric-los.md](sys-05-geometric-los.md)*  
*Status: Complete — ready for implementation*
