Skip to main content

steel_registry/loot_table/
entries.rs

1use super::{
2    ConditionalLootFunction, Identifier, ItemStack, LootCondition, LootContext, LootType,
3    NumberProvider, REGISTRY, RegistryExt, RngExt, TaggedRegistryExt,
4};
5
6/// A loot table entry that can generate items.
7#[derive(Debug, Clone)]
8pub enum LootEntry {
9    /// Drop a specific item.
10    Item {
11        name: Identifier,
12        weight: i32,
13        quality: i32,
14        conditions: &'static [LootCondition],
15        functions: &'static [ConditionalLootFunction],
16    },
17    /// Reference another loot table by name.
18    LootTableRef {
19        name: Identifier,
20        weight: i32,
21        quality: i32,
22        conditions: &'static [LootCondition],
23        functions: &'static [ConditionalLootFunction],
24    },
25    /// Inline loot table (embedded pools directly in entry).
26    InlineLootTable {
27        pools: &'static [LootPool],
28        weight: i32,
29        quality: i32,
30        conditions: &'static [LootCondition],
31        functions: &'static [ConditionalLootFunction],
32    },
33    /// Drop items from a tag.
34    Tag {
35        name: Identifier,
36        expand: bool,
37        weight: i32,
38        quality: i32,
39        conditions: &'static [LootCondition],
40        functions: &'static [ConditionalLootFunction],
41    },
42    /// Try children in order, use first that matches.
43    Alternatives {
44        children: &'static [LootEntry],
45        conditions: &'static [LootCondition],
46    },
47    /// Use all children.
48    Group {
49        children: &'static [LootEntry],
50        conditions: &'static [LootCondition],
51    },
52    /// Use children in sequence until one fails.
53    Sequence {
54        children: &'static [LootEntry],
55        conditions: &'static [LootCondition],
56    },
57    /// Empty entry (no drop).
58    Empty {
59        weight: i32,
60        conditions: &'static [LootCondition],
61    },
62    /// Dynamic content (e.g., block entity contents).
63    Dynamic {
64        name: Identifier,
65        conditions: &'static [LootCondition],
66    },
67    /// Select items from specific block entity slots.
68    Slots {
69        /// Slots to select from (can be single slot or range).
70        slots: SlotRange,
71        conditions: &'static [LootCondition],
72        functions: &'static [ConditionalLootFunction],
73    },
74}
75
76/// A range of slots for the Slots entry type.
77#[derive(Debug, Clone, Copy)]
78pub enum SlotRange {
79    /// A single specific slot index.
80    Single(i32),
81    /// A range of slots (inclusive).
82    Range { min: i32, max: i32 },
83    /// All contents slots.
84    Contents,
85    /// Specific named slots (for entities).
86    Named(&'static [&'static str]),
87}
88
89impl LootEntry {
90    /// Get the weight of this entry for random selection.
91    #[must_use]
92    pub const fn weight(&self) -> i32 {
93        match self {
94            Self::Item { weight, .. } => *weight,
95            Self::LootTableRef { weight, .. } => *weight,
96            Self::InlineLootTable { weight, .. } => *weight,
97            Self::Tag { weight, .. } => *weight,
98            Self::Empty { weight, .. } => *weight,
99            // Composite entries don't have weight
100            Self::Alternatives { .. }
101            | Self::Group { .. }
102            | Self::Sequence { .. }
103            | Self::Dynamic { .. }
104            | Self::Slots { .. } => 1,
105        }
106    }
107
108    /// Get the quality modifier for luck-based weight adjustment.
109    #[must_use]
110    pub const fn quality(&self) -> i32 {
111        match self {
112            Self::Item { quality, .. } => *quality,
113            Self::LootTableRef { quality, .. } => *quality,
114            Self::InlineLootTable { quality, .. } => *quality,
115            Self::Tag { quality, .. } => *quality,
116            Self::Empty { .. }
117            | Self::Alternatives { .. }
118            | Self::Group { .. }
119            | Self::Sequence { .. }
120            | Self::Dynamic { .. }
121            | Self::Slots { .. } => 0,
122        }
123    }
124
125    /// Get the effective weight adjusted for luck.
126    /// Formula: max(floor(weight + quality * luck), 0)
127    #[must_use]
128    pub fn effective_weight(&self, luck: f32) -> i32 {
129        let base = self.weight() as f32;
130        let quality = self.quality() as f32;
131        (base + quality * luck).floor().max(0.0) as i32
132    }
133
134    /// Get the conditions for this entry.
135    #[must_use]
136    pub const fn conditions(&self) -> &'static [LootCondition] {
137        match self {
138            Self::Item { conditions, .. } => conditions,
139            Self::LootTableRef { conditions, .. } => conditions,
140            Self::InlineLootTable { conditions, .. } => conditions,
141            Self::Tag { conditions, .. } => conditions,
142            Self::Alternatives { conditions, .. } => conditions,
143            Self::Group { conditions, .. } => conditions,
144            Self::Sequence { conditions, .. } => conditions,
145            Self::Empty { conditions, .. } => conditions,
146            Self::Dynamic { conditions, .. } => conditions,
147            Self::Slots { conditions, .. } => conditions,
148        }
149    }
150
151    /// Get the functions for this entry.
152    #[must_use]
153    pub const fn functions(&self) -> &'static [ConditionalLootFunction] {
154        match self {
155            Self::Item { functions, .. } => functions,
156            Self::LootTableRef { functions, .. } => functions,
157            Self::InlineLootTable { functions, .. } => functions,
158            Self::Tag { functions, .. } => functions,
159            Self::Slots { functions, .. } => functions,
160            Self::Empty { .. }
161            | Self::Alternatives { .. }
162            | Self::Group { .. }
163            | Self::Sequence { .. }
164            | Self::Dynamic { .. } => &[],
165        }
166    }
167}
168
169/// A pool of loot entries with roll counts.
170#[derive(Debug, Clone)]
171pub struct LootPool {
172    pub rolls: NumberProvider,
173    pub bonus_rolls: f32,
174    pub entries: &'static [LootEntry],
175    pub conditions: &'static [LootCondition],
176    pub functions: &'static [ConditionalLootFunction],
177}
178
179/// A complete loot table definition.
180#[derive(Debug)]
181pub struct LootTable {
182    pub key: Identifier,
183    pub loot_type: LootType,
184    pub pools: &'static [LootPool],
185    pub functions: &'static [ConditionalLootFunction],
186    pub random_sequence: Option<Identifier>,
187}
188
189impl LootTable {
190    /// Generate random items from this loot table.
191    // TODO: Add a world-aware entry point that selects the vanilla RNG before evaluation:
192    // nonzero loot seed -> LegacyRandom, table random_sequence -> RandomSequences (including
193    // world seed 0), otherwise the level random source.
194    ///
195    /// # Arguments
196    /// * `ctx` - The loot context containing RNG, luck, block state, tool, etc.
197    ///
198    /// This follows vanilla's approach:
199    /// 1. For each pool, check conditions
200    /// 2. Roll `rolls + floor(bonus_rolls * luck)` times
201    /// 3. Each roll does weighted random selection among valid entries
202    /// 4. Apply entry-level functions to each item
203    /// 5. Apply pool-level functions to all items from that pool
204    /// 6. Apply table-level functions to all items from the table
205    pub fn get_random_items<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> Vec<ItemStack> {
206        let mut result = Vec::new();
207        for pool in self.pools {
208            pool.add_random_items(ctx, &mut result);
209        }
210
211        // Apply table-level functions to all items
212        if !self.functions.is_empty() {
213            for item in &mut result {
214                for cond_func in self.functions {
215                    if cond_func.conditions.iter().all(|c| c.test(ctx)) {
216                        cond_func.function.apply(item, ctx);
217                    }
218                }
219            }
220            // Remove items with zero count after applying functions
221            result.retain(|item| item.count > 0);
222        }
223
224        result
225    }
226}
227
228impl LootPool {
229    /// Add random items from this pool to the result.
230    fn add_random_items<R: rand::Rng>(
231        &self,
232        ctx: &mut LootContext<'_, R>,
233        result: &mut Vec<ItemStack>,
234    ) {
235        // Check pool conditions
236        for condition in self.conditions {
237            if !condition.test(ctx) {
238                return;
239            }
240        }
241
242        // Track where items from this pool start
243        let start_index = result.len();
244
245        // Calculate number of rolls
246        let roll_count = self.rolls.get_int(ctx.rng) + (self.bonus_rolls * ctx.luck).floor() as i32;
247
248        for _ in 0..roll_count {
249            self.add_random_item(ctx, result);
250        }
251
252        // Apply pool-level functions to all items generated by this pool
253        if !self.functions.is_empty() {
254            for item in result.iter_mut().skip(start_index) {
255                for cond_func in self.functions {
256                    if cond_func.conditions.iter().all(|c| c.test(ctx)) {
257                        cond_func.function.apply(item, ctx);
258                    }
259                }
260            }
261            // Remove items with zero count after applying functions
262            result.retain(|item| item.count > 0);
263        }
264    }
265
266    /// Select and add a single random item from this pool.
267    fn add_random_item<R: rand::Rng>(
268        &self,
269        ctx: &mut LootContext<'_, R>,
270        result: &mut Vec<ItemStack>,
271    ) {
272        // Collect valid entries with their effective weights
273        let mut valid_entries: Vec<(&LootEntry, i32)> = Vec::new();
274        let mut total_weight = 0;
275
276        for entry in self.entries {
277            // Check entry conditions
278            let passes_conditions = entry.conditions().iter().all(|c| c.test(ctx));
279
280            if !passes_conditions {
281                continue;
282            }
283
284            let weight = entry.effective_weight(ctx.luck);
285            if weight > 0 {
286                valid_entries.push((entry, weight));
287                total_weight += weight;
288            }
289        }
290
291        if total_weight == 0 || valid_entries.is_empty() {
292            return;
293        }
294
295        // Weighted random selection
296        let selected = if valid_entries.len() == 1 {
297            valid_entries[0].0
298        } else {
299            let mut index = ctx.rng.random_range(0..total_weight);
300            let mut selected_entry = valid_entries[0].0;
301            for (entry, weight) in &valid_entries {
302                index -= weight;
303                if index < 0 {
304                    selected_entry = entry;
305                    break;
306                }
307            }
308            selected_entry
309        };
310
311        // Generate item(s) from the selected entry
312        selected.create_items(ctx, result);
313    }
314}
315
316impl LootEntry {
317    /// Create items from this entry and add them to the result.
318    fn create_items<R: rand::Rng>(
319        &self,
320        ctx: &mut LootContext<'_, R>,
321        result: &mut Vec<ItemStack>,
322    ) {
323        match self {
324            LootEntry::Item {
325                name, functions, ..
326            } => {
327                if let Some(item_ref) = REGISTRY.items.by_key(name) {
328                    let mut item = ItemStack::new(item_ref);
329
330                    // Apply functions
331                    for cond_func in *functions {
332                        if cond_func.conditions.iter().all(|c| c.test(ctx)) {
333                            cond_func.function.apply(&mut item, ctx);
334                        }
335                    }
336
337                    if item.count > 0 {
338                        result.push(item);
339                    }
340                }
341            }
342            LootEntry::LootTableRef {
343                name, functions, ..
344            } => {
345                // Recursively get items from referenced loot table
346                if let Some(table) = REGISTRY.loot_tables.by_key(name) {
347                    let mut items = table.get_random_items(ctx);
348                    // Apply functions to all items from the referenced table
349                    for item in &mut items {
350                        for cond_func in *functions {
351                            if cond_func.conditions.iter().all(|c| c.test(ctx)) {
352                                cond_func.function.apply(item, ctx);
353                            }
354                        }
355                    }
356                    result.extend(items.into_iter().filter(|i| i.count > 0));
357                }
358            }
359            LootEntry::InlineLootTable {
360                pools, functions, ..
361            } => {
362                // Process inline loot table pools directly
363                let mut items = Vec::new();
364                for pool in *pools {
365                    pool.add_random_items(ctx, &mut items);
366                }
367                // Apply functions to all items from the inline table
368                for item in &mut items {
369                    for cond_func in *functions {
370                        if cond_func.conditions.iter().all(|c| c.test(ctx)) {
371                            cond_func.function.apply(item, ctx);
372                        }
373                    }
374                }
375                result.extend(items.into_iter().filter(|i| i.count > 0));
376            }
377            LootEntry::Tag {
378                name,
379                expand,
380                functions,
381                ..
382            } => {
383                // Get all items in the tag
384                if let Some(items) = REGISTRY.items.get_tag(name) {
385                    if *expand {
386                        // Pick one random item from the tag (weighted equally)
387                        if !items.is_empty() {
388                            let index = ctx.rng.random_range(0..items.len());
389                            let mut item = ItemStack::new(items[index]);
390                            for cond_func in *functions {
391                                if cond_func.conditions.iter().all(|c| c.test(ctx)) {
392                                    cond_func.function.apply(&mut item, ctx);
393                                }
394                            }
395                            if item.count > 0 {
396                                result.push(item);
397                            }
398                        }
399                    } else {
400                        // Drop all items from the tag
401                        for item_ref in items {
402                            let mut item = ItemStack::new(item_ref);
403                            for cond_func in *functions {
404                                if cond_func.conditions.iter().all(|c| c.test(ctx)) {
405                                    cond_func.function.apply(&mut item, ctx);
406                                }
407                            }
408                            if item.count > 0 {
409                                result.push(item);
410                            }
411                        }
412                    }
413                }
414            }
415            LootEntry::Alternatives { children, .. } => {
416                // Try children in order, use first that passes conditions and produces items
417                for child in *children {
418                    // Check child's conditions first
419                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
420                    if !passes_conditions {
421                        continue; // Try next alternative
422                    }
423
424                    let before_len = result.len();
425                    child.create_items(ctx, result);
426                    if result.len() > before_len {
427                        break; // First successful child that produced items, stop
428                    }
429                }
430            }
431            LootEntry::Group { children, .. } => {
432                // Use all children that pass their conditions
433                for child in *children {
434                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
435                    if passes_conditions {
436                        child.create_items(ctx, result);
437                    }
438                }
439            }
440            LootEntry::Sequence { children, .. } => {
441                // Use children in sequence until one fails its conditions
442                // Note: Unlike Alternatives, Sequence stops when conditions FAIL,
443                // not when items are produced. A child can produce nothing but still "succeed".
444                for child in *children {
445                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
446                    if !passes_conditions {
447                        break; // Condition failed, stop sequence
448                    }
449                    child.create_items(ctx, result);
450                }
451            }
452            LootEntry::Empty { .. } => {
453                // Empty entry produces nothing
454            }
455            LootEntry::Dynamic { name, .. } => {
456                // Dynamic entries are used for block entity contents (like shulker boxes)
457                // The name identifies what content to retrieve:
458                // - "contents" = block entity inventory contents
459                // - Other names may exist for specific use cases
460                //
461                // TODO: Implement when block entity system supports inventory retrieval
462                // This requires:
463                // 1. Block entity reference in LootContext
464                // 2. Method to get inventory contents from block entity
465                // 3. Adding those items to the result
466                let _ = name;
467            }
468            LootEntry::Slots {
469                slots, functions, ..
470            } => {
471                // Slots entries select items from specific block entity slots
472                // TODO: Implement when block entity system supports slot access
473                // This requires:
474                // 1. Block entity reference in LootContext
475                // 2. Method to get items from specific slots
476                // 3. Apply functions to each retrieved item
477                let _ = slots;
478                let _ = functions;
479            }
480        }
481    }
482}