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