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