Skip to main content

proof_engine/procedural/
loot.rs

1//! Loot tables — item drops with rarity-tiered probability.
2
3use super::{Rng, spawn::SpawnTier};
4
5// ── LootTier ──────────────────────────────────────────────────────────────────
6
7/// Rarity tier of a loot drop.
8pub type LootTier = SpawnTier;
9
10// ── LootDrop ──────────────────────────────────────────────────────────────────
11
12/// One possible drop from a loot table.
13#[derive(Debug, Clone)]
14pub struct LootDrop {
15    pub id:       String,
16    pub name:     String,
17    pub tier:     LootTier,
18    pub quantity: (u32, u32),   // min, max stack size
19    pub weight:   f32,
20    pub depth_min: u32,
21    pub depth_max: u32,
22}
23
24impl LootDrop {
25    pub fn new(id: impl Into<String>, name: impl Into<String>, tier: LootTier) -> Self {
26        let weight = tier.base_weight();
27        Self {
28            id: id.into(), name: name.into(), tier, quantity: (1, 1),
29            weight, depth_min: 1, depth_max: u32::MAX,
30        }
31    }
32
33    pub fn with_quantity(mut self, min: u32, max: u32) -> Self {
34        self.quantity = (min, max); self
35    }
36
37    pub fn with_depth(mut self, min: u32, max: u32) -> Self {
38        self.depth_min = min; self.depth_max = max; self
39    }
40
41    pub fn with_weight(mut self, w: f32) -> Self { self.weight = w; self }
42
43    pub fn is_valid_at(&self, depth: u32) -> bool {
44        depth >= self.depth_min && depth <= self.depth_max
45    }
46}
47
48// ── LootTable ─────────────────────────────────────────────────────────────────
49
50/// A loot table mapping items to weights.
51#[derive(Debug, Clone, Default)]
52pub struct LootTable {
53    drops: Vec<LootDrop>,
54    /// Probability that any drop occurs at all (0=always empty, 1=always drop).
55    pub drop_chance: f32,
56    /// Number of rolls per drop event.
57    pub rolls: u32,
58}
59
60/// Rolled loot result.
61#[derive(Debug, Clone)]
62pub struct RolledLoot {
63    pub drop:     LootDrop,
64    pub quantity: u32,
65}
66
67impl LootTable {
68    pub fn new(drop_chance: f32, rolls: u32) -> Self {
69        Self { drops: Vec::new(), drop_chance: drop_chance.clamp(0.0, 1.0), rolls }
70    }
71
72    pub fn add(&mut self, drop: LootDrop) -> &mut Self {
73        self.drops.push(drop); self
74    }
75
76    /// Roll loot at given depth. Returns a vec of dropped items.
77    pub fn roll(&self, rng: &mut Rng, depth: u32) -> Vec<RolledLoot> {
78        let mut result = Vec::new();
79        for _ in 0..self.rolls {
80            if !rng.chance(self.drop_chance) { continue; }
81
82            let valid: Vec<(&LootDrop, f32)> = self.drops.iter()
83                .filter(|d| d.is_valid_at(depth))
84                .map(|d| (d, d.weight))
85                .collect();
86
87            if let Some(drop) = rng.pick_weighted(&valid).copied() {
88                let qty = rng.range_i32(drop.quantity.0 as i32, drop.quantity.1 as i32) as u32;
89                result.push(RolledLoot { drop: drop.clone(), quantity: qty });
90            }
91        }
92        result
93    }
94
95    /// Roll from a specific tier only.
96    pub fn roll_tier(&self, rng: &mut Rng, depth: u32, tier: LootTier) -> Option<RolledLoot> {
97        let valid: Vec<(&LootDrop, f32)> = self.drops.iter()
98            .filter(|d| d.tier == tier && d.is_valid_at(depth))
99            .map(|d| (d, d.weight))
100            .collect();
101        rng.pick_weighted(&valid).copied().map(|drop| {
102            let qty = rng.range_i32(drop.quantity.0 as i32, drop.quantity.1 as i32) as u32;
103            RolledLoot { drop: drop.clone(), quantity: qty }
104        })
105    }
106
107    pub fn len(&self) -> usize { self.drops.len() }
108}
109
110/// Default chaos-rpg general loot table.
111pub fn chaos_rpg_loot() -> LootTable {
112    let mut t = LootTable::new(0.65, 2);
113
114    // Consumables (very common)
115    t.add(LootDrop::new("health_potion",  "Health Potion",    LootTier::Common).with_quantity(1, 3));
116    t.add(LootDrop::new("mana_potion",    "Mana Potion",      LootTier::Common).with_quantity(1, 2));
117    t.add(LootDrop::new("antidote",       "Antidote",         LootTier::Common).with_quantity(1, 2));
118
119    // Gold
120    t.add(LootDrop::new("gold",           "Gold",             LootTier::Common).with_quantity(1, 50));
121
122    // Equipment
123    t.add(LootDrop::new("rusty_dagger",   "Rusty Dagger",     LootTier::Common).with_depth(1, 3));
124    t.add(LootDrop::new("iron_sword",     "Iron Sword",       LootTier::Common).with_depth(1, 5));
125    t.add(LootDrop::new("chainmail",      "Chainmail",        LootTier::Uncommon).with_depth(2, 7));
126    t.add(LootDrop::new("steel_sword",    "Steel Sword",      LootTier::Uncommon).with_depth(3, 10));
127    t.add(LootDrop::new("chaos_blade",    "Chaos Blade",      LootTier::Rare).with_depth(5, u32::MAX));
128    t.add(LootDrop::new("void_staff",     "Void Staff",       LootTier::Epic).with_depth(7, u32::MAX));
129    t.add(LootDrop::new("crown_of_chaos", "Crown of Chaos",   LootTier::Legendary).with_depth(9, u32::MAX));
130
131    // Materials
132    t.add(LootDrop::new("chaos_shard",    "Chaos Shard",      LootTier::Rare).with_quantity(1, 3));
133    t.add(LootDrop::new("void_crystal",   "Void Crystal",     LootTier::Epic).with_depth(7, u32::MAX));
134
135    t
136}