# GDD: Weight/Burden System

**System ID:** SYS-04  
**Design Order:** 6 of 33  
**Priority:** MVP — Core Layer  
**Dependencies:** SYS-14 (Item Database — item weight values), SYS-33 (FishNet Replication — SyncVar patterns), SYS-22 (Match State Machine — freeze at PostMatch)  
**Dependents:** SYS-01 (Runner Movement — reads tier for ability gating + speed), SYS-09 (Loot & Extraction — pickup/drop triggers weight changes), SYS-15 (Shrine/Shop — weight delta items), SYS-16 (Usable Items — grapple adds weight), SYS-26 (Runner HUD — weight meter display)  
**Status:** Designed  

---

## 1. Purpose

The Weight/Burden system is Pillar 2 made mechanical: **every advantage costs mobility**. It converts abstract greed into a physical consequence. A Runner who takes a gold bar is literally slower. A Runner who buys a grapple hook carries it. The system makes the extraction loop feel like a real negotiation — not a puzzle with a correct answer, but a constant risk assessment.

This system owns:
- Tracking current weight total per Runner
- Computing weight tier from that total
- Broadcasting tier changes so SYS-01 can update ability gates and speed
- Handling drops and stashes
- Receiving weight deltas from item pickups, drops, shrine purchases, and weight reducers

It does NOT own:
- Movement physics (SYS-01)
- The items themselves (SYS-14)
- The pickup interaction (SYS-09)
- HUD display (SYS-26)

---

## 2. Weight Tier Model

### 2.1 Tier Table

| Tier | Weight Range | Speed Multiplier | Lost Abilities |
|------|-------------|-----------------|----------------|
| **Naked** | 0.0 kg | 1.00x | None — full kit |
| **Light** | 0.001–5.0 kg | 0.90x | None (Air Dash threshold at >5 kg, see below) |
| **Moderate** | 5.001–12.0 kg | 0.75x | Dash, Double Jump, Air Dash |
| **Heavy** | 12.001–20.0 kg | 0.55x | + Slide, Wall Jump, Wall Run |
| **Overloaded** | 20.001+ kg | 0.35x | Walk + Jump only |

**Air Dash special threshold:** Air Dash is lost at weight > 5.0 kg (exactly at the Moderate boundary). However because it is consumed per-airtime and restored on landing, it cannot be "regained" mid-air by dropping weight — the ability re-enables only on the next landing if weight drops back below the threshold. See Section 5.3.

### 2.2 Tier Enum

```csharp
public enum WeightTier
{
    Naked,      // 0 kg
    Light,      // 0.001–5.0 kg
    Moderate,   // 5.001–12.0 kg
    Heavy,      // 12.001–20.0 kg
    Overloaded  // 20.001+ kg
}
```

### 2.3 Tier Thresholds in GlobalRules_SO

```csharp
// In GlobalRules_SO:
public float weightThresholdLight      = 0.001f;  // >= this → Light
public float weightThresholdModerate   = 5.001f;  // >= this → Moderate
public float weightThresholdHeavy      = 12.001f; // >= this → Heavy
public float weightThresholdOverloaded = 20.001f; // >= this → Overloaded

public float airDashWeightCutoff = 5.0f;  // Air Dash disabled above this (inclusive)

public WeightTier GetTier(float kg)
{
    if (kg >= weightThresholdOverloaded) return WeightTier.Overloaded;
    if (kg >= weightThresholdHeavy)      return WeightTier.Heavy;
    if (kg >= weightThresholdModerate)   return WeightTier.Moderate;
    if (kg >= weightThresholdLight)      return WeightTier.Light;
    return WeightTier.Naked;
}

public float GetSpeedMultiplier(WeightTier tier) => tier switch
{
    WeightTier.Naked      => 1.00f,
    WeightTier.Light      => 0.90f,
    WeightTier.Moderate   => 0.75f,
    WeightTier.Heavy      => 0.55f,
    WeightTier.Overloaded => 0.35f,
    _ => 1.0f
};
```

---

## 3. WeightComponent NetworkBehaviour

Each `RunnerController` has a `WeightComponent` child `NetworkBehaviour`. Separating it from movement keeps each class single-responsibility.

### 3.1 SyncVars

```csharp
public class WeightComponent : NetworkBehaviour
{
    // ── Synced to all clients ────────────────────────────────────────
    // Current weight is synced so Eye HUD can show encumbrance level
    // and other Runners can see who is heavily loaded (affects strategy)
    private readonly SyncVar<float>      _currentWeight =
        new(0f);

    private readonly SyncVar<WeightTier> _currentTier =
        new(WeightTier.Naked);

    // ── Events ──────────────────────────────────────────────────────
    public event Action<float, float, WeightTier, WeightTier> OnWeightChanged;
    // (prevKg, newKg, prevTier, newTier)

    // ── Properties ──────────────────────────────────────────────────
    public float      CurrentWeight => _currentWeight.Value;
    public WeightTier CurrentTier   => _currentTier.Value;
}
```

### 3.2 Initialization

```csharp
public override void OnStartServer()
{
    base.OnStartServer();
    _currentWeight.Value = 0f;
    _currentTier.Value   = WeightTier.Naked;
}

public override void OnStartNetwork()
{
    base.OnStartNetwork();
    _currentWeight.OnChange += HandleWeightChanged;
    _currentTier.OnChange   += HandleTierChanged;
}

private void HandleTierChanged(WeightTier prev, WeightTier next, bool asServer)
{
    // Fire public event — RunnerController subscribes to update ability flags
    OnWeightChanged?.Invoke(_currentWeight.Value, _currentWeight.Value, prev, next);
}
```

### 3.3 Server-Only Mutation Methods

Weight is **only changed on the server**. Clients never self-report weight. All calls go through `ServerRpc` or are called directly by server-side systems (SYS-09 pickup validation, SYS-15 shrine purchase).

```csharp
// Called by SYS-09 (Loot) after server validates pickup
[Server]
public void AddWeight(float kg)
{
    SetWeight(_currentWeight.Value + kg);
}

// Called by SYS-09 after server validates item drop
[Server]
public void RemoveWeight(float kg)
{
    SetWeight(Mathf.Max(0f, _currentWeight.Value - kg));
}

// Called by SYS-15 (Shrine) for WeightReducer item
[Server]
public void ApplyWeightDelta(float delta)
{
    SetWeight(Mathf.Max(0f, _currentWeight.Value + delta));  // delta is negative for reducer
}

[Server]
private void SetWeight(float newWeight)
{
    float oldWeight     = _currentWeight.Value;
    WeightTier oldTier  = _currentTier.Value;

    _currentWeight.Value = newWeight;
    _currentTier.Value   = GlobalRules_SO.Instance.GetTier(newWeight);

    // SyncVars fire callbacks automatically — no manual event needed
    // RunnerController subscribes to _currentTier.OnChange (SYS-01 contract)
}
```

---

## 4. Ability Gate Contract (Interface with SYS-01)

`RunnerController` subscribes to `WeightComponent.OnWeightChanged` and `_currentTier.OnChange`. The exact callback is defined in SYS-01:

```csharp
// In RunnerController.OnStartNetwork():
GetComponent<WeightComponent>()._currentTier.OnChange += OnWeightTierChanged;

private void OnWeightTierChanged(WeightTier prev, WeightTier next, bool asServer)
{
    if (asServer) return;  // server already handled this; clients react here
    _weightTier = next;
    RefreshAbilityAvailability();
}
```

The tier change fires on **all clients** via SyncVar replication — RunnerController on the owning client gets the callback, updates its ability flags, and begins enforcing gates on the NEXT input poll. There is no desync window: the SyncVar fires before the next input is processed at 30Hz.

### 4.1 Tier → Ability Gate Table

| Ability | Naked | Light (≤5kg) | Moderate (>5kg) | Heavy (>12kg) | Overloaded (>20kg) |
|---------|-------|-------------|-----------------|---------------|-------------------|
| Sprint | ✓ | ✓ | ✓ | ✓ | ✓ (slow) |
| Jump | ✓ | ✓ | ✓ | ✓ | ✓ |
| Double Jump | ✓ | ✓ | ✗ | ✗ | ✗ |
| Slide | ✓ | ✓ | ✓ | ✗ | ✗ |
| Wall Jump | ✓ | ✓ | ✓ | ✗ | ✗ |
| Wall Run | ✓ | ✓ | ✓ | ✗ | ✗ |
| Dash | ✓ | ✓ | ✗ | ✗ | ✗ |
| Air Dash | ✓ | ✓ (≤5.0) | ✗ | ✗ | ✗ |
| Grapple | Item-based | Item-based | Item-based | Item-based | Item-based |

**Note on Grapple:** Grapple Hook is a shrine item that itself weighs 3 kg. When equipped, those 3 kg are added to the Runner's total immediately. A Runner at exactly 4 kg (Light) who equips the grapple at 3 kg moves to 7 kg (Moderate) and instantly loses Dash and Double Jump. This is intentional — the grapple trades explosive mobility for traversal reach.

---

## 5. Drop and Stash Mechanic

### 5.1 Design Intent

Dropping items is the system's safety valve and its deepest strategic expression. A Runner who has been greedy can shed weight in a panic. A Runner who planned ahead stashes items in a safe room, runs light through a dangerous corridor, and returns for them.

**The Eye can camp dropped loot.** Drop positions are broadcast to all players (SyncVar on the item NetworkObject). This creates a cat-and-mouse: does the Runner go back for their stash? Does the Eye know where they dropped? Is the stash still there?

### 5.2 Drop Input and Server Flow

```
Player presses Drop (G key — configurable)
    → RunnerController.OnDropInput()
    → [ServerRpc] ServerRequestDrop(slotIndex)
        → Server validates: is item there, is Runner alive, is phase active?
        → SYS-09 spawns DroppedLootItem NetworkObject at Runner's feet
        → WeightComponent.RemoveWeight(item.weight)
        → Runner's loot value does NOT decrease (loot value is retained by Runner
          even when item is dropped — they already "banked" the points)
```

**Loot value is NOT removed on drop.** A Runner who picked up a 10-point item and then dropped it keeps 10 points toward their extraction threshold. They just no longer carry the weight. This means there is no mechanical reason to drop for points — drops are purely about weight management. The risk is the Eye camping the dropped item.

### 5.3 Mid-Air Drop Edge Case

If a Runner drops an item while airborne (common — players will drop to re-enable abilities while falling):

- `WeightComponent.RemoveWeight()` fires immediately on the server
- `_currentTier` SyncVar updates within one server tick (~33ms)
- `RunnerController.OnWeightTierChanged()` fires on the client on the next SyncVar sync
- **Air Dash: NOT re-enabled mid-air even if tier drops below threshold.** The `_airDashAvailable` flag only resets on landing. Dropping weight while airborne does not get you a free Air Dash.
- **Double Jump: NOT re-enabled mid-air if tier drops below Moderate.** Same rule.
- **Dash, Slide, Wall abilities:** These only apply when grounded — re-enabled immediately but won't matter until next grounded state.

This prevents the exploit: "pick up item, grab its loot value, drop it before landing to retain Air Dash." The one-per-airtime reset on landing makes the rule clean.

### 5.4 Stash Behavior

There is no explicit "stash" inventory. Dropping an item creates a `DroppedLootItem` NetworkObject in the world. The Runner can pick it up again later. Other Runners can also pick it up. The item persists until:
- A Runner picks it up
- Match ends (all items despawned in PostMatch cleanup)

`DroppedLootItem` is a simpler version of `LootItem` — same `ItemData_SO` reference, same weight and loot value, glows with the Runner's highlight color to aid identification (cosmetic, not gameplay-informative to the Eye).

---

## 6. Shrine Item Interactions

### 6.1 Weight Reducer

The `WeightReducer` shrine item permanently reduces the Runner's total weight by 3 kg (or to 0, whichever is less).

```csharp
// Called by SYS-15 after shrine purchase is validated
[Server]
public void ApplyShrineItem(ShrineItemData_SO item)
{
    if (item.shrineItemType == ShrineItemType.WeightReducer)
        ApplyWeightDelta(item.weightDelta);  // item.weightDelta = -3f
    // Other shrine items are processed by SYS-16 (Usable Items)
    // Weight reducer is the only one that directly modifies weight
}
```

**Design note:** Weight Reducer costs "Very High" loot (defined in SYS-14 starter roster as 40 points). The extraction threshold is 100 points. Spending 40 points on a weight reducer means the Runner needs significantly more loot to extract. This is Pillar 2 at the meta level — even the item that solves your weight problem costs you.

### 6.2 Grapple Hook Weight

The grapple hook adds 3 kg on equip and removes 3 kg on "unequip" (if Runner drops it as an item). However the grapple hook as a `UsableItem` (SYS-16) is an equipped item — it cannot be dropped without explicitly dropping it via the drop mechanic (not unequipped). This is intentional: once you take the grapple, you commit to its weight.

---

## 7. Visual Feedback (Client-Side)

Weight is communicated visually through:

### 7.1 Runner Posture (Cosmetic — SYS-32 VFX dependency)

Runners have 3 posture states driven by weight tier:
- **Naked/Light:** Upright, smooth movement
- **Moderate/Heavy:** Slightly hunched, visible item burden (backpack/bags on model)
- **Overloaded:** Visibly struggling — staggered walk, bent forward

These are **blend tree states** driven by `_currentTier` SyncVar on all clients. The Eye sees the posture change from the 2D view — a hunched Runner is visually identifiable as a high-value target.

### 7.2 HUD Weight Meter (SYS-26 dependency)

Runner HUD displays:
- Current weight in kg (numeric)
- Color-coded tier indicator (green → yellow → orange → red)
- Ability icons greyed out when their tier gate is lost
- Brief flash animation on tier change

This is all handled by SYS-26 (Runner HUD) which subscribes to `WeightComponent._currentTier.OnChange`.

### 7.3 Eye HUD Weight Indicators (SYS-27 dependency)

The Eye can see Runner weight through visual tells on the dollhouse view:
- Posture change (see 7.1)
- Name label color coded by tier (all clients receive `_currentTier` SyncVar)
- Optional: icon overlay on Runner silhouette showing burden level

Exact Eye HUD design owned by SYS-27.

---

## 8. Weight at Match End

At `MatchPhase.PostMatch`:
- No new weight changes are processed (input is frozen by SYS-22 contract)
- `WeightComponent` state is preserved for the results screen (SYS-23 displays final weight carried by each Runner as a stat)
- Scene unload clears all state

---

## 9. Multiplayer Invariants

| Invariant | Enforcement |
|-----------|-------------|
| Weight total can never go below 0 | `Mathf.Max(0f, ...)` in SetWeight |
| Weight changes are server-authoritative | All mutation methods are `[Server]` — no client self-report |
| Tier changes replicate to all clients | SyncVar handles replication — Eye, other Runners, and minions all see tier |
| Two Runners cannot take the same item | SYS-09 validates pickup server-side before calling AddWeight |
| Weight Reducer cannot drive weight negative | `Mathf.Max(0f, ...)` guard in ApplyWeightDelta |
| Mid-air drop does not restore per-airtime abilities | Per-airtime flags (AirDash, DoubleJump) only reset on landing regardless of weight change |

---

## 10. Balance Notes and Tuning Levers

The weight system's balance is the hardest part of the entire game. Target feel: **the first item should feel like a slight luxury, the third item should feel like a real decision, and being Overloaded should feel like walking through water.**

Key tuning levers (all in `GlobalRules_SO`):

| Lever | Current Value | Effect of increasing | Effect of decreasing |
|-------|-------------|---------------------|---------------------|
| `weightThresholdModerate` | 5.001 kg | Runners can carry more before losing Dash | Dash gone sooner — more pressure |
| `weightThresholdHeavy` | 12.001 kg | More range in Moderate tier | Heavy penalties come earlier |
| `speedMultiplierHeavy` | 0.55x | Make Heavy feel less punishing | Make Heavy more punishing |
| `speedMultiplierOverloaded` | 0.35x | Overloaded is more survivable | Overloaded is a death sentence |
| `airDashWeightCutoff` | 5.0 kg | Air Dash survives into Moderate | Air Dash is lost even at Light |
| Item weights (in SYS-14) | various | Heavier items push tiers faster | More items before penalties |

**Playtest signal:** If Runners are consistently going Overloaded and dying, reduce item weights or raise thresholds. If Runners are consistently staying Naked, increase loot value rewards to make carrying more items worthwhile (SYS-09 tuning, not weight tuning).

---

## 11. MVP Scope

- [x] WeightTier enum (5 tiers) defined
- [x] Tier thresholds and speed multipliers in GlobalRules_SO
- [x] WeightComponent NetworkBehaviour with SyncVar<float> and SyncVar<WeightTier>
- [x] Server-only mutation (AddWeight, RemoveWeight, ApplyWeightDelta)
- [x] SyncVar callbacks to RunnerController (SYS-01 interface)
- [x] Ability gate table documented (9 abilities × 5 tiers)
- [x] Drop mechanic: server flow, loot value retention on drop, mid-air edge case
- [x] Stash behavior: DroppedLootItem persistence, other-Runner pickup allowed
- [x] Shrine item interactions: WeightReducer, GrappleHook
- [x] Visual feedback contracts: posture (SYS-32), HUD (SYS-26), Eye HUD (SYS-27)
- [x] Multiplayer invariants table
- [x] Balance tuning lever table
- [ ] Posture blend tree implementation (SYS-32 dependency)
- [ ] Weight stat on results screen (SYS-23 dependency)
- [ ] "Weight change" audio feedback (SYS-19 dependency — a grunt/groan on tier crossing)

---

*System: SYS-04 — Weight/Burden System*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Depends on: [sys-01-item-database.md](sys-01-item-database.md), [sys-33-fishnet-replication.md](sys-33-fishnet-replication.md), [sys-22-match-state-machine.md](sys-22-match-state-machine.md)*  
*Status: Complete — ready for implementation*
