Skip to main content

steel_registry/enchantment/
effect.rs

1use crate::attribute::{AttributeModifierOperation, AttributeRef};
2use crate::damage_type::DamageTypeRef;
3use crate::items::ItemRef;
4use crate::mob_effect::MobEffectRef;
5use crate::sound_event::SoundEventRef;
6use glam::DVec3;
7use steel_utils::Identifier;
8use steel_utils::random::Random;
9use steel_utils::types::GameType;
10
11/// Vanilla enchantment effect component keys.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum EnchantmentEffectComponent {
14    DamageProtection,
15    DamageImmunity,
16    Damage,
17    SmashDamagePerFallenBlock,
18    Knockback,
19    ArmorEffectiveness,
20    PostAttack,
21    PostPiercingAttack,
22    HitBlock,
23    ItemDamage,
24    EquipmentDrops,
25    LocationChanged,
26    Tick,
27    AmmoUse,
28    ProjectilePiercing,
29    ProjectileSpawned,
30    ProjectileSpread,
31    ProjectileCount,
32    TridentReturnAcceleration,
33    FishingTimeReduction,
34    FishingLuckBonus,
35    BlockExperience,
36    MobExperience,
37    RepairWithXp,
38    Attributes,
39    CrossbowChargeTime,
40    CrossbowChargingSounds,
41    TridentSound,
42    PreventEquipmentDrop,
43    PreventArmorChange,
44    TridentSpinAttackStrength,
45}
46
47impl EnchantmentEffectComponent {
48    #[must_use]
49    pub const fn name(self) -> &'static str {
50        match self {
51            Self::DamageProtection => "minecraft:damage_protection",
52            Self::DamageImmunity => "minecraft:damage_immunity",
53            Self::Damage => "minecraft:damage",
54            Self::SmashDamagePerFallenBlock => "minecraft:smash_damage_per_fallen_block",
55            Self::Knockback => "minecraft:knockback",
56            Self::ArmorEffectiveness => "minecraft:armor_effectiveness",
57            Self::PostAttack => "minecraft:post_attack",
58            Self::PostPiercingAttack => "minecraft:post_piercing_attack",
59            Self::HitBlock => "minecraft:hit_block",
60            Self::ItemDamage => "minecraft:item_damage",
61            Self::EquipmentDrops => "minecraft:equipment_drops",
62            Self::LocationChanged => "minecraft:location_changed",
63            Self::Tick => "minecraft:tick",
64            Self::AmmoUse => "minecraft:ammo_use",
65            Self::ProjectilePiercing => "minecraft:projectile_piercing",
66            Self::ProjectileSpawned => "minecraft:projectile_spawned",
67            Self::ProjectileSpread => "minecraft:projectile_spread",
68            Self::ProjectileCount => "minecraft:projectile_count",
69            Self::TridentReturnAcceleration => "minecraft:trident_return_acceleration",
70            Self::FishingTimeReduction => "minecraft:fishing_time_reduction",
71            Self::FishingLuckBonus => "minecraft:fishing_luck_bonus",
72            Self::BlockExperience => "minecraft:block_experience",
73            Self::MobExperience => "minecraft:mob_experience",
74            Self::RepairWithXp => "minecraft:repair_with_xp",
75            Self::Attributes => "minecraft:attributes",
76            Self::CrossbowChargeTime => "minecraft:crossbow_charge_time",
77            Self::CrossbowChargingSounds => "minecraft:crossbow_charging_sounds",
78            Self::TridentSound => "minecraft:trident_sound",
79            Self::PreventEquipmentDrop => "minecraft:prevent_equipment_drop",
80            Self::PreventArmorChange => "minecraft:prevent_armor_change",
81            Self::TridentSpinAttackStrength => "minecraft:trident_spin_attack_strength",
82        }
83    }
84}
85
86#[derive(Debug, PartialEq)]
87pub enum LevelBasedValue {
88    Constant(f32),
89    Clamped {
90        value: &'static LevelBasedValue,
91        min: f32,
92        max: f32,
93    },
94    Exponent {
95        base: &'static LevelBasedValue,
96        power: &'static LevelBasedValue,
97    },
98    Fraction {
99        numerator: &'static LevelBasedValue,
100        denominator: &'static LevelBasedValue,
101    },
102    LevelsSquared {
103        added: f32,
104    },
105    Linear {
106        base: f32,
107        per_level_above_first: f32,
108    },
109    Lookup {
110        values: &'static [f32],
111        fallback: &'static LevelBasedValue,
112    },
113}
114
115impl LevelBasedValue {
116    #[must_use]
117    pub fn calculate(&self, level: i32) -> f32 {
118        match self {
119            Self::Constant(value) => *value,
120            Self::Clamped { value, min, max } => value.calculate(level).clamp(*min, *max),
121            Self::Exponent { base, power } => base.calculate(level).powf(power.calculate(level)),
122            Self::Fraction {
123                numerator,
124                denominator,
125            } => {
126                let denominator = denominator.calculate(level);
127                if denominator == 0.0 {
128                    0.0
129                } else {
130                    numerator.calculate(level) / denominator
131                }
132            }
133            Self::LevelsSquared { added } => level.pow(2) as f32 + added,
134            Self::Linear {
135                base,
136                per_level_above_first,
137            } => base + per_level_above_first * (level - 1) as f32,
138            Self::Lookup { values, fallback } => {
139                if level <= 0 {
140                    return fallback.calculate(level);
141                }
142                let index = (level - 1) as usize;
143                values
144                    .get(index)
145                    .copied()
146                    .unwrap_or_else(|| fallback.calculate(level))
147            }
148        }
149    }
150}
151
152#[derive(Debug, PartialEq)]
153pub enum EnchantmentValueEffect {
154    Add { value: &'static LevelBasedValue },
155    Set { value: &'static LevelBasedValue },
156    Multiply { factor: &'static LevelBasedValue },
157    RemoveBinomial { chance: &'static LevelBasedValue },
158}
159
160impl EnchantmentValueEffect {
161    /// Applies effects that do not require a random source.
162    ///
163    /// `remove_binomial` is excluded because it requires a random source; use
164    /// [`Self::process_with_random`] when the component permits random effects.
165    #[must_use]
166    pub fn process_without_random(&self, level: i32, input: f32) -> Option<f32> {
167        match self {
168            Self::Add { value } => Some(input + value.calculate(level)),
169            Self::Set { value } => Some(value.calculate(level)),
170            Self::Multiply { factor } => Some(input * factor.calculate(level)),
171            Self::RemoveBinomial { .. } => None,
172        }
173    }
174
175    /// Applies this effect with the random source required by Vanilla's
176    /// `EnchantmentValueEffect.process`.
177    #[must_use]
178    pub fn process_with_random(&self, level: i32, random: &mut impl Random, input: f32) -> f32 {
179        match self {
180            Self::Add { value } => input + value.calculate(level),
181            Self::Set { value } => value.calculate(level),
182            Self::Multiply { factor } => input * factor.calculate(level),
183            Self::RemoveBinomial { chance } => {
184                remove_binomial(random, input, chance.calculate(level))
185            }
186        }
187    }
188}
189
190#[expect(
191    clippy::neg_cmp_op_on_partial_ord,
192    reason = "negated comparisons preserve Vanilla's NaN branch behavior"
193)]
194fn remove_binomial(random: &mut impl Random, input: f32, chance: f32) -> f32 {
195    let mut removed = 0;
196    if !(input <= 128.0) && !(input * chance < 20.0) && !(input * (1.0 - chance) < 20.0) {
197        let mean = f64::from((input * chance).floor());
198        let deviation = f64::from(input * chance * (1.0 - chance)).sqrt();
199        removed = (mean + random.next_gaussian() * deviation + 0.5).floor() as i32;
200        removed = removed.clamp(0, input as i32);
201    } else {
202        let mut trial = 0;
203        while (trial as f32) < input {
204            if random.next_f32() < chance {
205                removed += 1;
206            }
207            trial += 1;
208        }
209    }
210
211    input - removed as f32
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum EnchantmentEntityTarget {
216    This,
217    Attacker,
218    DirectAttacker,
219}
220
221#[derive(Debug, PartialEq, Eq)]
222pub enum EntityTypePredicate {
223    Any,
224    Type(Identifier),
225    Tag(Identifier),
226    Unsupported,
227}
228
229#[derive(Debug, PartialEq, Eq)]
230pub struct EntityPredicate {
231    pub entity_type: EntityTypePredicate,
232    pub vehicle: EntityVehiclePredicate,
233    pub flags: EntityFlagsPredicate,
234    pub type_specific: EntityTypeSpecificPredicate,
235    pub unsupported: bool,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum EntityVehiclePredicate {
240    Any,
241    Present,
242    Unsupported,
243}
244
245#[derive(Debug, PartialEq, Eq)]
246pub struct EntityFlagsPredicate {
247    pub is_fall_flying: Option<bool>,
248    pub is_in_water: Option<bool>,
249    pub unsupported: bool,
250}
251
252impl EntityFlagsPredicate {
253    #[must_use]
254    pub const fn any() -> Self {
255        Self {
256            is_fall_flying: None,
257            is_in_water: None,
258            unsupported: false,
259        }
260    }
261
262    #[must_use]
263    pub const fn has_constraints(&self) -> bool {
264        self.is_fall_flying.is_some() || self.is_in_water.is_some() || self.unsupported
265    }
266}
267
268#[derive(Debug, PartialEq, Eq)]
269pub enum EntityTypeSpecificPredicate {
270    Any,
271    Player(PlayerPredicate),
272    Unsupported,
273}
274
275#[derive(Debug, PartialEq, Eq)]
276pub struct PlayerPredicate {
277    pub game_modes: &'static [GameType],
278    pub food_level_min: Option<i32>,
279    pub unsupported: bool,
280}
281
282#[derive(Debug, PartialEq, Eq)]
283pub struct DamageSourceTagPredicate {
284    pub tag: Identifier,
285    pub expected: bool,
286}
287
288#[derive(Debug, PartialEq, Eq)]
289pub struct DamageSourcePredicate {
290    pub tags: &'static [DamageSourceTagPredicate],
291    pub is_direct: Option<bool>,
292}
293
294/// Static holder set used by generated `match_tool` requirements.
295#[derive(Debug, PartialEq, Eq)]
296pub enum EnchantmentItemSet {
297    Tag(Identifier),
298    Direct(&'static [Identifier]),
299}
300
301impl EnchantmentItemSet {
302    #[must_use]
303    pub fn contains(&self, item: ItemRef) -> bool {
304        match self {
305            Self::Tag(tag) => item.has_tag(tag),
306            Self::Direct(items) => items.contains(&item.key),
307        }
308    }
309}
310
311/// Vanilla loot condition subset used by generated enchantment effects.
312#[derive(Debug, PartialEq)]
313pub enum EnchantmentEffectRequirements {
314    AllOf(&'static [&'static EnchantmentEffectRequirements]),
315    AnyOf(&'static [&'static EnchantmentEffectRequirements]),
316    Inverted(&'static EnchantmentEffectRequirements),
317    EntityProperties {
318        entity: EnchantmentEntityTarget,
319        predicate: EntityPredicate,
320    },
321    DamageSourceProperties(DamageSourcePredicate),
322    RandomChance {
323        chance: &'static LevelBasedValue,
324    },
325    MatchTool {
326        items: Option<EnchantmentItemSet>,
327    },
328    Unsupported {
329        condition: Identifier,
330    },
331}
332
333impl EnchantmentEffectRequirements {
334    /// Evaluates the subset of loot conditions supported by Vanilla's item-only
335    /// enchantment context. `None` keeps unsupported contexts fail-closed.
336    #[must_use]
337    pub fn matches_item_context(&self, item: ItemRef) -> Option<bool> {
338        match self {
339            Self::AllOf(terms) => {
340                let mut has_unknown = false;
341                for term in *terms {
342                    match term.matches_item_context(item) {
343                        Some(true) => {}
344                        Some(false) => return Some(false),
345                        None => has_unknown = true,
346                    }
347                }
348                if has_unknown { None } else { Some(true) }
349            }
350            Self::AnyOf(terms) => {
351                let mut has_unknown = false;
352                for term in *terms {
353                    match term.matches_item_context(item) {
354                        Some(true) => return Some(true),
355                        Some(false) => {}
356                        None => has_unknown = true,
357                    }
358                }
359                if has_unknown { None } else { Some(false) }
360            }
361            Self::Inverted(term) => term.matches_item_context(item).map(|matched| !matched),
362            Self::MatchTool { items } => {
363                Some(items.as_ref().is_none_or(|items| items.contains(item)))
364            }
365            Self::EntityProperties { .. }
366            | Self::DamageSourceProperties(_)
367            | Self::RandomChance { .. }
368            | Self::Unsupported { .. } => None,
369        }
370    }
371}
372
373#[derive(Debug, PartialEq)]
374pub struct ConditionalEnchantmentEffect<T> {
375    pub effect: T,
376    pub requirements: Option<&'static EnchantmentEffectRequirements>,
377}
378
379impl<T> ConditionalEnchantmentEffect<T> {
380    #[must_use]
381    pub const fn is_unconditional(&self) -> bool {
382        self.requirements.is_none()
383    }
384}
385
386#[derive(Debug, PartialEq)]
387pub struct ConditionalDamageImmunityEffect {
388    pub requirements: Option<&'static EnchantmentEffectRequirements>,
389}
390
391impl ConditionalDamageImmunityEffect {
392    #[must_use]
393    pub const fn is_unconditional(&self) -> bool {
394        self.requirements.is_none()
395    }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum EnchantmentTarget {
400    Attacker,
401    DamagingEntity,
402    Victim,
403}
404
405#[derive(Debug)]
406pub enum MobEffectSelection {
407    Single(MobEffectRef),
408    UnsupportedTag(Identifier),
409}
410
411#[derive(Debug)]
412pub enum EnchantmentEntityEffect {
413    AllOf(&'static [&'static EnchantmentEntityEffect]),
414    ChangeItemDamage {
415        amount: &'static LevelBasedValue,
416    },
417    ApplyExhaustion {
418        amount: &'static LevelBasedValue,
419    },
420    ApplyImpulse {
421        direction: DVec3,
422        coordinate_scale: DVec3,
423        magnitude: &'static LevelBasedValue,
424    },
425    PlaySound {
426        sounds: &'static [SoundEventRef],
427        volume: f32,
428        pitch: f32,
429    },
430    DamageEntity {
431        min_damage: &'static LevelBasedValue,
432        max_damage: &'static LevelBasedValue,
433        damage_type: DamageTypeRef,
434    },
435    Ignite {
436        duration: &'static LevelBasedValue,
437    },
438    ApplyMobEffect {
439        to_apply: MobEffectSelection,
440        min_duration: &'static LevelBasedValue,
441        max_duration: &'static LevelBasedValue,
442        min_amplifier: &'static LevelBasedValue,
443        max_amplifier: &'static LevelBasedValue,
444    },
445    Unsupported {
446        effect_type: Identifier,
447    },
448}
449
450#[derive(Debug)]
451pub struct TargetedConditionalEnchantmentEffect<T> {
452    pub effect: T,
453    pub enchanted: EnchantmentTarget,
454    pub affected: EnchantmentTarget,
455    pub requirements: Option<&'static EnchantmentEffectRequirements>,
456}
457
458#[derive(Debug)]
459pub struct EnchantmentAttributeEffect {
460    pub amount: &'static LevelBasedValue,
461    pub attribute: AttributeRef,
462    pub id: Identifier,
463    pub operation: AttributeModifierOperation,
464}
465
466#[derive(Debug)]
467pub struct CrossbowChargingSounds {
468    pub start: Option<SoundEventRef>,
469    pub mid: Option<SoundEventRef>,
470    pub end: Option<SoundEventRef>,
471}
472
473#[derive(Debug)]
474pub struct EnchantmentEffects {
475    pub damage_protection: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
476    pub damage_immunity: &'static [ConditionalDamageImmunityEffect],
477    pub damage: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
478    pub smash_damage_per_fallen_block:
479        &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
480    pub knockback: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
481    pub armor_effectiveness: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
482    pub post_attack: &'static [TargetedConditionalEnchantmentEffect<EnchantmentEntityEffect>],
483    pub post_piercing_attack: &'static [ConditionalEnchantmentEffect<EnchantmentEntityEffect>],
484    pub hit_block: bool,
485    pub item_damage: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
486    pub equipment_drops: &'static [TargetedConditionalEnchantmentEffect<EnchantmentValueEffect>],
487    pub location_changed: bool,
488    pub tick: bool,
489    pub ammo_use: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
490    pub projectile_piercing: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
491    pub projectile_spawned: &'static [ConditionalEnchantmentEffect<EnchantmentEntityEffect>],
492    pub projectile_spread: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
493    pub projectile_count: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
494    pub trident_return_acceleration:
495        &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
496    pub fishing_time_reduction: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
497    pub fishing_luck_bonus: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
498    pub block_experience: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
499    pub mob_experience: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
500    pub repair_with_xp: &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>],
501    pub attributes: &'static [EnchantmentAttributeEffect],
502    pub crossbow_charge_time: Option<EnchantmentValueEffect>,
503    pub crossbow_charging_sounds: &'static [CrossbowChargingSounds],
504    pub trident_sound: &'static [SoundEventRef],
505    pub prevent_equipment_drop: bool,
506    pub prevent_armor_change: bool,
507    pub trident_spin_attack_strength: Option<EnchantmentValueEffect>,
508}
509
510impl EnchantmentEffects {
511    pub const EMPTY: Self = Self {
512        damage_protection: &[],
513        damage_immunity: &[],
514        damage: &[],
515        smash_damage_per_fallen_block: &[],
516        knockback: &[],
517        armor_effectiveness: &[],
518        post_attack: &[],
519        post_piercing_attack: &[],
520        hit_block: false,
521        item_damage: &[],
522        equipment_drops: &[],
523        location_changed: false,
524        tick: false,
525        ammo_use: &[],
526        projectile_piercing: &[],
527        projectile_spawned: &[],
528        projectile_spread: &[],
529        projectile_count: &[],
530        trident_return_acceleration: &[],
531        fishing_time_reduction: &[],
532        fishing_luck_bonus: &[],
533        block_experience: &[],
534        mob_experience: &[],
535        repair_with_xp: &[],
536        attributes: &[],
537        crossbow_charge_time: None,
538        crossbow_charging_sounds: &[],
539        trident_sound: &[],
540        prevent_equipment_drop: false,
541        prevent_armor_change: false,
542        trident_spin_attack_strength: None,
543    };
544
545    #[must_use]
546    pub const fn has(&self, component: EnchantmentEffectComponent) -> bool {
547        match component {
548            EnchantmentEffectComponent::DamageProtection => !self.damage_protection.is_empty(),
549            EnchantmentEffectComponent::DamageImmunity => !self.damage_immunity.is_empty(),
550            EnchantmentEffectComponent::Damage => !self.damage.is_empty(),
551            EnchantmentEffectComponent::SmashDamagePerFallenBlock => {
552                !self.smash_damage_per_fallen_block.is_empty()
553            }
554            EnchantmentEffectComponent::Knockback => !self.knockback.is_empty(),
555            EnchantmentEffectComponent::ArmorEffectiveness => !self.armor_effectiveness.is_empty(),
556            EnchantmentEffectComponent::PostAttack => !self.post_attack.is_empty(),
557            EnchantmentEffectComponent::PostPiercingAttack => !self.post_piercing_attack.is_empty(),
558            EnchantmentEffectComponent::HitBlock => self.hit_block,
559            EnchantmentEffectComponent::ItemDamage => !self.item_damage.is_empty(),
560            EnchantmentEffectComponent::EquipmentDrops => !self.equipment_drops.is_empty(),
561            EnchantmentEffectComponent::LocationChanged => self.location_changed,
562            EnchantmentEffectComponent::Tick => self.tick,
563            EnchantmentEffectComponent::AmmoUse => !self.ammo_use.is_empty(),
564            EnchantmentEffectComponent::ProjectilePiercing => !self.projectile_piercing.is_empty(),
565            EnchantmentEffectComponent::ProjectileSpawned => !self.projectile_spawned.is_empty(),
566            EnchantmentEffectComponent::ProjectileSpread => !self.projectile_spread.is_empty(),
567            EnchantmentEffectComponent::ProjectileCount => !self.projectile_count.is_empty(),
568            EnchantmentEffectComponent::TridentReturnAcceleration => {
569                !self.trident_return_acceleration.is_empty()
570            }
571            EnchantmentEffectComponent::FishingTimeReduction => {
572                !self.fishing_time_reduction.is_empty()
573            }
574            EnchantmentEffectComponent::FishingLuckBonus => !self.fishing_luck_bonus.is_empty(),
575            EnchantmentEffectComponent::BlockExperience => !self.block_experience.is_empty(),
576            EnchantmentEffectComponent::MobExperience => !self.mob_experience.is_empty(),
577            EnchantmentEffectComponent::RepairWithXp => !self.repair_with_xp.is_empty(),
578            EnchantmentEffectComponent::Attributes => !self.attributes.is_empty(),
579            EnchantmentEffectComponent::CrossbowChargeTime => self.crossbow_charge_time.is_some(),
580            EnchantmentEffectComponent::CrossbowChargingSounds => {
581                !self.crossbow_charging_sounds.is_empty()
582            }
583            EnchantmentEffectComponent::TridentSound => !self.trident_sound.is_empty(),
584            EnchantmentEffectComponent::PreventEquipmentDrop => self.prevent_equipment_drop,
585            EnchantmentEffectComponent::PreventArmorChange => self.prevent_armor_change,
586            EnchantmentEffectComponent::TridentSpinAttackStrength => {
587                self.trident_spin_attack_strength.is_some()
588            }
589        }
590    }
591
592    #[must_use]
593    pub const fn value_effects(
594        &self,
595        component: EnchantmentEffectComponent,
596    ) -> &'static [ConditionalEnchantmentEffect<EnchantmentValueEffect>] {
597        match component {
598            EnchantmentEffectComponent::DamageProtection => self.damage_protection,
599            EnchantmentEffectComponent::Damage => self.damage,
600            EnchantmentEffectComponent::SmashDamagePerFallenBlock => {
601                self.smash_damage_per_fallen_block
602            }
603            EnchantmentEffectComponent::Knockback => self.knockback,
604            EnchantmentEffectComponent::ArmorEffectiveness => self.armor_effectiveness,
605            EnchantmentEffectComponent::ItemDamage => self.item_damage,
606            EnchantmentEffectComponent::AmmoUse => self.ammo_use,
607            EnchantmentEffectComponent::ProjectilePiercing => self.projectile_piercing,
608            EnchantmentEffectComponent::ProjectileSpread => self.projectile_spread,
609            EnchantmentEffectComponent::ProjectileCount => self.projectile_count,
610            EnchantmentEffectComponent::TridentReturnAcceleration => {
611                self.trident_return_acceleration
612            }
613            EnchantmentEffectComponent::FishingTimeReduction => self.fishing_time_reduction,
614            EnchantmentEffectComponent::FishingLuckBonus => self.fishing_luck_bonus,
615            EnchantmentEffectComponent::BlockExperience => self.block_experience,
616            EnchantmentEffectComponent::MobExperience => self.mob_experience,
617            EnchantmentEffectComponent::RepairWithXp => self.repair_with_xp,
618            _ => &[],
619        }
620    }
621
622    #[must_use]
623    pub const fn targeted_value_effects(
624        &self,
625        component: EnchantmentEffectComponent,
626    ) -> &'static [TargetedConditionalEnchantmentEffect<EnchantmentValueEffect>] {
627        match component {
628            EnchantmentEffectComponent::EquipmentDrops => self.equipment_drops,
629            _ => &[],
630        }
631    }
632
633    #[must_use]
634    pub const fn single_value_effect(
635        &self,
636        component: EnchantmentEffectComponent,
637    ) -> Option<&EnchantmentValueEffect> {
638        match component {
639            EnchantmentEffectComponent::CrossbowChargeTime => self.crossbow_charge_time.as_ref(),
640            EnchantmentEffectComponent::TridentSpinAttackStrength => {
641                self.trident_spin_attack_strength.as_ref()
642            }
643            _ => None,
644        }
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use steel_utils::random::{Random as _, xoroshiro::Xoroshiro};
651
652    use super::{EnchantmentValueEffect, LevelBasedValue};
653
654    static HALF: LevelBasedValue = LevelBasedValue::Constant(0.5);
655    static THREE_TENTHS: LevelBasedValue = LevelBasedValue::Constant(0.3);
656    static REMOVE_HALF: EnchantmentValueEffect =
657        EnchantmentValueEffect::RemoveBinomial { chance: &HALF };
658    static REMOVE_THREE_TENTHS: EnchantmentValueEffect = EnchantmentValueEffect::RemoveBinomial {
659        chance: &THREE_TENTHS,
660    };
661
662    #[test]
663    fn remove_binomial_matches_vanilla_small_input_loop() {
664        let mut expected_random = Xoroshiro::from_seed_unmixed(42);
665        let removed = (0..10).filter(|_| expected_random.next_f32() < 0.5).count() as f32;
666
667        let mut actual_random = Xoroshiro::from_seed_unmixed(42);
668        assert_eq!(
669            REMOVE_HALF.process_with_random(1, &mut actual_random, 10.0),
670            10.0 - removed
671        );
672    }
673
674    #[test]
675    fn remove_binomial_matches_vanilla_gaussian_approximation() {
676        let mut expected_random = Xoroshiro::from_seed_unmixed(42);
677        let input = 256.0_f32;
678        let chance = 0.3_f32;
679        let mean = f64::from((input * chance).floor());
680        let deviation = f64::from(input * chance * (1.0 - chance)).sqrt();
681        let expected_removed = (mean + expected_random.next_gaussian() * deviation + 0.5)
682            .floor()
683            .clamp(0.0, f64::from(input)) as f32;
684
685        let mut actual_random = Xoroshiro::from_seed_unmixed(42);
686        assert_eq!(
687            REMOVE_THREE_TENTHS.process_with_random(1, &mut actual_random, input),
688            input - expected_removed
689        );
690    }
691}