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#[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)]
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#[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 #[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 #[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#[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#[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 #[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}