Skip to main content

steel_registry/loot_table/
conditions.rs

1use super::{
2    BlockStateExt, DamageSourceInfo, DyeColor, EntityEquipmentRef, EntityRef, EntityRefFlags,
3    Identifier, ItemStack, LootContext, LootContextEntity, NumberProvider, NumberProviderRange,
4    REGISTRY, RegistryExt, RngExt, TaggedRegistryExt,
5};
6
7/// A property check for block state conditions.
8#[derive(Debug, Clone)]
9pub struct PropertyCheck {
10    pub name: &'static str,
11    pub value: &'static str,
12}
13
14/// A condition that must be met for a loot entry or pool to apply.
15#[derive(Debug, Clone)]
16#[expect(clippy::large_enum_variant)]
17pub enum LootCondition {
18    /// The loot survives explosion damage (random chance based on explosion radius).
19    /// Vanilla: 1/radius chance to pass. If no explosion, always passes.
20    SurvivesExplosion,
21    /// Check block state properties match expected values.
22    BlockStateProperty {
23        block: Identifier,
24        properties: &'static [PropertyCheck],
25    },
26    /// Simple random chance (0.0 to 1.0).
27    RandomChance(f32),
28    /// Random chance affected by an enchantment (e.g., looting).
29    /// Vanilla 1.21+: uses `enchanted_chance` which can be constant or linear.
30    RandomChanceWithEnchantedBonus {
31        enchantment: Identifier,
32        unenchanted_chance: f32,
33        /// For linear formula: chance = base + `per_level` * (level - 1)
34        enchanted_chance: EnchantedChance,
35    },
36    /// Match tool predicate - checks if the tool matches certain criteria.
37    MatchTool(ToolPredicate),
38    /// Table bonus condition - chance based on enchantment level from a table.
39    /// The chances array is indexed by enchantment level (0 = no enchant, 1 = level 1, etc.)
40    TableBonus {
41        enchantment: Identifier,
42        chances: &'static [f32],
43    },
44    /// Inverted condition - passes if the inner condition fails.
45    Inverted(&'static LootCondition),
46    /// Any of the conditions pass (OR logic).
47    AnyOf(&'static [LootCondition]),
48    /// All of the conditions pass (AND logic).
49    AllOf(&'static [LootCondition]),
50    /// Killed by player condition.
51    KilledByPlayer,
52    /// Entity properties condition - checks entity predicates.
53    EntityProperties {
54        entity: LootContextEntity,
55        predicate: EntityPredicate,
56    },
57    /// Damage source properties condition - checks how the entity was damaged.
58    DamageSourceProperties { predicate: DamageSourcePredicate },
59    /// Location check condition - checks the location predicate.
60    LocationCheck {
61        offset_x: i32,
62        offset_y: i32,
63        offset_z: i32,
64        predicate: LocationPredicate,
65    },
66    /// Weather check condition - checks current weather.
67    WeatherCheck {
68        raining: Option<bool>,
69        thundering: Option<bool>,
70    },
71    /// Time check condition - checks game time.
72    TimeCheck {
73        value: NumberProviderRange,
74        period: Option<i64>,
75    },
76    /// Value check condition - compares a number provider value against a range.
77    ValueCheck {
78        value: NumberProvider,
79        range: NumberProviderRange,
80    },
81    /// Check if a specific enchantment is active.
82    EnchantmentActiveCheck {
83        enchantment: Identifier,
84        active: bool,
85    },
86    /// Check entity scoreboard scores.
87    EntityScores {
88        entity: LootContextEntity,
89        scores: &'static [(&'static str, NumberProviderRange)],
90    },
91    /// Reference to a named condition in the registry.
92    Reference(Identifier),
93}
94
95/// Enchanted chance calculation method.
96#[derive(Debug, Clone, Copy)]
97pub enum EnchantedChance {
98    /// Constant chance regardless of enchantment level.
99    Constant(f32),
100    /// Linear formula: base + `per_level_above_first` * (level - 1)
101    Linear {
102        base: f32,
103        per_level_above_first: f32,
104    },
105}
106
107/// Predicate for matching tools.
108#[derive(Debug, Clone)]
109pub enum ToolPredicate {
110    /// Match a specific item.
111    Item(Identifier),
112    /// Match items with a specific enchantment at minimum level.
113    HasEnchantment {
114        enchantment: Identifier,
115        min_level: i32,
116    },
117    /// Match items in a tag.
118    Tag(Identifier),
119    /// Always matches (no predicate specified).
120    Any,
121}
122
123/// Predicate for checking location/block properties.
124#[derive(Debug, Clone)]
125pub struct LocationPredicate {
126    pub block: Option<BlockPredicate>,
127}
128
129/// Predicate for checking block properties.
130#[derive(Debug, Clone)]
131pub struct BlockPredicate {
132    pub blocks: Option<Identifier>,
133    pub state: &'static [(&'static str, &'static str)],
134}
135
136/// Predicate for checking entity properties.
137#[derive(Debug, Clone)]
138pub struct EntityPredicate {
139    pub entity_type: Option<Identifier>,
140    pub flags: Option<EntityFlags>,
141    pub equipment: Option<EntityEquipment>,
142    /// Vanilla `minecraft:components.sheep/color` entity data component check.
143    pub sheep_color: Option<DyeColor>,
144    /// Vanilla `minecraft:type_specific/sheep.sheared` check.
145    pub sheep_sheared: Option<bool>,
146    /// Vanilla `minecraft:components.chicken/variant` entity data component check.
147    pub chicken_variant: Option<Identifier>,
148}
149
150/// Entity flags (`is_on_fire`, `is_sneaking`, etc.)
151#[derive(Debug, Clone)]
152pub struct EntityFlags {
153    pub is_on_fire: Option<bool>,
154    pub is_sneaking: Option<bool>,
155    pub is_sprinting: Option<bool>,
156    pub is_swimming: Option<bool>,
157    pub is_baby: Option<bool>,
158}
159
160/// Entity equipment predicate
161#[derive(Debug, Clone)]
162pub struct EntityEquipment {
163    pub mainhand: Option<ToolPredicate>,
164    pub offhand: Option<ToolPredicate>,
165    pub head: Option<ToolPredicate>,
166    pub chest: Option<ToolPredicate>,
167    pub legs: Option<ToolPredicate>,
168    pub feet: Option<ToolPredicate>,
169}
170
171/// Predicate for checking damage source properties.
172#[derive(Debug, Clone)]
173pub struct DamageSourcePredicate {
174    /// Tags that must be present on the damage source.
175    pub tags: &'static [DamageTagPredicate],
176    /// Source entity predicate (e.g., the player/mob that caused damage).
177    pub source_entity: Option<EntityPredicate>,
178    /// Direct entity predicate (e.g., the arrow/fireball).
179    pub direct_entity: Option<EntityPredicate>,
180    /// Whether damage bypasses armor.
181    pub is_direct: Option<bool>,
182}
183
184/// A tag check for damage source.
185#[derive(Debug, Clone)]
186pub struct DamageTagPredicate {
187    pub id: Identifier,
188    pub expected: bool,
189}
190
191impl LootCondition {
192    /// Test if this condition passes given the loot context.
193    pub fn test<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> bool {
194        match self {
195            LootCondition::SurvivesExplosion => {
196                if let Some(radius) = ctx.explosion_radius {
197                    // Vanilla: 1/radius chance to survive
198                    ctx.rng.random::<f32>() <= (1.0 / radius)
199                } else {
200                    true // No explosion, always survives
201                }
202            }
203            LootCondition::BlockStateProperty { block, properties } => {
204                if let Some(state) = ctx.block_state {
205                    let state_block = state.get_block();
206                    // Check block matches
207                    if state_block.key != *block {
208                        return false;
209                    }
210                    // Check all properties match
211                    for prop in *properties {
212                        if let Some(value) = state.get_property_str(prop.name) {
213                            if value != prop.value {
214                                return false;
215                            }
216                        } else {
217                            return false; // Property doesn't exist
218                        }
219                    }
220                    true
221                } else {
222                    false // No block state in context
223                }
224            }
225            LootCondition::RandomChance(chance) => ctx.rng.random::<f32>() < *chance,
226            LootCondition::RandomChanceWithEnchantedBonus {
227                enchantment,
228                unenchanted_chance,
229                enchanted_chance,
230            } => {
231                let level = ctx.get_enchantment_level_by_id(enchantment);
232                let effective_chance = if level > 0 {
233                    match enchanted_chance {
234                        EnchantedChance::Constant(c) => *c,
235                        EnchantedChance::Linear {
236                            base,
237                            per_level_above_first,
238                        } => base + per_level_above_first * (level - 1) as f32,
239                    }
240                } else {
241                    *unenchanted_chance
242                };
243                ctx.rng.random::<f32>() < effective_chance
244            }
245            LootCondition::MatchTool(predicate) => {
246                if let Some(tool) = ctx.tool {
247                    predicate.test(tool, ctx)
248                } else {
249                    // No tool in context - only passes if predicate is Any
250                    matches!(predicate, ToolPredicate::Any)
251                }
252            }
253            LootCondition::TableBonus {
254                enchantment,
255                chances,
256            } => {
257                let level = ctx.get_enchantment_level_by_id(enchantment);
258                let index = (level as usize).min(chances.len().saturating_sub(1));
259                let chance = chances.get(index).copied().unwrap_or(0.0);
260                ctx.rng.random::<f32>() < chance
261            }
262            LootCondition::Inverted(inner) => !inner.test(ctx),
263            LootCondition::AnyOf(conditions) => conditions.iter().any(|c| c.test(ctx)),
264            LootCondition::AllOf(conditions) => conditions.iter().all(|c| c.test(ctx)),
265            LootCondition::KilledByPlayer => ctx.killed_by_player,
266            LootCondition::EntityProperties { entity, predicate } => {
267                let Some(entity) = ctx.get_entity(*entity) else {
268                    return false;
269                };
270                predicate.test(entity, ctx)
271            }
272            LootCondition::DamageSourceProperties { predicate } => predicate.test(ctx),
273            LootCondition::LocationCheck { .. } => {
274                // TODO: Implement when world position data is available in context
275                true
276            }
277            LootCondition::WeatherCheck {
278                raining,
279                thundering,
280            } => {
281                let weather = ctx.weather.unwrap_or_default();
282                raining.is_none_or(|r| r == weather.raining)
283                    && thundering.is_none_or(|t| t == weather.thundering)
284            }
285            LootCondition::TimeCheck { value, period } => {
286                let game_time = ctx.game_time.unwrap_or(0);
287                let time = if let Some(p) = period {
288                    game_time % p
289                } else {
290                    game_time
291                };
292                value.test(time as f32, ctx.rng)
293            }
294            LootCondition::ValueCheck { value, range } => {
295                let v = value.get_simple(ctx.rng);
296                range.test(v, ctx.rng)
297            }
298            LootCondition::EnchantmentActiveCheck {
299                enchantment,
300                active,
301            } => {
302                let level = ctx.get_enchantment_level_by_id(enchantment);
303                let is_active = level > 0;
304                is_active == *active
305            }
306            LootCondition::EntityScores { .. } => {
307                // TODO: Implement when scoreboard system is available
308                true
309            }
310            LootCondition::Reference(_name) => {
311                // TODO: Implement condition registry lookup
312                // For now, return true (permissive)
313                true
314            }
315        }
316    }
317}
318
319impl ToolPredicate {
320    /// Test if the tool matches this predicate.
321    #[must_use]
322    pub fn test<R: rand::Rng>(&self, tool: &ItemStack, _ctx: &LootContext<'_, R>) -> bool {
323        match self {
324            ToolPredicate::Item(item_id) => tool.item.key == *item_id,
325            ToolPredicate::HasEnchantment {
326                enchantment,
327                min_level,
328            } => tool_enchantment_matches(tool, enchantment, *min_level),
329            ToolPredicate::Tag(tag) => {
330                // Check if the tool's item is in the specified tag
331                REGISTRY.items.is_in_tag(tool.item, tag)
332            }
333            ToolPredicate::Any => true,
334        }
335    }
336}
337
338fn tool_enchantment_matches(
339    tool: &ItemStack,
340    enchantment_or_tag: &Identifier,
341    min_level: i32,
342) -> bool {
343    if tool.get_enchantment_level(enchantment_or_tag) >= min_level {
344        return true;
345    }
346
347    let Some(enchantments) = tool.get_enchantments() else {
348        return false;
349    };
350
351    for (key, level) in enchantments.iter() {
352        if *level < min_level as u32 {
353            continue;
354        }
355        let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
356            continue;
357        };
358        if REGISTRY
359            .enchantments
360            .is_in_tag(enchantment, enchantment_or_tag)
361        {
362            return true;
363        }
364    }
365
366    false
367}
368
369impl EntityPredicate {
370    fn test<R: rand::Rng>(&self, entity: EntityRef<'_>, ctx: &LootContext<'_, R>) -> bool {
371        if let Some(entity_type) = &self.entity_type
372            && entity.entity_type != Some(entity_type)
373        {
374            return false;
375        }
376
377        if let Some(flags) = &self.flags
378            && !flags.test(entity.flags)
379        {
380            return false;
381        }
382
383        if let Some(equipment) = &self.equipment
384            && !equipment.test(entity.equipment, ctx)
385        {
386            return false;
387        }
388
389        if let Some(expected_color) = &self.sheep_color
390            && entity.sheep_color != Some(*expected_color)
391        {
392            return false;
393        }
394
395        if let Some(expected_sheared) = &self.sheep_sheared
396            && entity.sheep_sheared != Some(*expected_sheared)
397        {
398            return false;
399        }
400
401        if let Some(expected_variant) = &self.chicken_variant
402            && entity.chicken_variant != Some(expected_variant)
403        {
404            return false;
405        }
406
407        true
408    }
409}
410
411impl EntityFlags {
412    fn test(&self, flags: EntityRefFlags) -> bool {
413        self.is_on_fire
414            .is_none_or(|expected| expected == flags.is_on_fire)
415            && self
416                .is_sneaking
417                .is_none_or(|expected| expected == flags.is_sneaking)
418            && self
419                .is_sprinting
420                .is_none_or(|expected| expected == flags.is_sprinting)
421            && self
422                .is_swimming
423                .is_none_or(|expected| expected == flags.is_swimming)
424            && self
425                .is_baby
426                .is_none_or(|expected| expected == flags.is_baby)
427    }
428}
429
430impl EntityEquipment {
431    fn test<R: rand::Rng>(
432        &self,
433        equipment: Option<&EntityEquipmentRef<'_>>,
434        ctx: &LootContext<'_, R>,
435    ) -> bool {
436        let has_predicate = self.mainhand.is_some()
437            || self.offhand.is_some()
438            || self.head.is_some()
439            || self.chest.is_some()
440            || self.legs.is_some()
441            || self.feet.is_some();
442        if !has_predicate {
443            return true;
444        }
445
446        let Some(equipment) = equipment else {
447            return false;
448        };
449
450        slot_predicate_matches(&self.mainhand, equipment.mainhand, ctx)
451            && slot_predicate_matches(&self.offhand, equipment.offhand, ctx)
452            && slot_predicate_matches(&self.head, equipment.head, ctx)
453            && slot_predicate_matches(&self.chest, equipment.chest, ctx)
454            && slot_predicate_matches(&self.legs, equipment.legs, ctx)
455            && slot_predicate_matches(&self.feet, equipment.feet, ctx)
456    }
457}
458
459fn slot_predicate_matches<R: rand::Rng>(
460    predicate: &Option<ToolPredicate>,
461    item_stack: Option<&ItemStack>,
462    ctx: &LootContext<'_, R>,
463) -> bool {
464    let Some(predicate) = predicate else {
465        return true;
466    };
467    let Some(item_stack) = item_stack else {
468        return false;
469    };
470    predicate.test(item_stack, ctx)
471}
472
473impl DamageSourcePredicate {
474    fn test<R: rand::Rng>(&self, ctx: &LootContext<'_, R>) -> bool {
475        let Some(damage_source) = ctx.damage_source else {
476            return false;
477        };
478
479        for tag in self.tags {
480            if damage_source_has_tag(damage_source, &tag.id) != tag.expected {
481                return false;
482            }
483        }
484
485        if let Some(expected) = self.is_direct
486            && damage_source.is_direct != expected
487        {
488            return false;
489        }
490
491        if let Some(predicate) = &self.source_entity {
492            let Some(entity) = ctx.killer_entity else {
493                return false;
494            };
495            if !predicate.test(entity, ctx) {
496                return false;
497            }
498        }
499
500        if let Some(predicate) = &self.direct_entity {
501            let Some(entity) = ctx.direct_killer_entity else {
502                return false;
503            };
504            if !predicate.test(entity, ctx) {
505                return false;
506            }
507        }
508
509        true
510    }
511}
512
513fn damage_source_has_tag(damage_source: DamageSourceInfo<'_>, tag: &Identifier) -> bool {
514    if let Some(damage_type) = damage_source.damage_type
515        && let Some(damage_type) = REGISTRY.damage_types.by_key(damage_type)
516    {
517        return REGISTRY.damage_types.is_in_tag(damage_type, tag);
518    }
519
520    damage_source.tags.iter().any(|candidate| candidate == tag)
521}