Skip to main content

steel_registry/
loot_table.rs

1use crate::{
2    REGISTRY, RegistryExt, TaggedRegistryExt, blocks::block_state_ext::BlockStateExt,
3    item_stack::ItemStack,
4};
5use rand::RngExt;
6use rustc_hash::FxHashMap;
7use steel_utils::{BlockStateId, Identifier};
8
9/// Entity target for loot context lookups.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LootContextEntity {
12    /// The entity being looted (killed mob, block entity owner).
13    This,
14    /// The entity that killed the target.
15    Killer,
16    /// The direct attacker (e.g., arrow, not the player who shot it).
17    DirectKiller,
18    /// The player who dealt the final damage.
19    KillerPlayer,
20    /// The entity interacting with a block/entity.
21    Interacting,
22}
23
24/// Equipment/attribute slot group for enchantments and attributes.
25///
26/// Vanilla's `EquipmentSlotGroup` — a grouping/predicate over concrete `EquipmentSlot` values.
27/// `Hand` matches both main/offhand, `Armor` matches all armor slots, `Any` matches everything.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum EquipmentSlotGroup {
30    Any,
31    MainHand,
32    OffHand,
33    Hand,
34    Head,
35    Chest,
36    Legs,
37    Feet,
38    Armor,
39    Body,
40}
41
42impl EquipmentSlotGroup {
43    #[must_use]
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::Any => "any",
47            Self::MainHand => "mainhand",
48            Self::OffHand => "offhand",
49            Self::Hand => "hand",
50            Self::Head => "head",
51            Self::Chest => "chest",
52            Self::Legs => "legs",
53            Self::Feet => "feet",
54            Self::Armor => "armor",
55            Self::Body => "body",
56        }
57    }
58}
59
60/// Dye/banner color.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum DyeColor {
63    White,
64    Orange,
65    Magenta,
66    LightBlue,
67    Yellow,
68    Lime,
69    Pink,
70    Gray,
71    LightGray,
72    Cyan,
73    Purple,
74    Blue,
75    Brown,
76    Green,
77    Red,
78    Black,
79}
80
81/// The type of loot table, determining when/how it's used.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LootType {
84    Block,
85    Entity,
86    Chest,
87    Fishing,
88    Gift,
89    Archaeology,
90    Vault,
91    Shearing,
92    Equipment,
93    Selector,
94    EntityInteract,
95    BlockInteract,
96    Barter,
97}
98
99/// A number provider that can be constant or random.
100#[derive(Debug, Clone)]
101pub enum NumberProvider {
102    Constant(f32),
103    Uniform {
104        min: f32,
105        max: f32,
106    },
107    Binomial {
108        n: i32,
109        p: f32,
110    },
111    /// Get value from entity scoreboard score.
112    Score {
113        target: ScoreboardTarget,
114        score: &'static str,
115        scale: f32,
116    },
117    /// Get value from command storage.
118    Storage {
119        storage: Identifier,
120        path: &'static str,
121    },
122    /// Get enchantment level from context tool.
123    EnchantmentLevel {
124        enchantment: Identifier,
125    },
126}
127
128/// Target for scoreboard number provider.
129#[derive(Debug, Clone, Copy)]
130pub enum ScoreboardTarget {
131    /// The entity being looted.
132    This,
133    /// The entity that killed the target.
134    Killer,
135    /// The direct killer (e.g., arrow vs player).
136    DirectKiller,
137    /// The player who dealt the last damage.
138    KillerPlayer,
139    /// A fixed entity name.
140    Fixed(&'static str),
141}
142
143impl NumberProvider {
144    /// Get a value from this provider using the given RNG.
145    pub fn get<R: rand::Rng>(&self, rng: &mut R, ctx: Option<&LootContextRef<'_>>) -> f32 {
146        match self {
147            Self::Constant(v) => *v,
148            Self::Uniform { min, max } => rng.random_range(*min..=*max),
149            Self::Binomial { n, p } => {
150                let mut count = 0;
151                for _ in 0..*n {
152                    if rng.random::<f32>() < *p {
153                        count += 1;
154                    }
155                }
156                count as f32
157            }
158            Self::Score { .. } => {
159                // TODO: Implement when scoreboard system is available
160                let _ = ctx;
161                0.0
162            }
163            Self::Storage { .. } => {
164                // TODO: Implement when command storage system is available
165                let _ = ctx;
166                0.0
167            }
168            Self::EnchantmentLevel { enchantment } => ctx
169                .and_then(|c| c.tool)
170                .map(|t| t.get_enchantment_level(enchantment) as f32)
171                .unwrap_or(0.0),
172        }
173    }
174
175    /// Get a value without context (for backwards compatibility).
176    pub fn get_simple(&self, rng: &mut impl rand::Rng) -> f32 {
177        match self {
178            Self::Constant(v) => *v,
179            Self::Uniform { min, max } => rng.random_range(*min..=*max),
180            Self::Binomial { n, p } => {
181                let mut count = 0;
182                for _ in 0..*n {
183                    if rng.random::<f32>() < *p {
184                        count += 1;
185                    }
186                }
187                count as f32
188            }
189            // Context-dependent providers return 0 when no context available
190            Self::Score { .. } | Self::Storage { .. } | Self::EnchantmentLevel { .. } => 0.0,
191        }
192    }
193
194    /// Get the value as an integer.
195    pub fn get_int(&self, rng: &mut impl rand::Rng) -> i32 {
196        self.get_simple(rng).floor() as i32
197    }
198
199    /// Get the value as an integer with context.
200    pub fn get_int_with_ctx<R: rand::Rng>(
201        &self,
202        rng: &mut R,
203        ctx: Option<&LootContextRef<'_>>,
204    ) -> i32 {
205        self.get(rng, ctx).floor() as i32
206    }
207}
208
209/// A range for number comparisons (used in ValueCheck, TimeCheck, EntityScores).
210#[derive(Debug, Clone)]
211pub struct NumberProviderRange {
212    pub min: Option<NumberProvider>,
213    pub max: Option<NumberProvider>,
214}
215
216impl NumberProviderRange {
217    /// Check if a value is within this range.
218    pub fn test(&self, value: f32, rng: &mut impl rand::Rng) -> bool {
219        if let Some(min) = &self.min
220            && value < min.get_simple(rng)
221        {
222            return false;
223        }
224        if let Some(max) = &self.max
225            && value > max.get_simple(rng)
226        {
227            return false;
228        }
229        true
230    }
231
232    /// Create an exact match range.
233    pub const fn exact(value: f32) -> Self {
234        Self {
235            min: Some(NumberProvider::Constant(value)),
236            max: Some(NumberProvider::Constant(value)),
237        }
238    }
239
240    /// Create an at-least range.
241    pub const fn at_least(min: f32) -> Self {
242        Self {
243            min: Some(NumberProvider::Constant(min)),
244            max: None,
245        }
246    }
247
248    /// Create an at-most range.
249    pub const fn at_most(max: f32) -> Self {
250        Self {
251            min: None,
252            max: Some(NumberProvider::Constant(max)),
253        }
254    }
255
256    /// Create a between range.
257    pub const fn between(min: f32, max: f32) -> Self {
258        Self {
259            min: Some(NumberProvider::Constant(min)),
260            max: Some(NumberProvider::Constant(max)),
261        }
262    }
263}
264
265/// Reference to loot context for number provider evaluation.
266/// This is a simplified view to avoid circular references.
267pub struct LootContextRef<'a> {
268    pub tool: Option<&'a ItemStack>,
269    // Add more fields as needed for Score/Storage providers
270}
271
272/// Context for loot table evaluation, containing all relevant game state.
273///
274/// This mirrors vanilla's `LootContext` / `LootParams` system.
275pub struct LootContext<'a, R: rand::Rng> {
276    /// Random number generator.
277    pub rng: &'a mut R,
278    /// Luck value (e.g., from Luck of the Sea enchantment).
279    pub luck: f32,
280    /// The block state being broken (for block loot tables).
281    pub block_state: Option<BlockStateId>,
282    /// The tool used to break the block.
283    pub tool: Option<&'a ItemStack>,
284    /// Explosion radius if caused by an explosion.
285    pub explosion_radius: Option<f32>,
286    /// Whether the entity was killed by a player.
287    pub killed_by_player: bool,
288
289    /// World position where the loot is generated (block position or entity death location).
290    pub origin: Option<(f64, f64, f64)>,
291    /// Current game time in ticks (for TimeCheck condition).
292    pub game_time: Option<i64>,
293    /// Current weather state.
294    pub weather: Option<WeatherState>,
295    /// The entity being looted (the killed mob, block entity owner, etc.).
296    /// This is a type-erased pointer; actual entity data depends on game implementation.
297    pub this_entity: Option<EntityRef<'a>>,
298    /// The entity that killed this_entity (for mob loot tables).
299    pub killer_entity: Option<EntityRef<'a>>,
300    /// The direct attacker entity (e.g., arrow, not the player who shot it).
301    pub direct_killer_entity: Option<EntityRef<'a>>,
302    /// The player who dealt the final damage (may be different from killer).
303    pub last_damage_player: Option<EntityRef<'a>>,
304    /// Damage source information for entity deaths.
305    pub damage_source: Option<DamageSourceInfo<'a>>,
306    /// Block entity reference for container/block loot.
307    pub block_entity: Option<BlockEntityRef<'a>>,
308    /// The entity interacting with a block/entity (e.g., player opening a chest).
309    pub interacting_entity: Option<EntityRef<'a>>,
310}
311
312/// Weather state for WeatherCheck condition.
313#[derive(Debug, Clone, Copy, Default)]
314pub struct WeatherState {
315    pub raining: bool,
316    pub thundering: bool,
317}
318
319/// A reference to an entity for loot context.
320/// This is intentionally opaque - the actual entity type depends on game implementation.
321#[derive(Debug, Clone, Copy)]
322pub struct EntityRef<'a> {
323    /// Type identifier for the entity.
324    pub entity_type: Option<&'a Identifier>,
325    /// Entity flags for predicate checking.
326    pub flags: EntityRefFlags,
327    /// Equipment slots for equipment predicates.
328    pub equipment: Option<&'a EntityEquipmentRef<'a>>,
329    /// Entity name (for copy_name function).
330    pub custom_name: Option<&'a str>,
331}
332
333/// Entity flags for predicate checking.
334#[derive(Debug, Clone, Copy, Default)]
335pub struct EntityRefFlags {
336    pub is_on_fire: bool,
337    pub is_sneaking: bool,
338    pub is_sprinting: bool,
339    pub is_swimming: bool,
340    pub is_baby: bool,
341}
342
343/// Equipment references for an entity.
344#[derive(Debug, Clone, Copy)]
345pub struct EntityEquipmentRef<'a> {
346    pub mainhand: Option<&'a ItemStack>,
347    pub offhand: Option<&'a ItemStack>,
348    pub head: Option<&'a ItemStack>,
349    pub chest: Option<&'a ItemStack>,
350    pub legs: Option<&'a ItemStack>,
351    pub feet: Option<&'a ItemStack>,
352}
353
354/// Damage source information for loot context.
355#[derive(Debug, Clone, Copy)]
356pub struct DamageSourceInfo<'a> {
357    /// The damage type identifier.
358    pub damage_type: Option<&'a Identifier>,
359    /// Tags associated with this damage source.
360    pub tags: &'a [Identifier],
361    /// Whether this is direct damage (not from a projectile).
362    pub is_direct: bool,
363}
364
365/// A reference to a block entity for loot context.
366#[derive(Debug, Clone, Copy)]
367pub struct BlockEntityRef<'a> {
368    /// The block entity type identifier.
369    pub block_entity_type: Option<&'a Identifier>,
370    /// Custom name of the block entity (for copy_name).
371    pub custom_name: Option<&'a str>,
372    /// Inventory contents (for dynamic/slots entries).
373    pub inventory: Option<&'a [ItemStack]>,
374}
375
376impl<'a, R: rand::Rng> LootContext<'a, R> {
377    /// Create a new loot context with just an RNG.
378    pub fn new(rng: &'a mut R) -> Self {
379        Self {
380            rng,
381            luck: 0.0,
382            block_state: None,
383            tool: None,
384            explosion_radius: None,
385            killed_by_player: false,
386            origin: None,
387            game_time: None,
388            weather: None,
389            this_entity: None,
390            killer_entity: None,
391            direct_killer_entity: None,
392            last_damage_player: None,
393            damage_source: None,
394            block_entity: None,
395            interacting_entity: None,
396        }
397    }
398
399    /// Set the luck value.
400    pub fn with_luck(mut self, luck: f32) -> Self {
401        self.luck = luck;
402        self
403    }
404
405    /// Set the block state.
406    pub fn with_block_state(mut self, state: BlockStateId) -> Self {
407        self.block_state = Some(state);
408        self
409    }
410
411    /// Set the tool used.
412    pub fn with_tool(mut self, tool: &'a ItemStack) -> Self {
413        self.tool = Some(tool);
414        self
415    }
416
417    /// Set the explosion radius.
418    pub fn with_explosion(mut self, radius: f32) -> Self {
419        self.explosion_radius = Some(radius);
420        self
421    }
422
423    /// Set whether killed by player.
424    pub fn with_killed_by_player(mut self, killed: bool) -> Self {
425        self.killed_by_player = killed;
426        self
427    }
428
429    /// Set the world origin position.
430    pub fn with_origin(mut self, x: f64, y: f64, z: f64) -> Self {
431        self.origin = Some((x, y, z));
432        self
433    }
434
435    /// Set the game time.
436    pub fn with_game_time(mut self, time: i64) -> Self {
437        self.game_time = Some(time);
438        self
439    }
440
441    /// Set the weather state.
442    pub fn with_weather(mut self, weather: WeatherState) -> Self {
443        self.weather = Some(weather);
444        self
445    }
446
447    /// Set the entity being looted.
448    pub fn with_this_entity(mut self, entity: EntityRef<'a>) -> Self {
449        self.this_entity = Some(entity);
450        self
451    }
452
453    /// Set the killer entity.
454    pub fn with_killer_entity(mut self, entity: EntityRef<'a>) -> Self {
455        self.killer_entity = Some(entity);
456        self
457    }
458
459    /// Set the direct killer entity (e.g., projectile).
460    pub fn with_direct_killer_entity(mut self, entity: EntityRef<'a>) -> Self {
461        self.direct_killer_entity = Some(entity);
462        self
463    }
464
465    /// Set the player who dealt the final damage.
466    pub fn with_last_damage_player(mut self, entity: EntityRef<'a>) -> Self {
467        self.last_damage_player = Some(entity);
468        self
469    }
470
471    /// Set the damage source information.
472    pub fn with_damage_source(mut self, damage_source: DamageSourceInfo<'a>) -> Self {
473        self.damage_source = Some(damage_source);
474        self
475    }
476
477    /// Set the block entity reference.
478    pub fn with_block_entity(mut self, block_entity: BlockEntityRef<'a>) -> Self {
479        self.block_entity = Some(block_entity);
480        self
481    }
482
483    /// Set the interacting entity (e.g., player opening a chest).
484    pub fn with_interacting_entity(mut self, entity: EntityRef<'a>) -> Self {
485        self.interacting_entity = Some(entity);
486        self
487    }
488
489    /// Get the level of an enchantment on the tool by identifier.
490    pub fn get_enchantment_level_by_id(&self, enchantment: &Identifier) -> i32 {
491        self.tool
492            .map(|t| t.get_enchantment_level(enchantment))
493            .unwrap_or(0)
494    }
495
496    /// Get an entity reference by target.
497    pub fn get_entity(&self, target: LootContextEntity) -> Option<EntityRef<'a>> {
498        match target {
499            LootContextEntity::This => self.this_entity,
500            LootContextEntity::Killer => self.killer_entity,
501            LootContextEntity::DirectKiller => self.direct_killer_entity,
502            LootContextEntity::KillerPlayer => self.last_damage_player,
503            LootContextEntity::Interacting => self.interacting_entity,
504        }
505    }
506}
507
508/// A property check for block state conditions.
509#[derive(Debug, Clone)]
510pub struct PropertyCheck {
511    pub name: &'static str,
512    pub value: &'static str,
513}
514
515/// A condition that must be met for a loot entry or pool to apply.
516#[derive(Debug, Clone)]
517#[expect(clippy::large_enum_variant)]
518pub enum LootCondition {
519    /// The loot survives explosion damage (random chance based on explosion radius).
520    /// Vanilla: 1/radius chance to pass. If no explosion, always passes.
521    SurvivesExplosion,
522    /// Check block state properties match expected values.
523    BlockStateProperty {
524        block: Identifier,
525        properties: &'static [PropertyCheck],
526    },
527    /// Simple random chance (0.0 to 1.0).
528    RandomChance(f32),
529    /// Random chance affected by an enchantment (e.g., looting).
530    /// Vanilla 1.21+: uses enchanted_chance which can be constant or linear.
531    RandomChanceWithEnchantedBonus {
532        enchantment: Identifier,
533        unenchanted_chance: f32,
534        /// For linear formula: chance = base + per_level * (level - 1)
535        enchanted_chance: EnchantedChance,
536    },
537    /// Match tool predicate - checks if the tool matches certain criteria.
538    MatchTool(ToolPredicate),
539    /// Table bonus condition - chance based on enchantment level from a table.
540    /// The chances array is indexed by enchantment level (0 = no enchant, 1 = level 1, etc.)
541    TableBonus {
542        enchantment: Identifier,
543        chances: &'static [f32],
544    },
545    /// Inverted condition - passes if the inner condition fails.
546    Inverted(&'static LootCondition),
547    /// Any of the conditions pass (OR logic).
548    AnyOf(&'static [LootCondition]),
549    /// All of the conditions pass (AND logic).
550    AllOf(&'static [LootCondition]),
551    /// Killed by player condition.
552    KilledByPlayer,
553    /// Entity properties condition - checks entity predicates.
554    EntityProperties {
555        entity: LootContextEntity,
556        predicate: EntityPredicate,
557    },
558    /// Damage source properties condition - checks how the entity was damaged.
559    DamageSourceProperties { predicate: DamageSourcePredicate },
560    /// Location check condition - checks the location predicate.
561    LocationCheck {
562        offset_x: i32,
563        offset_y: i32,
564        offset_z: i32,
565        predicate: LocationPredicate,
566    },
567    /// Weather check condition - checks current weather.
568    WeatherCheck {
569        raining: Option<bool>,
570        thundering: Option<bool>,
571    },
572    /// Time check condition - checks game time.
573    TimeCheck {
574        value: NumberProviderRange,
575        period: Option<i64>,
576    },
577    /// Value check condition - compares a number provider value against a range.
578    ValueCheck {
579        value: NumberProvider,
580        range: NumberProviderRange,
581    },
582    /// Check if a specific enchantment is active.
583    EnchantmentActiveCheck {
584        enchantment: Identifier,
585        active: bool,
586    },
587    /// Check entity scoreboard scores.
588    EntityScores {
589        entity: LootContextEntity,
590        scores: &'static [(&'static str, NumberProviderRange)],
591    },
592    /// Reference to a named condition in the registry.
593    Reference(Identifier),
594}
595
596/// Enchanted chance calculation method.
597#[derive(Debug, Clone, Copy)]
598pub enum EnchantedChance {
599    /// Constant chance regardless of enchantment level.
600    Constant(f32),
601    /// Linear formula: base + per_level_above_first * (level - 1)
602    Linear {
603        base: f32,
604        per_level_above_first: f32,
605    },
606}
607
608/// Predicate for matching tools.
609#[derive(Debug, Clone)]
610pub enum ToolPredicate {
611    /// Match a specific item.
612    Item(Identifier),
613    /// Match items with a specific enchantment at minimum level.
614    HasEnchantment {
615        enchantment: Identifier,
616        min_level: i32,
617    },
618    /// Match items in a tag.
619    Tag(Identifier),
620    /// Always matches (no predicate specified).
621    Any,
622}
623
624/// Predicate for checking location/block properties.
625#[derive(Debug, Clone)]
626pub struct LocationPredicate {
627    pub block: Option<BlockPredicate>,
628}
629
630/// Predicate for checking block properties.
631#[derive(Debug, Clone)]
632pub struct BlockPredicate {
633    pub blocks: Option<Identifier>,
634    pub state: &'static [(&'static str, &'static str)],
635}
636
637/// Predicate for checking entity properties.
638#[derive(Debug, Clone)]
639pub struct EntityPredicate {
640    pub entity_type: Option<Identifier>,
641    pub flags: Option<EntityFlags>,
642    pub equipment: Option<EntityEquipment>,
643}
644
645/// Entity flags (is_on_fire, is_sneaking, etc.)
646#[derive(Debug, Clone)]
647pub struct EntityFlags {
648    pub is_on_fire: Option<bool>,
649    pub is_sneaking: Option<bool>,
650    pub is_sprinting: Option<bool>,
651    pub is_swimming: Option<bool>,
652    pub is_baby: Option<bool>,
653}
654
655/// Entity equipment predicate
656#[derive(Debug, Clone)]
657pub struct EntityEquipment {
658    pub mainhand: Option<ToolPredicate>,
659    pub offhand: Option<ToolPredicate>,
660    pub head: Option<ToolPredicate>,
661    pub chest: Option<ToolPredicate>,
662    pub legs: Option<ToolPredicate>,
663    pub feet: Option<ToolPredicate>,
664}
665
666/// Predicate for checking damage source properties.
667#[derive(Debug, Clone)]
668pub struct DamageSourcePredicate {
669    /// Tags that must be present on the damage source.
670    pub tags: &'static [DamageTagPredicate],
671    /// Source entity predicate (e.g., the player/mob that caused damage).
672    pub source_entity: Option<EntityPredicate>,
673    /// Direct entity predicate (e.g., the arrow/fireball).
674    pub direct_entity: Option<EntityPredicate>,
675    /// Whether damage bypasses armor.
676    pub is_direct: Option<bool>,
677}
678
679/// A tag check for damage source.
680#[derive(Debug, Clone)]
681pub struct DamageTagPredicate {
682    pub id: Identifier,
683    pub expected: bool,
684}
685
686impl LootCondition {
687    /// Test if this condition passes given the loot context.
688    pub fn test<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> bool {
689        match self {
690            LootCondition::SurvivesExplosion => {
691                if let Some(radius) = ctx.explosion_radius {
692                    // Vanilla: 1/radius chance to survive
693                    ctx.rng.random::<f32>() <= (1.0 / radius)
694                } else {
695                    true // No explosion, always survives
696                }
697            }
698            LootCondition::BlockStateProperty { block, properties } => {
699                if let Some(state) = ctx.block_state {
700                    let state_block = state.get_block();
701                    // Check block matches
702                    if state_block.key != *block {
703                        return false;
704                    }
705                    // Check all properties match
706                    for prop in *properties {
707                        if let Some(value) = state.get_property_str(prop.name) {
708                            if value != prop.value {
709                                return false;
710                            }
711                        } else {
712                            return false; // Property doesn't exist
713                        }
714                    }
715                    true
716                } else {
717                    false // No block state in context
718                }
719            }
720            LootCondition::RandomChance(chance) => ctx.rng.random::<f32>() < *chance,
721            LootCondition::RandomChanceWithEnchantedBonus {
722                enchantment,
723                unenchanted_chance,
724                enchanted_chance,
725            } => {
726                let level = ctx.get_enchantment_level_by_id(enchantment);
727                let effective_chance = if level > 0 {
728                    match enchanted_chance {
729                        EnchantedChance::Constant(c) => *c,
730                        EnchantedChance::Linear {
731                            base,
732                            per_level_above_first,
733                        } => base + per_level_above_first * (level - 1) as f32,
734                    }
735                } else {
736                    *unenchanted_chance
737                };
738                ctx.rng.random::<f32>() < effective_chance
739            }
740            LootCondition::MatchTool(predicate) => {
741                if let Some(tool) = ctx.tool {
742                    predicate.test(tool, ctx)
743                } else {
744                    // No tool in context - only passes if predicate is Any
745                    matches!(predicate, ToolPredicate::Any)
746                }
747            }
748            LootCondition::TableBonus {
749                enchantment,
750                chances,
751            } => {
752                let level = ctx.get_enchantment_level_by_id(enchantment);
753                let index = (level as usize).min(chances.len().saturating_sub(1));
754                let chance = chances.get(index).copied().unwrap_or(0.0);
755                ctx.rng.random::<f32>() < chance
756            }
757            LootCondition::Inverted(inner) => !inner.test(ctx),
758            LootCondition::AnyOf(conditions) => conditions.iter().any(|c| c.test(ctx)),
759            LootCondition::AllOf(conditions) => conditions.iter().all(|c| c.test(ctx)),
760            LootCondition::KilledByPlayer => ctx.killed_by_player,
761            LootCondition::EntityProperties { .. } => {
762                // TODO: Implement when entity data is available in context
763                true
764            }
765            LootCondition::DamageSourceProperties { .. } => {
766                // TODO: Implement when damage source data is available in context
767                true
768            }
769            LootCondition::LocationCheck { .. } => {
770                // TODO: Implement when world position data is available in context
771                true
772            }
773            LootCondition::WeatherCheck {
774                raining,
775                thundering,
776            } => {
777                let weather = ctx.weather.unwrap_or_default();
778                raining.is_none_or(|r| r == weather.raining)
779                    && thundering.is_none_or(|t| t == weather.thundering)
780            }
781            LootCondition::TimeCheck { value, period } => {
782                let game_time = ctx.game_time.unwrap_or(0);
783                let time = if let Some(p) = period {
784                    game_time % p
785                } else {
786                    game_time
787                };
788                value.test(time as f32, ctx.rng)
789            }
790            LootCondition::ValueCheck { value, range } => {
791                let v = value.get_simple(ctx.rng);
792                range.test(v, ctx.rng)
793            }
794            LootCondition::EnchantmentActiveCheck {
795                enchantment,
796                active,
797            } => {
798                let level = ctx.get_enchantment_level_by_id(enchantment);
799                let is_active = level > 0;
800                is_active == *active
801            }
802            LootCondition::EntityScores { .. } => {
803                // TODO: Implement when scoreboard system is available
804                true
805            }
806            LootCondition::Reference(_name) => {
807                // TODO: Implement condition registry lookup
808                // For now, return true (permissive)
809                true
810            }
811        }
812    }
813}
814
815impl ToolPredicate {
816    /// Test if the tool matches this predicate.
817    pub fn test<R: rand::Rng>(&self, tool: &ItemStack, _ctx: &LootContext<'_, R>) -> bool {
818        match self {
819            ToolPredicate::Item(item_id) => tool.item.key == *item_id,
820            ToolPredicate::HasEnchantment {
821                enchantment,
822                min_level,
823            } => {
824                // Check if tool has the enchantment at the required level
825                tool.get_enchantment_level(enchantment) >= *min_level
826            }
827            ToolPredicate::Tag(tag) => {
828                // Check if the tool's item is in the specified tag
829                REGISTRY.items.is_in_tag(tool.item, tag)
830            }
831            ToolPredicate::Any => true,
832        }
833    }
834}
835
836/// Options for selecting enchantments - either a tag reference or explicit list.
837#[derive(Debug, Clone)]
838pub enum EnchantmentOptions {
839    /// Reference to an enchantment tag (e.g., "on_random_loot").
840    Tag(Identifier),
841    /// Explicit list of enchantment IDs.
842    List(&'static [Identifier]),
843}
844
845/// A function with optional conditions.
846#[derive(Debug, Clone)]
847pub struct ConditionalLootFunction {
848    pub function: LootFunction,
849    pub conditions: &'static [LootCondition],
850}
851
852/// A function that modifies loot items.
853#[derive(Debug, Clone)]
854pub enum LootFunction {
855    /// Set the count of the item.
856    SetCount { count: NumberProvider, add: bool },
857    /// Apply explosion decay - each item has 1/radius chance to survive.
858    ExplosionDecay,
859    /// Apply bonus count based on enchantment level.
860    ApplyBonus {
861        enchantment: Identifier,
862        formula: BonusFormula,
863    },
864    /// Increase count based on enchantment (like looting).
865    EnchantedCountIncrease {
866        enchantment: Identifier,
867        count: NumberProvider,
868        limit: i32,
869    },
870    /// Limit the count to a range.
871    LimitCount { min: Option<i32>, max: Option<i32> },
872    /// Set the damage of the item (0.0 = broken, 1.0 = full durability).
873    SetDamage { damage: NumberProvider, add: bool },
874    /// Enchant the item randomly with enchantments from options.
875    EnchantRandomly { options: EnchantmentOptions },
876    /// Enchant the item as if using an enchanting table at the specified level.
877    EnchantWithLevels {
878        levels: NumberProvider,
879        options: EnchantmentOptions,
880    },
881    /// Copy components from the block entity to the item.
882    CopyComponents {
883        source: CopySource,
884        include: &'static [Identifier],
885    },
886    /// Copy block state properties to the item.
887    CopyState {
888        block: Identifier,
889        properties: &'static [&'static str],
890    },
891    /// Set components on the item.
892    SetComponents { components: &'static str },
893    /// Set custom NBT data on the item (merges with existing custom_data).
894    SetCustomData { tag: &'static str },
895    /// Smelt the item (convert raw to cooked, ore to ingot, etc.).
896    FurnaceSmelt,
897    /// Create an exploration map pointing to a structure.
898    ExplorationMap {
899        destination: Identifier,
900        decoration: Identifier,
901        zoom: i32,
902        skip_existing_chunks: bool,
903    },
904    /// Set the custom name of the item.
905    SetName {
906        name: &'static str,
907        target: NameTarget,
908    },
909    /// Set the ominous bottle amplifier.
910    SetOminousBottleAmplifier { amplifier: NumberProvider },
911    /// Set the potion type.
912    SetPotion { id: Identifier },
913    /// Set the suspicious stew effects.
914    SetStewEffect { effects: &'static [StewEffect] },
915    /// Set the instrument for goat horns.
916    SetInstrument { options: Identifier },
917    /// Set enchantments on the item.
918    SetEnchantments {
919        enchantments: &'static [(Identifier, NumberProvider)],
920        add: bool,
921    },
922    /// Change the item type entirely.
923    SetItem { item: Identifier },
924    /// Copy name from source entity/block to item.
925    CopyName { source: CopySource },
926    /// Add lore lines to the item.
927    SetLore {
928        lore: &'static [&'static str],
929        mode: ListOperation,
930    },
931    /// Set container inventory contents.
932    SetContents {
933        entries: &'static [LootEntry],
934        component_type: Identifier,
935    },
936    /// Modify existing container contents.
937    ModifyContents {
938        modifier: &'static [ConditionalLootFunction],
939        component_type: Identifier,
940    },
941    /// Set container's loot table reference.
942    SetLootTable {
943        loot_table: Identifier,
944        seed: Option<i64>,
945    },
946    /// Set attribute modifiers on the item.
947    SetAttributes {
948        modifiers: &'static [AttributeModifier],
949        replace: bool,
950    },
951    /// Fill player head with texture from entity.
952    FillPlayerHead { entity: LootContextEntity },
953    /// Copy NBT/custom data from source.
954    CopyCustomData {
955        source: CopySource,
956        operations: &'static [CopyDataOperation],
957    },
958    /// Set banner pattern layers.
959    SetBannerPattern {
960        patterns: &'static [BannerPattern],
961        append: bool,
962    },
963    /// Set firework rocket properties.
964    SetFireworks {
965        explosions: Option<&'static [FireworkExplosion]>,
966        flight_duration: Option<i32>,
967    },
968    /// Set firework star explosion properties.
969    SetFireworkExplosion { explosion: FireworkExplosion },
970    /// Set book cover (title/author for written books).
971    SetBookCover {
972        title: Option<&'static str>,
973        author: Option<&'static str>,
974        generation: Option<i32>,
975    },
976    /// Set written book page contents.
977    SetWrittenBookPages {
978        pages: &'static [&'static str],
979        mode: ListOperation,
980    },
981    /// Set writable book page contents.
982    SetWritableBookPages {
983        pages: &'static [&'static str],
984        mode: ListOperation,
985    },
986    /// Toggle tooltip visibility.
987    ToggleTooltips {
988        toggles: &'static [(Identifier, bool)],
989    },
990    /// Set custom model data.
991    SetCustomModelData { value: NumberProvider },
992    /// Discard/delete the item entirely.
993    Discard,
994    /// Reference to a named function in the registry.
995    Reference(Identifier),
996    /// Apply multiple functions in sequence.
997    Sequence {
998        functions: &'static [ConditionalLootFunction],
999    },
1000    /// Conditionally apply function to specific item predicate matches.
1001    Filtered {
1002        item_filter: ToolPredicate,
1003        modifier: &'static ConditionalLootFunction,
1004    },
1005}
1006
1007/// Operation mode for list modifications (lore, book pages).
1008#[derive(Debug, Clone, Copy)]
1009pub enum ListOperation {
1010    /// Replace all existing entries.
1011    ReplaceAll,
1012    /// Replace a section of entries.
1013    ReplaceSection { offset: i32, size: Option<i32> },
1014    /// Insert before existing entries.
1015    InsertBefore { offset: i32 },
1016    /// Insert after existing entries.
1017    InsertAfter { offset: i32 },
1018    /// Append to the end.
1019    Append,
1020}
1021
1022/// An attribute modifier for SetAttributes function.
1023#[derive(Debug, Clone)]
1024pub struct AttributeModifier {
1025    pub attribute: Identifier,
1026    pub operation: AttributeOperation,
1027    pub amount: NumberProvider,
1028    pub id: Identifier,
1029    pub slot: EquipmentSlotGroup,
1030}
1031
1032/// Attribute modifier operation type.
1033#[expect(clippy::enum_variant_names, reason = "matches Vanilla naming")]
1034#[derive(Debug, Clone, Copy)]
1035pub enum AttributeOperation {
1036    AddValue,
1037    AddMultipliedBase,
1038    AddMultipliedTotal,
1039}
1040
1041/// Copy data operation for CopyCustomData.
1042#[derive(Debug, Clone)]
1043pub struct CopyDataOperation {
1044    pub source_path: &'static str,
1045    pub target_path: &'static str,
1046    pub op: CopyDataOp,
1047}
1048
1049/// Operation type for data copying.
1050#[derive(Debug, Clone, Copy)]
1051pub enum CopyDataOp {
1052    Replace,
1053    Append,
1054    Merge,
1055}
1056
1057/// A banner pattern layer.
1058#[derive(Debug, Clone)]
1059pub struct BannerPattern {
1060    pub pattern: Identifier,
1061    pub color: DyeColor,
1062}
1063
1064/// A firework explosion definition.
1065#[derive(Debug, Clone)]
1066pub struct FireworkExplosion {
1067    pub shape: FireworkShape,
1068    pub colors: &'static [i32],
1069    pub fade_colors: &'static [i32],
1070    pub has_trail: bool,
1071    pub has_twinkle: bool,
1072}
1073
1074/// Firework explosion shape.
1075#[derive(Debug, Clone, Copy)]
1076pub enum FireworkShape {
1077    SmallBall,
1078    LargeBall,
1079    Star,
1080    Creeper,
1081    Burst,
1082}
1083
1084/// Formula types for apply_bonus function.
1085#[derive(Debug, Clone, Copy)]
1086pub enum BonusFormula {
1087    /// Ore drops formula: count * (max(0, random(0..level+2) - 1) + 1)
1088    OreDrops,
1089    /// Uniform bonus: count + random(0..bonusMultiplier * level + 1)
1090    UniformBonusCount { bonus_multiplier: i32 },
1091    /// Binomial with bonus count: for each of (level + extra) trials, probability p to add 1
1092    BinomialWithBonusCount { extra: i32, probability: f32 },
1093}
1094
1095/// Source for copying components.
1096#[derive(Debug, Clone, Copy)]
1097pub enum CopySource {
1098    BlockEntity,
1099    This,
1100    Attacker,
1101    DirectAttacker,
1102}
1103
1104/// Target for set_name function.
1105#[derive(Debug, Clone, Copy)]
1106pub enum NameTarget {
1107    CustomName,
1108    ItemName,
1109}
1110
1111/// A stew effect for suspicious stew.
1112#[derive(Debug, Clone)]
1113pub struct StewEffect {
1114    pub effect_type: Identifier,
1115    pub duration: NumberProvider,
1116}
1117
1118/// A loot table entry that can generate items.
1119#[derive(Debug, Clone)]
1120pub enum LootEntry {
1121    /// Drop a specific item.
1122    Item {
1123        name: Identifier,
1124        weight: i32,
1125        quality: i32,
1126        conditions: &'static [LootCondition],
1127        functions: &'static [ConditionalLootFunction],
1128    },
1129    /// Reference another loot table by name.
1130    LootTableRef {
1131        name: Identifier,
1132        weight: i32,
1133        quality: i32,
1134        conditions: &'static [LootCondition],
1135        functions: &'static [ConditionalLootFunction],
1136    },
1137    /// Inline loot table (embedded pools directly in entry).
1138    InlineLootTable {
1139        pools: &'static [LootPool],
1140        weight: i32,
1141        quality: i32,
1142        conditions: &'static [LootCondition],
1143        functions: &'static [ConditionalLootFunction],
1144    },
1145    /// Drop items from a tag.
1146    Tag {
1147        name: Identifier,
1148        expand: bool,
1149        weight: i32,
1150        quality: i32,
1151        conditions: &'static [LootCondition],
1152        functions: &'static [ConditionalLootFunction],
1153    },
1154    /// Try children in order, use first that matches.
1155    Alternatives {
1156        children: &'static [LootEntry],
1157        conditions: &'static [LootCondition],
1158    },
1159    /// Use all children.
1160    Group {
1161        children: &'static [LootEntry],
1162        conditions: &'static [LootCondition],
1163    },
1164    /// Use children in sequence until one fails.
1165    Sequence {
1166        children: &'static [LootEntry],
1167        conditions: &'static [LootCondition],
1168    },
1169    /// Empty entry (no drop).
1170    Empty {
1171        weight: i32,
1172        conditions: &'static [LootCondition],
1173    },
1174    /// Dynamic content (e.g., block entity contents).
1175    Dynamic {
1176        name: Identifier,
1177        conditions: &'static [LootCondition],
1178    },
1179    /// Select items from specific block entity slots.
1180    Slots {
1181        /// Slots to select from (can be single slot or range).
1182        slots: SlotRange,
1183        conditions: &'static [LootCondition],
1184        functions: &'static [ConditionalLootFunction],
1185    },
1186}
1187
1188/// A range of slots for the Slots entry type.
1189#[derive(Debug, Clone, Copy)]
1190pub enum SlotRange {
1191    /// A single specific slot index.
1192    Single(i32),
1193    /// A range of slots (inclusive).
1194    Range { min: i32, max: i32 },
1195    /// All contents slots.
1196    Contents,
1197    /// Specific named slots (for entities).
1198    Named(&'static [&'static str]),
1199}
1200
1201impl LootEntry {
1202    /// Get the weight of this entry for random selection.
1203    pub fn weight(&self) -> i32 {
1204        match self {
1205            Self::Item { weight, .. } => *weight,
1206            Self::LootTableRef { weight, .. } => *weight,
1207            Self::InlineLootTable { weight, .. } => *weight,
1208            Self::Tag { weight, .. } => *weight,
1209            Self::Empty { weight, .. } => *weight,
1210            // Composite entries don't have weight
1211            Self::Alternatives { .. }
1212            | Self::Group { .. }
1213            | Self::Sequence { .. }
1214            | Self::Dynamic { .. }
1215            | Self::Slots { .. } => 1,
1216        }
1217    }
1218
1219    /// Get the quality modifier for luck-based weight adjustment.
1220    pub fn quality(&self) -> i32 {
1221        match self {
1222            Self::Item { quality, .. } => *quality,
1223            Self::LootTableRef { quality, .. } => *quality,
1224            Self::InlineLootTable { quality, .. } => *quality,
1225            Self::Tag { quality, .. } => *quality,
1226            Self::Empty { .. }
1227            | Self::Alternatives { .. }
1228            | Self::Group { .. }
1229            | Self::Sequence { .. }
1230            | Self::Dynamic { .. }
1231            | Self::Slots { .. } => 0,
1232        }
1233    }
1234
1235    /// Get the effective weight adjusted for luck.
1236    /// Formula: max(floor(weight + quality * luck), 0)
1237    pub fn effective_weight(&self, luck: f32) -> i32 {
1238        let base = self.weight() as f32;
1239        let quality = self.quality() as f32;
1240        (base + quality * luck).floor().max(0.0) as i32
1241    }
1242
1243    /// Get the conditions for this entry.
1244    pub fn conditions(&self) -> &'static [LootCondition] {
1245        match self {
1246            Self::Item { conditions, .. } => conditions,
1247            Self::LootTableRef { conditions, .. } => conditions,
1248            Self::InlineLootTable { conditions, .. } => conditions,
1249            Self::Tag { conditions, .. } => conditions,
1250            Self::Alternatives { conditions, .. } => conditions,
1251            Self::Group { conditions, .. } => conditions,
1252            Self::Sequence { conditions, .. } => conditions,
1253            Self::Empty { conditions, .. } => conditions,
1254            Self::Dynamic { conditions, .. } => conditions,
1255            Self::Slots { conditions, .. } => conditions,
1256        }
1257    }
1258
1259    /// Get the functions for this entry.
1260    pub fn functions(&self) -> &'static [ConditionalLootFunction] {
1261        match self {
1262            Self::Item { functions, .. } => functions,
1263            Self::LootTableRef { functions, .. } => functions,
1264            Self::InlineLootTable { functions, .. } => functions,
1265            Self::Tag { functions, .. } => functions,
1266            Self::Slots { functions, .. } => functions,
1267            Self::Empty { .. }
1268            | Self::Alternatives { .. }
1269            | Self::Group { .. }
1270            | Self::Sequence { .. }
1271            | Self::Dynamic { .. } => &[],
1272        }
1273    }
1274}
1275
1276/// A pool of loot entries with roll counts.
1277#[derive(Debug, Clone)]
1278pub struct LootPool {
1279    pub rolls: NumberProvider,
1280    pub bonus_rolls: f32,
1281    pub entries: &'static [LootEntry],
1282    pub conditions: &'static [LootCondition],
1283    pub functions: &'static [ConditionalLootFunction],
1284}
1285
1286/// A complete loot table definition.
1287#[derive(Debug)]
1288pub struct LootTable {
1289    pub key: Identifier,
1290    pub loot_type: LootType,
1291    pub pools: &'static [LootPool],
1292    pub functions: &'static [ConditionalLootFunction],
1293    pub random_sequence: Option<Identifier>,
1294}
1295
1296impl LootTable {
1297    /// Generate random items from this loot table.
1298    ///
1299    /// # Arguments
1300    /// * `ctx` - The loot context containing RNG, luck, block state, tool, etc.
1301    ///
1302    /// This follows vanilla's approach:
1303    /// 1. For each pool, check conditions
1304    /// 2. Roll `rolls + floor(bonus_rolls * luck)` times
1305    /// 3. Each roll does weighted random selection among valid entries
1306    /// 4. Apply entry-level functions to each item
1307    /// 5. Apply pool-level functions to all items from that pool
1308    /// 6. Apply table-level functions to all items from the table
1309    pub fn get_random_items<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> Vec<ItemStack> {
1310        let mut result = Vec::new();
1311        for pool in self.pools {
1312            pool.add_random_items(ctx, &mut result);
1313        }
1314
1315        // Apply table-level functions to all items
1316        if !self.functions.is_empty() {
1317            for item in &mut result {
1318                for cond_func in self.functions {
1319                    if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1320                        cond_func.function.apply(item, ctx);
1321                    }
1322                }
1323            }
1324            // Remove items with zero count after applying functions
1325            result.retain(|item| item.count > 0);
1326        }
1327
1328        result
1329    }
1330}
1331
1332impl LootPool {
1333    /// Add random items from this pool to the result.
1334    fn add_random_items<R: rand::Rng>(
1335        &self,
1336        ctx: &mut LootContext<'_, R>,
1337        result: &mut Vec<ItemStack>,
1338    ) {
1339        // Check pool conditions
1340        for condition in self.conditions {
1341            if !condition.test(ctx) {
1342                return;
1343            }
1344        }
1345
1346        // Track where items from this pool start
1347        let start_index = result.len();
1348
1349        // Calculate number of rolls
1350        let roll_count = self.rolls.get_int(ctx.rng) + (self.bonus_rolls * ctx.luck).floor() as i32;
1351
1352        for _ in 0..roll_count {
1353            self.add_random_item(ctx, result);
1354        }
1355
1356        // Apply pool-level functions to all items generated by this pool
1357        if !self.functions.is_empty() {
1358            for item in result.iter_mut().skip(start_index) {
1359                for cond_func in self.functions {
1360                    if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1361                        cond_func.function.apply(item, ctx);
1362                    }
1363                }
1364            }
1365            // Remove items with zero count after applying functions
1366            result.retain(|item| item.count > 0);
1367        }
1368    }
1369
1370    /// Select and add a single random item from this pool.
1371    fn add_random_item<R: rand::Rng>(
1372        &self,
1373        ctx: &mut LootContext<'_, R>,
1374        result: &mut Vec<ItemStack>,
1375    ) {
1376        // Collect valid entries with their effective weights
1377        let mut valid_entries: Vec<(&LootEntry, i32)> = Vec::new();
1378        let mut total_weight = 0;
1379
1380        for entry in self.entries {
1381            // Check entry conditions
1382            let passes_conditions = entry.conditions().iter().all(|c| c.test(ctx));
1383
1384            if !passes_conditions {
1385                continue;
1386            }
1387
1388            let weight = entry.effective_weight(ctx.luck);
1389            if weight > 0 {
1390                valid_entries.push((entry, weight));
1391                total_weight += weight;
1392            }
1393        }
1394
1395        if total_weight == 0 || valid_entries.is_empty() {
1396            return;
1397        }
1398
1399        // Weighted random selection
1400        let selected = if valid_entries.len() == 1 {
1401            valid_entries[0].0
1402        } else {
1403            let mut index = ctx.rng.random_range(0..total_weight);
1404            let mut selected_entry = valid_entries[0].0;
1405            for (entry, weight) in &valid_entries {
1406                index -= weight;
1407                if index < 0 {
1408                    selected_entry = entry;
1409                    break;
1410                }
1411            }
1412            selected_entry
1413        };
1414
1415        // Generate item(s) from the selected entry
1416        selected.create_items(ctx, result);
1417    }
1418}
1419
1420impl LootEntry {
1421    /// Create items from this entry and add them to the result.
1422    fn create_items<R: rand::Rng>(
1423        &self,
1424        ctx: &mut LootContext<'_, R>,
1425        result: &mut Vec<ItemStack>,
1426    ) {
1427        match self {
1428            LootEntry::Item {
1429                name, functions, ..
1430            } => {
1431                if let Some(item_ref) = REGISTRY.items.by_key(name) {
1432                    let mut item = ItemStack::new(item_ref);
1433
1434                    // Apply functions
1435                    for cond_func in *functions {
1436                        if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1437                            cond_func.function.apply(&mut item, ctx);
1438                        }
1439                    }
1440
1441                    if item.count > 0 {
1442                        result.push(item);
1443                    }
1444                }
1445            }
1446            LootEntry::LootTableRef {
1447                name, functions, ..
1448            } => {
1449                // Recursively get items from referenced loot table
1450                if let Some(table) = REGISTRY.loot_tables.by_key(name) {
1451                    let mut items = table.get_random_items(ctx);
1452                    // Apply functions to all items from the referenced table
1453                    for item in &mut items {
1454                        for cond_func in *functions {
1455                            if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1456                                cond_func.function.apply(item, ctx);
1457                            }
1458                        }
1459                    }
1460                    result.extend(items.into_iter().filter(|i| i.count > 0));
1461                }
1462            }
1463            LootEntry::InlineLootTable {
1464                pools, functions, ..
1465            } => {
1466                // Process inline loot table pools directly
1467                let mut items = Vec::new();
1468                for pool in *pools {
1469                    pool.add_random_items(ctx, &mut items);
1470                }
1471                // Apply functions to all items from the inline table
1472                for item in &mut items {
1473                    for cond_func in *functions {
1474                        if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1475                            cond_func.function.apply(item, ctx);
1476                        }
1477                    }
1478                }
1479                result.extend(items.into_iter().filter(|i| i.count > 0));
1480            }
1481            LootEntry::Tag {
1482                name,
1483                expand,
1484                functions,
1485                ..
1486            } => {
1487                // Get all items in the tag
1488                if let Some(items) = REGISTRY.items.get_tag(name) {
1489                    if *expand {
1490                        // Pick one random item from the tag (weighted equally)
1491                        if !items.is_empty() {
1492                            let index = ctx.rng.random_range(0..items.len());
1493                            let mut item = ItemStack::new(items[index]);
1494                            for cond_func in *functions {
1495                                if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1496                                    cond_func.function.apply(&mut item, ctx);
1497                                }
1498                            }
1499                            if item.count > 0 {
1500                                result.push(item);
1501                            }
1502                        }
1503                    } else {
1504                        // Drop all items from the tag
1505                        for item_ref in items {
1506                            let mut item = ItemStack::new(item_ref);
1507                            for cond_func in *functions {
1508                                if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1509                                    cond_func.function.apply(&mut item, ctx);
1510                                }
1511                            }
1512                            if item.count > 0 {
1513                                result.push(item);
1514                            }
1515                        }
1516                    }
1517                }
1518            }
1519            LootEntry::Alternatives { children, .. } => {
1520                // Try children in order, use first that passes conditions and produces items
1521                for child in *children {
1522                    // Check child's conditions first
1523                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1524                    if !passes_conditions {
1525                        continue; // Try next alternative
1526                    }
1527
1528                    let before_len = result.len();
1529                    child.create_items(ctx, result);
1530                    if result.len() > before_len {
1531                        break; // First successful child that produced items, stop
1532                    }
1533                }
1534            }
1535            LootEntry::Group { children, .. } => {
1536                // Use all children that pass their conditions
1537                for child in *children {
1538                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1539                    if passes_conditions {
1540                        child.create_items(ctx, result);
1541                    }
1542                }
1543            }
1544            LootEntry::Sequence { children, .. } => {
1545                // Use children in sequence until one fails its conditions
1546                // Note: Unlike Alternatives, Sequence stops when conditions FAIL,
1547                // not when items are produced. A child can produce nothing but still "succeed".
1548                for child in *children {
1549                    let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1550                    if !passes_conditions {
1551                        break; // Condition failed, stop sequence
1552                    }
1553                    child.create_items(ctx, result);
1554                }
1555            }
1556            LootEntry::Empty { .. } => {
1557                // Empty entry produces nothing
1558            }
1559            LootEntry::Dynamic { name, .. } => {
1560                // Dynamic entries are used for block entity contents (like shulker boxes)
1561                // The name identifies what content to retrieve:
1562                // - "contents" = block entity inventory contents
1563                // - Other names may exist for specific use cases
1564                //
1565                // TODO: Implement when block entity system supports inventory retrieval
1566                // This requires:
1567                // 1. Block entity reference in LootContext
1568                // 2. Method to get inventory contents from block entity
1569                // 3. Adding those items to the result
1570                let _ = name;
1571            }
1572            LootEntry::Slots {
1573                slots, functions, ..
1574            } => {
1575                // Slots entries select items from specific block entity slots
1576                // TODO: Implement when block entity system supports slot access
1577                // This requires:
1578                // 1. Block entity reference in LootContext
1579                // 2. Method to get items from specific slots
1580                // 3. Apply functions to each retrieved item
1581                let _ = slots;
1582                let _ = functions;
1583            }
1584        }
1585    }
1586}
1587
1588impl LootFunction {
1589    /// Apply this function to modify the item stack.
1590    ///
1591    /// This modifies the item in place. Functions can change:
1592    /// - Count (SetCount, ExplosionDecay, ApplyBonus, etc.)
1593    /// - Damage/durability (SetDamage)
1594    /// - Enchantments (EnchantRandomly, EnchantWithLevels, SetEnchantments)
1595    /// - Components/NBT (CopyComponents, SetComponents, CopyState)
1596    /// - Item type (FurnaceSmelt)
1597    /// - And more...
1598    pub fn apply<R: rand::Rng>(&self, item: &mut ItemStack, ctx: &mut LootContext<'_, R>) {
1599        match self {
1600            LootFunction::SetCount {
1601                count: provider,
1602                add,
1603            } => {
1604                let value = provider.get_int(ctx.rng);
1605                if *add {
1606                    item.count += value;
1607                } else {
1608                    item.count = value;
1609                }
1610            }
1611            LootFunction::ExplosionDecay => {
1612                if let Some(radius) = ctx.explosion_radius {
1613                    // Each item has 1/radius chance to survive
1614                    let probability = 1.0 / radius;
1615                    let mut result_count = 0;
1616                    for _ in 0..item.count {
1617                        if ctx.rng.random::<f32>() <= probability {
1618                            result_count += 1;
1619                        }
1620                    }
1621                    item.count = result_count;
1622                }
1623            }
1624            LootFunction::ApplyBonus {
1625                enchantment,
1626                formula,
1627            } => {
1628                let level = ctx.get_enchantment_level_by_id(enchantment);
1629                item.count = formula.apply(item.count, level, ctx.rng);
1630            }
1631            LootFunction::EnchantedCountIncrease {
1632                enchantment,
1633                count: provider,
1634                limit,
1635            } => {
1636                let level = ctx.get_enchantment_level_by_id(enchantment);
1637                if level > 0 {
1638                    let bonus = (provider.get_simple(ctx.rng) * level as f32).round() as i32;
1639                    let bonus = if *limit > 0 { bonus.min(*limit) } else { bonus };
1640                    item.count += bonus;
1641                }
1642            }
1643            LootFunction::LimitCount { min, max } => {
1644                if let Some(min_val) = min {
1645                    item.count = item.count.max(*min_val);
1646                }
1647                if let Some(max_val) = max {
1648                    item.count = item.count.min(*max_val);
1649                }
1650            }
1651            LootFunction::SetDamage { damage, add } => {
1652                item.set_damage_fraction(damage.get_simple(ctx.rng), *add);
1653            }
1654            LootFunction::EnchantRandomly { options } => {
1655                // TODO: Implement when enchantment system is ready
1656                item.enchant_randomly(options, ctx.rng);
1657            }
1658            LootFunction::EnchantWithLevels { levels, options } => {
1659                // TODO: Implement when enchantment system is ready
1660                let level = levels.get_int(ctx.rng);
1661                item.enchant_with_levels(level, options, ctx.rng);
1662            }
1663            LootFunction::CopyComponents { source, include } => {
1664                // TODO: Implement when block entity system is ready
1665                item.copy_components(*source, include, ctx);
1666            }
1667            LootFunction::CopyState { block, properties } => {
1668                // TODO: Implement block state copying
1669                item.copy_block_state(block, properties, ctx);
1670            }
1671            LootFunction::SetComponents { components } => {
1672                // TODO: Implement component setting from JSON
1673                item.set_components_from_json(components);
1674            }
1675            LootFunction::SetCustomData { tag } => {
1676                item.set_custom_data(tag);
1677            }
1678            LootFunction::FurnaceSmelt => {
1679                // TODO: Implement smelting recipe lookup
1680                item.apply_furnace_smelt();
1681            }
1682            LootFunction::ExplorationMap {
1683                destination,
1684                decoration,
1685                zoom,
1686                skip_existing_chunks,
1687            } => {
1688                // TODO: Implement exploration map creation
1689                item.create_exploration_map(destination, decoration, *zoom, *skip_existing_chunks);
1690            }
1691            LootFunction::SetName { name, target } => {
1692                // TODO: Implement name setting
1693                item.set_name(name, *target);
1694            }
1695            LootFunction::SetOminousBottleAmplifier { amplifier } => {
1696                let amp = amplifier.get_int(ctx.rng);
1697                item.set_ominous_bottle_amplifier(amp);
1698            }
1699            LootFunction::SetPotion { id } => {
1700                item.set_potion(id);
1701            }
1702            LootFunction::SetStewEffect { effects } => {
1703                item.set_stew_effects(effects, ctx.rng);
1704            }
1705            LootFunction::SetInstrument { options } => {
1706                item.set_instrument(options, ctx.rng);
1707            }
1708            LootFunction::SetEnchantments { enchantments, add } => {
1709                let resolved: Vec<_> = enchantments
1710                    .iter()
1711                    .map(|(key, provider)| (key.clone(), provider.get_int(ctx.rng).max(0) as u32))
1712                    .collect();
1713                item.set_enchantments(&resolved, *add);
1714            }
1715            LootFunction::SetItem { item: new_item } => {
1716                item.set_item(new_item);
1717            }
1718            LootFunction::CopyName { source } => {
1719                item.copy_name(*source, ctx);
1720            }
1721            LootFunction::SetLore { lore, mode } => {
1722                item.set_lore(lore, *mode);
1723            }
1724            LootFunction::SetContents {
1725                entries,
1726                component_type,
1727            } => {
1728                item.set_contents(entries, component_type, ctx);
1729            }
1730            LootFunction::ModifyContents {
1731                modifier,
1732                component_type,
1733            } => {
1734                item.modify_contents(modifier, component_type, ctx);
1735            }
1736            LootFunction::SetLootTable { loot_table, seed } => {
1737                item.set_loot_table(loot_table, *seed);
1738            }
1739            LootFunction::SetAttributes { modifiers, replace } => {
1740                item.set_attributes(modifiers, *replace, ctx.rng);
1741            }
1742            LootFunction::FillPlayerHead { entity } => {
1743                item.fill_player_head(*entity, ctx);
1744            }
1745            LootFunction::CopyCustomData { source, operations } => {
1746                item.copy_custom_data(*source, operations, ctx);
1747            }
1748            LootFunction::SetBannerPattern { patterns, append } => {
1749                item.set_banner_pattern(patterns, *append);
1750            }
1751            LootFunction::SetFireworks {
1752                explosions,
1753                flight_duration,
1754            } => {
1755                item.set_fireworks(*explosions, *flight_duration);
1756            }
1757            LootFunction::SetFireworkExplosion { explosion } => {
1758                item.set_firework_explosion(explosion);
1759            }
1760            LootFunction::SetBookCover {
1761                title,
1762                author,
1763                generation,
1764            } => {
1765                item.set_book_cover(*title, *author, *generation);
1766            }
1767            LootFunction::SetWrittenBookPages { pages, mode } => {
1768                item.set_written_book_pages(pages, *mode);
1769            }
1770            LootFunction::SetWritableBookPages { pages, mode } => {
1771                item.set_writable_book_pages(pages, *mode);
1772            }
1773            LootFunction::ToggleTooltips { toggles } => {
1774                item.toggle_tooltips(toggles);
1775            }
1776            LootFunction::SetCustomModelData { value } => {
1777                item.set_custom_model_data(value.get_int(ctx.rng));
1778            }
1779            LootFunction::Discard => {
1780                item.count = 0;
1781            }
1782            LootFunction::Reference(_name) => {
1783                // TODO: Implement function registry lookup
1784            }
1785            LootFunction::Sequence { functions } => {
1786                for cond_func in *functions {
1787                    if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1788                        cond_func.function.apply(item, ctx);
1789                    }
1790                }
1791            }
1792            LootFunction::Filtered {
1793                item_filter,
1794                modifier,
1795            } => {
1796                if item_filter.test(item, ctx) && modifier.conditions.iter().all(|c| c.test(ctx)) {
1797                    modifier.function.apply(item, ctx);
1798                }
1799            }
1800        }
1801    }
1802}
1803
1804impl BonusFormula {
1805    /// Apply the bonus formula to calculate new count.
1806    pub fn apply<R: rand::Rng>(&self, count: i32, level: i32, rng: &mut R) -> i32 {
1807        match self {
1808            BonusFormula::OreDrops => {
1809                if level > 0 {
1810                    // Vanilla: count * (max(0, random(0..level+2) - 1) + 1)
1811                    let bonus = rng.random_range(0..level + 2) - 1;
1812                    let multiplier = bonus.max(0) + 1;
1813                    count * multiplier
1814                } else {
1815                    count
1816                }
1817            }
1818            BonusFormula::UniformBonusCount { bonus_multiplier } => {
1819                // Vanilla: count + random(0..bonusMultiplier * level + 1)
1820                if level > 0 {
1821                    count + rng.random_range(0..bonus_multiplier * level + 1)
1822                } else {
1823                    count
1824                }
1825            }
1826            BonusFormula::BinomialWithBonusCount { extra, probability } => {
1827                // Vanilla: for each of (level + extra) trials, probability p to add 1
1828                let trials = level + extra;
1829                let mut bonus = 0;
1830                for _ in 0..trials {
1831                    if rng.random::<f32>() < *probability {
1832                        bonus += 1;
1833                    }
1834                }
1835                count + bonus
1836            }
1837        }
1838    }
1839}
1840
1841pub type LootTableRef = &'static LootTable;
1842
1843/// Registry for loot tables.
1844pub struct LootTableRegistry {
1845    tables_by_id: Vec<LootTableRef>,
1846    tables_by_key: FxHashMap<Identifier, usize>,
1847    allows_registering: bool,
1848}
1849
1850impl LootTableRegistry {
1851    #[must_use]
1852    pub fn new() -> Self {
1853        Self {
1854            tables_by_id: Vec::new(),
1855            tables_by_key: FxHashMap::default(),
1856            allows_registering: true,
1857        }
1858    }
1859
1860    pub fn register(&mut self, table: LootTableRef) -> usize {
1861        assert!(
1862            self.allows_registering,
1863            "Cannot register loot tables after the registry has been frozen"
1864        );
1865
1866        let id = self.tables_by_id.len();
1867        self.tables_by_key.insert(table.key.clone(), id);
1868        self.tables_by_id.push(table);
1869        id
1870    }
1871
1872    pub fn iter(&self) -> impl Iterator<Item = (usize, LootTableRef)> + '_ {
1873        self.tables_by_id
1874            .iter()
1875            .enumerate()
1876            .map(|(id, &table)| (id, table))
1877    }
1878}
1879
1880impl Default for LootTableRegistry {
1881    fn default() -> Self {
1882        Self::new()
1883    }
1884}
1885
1886crate::impl_registry!(
1887    LootTableRegistry,
1888    LootTable,
1889    tables_by_id,
1890    tables_by_key,
1891    loot_tables
1892);
1893
1894#[cfg(test)]
1895mod tests {
1896    use crate::{Registry, vanilla_blocks, vanilla_items, vanilla_loot_tables};
1897
1898    use super::*;
1899    use rand::SeedableRng;
1900
1901    fn test_rng() -> rand::rngs::StdRng {
1902        rand::rngs::StdRng::seed_from_u64(12345)
1903    }
1904
1905    fn init_test_registries() {
1906        REGISTRY.get_or_init(|| {
1907            let mut registry = Registry::new_empty();
1908            vanilla_loot_tables::register_loot_tables(&mut registry.loot_tables);
1909            vanilla_items::register_items(&mut registry.items);
1910            vanilla_blocks::register_blocks(&mut registry.blocks);
1911            registry.freeze();
1912            registry
1913        });
1914    }
1915
1916    #[test]
1917    fn test_oak_log_loot() {
1918        init_test_registries();
1919        let mut rng = test_rng();
1920
1921        let mut ctx = LootContext::new(&mut rng);
1922        let items = vanilla_loot_tables::BLOCKS_OAK_LOG.get_random_items(&mut ctx);
1923
1924        assert_eq!(items.len(), 1);
1925        assert_eq!(items[0].count, 1);
1926        assert_eq!(items[0].item.key, Identifier::vanilla_static("oak_log"));
1927    }
1928
1929    #[test]
1930    fn test_diamond_ore_loot_no_silk_touch() {
1931        // Without silk touch, diamond ore should drop diamond (not the ore block)
1932        init_test_registries();
1933        let mut rng = test_rng();
1934
1935        let mut ctx = LootContext::new(&mut rng);
1936        let items = vanilla_loot_tables::BLOCKS_DIAMOND_ORE.get_random_items(&mut ctx);
1937
1938        assert_eq!(items.len(), 1);
1939        assert_eq!(items[0].count, 1);
1940        // Without silk touch enchantment, diamond ore drops diamond
1941        assert_eq!(items[0].item.key, Identifier::vanilla_static("diamond"));
1942    }
1943
1944    #[test]
1945    fn test_grass_block_loot_no_silk_touch() {
1946        // Without silk touch, grass block should drop dirt
1947        init_test_registries();
1948        let mut rng = test_rng();
1949
1950        let mut ctx = LootContext::new(&mut rng);
1951        let items = vanilla_loot_tables::BLOCKS_GRASS_BLOCK.get_random_items(&mut ctx);
1952
1953        assert_eq!(items.len(), 1);
1954        assert_eq!(items[0].count, 1);
1955        // Without silk touch, grass block drops dirt
1956        assert_eq!(items[0].item.key, Identifier::vanilla_static("dirt"));
1957    }
1958
1959    #[test]
1960    fn test_stone_loot_no_silk_touch() {
1961        // Without silk touch, stone should drop cobblestone
1962        init_test_registries();
1963        let mut rng = test_rng();
1964
1965        let mut ctx = LootContext::new(&mut rng);
1966        let items = vanilla_loot_tables::BLOCKS_STONE.get_random_items(&mut ctx);
1967
1968        assert_eq!(items.len(), 1);
1969        assert_eq!(items[0].count, 1);
1970        // Without silk touch, stone drops cobblestone
1971        assert_eq!(items[0].item.key, Identifier::vanilla_static("cobblestone"));
1972    }
1973
1974    #[test]
1975    fn test_explosion_decay_function() {
1976        // Test the explosion_decay function directly
1977        init_test_registries();
1978
1979        // explosion_decay reduces count based on 1/radius probability per item
1980        let cond_func = ConditionalLootFunction {
1981            function: LootFunction::ExplosionDecay,
1982            conditions: &[],
1983        };
1984
1985        let mut total_survived = 0;
1986        let initial_count = 10;
1987
1988        for seed in 0u64..100 {
1989            let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1990            let mut ctx = LootContext::new(&mut rng).with_explosion(4.0);
1991            let mut item = ItemStack::with_count(&crate::vanilla_items::ITEMS.stone, initial_count);
1992            cond_func.function.apply(&mut item, &mut ctx);
1993            total_survived += item.count;
1994        }
1995
1996        // With 10 items each trial, 100 trials = 1000 items total
1997        // Each has 25% (1/4.0) chance to survive = ~250 expected
1998        // Allow for variance: 150-350 range
1999        assert!(
2000            total_survived > 150 && total_survived < 350,
2001            "Expected ~250 items with explosion decay (25% of 1000), got {}",
2002            total_survived
2003        );
2004    }
2005
2006    #[test]
2007    fn test_survives_explosion_condition() {
2008        init_test_registries();
2009
2010        // Test that survives_explosion condition works
2011        // Gravel has survives_explosion on its alternatives
2012        let mut survived = 0;
2013        for seed in 0..100 {
2014            let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
2015            let mut ctx = LootContext::new(&mut rng).with_explosion(4.0);
2016            let items = vanilla_loot_tables::BLOCKS_GRAVEL.get_random_items(&mut ctx);
2017            if !items.is_empty() {
2018                survived += 1;
2019            }
2020        }
2021
2022        // With radius 4.0, ~25% should survive
2023        assert!(
2024            survived > 10 && survived < 50,
2025            "Expected ~25% survival rate, got {}%",
2026            survived
2027        );
2028    }
2029}