# GDD: Item Database (ScriptableObject Architecture)

**System ID:** SYS-01  
**Design Order:** 1 of 33  
**Priority:** MVP — Foundation Layer  
**Dependencies:** None  
**Dependents:** All systems that touch data (SYS-04 Weight, SYS-06 Laser, SYS-09 Loot, SYS-10 Props, SYS-15 Shrine, SYS-16 Usables, SYS-18 Traps, SYS-21 Procgen)  
**Status:** Designed  

---

## 1. Purpose

This system defines the ScriptableObject schema for every data-driven entity in IStalk. It is pure data — no runtime logic, no MonoBehaviours, no scene objects. Its sole job is to be a single source of truth that every other system reads from.

Getting this schema right before writing any gameplay code prevents the most common form of technical debt in Unity games: scattered magic numbers, duplicated data, and mid-project refactors when systems disagree on what an "item" is.

---

## 2. ScriptableObject Classes

### 2.1 `ItemData_SO` — Loot Items

Loot items are scattered throughout the dollhouse. Runners pick them up to build toward the extraction threshold. Every item has a cost (weight, mobility) and a benefit (loot value toward threshold).

```csharp
[CreateAssetMenu(fileName = "Item_", menuName = "IStalk/Item Database/Loot Item")]
public class ItemData_SO : ScriptableObject
{
    [Header("Identity")]
    public string itemId;           // Unique identifier (e.g. "gold_bar", "gem_cluster")
    public string displayName;      // Shown in UI ("Gold Bar")
    public Sprite icon;             // HUD and world-space billboard icon
    public GameObject worldPrefab;  // The physical pickup object in the scene

    [Header("Economy")]
    [Range(1, 100)]
    public int lootValue;           // Points toward extraction threshold
    [Range(0.1f, 30f)]
    public float weight;            // Kilograms added to Runner's total

    [Header("Visuals")]
    public Rarity rarity;           // Affects glow colour and spawn probability
    public Color glowColour;        // World-space emission for readability at Eye zoom

    [Header("Audio")]
    public AudioClip pickupSound;
    public AudioClip dropSound;
    public float noiseRadius;       // Metres — how far the pickup/drop sound travels
                                    // (feeds into Sound Radius System, SYS-19)
}

public enum Rarity { Common, Uncommon, Rare, Legendary }
```

**Starter item roster (MVP — 3 items minimum):**

| Item ID | Display Name | Loot Value | Weight (kg) | Rarity | Notes |
|---------|-------------|-----------|------------|--------|-------|
| `coin_pouch` | Coin Pouch | 5 | 0.5 | Common | Light, low value — safe early grabs |
| `gem_cluster` | Gem Cluster | 12 | 1.5 | Uncommon | Good value-to-weight ratio |
| `gold_bar` | Gold Bar | 30 | 8.0 | Rare | High value, puts Runner in Moderate tier alone |
| `relic_small` | Ancient Relic | 20 | 3.0 | Uncommon | Mid-tier in both dimensions |
| `relic_cursed` | Cursed Idol | 40 | 12.0 | Legendary | Near-extraction in one pickup; nearly Overloaded |
| `jewel_case` | Jewel Case | 15 | 2.0 | Uncommon | Safe choice — balanced |
| `trophy` | Trophy | 25 | 5.0 | Rare | Puts Light Runner into Moderate |

**Weight-to-value design principle:** Items should never be "always take" or "never take." The sweet spot is items that push a Runner close to — but not past — the next weight tier threshold, forcing a decision.

---

### 2.2 `ShrineItemData_SO` — Shrine/Shop Items

Shrine items are purchased at shrine locations by spending collected loot. They provide defensive or traversal advantages but always cost weight (Pillar 2: Greed Has Weight — no free power).

```csharp
[CreateAssetMenu(fileName = "ShrineItem_", menuName = "IStalk/Item Database/Shrine Item")]
public class ShrineItemData_SO : ScriptableObject
{
    [Header("Identity")]
    public string itemId;
    public string displayName;
    public string description;      // Shown in shrine purchase UI
    public Sprite icon;

    [Header("Economy")]
    [Range(1, 200)]
    public int lootCost;            // Loot points spent to purchase (deducted from Runner total)
    [Range(-5f, 10f)]
    public float weightDelta;       // Positive = adds weight. Negative = removes weight (Weight Reducer only).

    [Header("Activation")]
    public ShrineItemType itemType; // Determines which Usable Items System handler runs
    public float cooldown;          // Seconds between uses (0 = single-use consumed item)
    public bool isConsumedOnUse;    // True = disappears after one use

    [Header("Effect Parameters")]
    // These are read by the Usable Items System (SYS-16).
    // Each itemType has a corresponding handler that reads these fields.
    public float effectRadius;      // For area effects (Smoke Bomb cloud size)
    public float effectDuration;    // For timed effects (smoke, blind, decoy lifetime)
    public float effectIntensity;   // Generic scalar (Mirror blind strength, Decoy similarity)

    [Header("Audio/Visual")]
    public AudioClip activationSound;
    public GameObject activationVfxPrefab;
}

public enum ShrineItemType
{
    OcularPaint,        // Disables one incarnation prop permanently
    Mirror,             // Blinds Eye for effectDuration seconds
    SmokeBomb,          // Blocks LoS raycasts in radius for effectDuration
    Decoy,              // Spawns moving hologram for effectDuration
    WeightReducer,      // Applies negative weightDelta permanently (consumed)
    GrapplingHook,      // Grants Grapple ability (adds weight, persists until dropped)
}
```

**Full shrine item roster:**

| Item ID | Type | Loot Cost | Weight Delta | Duration | Consumed? | Notes |
|---------|------|-----------|-------------|---------|-----------|-------|
| `ocular_paint` | OcularPaint | 25 | 0 | Permanent | Yes | No weight cost — the cost is the loot spent |
| `mirror` | Mirror | 40 | +2.0 | 2.5s | No | Weight persists; reusable with cooldown |
| `smoke_bomb` | SmokeBomb | 15 | +1.0 | 5.0s | Yes | One-use; cheap but single-use |
| `decoy` | Decoy | 20 | 0 | 4.0s | Yes | No weight — cost is purely loot |
| `weight_reducer` | WeightReducer | 80 | -3.0 | Permanent | Yes | Very expensive; only viable when overloaded |
| `grappling_hook` | GrapplingHook | 30 | +3.0 | Permanent | No | Persists until Runner drops it manually |

**Design note on weight costs:** Ocular Paint and Decoy have zero weight delta because their cost is purely economic (spending loot). Mirror and Grappling Hook have positive weight deltas because they are persistent tools the Runner carries — the burden IS the cost. Weight Reducer has a negative delta because that is literally its function.

---

### 2.3 `TrapData_SO` — Eye Traps

Traps are placed by the Eye during a match, funded by Dread currency. They define area-denial and intelligence-gathering tools.

```csharp
[CreateAssetMenu(fileName = "Trap_", menuName = "IStalk/Item Database/Eye Trap")]
public class TrapData_SO : ScriptableObject
{
    [Header("Identity")]
    public string trapId;
    public string displayName;
    public string description;
    public Sprite eyeHudIcon;       // Shown in Eye's trap selection UI

    [Header("Economy")]
    [Range(5, 200)]
    public int dreadCost;           // Dread units spent to place

    [Header("Placement")]
    public TrapPlacementType placementType;  // Floor, Wall, or Object (on a prop)
    public float placementRadius;            // Minimum distance from another trap of same type
    public int maxActivePerMatch;            // Cap on simultaneous placements (0 = unlimited)
    public GameObject placedPrefab;          // Visual representation after placement

    [Header("Trigger")]
    public TrapTriggerType triggerType;
    public float triggerRadius;     // Metres — Runner enters this radius to activate
    public float armDelay;          // Seconds after placement before trap can trigger

    [Header("Effect")]
    public TrapEffectType effectType;
    public float effectRadius;      // Area of effect when triggered
    public float effectDuration;    // How long the effect lasts
    public float effectIntensity;   // Speed multiplier for Sludge, reveal duration for False Chest, etc.

    [Header("Feedback")]
    public AudioClip placementSound;
    public AudioClip triggerSound;
    public GameObject triggerVfxPrefab;
    public bool revealsToRunners;   // If true, Runners see a visual tell after placement
}

public enum TrapPlacementType  { Floor, Wall, Object }
public enum TrapTriggerType    { ProximityEnter, Interaction, Timed }
public enum TrapEffectType     { Slow, RevealPosition, NoiseEmit, BlockPath }
```

**Starter trap roster:**

| Trap ID | Display Name | Dread Cost | Placement | Trigger | Effect | Duration | Intensity | Max Active |
|---------|-------------|-----------|---------|---------|--------|---------|-----------|-----------|
| `sludge` | Sludge Puddle | 40 | Floor | ProximityEnter | Slow | 4.0s | 0.4x speed | 3 |
| `false_chest` | False Chest | 20 | Object | Interaction | RevealPosition | 8.0s reveal | — | 5 |
| `noise_spike` | Haunted Bell | 10 | Object | Timed | NoiseEmit | 3.0s | High noise | 8 |

**Design note on Dread economy:** Sludge (40 Dread) is expensive — it denies mobility, which directly exploits the weight system. False Chest (20 Dread) is mid-cost intelligence, used for baiting and position tracking. Noise Spike (10 Dread) is cheap harassment, useful in bulk near extraction altars. The economy forces real choices: four Noise Spikes or one Sludge.

---

### 2.4 `RoomNode_SO` — Room Definitions

Used by the Procedural Map Generator (SYS-10) to define legal room types, their dimensions, and how they connect to adjacent rooms.

```csharp
[CreateAssetMenu(fileName = "Room_", menuName = "IStalk/Item Database/Room Node")]
public class RoomNode_SO : ScriptableObject
{
    [Header("Identity")]
    public string roomId;               // e.g. "library", "dungeon", "kitchen"
    public string displayName;
    public GameObject roomPrefab;       // The actual Unity prefab for this room

    [Header("Dimensions")]
    public Vector3Int gridSize;         // Room occupancy in generator grid units (X, Y, Z)
                                        // Z is always 1 for side-view dollhouse layout

    [Header("Connections")]
    public List<ConnectionTag> openConnections;  // Which sides expose connectors
    // Connector positions are defined as child empty GameObjects in the prefab,
    // tagged with the direction they face (Up/Down/Left/Right)

    [Header("Spawn Rules")]
    [Range(0f, 1f)]
    public float spawnWeight;           // Probability relative to other rooms in pool
    public int maxPerMap;               // 0 = unlimited (Dungeon might be limited to 1)
    public RoomCategory category;

    [Header("Contents")]
    public int lootSpawnSlots;          // How many loot items this room can contain
    public bool canContainShrine;       // Is a shrine slot present in this room?
    public int incarnationPropSlots;    // How many eye-decorated props can spawn here
    public bool isValidRunnerSpawn;     // Can Runners start here?
    public bool isValidAltarLocation;   // Can an extraction altar appear here?
}

public enum ConnectionTag  { Up, Down, Left, Right }
public enum RoomCategory   { Corridor, SmallRoom, LargeRoom, StartingArea, BossRoom }
```

---

### 2.5 `GlobalRules_SO` — Match Parameters

A singleton ScriptableObject loaded at match start. All tunable match-level constants live here so designers can adjust balance without touching code.

```csharp
[CreateAssetMenu(fileName = "GlobalRules", menuName = "IStalk/Global Rules")]
public class GlobalRules_SO : ScriptableObject
{
    [Header("Extraction")]
    public int extractionThreshold = 100;   // Total loot value needed to activate altars
    public float altarChannelTime  = 6.0f;  // Seconds to channel at altar for extraction

    [Header("Match Timing")]
    public float gracePeriodDuration  = 30f;   // Seconds of Eye-blind at match start
    public float matchTimeLimit       = 1080f; // 18 minutes in seconds
    public float overtimeRoomSealRate = 30f;   // Seconds between room seals in overtime

    [Header("Eye — Dread Economy")]
    public float dreadBaseRegenRate  = 3.0f;   // Dread units per second passive regen
    public float dreadNoiseBonusMultiplier = 2.0f; // Multiplier on noise events
    public float dreadStartingAmount = 0f;

    [Header("Weight Tier Thresholds (kg)")]
    public float weightThresholdLight      = 1f;
    public float weightThresholdModerate   = 6f;
    public float weightThresholdHeavy      = 13f;
    public float weightThresholdOverloaded = 21f;

    [Header("Weight Speed Multipliers")]
    public float speedMultNaked      = 1.00f;
    public float speedMultLight      = 0.90f;
    public float speedMultModerate   = 0.75f;
    public float speedMultHeavy      = 0.55f;
    public float speedMultOverloaded = 0.35f;

    [Header("Map Generation")]
    public int minRoomsPerMap = 8;
    public int maxRoomsPerMap = 30;

    [Header("Persistent Currency")]
    public float lootToCurrencyRate = 0.5f;  // Excess loot × this rate = persistent coins
}
```

---

## 3. Database Architecture

### 3.1 `ItemDatabase` — Runtime Registry

A single MonoBehaviour (or static class) that loads all SO assets at match start and provides lookup by `itemId`. All gameplay systems query this — never hold direct SO references in NetworkBehaviours (they break serialization across clients).

```csharp
public class ItemDatabase : MonoBehaviour
{
    // Populated via Resources.LoadAll or Addressables at startup
    private Dictionary<string, ItemData_SO>       _lootItems   = new();
    private Dictionary<string, ShrineItemData_SO> _shrineItems = new();
    private Dictionary<string, TrapData_SO>       _traps       = new();
    private Dictionary<string, RoomNode_SO>        _rooms       = new();

    public static ItemDatabase Instance { get; private set; }

    public ItemData_SO       GetLootItem  (string id) => _lootItems.GetValueOrDefault(id);
    public ShrineItemData_SO GetShrineItem(string id) => _shrineItems.GetValueOrDefault(id);
    public TrapData_SO       GetTrap      (string id) => _traps.GetValueOrDefault(id);
    public RoomNode_SO       GetRoom      (string id) => _rooms.GetValueOrDefault(id);

    // Returns all rooms valid for spawning a loot item
    public List<RoomNode_SO> GetRoomsWithLootSlots() =>
        _rooms.Values.Where(r => r.lootSpawnSlots > 0).ToList();
}
```

**Why string IDs instead of direct SO references across the network:**  
FishNet syncs data by value, not by Unity object reference. Syncing an `itemId` string is clean and deterministic. The receiver calls `ItemDatabase.Instance.GetLootItem(id)` locally to resolve the full SO. This pattern is used in all networked pickup, drop, and purchase RPCs.

### 3.2 Asset Folder Structure

```
Assets/
  IStalk/
    Data/
      Items/
        Loot/
          Item_CoinPouch.asset
          Item_GemCluster.asset
          Item_GoldBar.asset
          Item_RelicSmall.asset
          Item_RelicCursed.asset
          Item_JewelCase.asset
          Item_Trophy.asset
        Shrine/
          Shrine_OcularPaint.asset
          Shrine_Mirror.asset
          Shrine_SmokeBomb.asset
          Shrine_Decoy.asset
          Shrine_WeightReducer.asset
          Shrine_GrapplingHook.asset
        Traps/
          Trap_Sludge.asset
          Trap_FalseChest.asset
          Trap_NoiseSp.asset
        Rooms/
          Room_Corridor.asset
          Room_Library.asset
          Room_Kitchen.asset
          Room_Dungeon.asset
          Room_StartArea.asset
      GlobalRules.asset
```

All `Loot/`, `Shrine/`, `Traps/`, and `Rooms/` folders are marked as `Resources` subfolders so `Resources.LoadAll<T>()` can populate the `ItemDatabase` dictionaries at startup without manual Inspector wiring.

---

## 4. Designer Workflow

### Adding a New Loot Item
1. Right-click in `Assets/IStalk/Data/Items/Loot/`
2. Create → IStalk → Item Database → Loot Item
3. Fill `itemId` (snake_case, unique), `displayName`, `lootValue`, `weight`
4. Assign `worldPrefab` (the physical pickup prefab)
5. Done — runtime registers it automatically via `Resources.LoadAll`

### Adding a New Shrine Item
1. Create → IStalk → Item Database → Shrine Item
2. Set `itemType` to the appropriate `ShrineItemType` enum value
3. The `Usable Items System (SYS-16)` handler for that type reads `effectRadius`, `effectDuration`, `effectIntensity` — fill as appropriate
4. Set `lootCost` and `weightDelta` per the "no free power" design principle

### Tuning Balance
All numeric values (extraction threshold, weight tier cutoffs, speed multipliers, Dread regen) live in `GlobalRules.asset`. Open it, change numbers, enter Play Mode — no code recompile needed.

---

## 5. Networking Contract

This system itself has zero network code — it is pure data. But every system that sends item data over FishNet must follow this contract:

- **Always sync `itemId` (string), never the SO asset reference**
- On receiving end, resolve: `ItemDatabase.Instance.GetLootItem(itemId)`
- `GlobalRules_SO` is loaded identically on all clients from the same asset — no sync needed for read-only match parameters
- If `GlobalRules` values must vary per match (custom lobbies), the host sends a `MatchConfig` struct at session start containing the overrides — the SO itself is never mutated at runtime

---

## 6. Pillar Compliance

| Pillar | How This System Serves It |
|--------|--------------------------|
| **Greed Has Weight** | Every `ItemData_SO` has a `weight` field. Every `ShrineItemData_SO` has a `weightDelta`. No item is free — the schema enforces it. |
| **Movement is Identity** | Weight data flows directly to the Weight/Burden System (SYS-04) which gates movement abilities. The database is the origin of that tension. |
| **The Eye Orchestrates** | `TrapData_SO.dreadCost` creates the economy that forces Eye decisions. The schema exposes all trap economics to the designer for rapid balance iteration. |
| **Escalating Inevitability** | `GlobalRules_SO.overtimeRoomSealRate` and match timing parameters are designer-tunable without code changes — balance the escalation curve in the editor. |

---

## 7. MVP Scope

For the 2-month prototype, implement:

- [x] All 4 SO classes (`ItemData_SO`, `ShrineItemData_SO`, `TrapData_SO`, `RoomNode_SO`)
- [x] `GlobalRules_SO` with all weight tier thresholds and match parameters
- [x] `ItemDatabase` singleton with dictionary lookups
- [x] Minimum loot roster: `coin_pouch`, `gem_cluster`, `gold_bar` (3 items)
- [x] Minimum shrine roster: `smoke_bomb`, `grappling_hook` (2 items — Alpha adds the rest)
- [x] Minimum trap roster: `sludge`, `false_chest` (2 traps — Alpha adds Noise Spike)
- [x] Minimum room roster: 10 rooms for procgen variety (see SYS-10 starter room set)
- [ ] Full item roster (Alpha)
- [ ] Custom lobby `MatchConfig` override system (Launch)

---

*System: SYS-01 — Item Database*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Status: Complete — ready for implementation*
