Skip to main content

steel_core/
enchantment_helper.rs

1use steel_registry::enchantment_effect::{
2    DamageSourcePredicate, EnchantmentEffectComponent, EnchantmentEffectRequirements,
3    EnchantmentEntityEffect, EnchantmentEntityTarget, EnchantmentTarget, EntityPredicate,
4    EntityTypePredicate, EntityTypeSpecificPredicate, EntityVehiclePredicate, MobEffectSelection,
5};
6use steel_registry::entity_type::EntityTypeRef;
7use steel_registry::item_stack::ItemStack;
8use steel_registry::{REGISTRY, RegistryExt, TaggedRegistryExt, vanilla_entities};
9
10use crate::entity::damage::DamageSource;
11use crate::entity::{Entity, LivingEntity, MobEffectInstance};
12use crate::inventory::equipment::EquipmentSlot;
13use crate::world::World;
14
15#[derive(Debug, Clone, Copy)]
16pub(crate) struct EnchantmentDamageContext<'a> {
17    this_entity_type: EntityTypeRef,
18    attacker_entity_type: Option<EntityTypeRef>,
19    direct_attacker_entity_type: Option<EntityTypeRef>,
20    damage_source: &'a DamageSource,
21}
22
23impl<'a> EnchantmentDamageContext<'a> {
24    #[must_use]
25    pub(crate) const fn new(
26        this_entity_type: EntityTypeRef,
27        attacker_entity_type: Option<EntityTypeRef>,
28        direct_attacker_entity_type: Option<EntityTypeRef>,
29        damage_source: &'a DamageSource,
30    ) -> Self {
31        Self {
32            this_entity_type,
33            attacker_entity_type,
34            direct_attacker_entity_type,
35            damage_source,
36        }
37    }
38
39    #[must_use]
40    pub(crate) fn from_damage_source(
41        world: &World,
42        this_entity_type: EntityTypeRef,
43        damage_source: &'a DamageSource,
44    ) -> Self {
45        let attacker_entity_type = damage_source
46            .causing_entity_id
47            .and_then(|entity_id| world.get_entity_by_id(entity_id))
48            .map(|entity| entity.entity_type());
49        let direct_attacker_entity_type = damage_source
50            .direct_entity_id
51            .and_then(|entity_id| world.get_entity_by_id(entity_id))
52            .map(|entity| entity.entity_type());
53        Self::new(
54            this_entity_type,
55            attacker_entity_type,
56            direct_attacker_entity_type,
57            damage_source,
58        )
59    }
60
61    const fn entity_type(self, target: EnchantmentEntityTarget) -> Option<EntityTypeRef> {
62        match target {
63            EnchantmentEntityTarget::This => Some(self.this_entity_type),
64            EnchantmentEntityTarget::Attacker => self.attacker_entity_type,
65            EnchantmentEntityTarget::DirectAttacker => self.direct_attacker_entity_type,
66        }
67    }
68}
69
70pub(crate) struct EnchantmentPostAttackContext<'a> {
71    victim: &'a dyn Entity,
72    attacker: Option<&'a dyn Entity>,
73    direct_attacker: Option<&'a dyn Entity>,
74    damage_source: &'a DamageSource,
75}
76
77impl<'a> EnchantmentPostAttackContext<'a> {
78    #[must_use]
79    pub(crate) const fn new(
80        victim: &'a dyn Entity,
81        attacker: Option<&'a dyn Entity>,
82        direct_attacker: Option<&'a dyn Entity>,
83        damage_source: &'a DamageSource,
84    ) -> Self {
85        Self {
86            victim,
87            attacker,
88            direct_attacker,
89            damage_source,
90        }
91    }
92
93    fn damage_context(&self) -> EnchantmentDamageContext<'a> {
94        EnchantmentDamageContext::new(
95            self.victim.entity_type(),
96            self.attacker.map(Entity::entity_type),
97            self.direct_attacker.map(Entity::entity_type),
98            self.damage_source,
99        )
100    }
101
102    fn affected_entity(&self, target: EnchantmentTarget) -> Option<&'a dyn Entity> {
103        match target {
104            EnchantmentTarget::Attacker => self.attacker,
105            EnchantmentTarget::DamagingEntity => self.direct_attacker,
106            EnchantmentTarget::Victim => Some(self.victim),
107        }
108    }
109}
110
111pub(crate) fn modify_damage(
112    item: &ItemStack,
113    context: &EnchantmentDamageContext<'_>,
114    damage: f32,
115) -> f32 {
116    apply_value_effects(item, EnchantmentEffectComponent::Damage, context, damage)
117}
118
119pub(crate) fn modify_knockback(
120    item: &ItemStack,
121    context: &EnchantmentDamageContext<'_>,
122    knockback: f32,
123) -> f32 {
124    apply_value_effects(
125        item,
126        EnchantmentEffectComponent::Knockback,
127        context,
128        knockback,
129    )
130}
131
132pub(crate) fn modify_armor_effectiveness(
133    item: &ItemStack,
134    context: &EnchantmentDamageContext<'_>,
135    armor_fraction: f32,
136) -> f32 {
137    apply_value_effects(
138        item,
139        EnchantmentEffectComponent::ArmorEffectiveness,
140        context,
141        armor_fraction,
142    )
143}
144
145pub(crate) fn modify_smash_damage_per_fallen_block(
146    item: &ItemStack,
147    context: &EnchantmentDamageContext<'_>,
148    damage: f32,
149) -> f32 {
150    apply_value_effects(
151        item,
152        EnchantmentEffectComponent::SmashDamagePerFallenBlock,
153        context,
154        damage,
155    )
156}
157
158pub(crate) fn is_immune_to_damage<V: LivingEntity + ?Sized>(
159    world: &World,
160    victim: &V,
161    damage_source: &DamageSource,
162) -> bool {
163    let context =
164        EnchantmentDamageContext::from_damage_source(world, victim.entity_type(), damage_source);
165
166    for slot in EquipmentSlot::ALL {
167        let mut slot_matches = false;
168        victim.with_equipment_slot(slot, &mut |item| {
169            slot_matches = item_damage_immunity_matches(item, slot, &context);
170        });
171        if slot_matches {
172            return true;
173        }
174    }
175
176    false
177}
178
179pub(crate) fn get_damage_protection<V: LivingEntity + ?Sized>(
180    world: &World,
181    victim: &V,
182    damage_source: &DamageSource,
183) -> f32 {
184    let context =
185        EnchantmentDamageContext::from_damage_source(world, victim.entity_type(), damage_source);
186    let mut protection = 0.0;
187
188    for slot in EquipmentSlot::ALL {
189        victim.with_equipment_slot(slot, &mut |item| {
190            protection = apply_value_effects_for_slot(
191                item,
192                Some(slot),
193                EnchantmentEffectComponent::DamageProtection,
194                &context,
195                protection,
196            );
197        });
198    }
199
200    protection
201}
202
203pub(crate) fn do_post_attack_effects_from_item(
204    world: &World,
205    item: &ItemStack,
206    context: &EnchantmentPostAttackContext<'_>,
207) {
208    let mut item = item.copy_with_count(item.count());
209    let _ = apply_post_attack_effects(
210        world,
211        &mut item,
212        Some(EquipmentSlot::MainHand),
213        EnchantmentTarget::Attacker,
214        context,
215    );
216}
217
218pub(crate) fn do_post_attack_effects_with_item_source(
219    world: &World,
220    victim: &dyn Entity,
221    source: &ItemStack,
222    context: &EnchantmentPostAttackContext<'_>,
223) {
224    if let Some(living_victim) = victim.as_living_entity() {
225        for slot in EquipmentSlot::ALL {
226            let mut item_broke = false;
227            living_victim.with_equipment_slot_mut(slot, &mut |item| {
228                item_broke = apply_post_attack_effects(
229                    world,
230                    item,
231                    Some(slot),
232                    EnchantmentTarget::Victim,
233                    context,
234                );
235            });
236            if item_broke {
237                living_victim.on_equipped_item_broken(slot);
238            }
239        }
240    }
241
242    let mut source = source.copy_with_count(source.count());
243    let _ = apply_post_attack_effects(
244        world,
245        &mut source,
246        Some(EquipmentSlot::MainHand),
247        EnchantmentTarget::Attacker,
248        context,
249    );
250}
251
252fn apply_value_effects(
253    item: &ItemStack,
254    component: EnchantmentEffectComponent,
255    context: &EnchantmentDamageContext<'_>,
256    input: f32,
257) -> f32 {
258    apply_value_effects_for_slot(item, None, component, context, input)
259}
260
261fn apply_value_effects_for_slot(
262    item: &ItemStack,
263    slot: Option<EquipmentSlot>,
264    component: EnchantmentEffectComponent,
265    context: &EnchantmentDamageContext<'_>,
266    input: f32,
267) -> f32 {
268    if slot.is_some() && item.is_empty() {
269        return input;
270    }
271    let Some(enchantments) = item.get_enchantments() else {
272        return input;
273    };
274
275    let mut value = input;
276    for (key, level) in enchantments.iter() {
277        if *level == 0 {
278            continue;
279        }
280        let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
281            continue;
282        };
283        if slot.is_some_and(|slot| !enchantment.matching_slot(slot)) {
284            continue;
285        }
286        let level = *level as i32;
287
288        for effect in enchantment.effects.value_effects(component) {
289            if !requirements_match(effect.requirements, context) {
290                continue;
291            }
292            if let Some(updated) = effect.effect.process_without_random(level, value) {
293                value = updated;
294            }
295        }
296
297        let Some(effect) = enchantment.effects.single_value_effect(component) else {
298            continue;
299        };
300        if let Some(updated) = effect.process_without_random(level, value) {
301            value = updated;
302        }
303    }
304
305    value
306}
307
308fn item_damage_immunity_matches(
309    item: &ItemStack,
310    slot: EquipmentSlot,
311    context: &EnchantmentDamageContext<'_>,
312) -> bool {
313    if item.is_empty() {
314        return false;
315    }
316    let Some(enchantments) = item.get_enchantments() else {
317        return false;
318    };
319
320    for (key, level) in enchantments.iter() {
321        if *level == 0 {
322            continue;
323        }
324        let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
325            continue;
326        };
327        if !enchantment.matching_slot(slot) {
328            continue;
329        }
330        if enchantment
331            .effects
332            .damage_immunity
333            .iter()
334            .any(|effect| requirements_match(effect.requirements, context))
335        {
336            return true;
337        }
338    }
339
340    false
341}
342
343fn apply_post_attack_effects(
344    world: &World,
345    item: &mut ItemStack,
346    slot: Option<EquipmentSlot>,
347    enchanted_target: EnchantmentTarget,
348    context: &EnchantmentPostAttackContext<'_>,
349) -> bool {
350    if slot.is_some() && item.is_empty() {
351        return false;
352    }
353    let Some(enchantments) = item.get_enchantments().cloned() else {
354        return false;
355    };
356    let damage_context = context.damage_context();
357    let mut enchanted_item_broke = false;
358
359    for (key, level) in enchantments.iter() {
360        if *level == 0 {
361            continue;
362        }
363        let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
364            continue;
365        };
366        if slot.is_some_and(|slot| !enchantment.matching_slot(slot)) {
367            continue;
368        }
369        let level = *level as i32;
370
371        for effect in enchantment.effects.post_attack {
372            if effect.enchanted != enchanted_target {
373                continue;
374            }
375            let Some(enchanted_entity) = context.affected_entity(enchanted_target) else {
376                continue;
377            };
378            let requirements_match =
379                post_attack_requirements_match(effect.requirements, &damage_context, level);
380            if !requirements_match {
381                continue;
382            }
383            let Some(affected_entity) = context.affected_entity(effect.affected) else {
384                continue;
385            };
386            enchanted_item_broke |= apply_entity_effect(
387                world,
388                &effect.effect,
389                level,
390                affected_entity,
391                Some(enchanted_entity),
392                item,
393            );
394        }
395    }
396
397    enchanted_item_broke
398}
399
400/// Mirrors vanilla `EnchantmentHelper.onProjectileSpawned` after the projectile
401/// has been handed to the world.
402pub(crate) fn on_projectile_spawned(
403    world: &World,
404    weapon: &mut ItemStack,
405    projectile: &dyn Entity,
406    owner: Option<&dyn Entity>,
407) {
408    let Some(enchantments) = weapon.get_enchantments() else {
409        return;
410    };
411    let enchantments = enchantments
412        .iter()
413        .filter_map(|(key, level)| {
414            if *level == 0 {
415                return None;
416            }
417            REGISTRY
418                .enchantments
419                .by_key(key)
420                .map(|enchantment| (enchantment, *level))
421        })
422        .collect::<Vec<_>>();
423
424    for (enchantment, level) in enchantments {
425        let level = level as i32;
426        for effect in enchantment.effects.projectile_spawned {
427            if entity_requirements_match(effect.requirements, projectile, level) {
428                let _ =
429                    apply_entity_effect(world, &effect.effect, level, projectile, owner, weapon);
430            }
431        }
432    }
433}
434
435pub(crate) fn do_post_piercing_attack_effects(world: &World, user: &dyn LivingEntity) {
436    let mut item_stack = ItemStack::empty();
437    user.with_equipment_slot(EquipmentSlot::MainHand, &mut |stack| {
438        item_stack = stack.copy_with_count(stack.count());
439    });
440    let enchantments = item_stack.get_enchantments().cloned();
441    let Some(enchantments) = enchantments else {
442        return;
443    };
444
445    for (key, level) in enchantments.iter() {
446        if *level == 0 {
447            continue;
448        }
449        let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
450            continue;
451        };
452        if !enchantment.matching_slot(EquipmentSlot::MainHand) {
453            continue;
454        }
455
456        let level = *level as i32;
457        for effect in enchantment.effects.post_piercing_attack {
458            if !entity_requirements_match(effect.requirements, user, level) {
459                continue;
460            }
461            if apply_post_piercing_entity_effect(world, &effect.effect, level, user) {
462                user.on_equipped_item_broken(EquipmentSlot::MainHand);
463            }
464        }
465    }
466}
467
468fn apply_entity_effect(
469    world: &World,
470    effect: &EnchantmentEntityEffect,
471    level: i32,
472    entity: &dyn Entity,
473    enchanted_entity: Option<&dyn Entity>,
474    enchanted_item: &mut ItemStack,
475) -> bool {
476    if !entity_effect_is_supported(effect) {
477        return false;
478    }
479
480    apply_supported_entity_effect(
481        world,
482        effect,
483        level,
484        entity,
485        enchanted_entity,
486        enchanted_item,
487    )
488}
489
490fn entity_effect_is_supported(effect: &EnchantmentEntityEffect) -> bool {
491    match effect {
492        EnchantmentEntityEffect::AllOf(effects) => effects
493            .iter()
494            .all(|effect| entity_effect_is_supported(effect)),
495        EnchantmentEntityEffect::ApplyExhaustion { .. }
496        | EnchantmentEntityEffect::ApplyImpulse { .. }
497        | EnchantmentEntityEffect::PlaySound { .. }
498        | EnchantmentEntityEffect::Unsupported { .. } => false,
499        EnchantmentEntityEffect::ChangeItemDamage { .. }
500        | EnchantmentEntityEffect::DamageEntity { .. }
501        | EnchantmentEntityEffect::Ignite { .. } => true,
502        EnchantmentEntityEffect::ApplyMobEffect { to_apply, .. } => {
503            matches!(to_apply, MobEffectSelection::Single(_))
504        }
505    }
506}
507
508fn apply_supported_entity_effect(
509    world: &World,
510    effect: &EnchantmentEntityEffect,
511    level: i32,
512    entity: &dyn Entity,
513    enchanted_entity: Option<&dyn Entity>,
514    enchanted_item: &mut ItemStack,
515) -> bool {
516    match effect {
517        EnchantmentEntityEffect::AllOf(effects) => {
518            let mut enchanted_item_broke = false;
519            for effect in *effects {
520                enchanted_item_broke |= apply_supported_entity_effect(
521                    world,
522                    effect,
523                    level,
524                    entity,
525                    enchanted_entity,
526                    enchanted_item,
527                );
528            }
529            enchanted_item_broke
530        }
531        EnchantmentEntityEffect::ChangeItemDamage { amount } => {
532            let amount = amount.calculate(level) as i32;
533            let has_infinite_materials = enchanted_entity
534                .and_then(Entity::as_living_entity)
535                .is_some_and(LivingEntity::has_infinite_materials);
536            enchanted_item.hurt_and_break(amount, has_infinite_materials)
537        }
538        EnchantmentEntityEffect::DamageEntity {
539            min_damage,
540            max_damage,
541            damage_type,
542        } => {
543            let min_damage = min_damage.calculate(level);
544            let max_damage = max_damage.calculate(level);
545            let damage = random_between(min_damage, max_damage);
546            let mut source = DamageSource::environment(damage_type);
547            if let Some(enchanted_entity) = enchanted_entity {
548                source = source
549                    .with_causing_entity(enchanted_entity.id())
550                    .with_direct_entity(enchanted_entity.id());
551            }
552            entity.hurt(world, &source, damage);
553            false
554        }
555        EnchantmentEntityEffect::Ignite { duration } => {
556            let ticks = (duration.calculate(level) * 20.0).floor() as i32;
557            entity.ignite_for_ticks(ticks);
558            false
559        }
560        EnchantmentEntityEffect::ApplyMobEffect {
561            to_apply: MobEffectSelection::Single(effect),
562            min_duration,
563            max_duration,
564            min_amplifier,
565            max_amplifier,
566        } => {
567            let Some(living) = entity.as_living_entity() else {
568                return false;
569            };
570            let min_duration = min_duration.calculate(level);
571            let max_duration = max_duration.calculate(level);
572            let min_amplifier = min_amplifier.calculate(level);
573            let max_amplifier = max_amplifier.calculate(level);
574            let duration_seconds = random_between(min_duration, max_duration);
575            let amplifier = random_between(min_amplifier, max_amplifier);
576            let duration_ticks = java_round(duration_seconds * 20.0);
577            let amplifier = java_round(amplifier).max(0);
578            living.add_mob_effect(MobEffectInstance::with_duration(
579                effect,
580                duration_ticks,
581                amplifier,
582            ));
583            false
584        }
585        EnchantmentEntityEffect::ApplyMobEffect { .. }
586        | EnchantmentEntityEffect::ApplyExhaustion { .. }
587        | EnchantmentEntityEffect::ApplyImpulse { .. }
588        | EnchantmentEntityEffect::PlaySound { .. }
589        | EnchantmentEntityEffect::Unsupported { .. } => false,
590    }
591}
592
593fn apply_post_piercing_entity_effect(
594    world: &World,
595    effect: &EnchantmentEntityEffect,
596    level: i32,
597    user: &dyn LivingEntity,
598) -> bool {
599    if !post_piercing_entity_effect_is_supported(effect) {
600        return false;
601    }
602
603    apply_supported_post_piercing_entity_effect(world, effect, level, user)
604}
605
606fn post_piercing_entity_effect_is_supported(effect: &EnchantmentEntityEffect) -> bool {
607    match effect {
608        EnchantmentEntityEffect::AllOf(effects) => effects
609            .iter()
610            .all(|effect| post_piercing_entity_effect_is_supported(effect)),
611        EnchantmentEntityEffect::ChangeItemDamage { .. }
612        | EnchantmentEntityEffect::ApplyExhaustion { .. }
613        | EnchantmentEntityEffect::Ignite { .. } => true,
614        EnchantmentEntityEffect::PlaySound { sounds, .. } => !sounds.is_empty(),
615        EnchantmentEntityEffect::ApplyImpulse { direction, .. } => {
616            direction.x == 0.0 && direction.y == 0.0
617        }
618        EnchantmentEntityEffect::ApplyMobEffect { to_apply, .. } => {
619            matches!(to_apply, MobEffectSelection::Single(_))
620        }
621        EnchantmentEntityEffect::DamageEntity { .. }
622        | EnchantmentEntityEffect::Unsupported { .. } => false,
623    }
624}
625
626fn apply_supported_post_piercing_entity_effect(
627    world: &World,
628    effect: &EnchantmentEntityEffect,
629    level: i32,
630    user: &dyn LivingEntity,
631) -> bool {
632    match effect {
633        EnchantmentEntityEffect::AllOf(effects) => {
634            let mut enchanted_item_broke = false;
635            for effect in *effects {
636                enchanted_item_broke |=
637                    apply_supported_post_piercing_entity_effect(world, effect, level, user);
638            }
639            enchanted_item_broke
640        }
641        EnchantmentEntityEffect::ChangeItemDamage { amount } => {
642            let amount = amount.calculate(level) as i32;
643            let has_infinite_materials = user.has_infinite_materials();
644            let mut item_broke = false;
645            user.with_equipment_slot_mut(EquipmentSlot::MainHand, &mut |stack| {
646                item_broke = stack.hurt_and_break(amount, has_infinite_materials);
647            });
648            item_broke
649        }
650        EnchantmentEntityEffect::ApplyExhaustion { amount } => {
651            if let Some(player) = user.as_player() {
652                player.cause_food_exhaustion(amount.calculate(level));
653            }
654            false
655        }
656        EnchantmentEntityEffect::ApplyImpulse {
657            direction,
658            coordinate_scale,
659            magnitude,
660        } => {
661            let impulse = (user.look_angle() * direction.z)
662                * *coordinate_scale
663                * f64::from(magnitude.calculate(level));
664            user.push_impulse(impulse);
665            user.apply_post_impulse_grace_time(10);
666            false
667        }
668        EnchantmentEntityEffect::PlaySound {
669            sounds,
670            volume,
671            pitch,
672        } => {
673            let index = (level - 1).clamp(0, sounds.len() as i32 - 1) as usize;
674            user.play_sound(sounds[index], *volume, *pitch);
675            false
676        }
677        EnchantmentEntityEffect::Ignite { .. } | EnchantmentEntityEffect::ApplyMobEffect { .. } => {
678            let mut ignored_item = ItemStack::empty();
679            apply_supported_entity_effect(world, effect, level, user, Some(user), &mut ignored_item)
680        }
681        EnchantmentEntityEffect::DamageEntity { .. }
682        | EnchantmentEntityEffect::Unsupported { .. } => false,
683    }
684}
685
686fn random_between(min: f32, max: f32) -> f32 {
687    min + rand::random::<f32>() * (max - min)
688}
689
690fn java_round(value: f32) -> i32 {
691    (value + 0.5).floor() as i32
692}
693
694fn requirements_match(
695    requirements: Option<&'static EnchantmentEffectRequirements>,
696    context: &EnchantmentDamageContext<'_>,
697) -> bool {
698    let Some(requirements) = requirements else {
699        return true;
700    };
701
702    matches!(requirements_state(requirements, context), Some(true))
703}
704
705fn post_attack_requirements_match(
706    requirements: Option<&'static EnchantmentEffectRequirements>,
707    context: &EnchantmentDamageContext<'_>,
708    level: i32,
709) -> bool {
710    let Some(requirements) = requirements else {
711        return true;
712    };
713
714    matches!(
715        requirements_state_with_random(requirements, context, level),
716        Some(true)
717    )
718}
719
720fn requirements_state(
721    requirements: &'static EnchantmentEffectRequirements,
722    context: &EnchantmentDamageContext<'_>,
723) -> Option<bool> {
724    match requirements {
725        EnchantmentEffectRequirements::AllOf(terms) => {
726            let mut has_unknown = false;
727            for term in *terms {
728                match requirements_state(term, context) {
729                    Some(true) => {}
730                    Some(false) => return Some(false),
731                    None => has_unknown = true,
732                }
733            }
734            if has_unknown { None } else { Some(true) }
735        }
736        EnchantmentEffectRequirements::AnyOf(terms) => {
737            let mut has_unknown = false;
738            for term in *terms {
739                match requirements_state(term, context) {
740                    Some(true) => return Some(true),
741                    Some(false) => {}
742                    None => has_unknown = true,
743                }
744            }
745            if has_unknown { None } else { Some(false) }
746        }
747        EnchantmentEffectRequirements::Inverted(term) => {
748            requirements_state(term, context).map(|matched| !matched)
749        }
750        EnchantmentEffectRequirements::EntityProperties { entity, predicate } => context
751            .entity_type(*entity)
752            .and_then(|entity_type| entity_predicate_matches_type(predicate, entity_type)),
753        EnchantmentEffectRequirements::DamageSourceProperties(predicate) => Some(
754            damage_source_predicate_matches(predicate, context.damage_source),
755        ),
756        EnchantmentEffectRequirements::RandomChance { .. }
757        | EnchantmentEffectRequirements::MatchTool { .. }
758        | EnchantmentEffectRequirements::Unsupported { .. } => None,
759    }
760}
761
762fn requirements_state_with_random(
763    requirements: &'static EnchantmentEffectRequirements,
764    context: &EnchantmentDamageContext<'_>,
765    level: i32,
766) -> Option<bool> {
767    match requirements {
768        EnchantmentEffectRequirements::AllOf(terms) => {
769            let mut has_unknown = false;
770            for term in *terms {
771                match requirements_state_with_random(term, context, level) {
772                    Some(true) => {}
773                    Some(false) => return Some(false),
774                    None => has_unknown = true,
775                }
776            }
777            if has_unknown { None } else { Some(true) }
778        }
779        EnchantmentEffectRequirements::AnyOf(terms) => {
780            let mut has_unknown = false;
781            for term in *terms {
782                match requirements_state_with_random(term, context, level) {
783                    Some(true) => return Some(true),
784                    Some(false) => {}
785                    None => has_unknown = true,
786                }
787            }
788            if has_unknown { None } else { Some(false) }
789        }
790        EnchantmentEffectRequirements::Inverted(term) => {
791            requirements_state_with_random(term, context, level).map(|matched| !matched)
792        }
793        EnchantmentEffectRequirements::EntityProperties { entity, predicate } => context
794            .entity_type(*entity)
795            .and_then(|entity_type| entity_predicate_matches_type(predicate, entity_type)),
796        EnchantmentEffectRequirements::DamageSourceProperties(predicate) => Some(
797            damage_source_predicate_matches(predicate, context.damage_source),
798        ),
799        EnchantmentEffectRequirements::RandomChance { chance } => {
800            Some(rand::random::<f32>() < chance.calculate(level))
801        }
802        EnchantmentEffectRequirements::MatchTool { .. }
803        | EnchantmentEffectRequirements::Unsupported { .. } => None,
804    }
805}
806
807fn entity_requirements_match(
808    requirements: Option<&'static EnchantmentEffectRequirements>,
809    entity: &dyn Entity,
810    level: i32,
811) -> bool {
812    let Some(requirements) = requirements else {
813        return true;
814    };
815
816    matches!(
817        entity_requirements_state(requirements, entity, level),
818        Some(true)
819    )
820}
821
822fn entity_requirements_state(
823    requirements: &'static EnchantmentEffectRequirements,
824    entity: &dyn Entity,
825    level: i32,
826) -> Option<bool> {
827    match requirements {
828        EnchantmentEffectRequirements::AllOf(terms) => {
829            let mut has_unknown = false;
830            for term in *terms {
831                match entity_requirements_state(term, entity, level) {
832                    Some(true) => {}
833                    Some(false) => return Some(false),
834                    None => has_unknown = true,
835                }
836            }
837            if has_unknown { None } else { Some(true) }
838        }
839        EnchantmentEffectRequirements::AnyOf(terms) => {
840            let mut has_unknown = false;
841            for term in *terms {
842                match entity_requirements_state(term, entity, level) {
843                    Some(true) => return Some(true),
844                    Some(false) => {}
845                    None => has_unknown = true,
846                }
847            }
848            if has_unknown { None } else { Some(false) }
849        }
850        EnchantmentEffectRequirements::Inverted(term) => {
851            entity_requirements_state(term, entity, level).map(|matched| !matched)
852        }
853        EnchantmentEffectRequirements::EntityProperties {
854            entity: EnchantmentEntityTarget::This,
855            predicate,
856        } => entity_predicate_matches_entity(predicate, entity),
857        EnchantmentEffectRequirements::RandomChance { chance } => {
858            Some(rand::random::<f32>() < chance.calculate(level))
859        }
860        EnchantmentEffectRequirements::EntityProperties { .. }
861        | EnchantmentEffectRequirements::DamageSourceProperties(_)
862        | EnchantmentEffectRequirements::MatchTool { .. }
863        | EnchantmentEffectRequirements::Unsupported { .. } => None,
864    }
865}
866
867fn entity_predicate_matches_type(
868    predicate: &EntityPredicate,
869    entity_type: EntityTypeRef,
870) -> Option<bool> {
871    if predicate.unsupported
872        || !matches!(predicate.vehicle, EntityVehiclePredicate::Any)
873        || predicate.flags.has_constraints()
874    {
875        return None;
876    }
877
878    let type_matches = entity_type_predicate_matches(&predicate.entity_type, entity_type)?;
879    if !type_matches {
880        return Some(false);
881    }
882
883    match &predicate.type_specific {
884        EntityTypeSpecificPredicate::Any => Some(true),
885        EntityTypeSpecificPredicate::Unsupported => None,
886        EntityTypeSpecificPredicate::Player(player_predicate) => {
887            if entity_type != &vanilla_entities::PLAYER {
888                return Some(false);
889            }
890            if player_predicate.unsupported
891                || !player_predicate.game_modes.is_empty()
892                || player_predicate.food_level_min.is_some()
893            {
894                None
895            } else {
896                Some(true)
897            }
898        }
899    }
900}
901
902fn entity_predicate_matches_entity(
903    predicate: &EntityPredicate,
904    entity: &dyn Entity,
905) -> Option<bool> {
906    if predicate.unsupported {
907        return None;
908    }
909
910    if !entity_type_predicate_matches(&predicate.entity_type, entity.entity_type())? {
911        return Some(false);
912    }
913
914    match predicate.vehicle {
915        EntityVehiclePredicate::Any => {}
916        EntityVehiclePredicate::Present => {
917            if entity.vehicle().is_none() {
918                return Some(false);
919            }
920        }
921        EntityVehiclePredicate::Unsupported => return None,
922    }
923
924    if predicate.flags.unsupported {
925        return None;
926    }
927    if let Some(expected) = predicate.flags.is_fall_flying {
928        let is_fall_flying = entity
929            .as_living_entity()
930            .is_some_and(LivingEntity::is_fall_flying);
931        if is_fall_flying != expected {
932            return Some(false);
933        }
934    }
935    if let Some(expected) = predicate.flags.is_in_water
936        && entity.is_in_water() != expected
937    {
938        return Some(false);
939    }
940
941    match &predicate.type_specific {
942        EntityTypeSpecificPredicate::Any => Some(true),
943        EntityTypeSpecificPredicate::Unsupported => None,
944        EntityTypeSpecificPredicate::Player(player_predicate) => {
945            let Some(player) = entity.as_player() else {
946                return Some(false);
947            };
948            if player_predicate.unsupported {
949                return None;
950            }
951            if !player_predicate.game_modes.is_empty()
952                && !player_predicate
953                    .game_modes
954                    .iter()
955                    .any(|game_mode| *game_mode == player.game_mode())
956            {
957                return Some(false);
958            }
959            if let Some(min_food_level) = player_predicate.food_level_min
960                && player.food_data.lock().food_level < min_food_level
961            {
962                return Some(false);
963            }
964            Some(true)
965        }
966    }
967}
968
969fn entity_type_predicate_matches(
970    predicate: &EntityTypePredicate,
971    entity_type: EntityTypeRef,
972) -> Option<bool> {
973    match predicate {
974        EntityTypePredicate::Any => Some(true),
975        EntityTypePredicate::Type(expected) => Some(entity_type.key == *expected),
976        EntityTypePredicate::Tag(tag) => Some(REGISTRY.entity_types.is_in_tag(entity_type, tag)),
977        EntityTypePredicate::Unsupported => None,
978    }
979}
980
981fn damage_source_predicate_matches(
982    predicate: &DamageSourcePredicate,
983    damage_source: &DamageSource,
984) -> bool {
985    if let Some(is_direct) = predicate.is_direct
986        && damage_source.is_direct() != is_direct
987    {
988        return false;
989    }
990
991    predicate
992        .tags
993        .iter()
994        .all(|tag| damage_source.is(&tag.tag) == tag.expected)
995}
996
997#[cfg(test)]
998mod tests {
999    use std::sync::Weak;
1000
1001    use glam::DVec3;
1002    use steel_registry::data_components::vanilla_components::{ENCHANTMENTS, ItemEnchantments};
1003    use steel_registry::entity_type::EntityTypeRef;
1004    use steel_registry::items::ItemRef;
1005    use steel_registry::{
1006        init_vanilla_registry, vanilla_damage_types, vanilla_enchantments, vanilla_entities,
1007        vanilla_items, vanilla_mob_effects,
1008    };
1009    use steel_utils::Identifier;
1010    use steel_utils::locks::SyncMutex;
1011
1012    use super::*;
1013    use crate::entity::{
1014        EntityBase, LivingEntity, LivingEntityBase, entities::FireworkRocketEntity,
1015    };
1016    use crate::test_support::test_world;
1017
1018    struct TestLivingEntity {
1019        base: EntityBase,
1020        living_base: LivingEntityBase,
1021        health: SyncMutex<f32>,
1022        broken_slots: SyncMutex<Vec<EquipmentSlot>>,
1023        entity_type: EntityTypeRef,
1024    }
1025
1026    impl TestLivingEntity {
1027        fn new(id: i32, entity_type: EntityTypeRef) -> Self {
1028            Self {
1029                base: EntityBase::new(id, DVec3::ZERO, entity_type.dimensions, Weak::new()),
1030                living_base: LivingEntityBase::new(entity_type),
1031                health: SyncMutex::new(20.0),
1032                broken_slots: SyncMutex::new(Vec::new()),
1033                entity_type,
1034            }
1035        }
1036
1037        fn equip(&self, slot: EquipmentSlot, stack: ItemStack) {
1038            self.living_base.equipment().lock().set(slot, stack);
1039        }
1040
1041        fn broken_slots(&self) -> Vec<EquipmentSlot> {
1042            self.broken_slots.lock().clone()
1043        }
1044    }
1045
1046    crate::entity::impl_test_downcast_type!(TestLivingEntity);
1047
1048    impl Entity for TestLivingEntity {
1049        fn base(&self) -> &EntityBase {
1050            &self.base
1051        }
1052
1053        fn entity_type(&self) -> EntityTypeRef {
1054            self.entity_type
1055        }
1056    }
1057
1058    impl LivingEntity for TestLivingEntity {
1059        fn living_base(&self) -> &LivingEntityBase {
1060            &self.living_base
1061        }
1062
1063        fn get_health(&self) -> f32 {
1064            *self.health.lock()
1065        }
1066
1067        fn set_health(&self, health: f32) {
1068            *self.health.lock() = health.clamp(0.0, self.get_max_health());
1069        }
1070
1071        fn get_absorption_amount(&self) -> f32 {
1072            0.0
1073        }
1074
1075        fn set_absorption_amount(&self, _amount: f32) {}
1076
1077        fn on_equipped_item_broken(&self, slot: EquipmentSlot) {
1078            self.broken_slots.lock().push(slot);
1079        }
1080    }
1081
1082    fn enchanted_item(item: ItemRef, enchantment: Identifier, level: u32) -> ItemStack {
1083        let mut enchantments = ItemEnchantments::empty();
1084        enchantments.set(enchantment, level);
1085
1086        let mut stack = ItemStack::new(item);
1087        stack.set(ENCHANTMENTS, enchantments);
1088        stack
1089    }
1090
1091    fn assert_f32_eq(actual: f32, expected: f32) {
1092        assert_eq!(
1093            actual.to_bits(),
1094            expected.to_bits(),
1095            "actual: {actual}, expected: {expected}"
1096        );
1097    }
1098
1099    #[test]
1100    fn unsupported_requirement_does_not_match_through_inversion() {
1101        static UNSUPPORTED: EnchantmentEffectRequirements =
1102            EnchantmentEffectRequirements::Unsupported {
1103                condition: Identifier::vanilla_static("match_tool"),
1104            };
1105        static INVERTED: EnchantmentEffectRequirements =
1106            EnchantmentEffectRequirements::Inverted(&UNSUPPORTED);
1107
1108        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK);
1109        let context = EnchantmentDamageContext::new(
1110            &vanilla_entities::PLAYER,
1111            Some(&vanilla_entities::PLAYER),
1112            Some(&vanilla_entities::PLAYER),
1113            &damage_source,
1114        );
1115
1116        assert!(!requirements_match(Some(&UNSUPPORTED), &context));
1117        assert!(!requirements_match(Some(&INVERTED), &context));
1118    }
1119
1120    #[test]
1121    fn damage_enchantments_match_target_entity_tags() {
1122        init_vanilla_registry();
1123
1124        let stack = enchanted_item(
1125            &vanilla_items::DIAMOND_SWORD,
1126            Identifier::vanilla_static("smite"),
1127            5,
1128        );
1129        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK);
1130        let zombie_context = EnchantmentDamageContext::new(
1131            &vanilla_entities::ZOMBIE,
1132            Some(&vanilla_entities::PLAYER),
1133            Some(&vanilla_entities::PLAYER),
1134            &damage_source,
1135        );
1136        let spider_context = EnchantmentDamageContext::new(
1137            &vanilla_entities::SPIDER,
1138            Some(&vanilla_entities::PLAYER),
1139            Some(&vanilla_entities::PLAYER),
1140            &damage_source,
1141        );
1142
1143        assert_f32_eq(modify_damage(&stack, &zombie_context, 7.0), 19.5);
1144        assert_f32_eq(modify_damage(&stack, &spider_context, 7.0), 7.0);
1145    }
1146
1147    #[test]
1148    fn breach_modifies_armor_effectiveness() {
1149        init_vanilla_registry();
1150
1151        let stack = enchanted_item(
1152            &vanilla_items::MACE,
1153            Identifier::vanilla_static("breach"),
1154            2,
1155        );
1156        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK);
1157        let context = EnchantmentDamageContext::new(
1158            &vanilla_entities::PLAYER,
1159            Some(&vanilla_entities::PLAYER),
1160            Some(&vanilla_entities::PLAYER),
1161            &damage_source,
1162        );
1163
1164        assert_f32_eq(
1165            modify_armor_effectiveness(&stack, &context, 0.4),
1166            0.4_f32 - 0.15_f32 * 2.0,
1167        );
1168    }
1169
1170    #[test]
1171    fn projectile_knockback_checks_direct_attacker_entity_tag() {
1172        init_vanilla_registry();
1173
1174        let stack = enchanted_item(&vanilla_items::BOW, Identifier::vanilla_static("punch"), 2);
1175        let damage_source = DamageSource::environment(&vanilla_damage_types::ARROW);
1176        let melee_context = EnchantmentDamageContext::new(
1177            &vanilla_entities::ZOMBIE,
1178            Some(&vanilla_entities::PLAYER),
1179            Some(&vanilla_entities::PLAYER),
1180            &damage_source,
1181        );
1182        let arrow_context = EnchantmentDamageContext::new(
1183            &vanilla_entities::ZOMBIE,
1184            Some(&vanilla_entities::PLAYER),
1185            Some(&vanilla_entities::ARROW),
1186            &damage_source,
1187        );
1188
1189        assert_f32_eq(modify_knockback(&stack, &melee_context, 0.0), 0.0);
1190        assert_f32_eq(modify_knockback(&stack, &arrow_context, 0.0), 2.0);
1191    }
1192
1193    #[test]
1194    fn damage_source_properties_match_damage_type_tags() {
1195        init_vanilla_registry();
1196
1197        let stack = enchanted_item(
1198            &vanilla_items::DIAMOND_SWORD,
1199            Identifier::vanilla_static("fire_protection"),
1200            4,
1201        );
1202        let fire_source = DamageSource::environment(&vanilla_damage_types::IN_FIRE);
1203        let fall_source = DamageSource::environment(&vanilla_damage_types::FALL);
1204        let fire_context =
1205            EnchantmentDamageContext::new(&vanilla_entities::PLAYER, None, None, &fire_source);
1206        let fall_context =
1207            EnchantmentDamageContext::new(&vanilla_entities::PLAYER, None, None, &fall_source);
1208
1209        assert_f32_eq(
1210            apply_value_effects(
1211                &stack,
1212                EnchantmentEffectComponent::DamageProtection,
1213                &fire_context,
1214                0.0,
1215            ),
1216            8.0,
1217        );
1218        assert_f32_eq(
1219            apply_value_effects(
1220                &stack,
1221                EnchantmentEffectComponent::DamageProtection,
1222                &fall_context,
1223                0.0,
1224            ),
1225            0.0,
1226        );
1227    }
1228
1229    #[test]
1230    fn damage_protection_accumulates_across_equipment() {
1231        init_vanilla_registry();
1232
1233        let victim = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1234        victim.equip(
1235            EquipmentSlot::Feet,
1236            enchanted_item(
1237                &vanilla_items::DIAMOND_BOOTS,
1238                Identifier::vanilla_static("protection"),
1239                2,
1240            ),
1241        );
1242        victim.equip(
1243            EquipmentSlot::Chest,
1244            enchanted_item(
1245                &vanilla_items::DIAMOND_CHESTPLATE,
1246                Identifier::vanilla_static("blast_protection"),
1247                3,
1248            ),
1249        );
1250        let source = DamageSource::environment(&vanilla_damage_types::FIREWORKS);
1251
1252        assert_f32_eq(get_damage_protection(test_world(), &victim, &source), 8.0);
1253    }
1254
1255    #[test]
1256    fn damage_protection_ignores_enchantment_in_non_matching_slot() {
1257        init_vanilla_registry();
1258
1259        let victim = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1260        victim.equip(
1261            EquipmentSlot::MainHand,
1262            enchanted_item(
1263                &vanilla_items::DIAMOND_BOOTS,
1264                Identifier::vanilla_static("protection"),
1265                4,
1266            ),
1267        );
1268        let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
1269
1270        assert_f32_eq(get_damage_protection(test_world(), &victim, &source), 0.0);
1271    }
1272
1273    #[test]
1274    fn damage_protection_ignores_broken_equipment() {
1275        init_vanilla_registry();
1276
1277        let mut broken_chestplate = enchanted_item(
1278            &vanilla_items::DIAMOND_CHESTPLATE,
1279            Identifier::vanilla_static("protection"),
1280            4,
1281        );
1282        broken_chestplate.set_count(0);
1283        let victim = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1284        victim.equip(EquipmentSlot::Chest, broken_chestplate);
1285        let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
1286
1287        assert_f32_eq(get_damage_protection(test_world(), &victim, &source), 0.0);
1288    }
1289
1290    #[test]
1291    fn damage_immunity_matches_equipment_slot_and_requirements() {
1292        init_vanilla_registry();
1293
1294        let boots = enchanted_item(
1295            &vanilla_items::LEATHER_BOOTS,
1296            Identifier::vanilla_static("frost_walker"),
1297            1,
1298        );
1299        let victim = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1300        victim.equip(EquipmentSlot::Feet, boots);
1301
1302        assert!(is_immune_to_damage(
1303            test_world(),
1304            &victim,
1305            &DamageSource::environment(&vanilla_damage_types::HOT_FLOOR)
1306        ));
1307        assert!(!is_immune_to_damage(
1308            test_world(),
1309            &victim,
1310            &DamageSource::environment(&vanilla_damage_types::IN_FIRE)
1311        ));
1312
1313        let helmet = enchanted_item(
1314            &vanilla_items::LEATHER_HELMET,
1315            Identifier::vanilla_static("frost_walker"),
1316            1,
1317        );
1318        let wrong_slot_victim = TestLivingEntity::new(2, &vanilla_entities::PLAYER);
1319        wrong_slot_victim.equip(EquipmentSlot::Head, helmet);
1320
1321        assert!(!is_immune_to_damage(
1322            test_world(),
1323            &wrong_slot_victim,
1324            &DamageSource::environment(&vanilla_damage_types::HOT_FLOOR)
1325        ));
1326    }
1327
1328    #[test]
1329    fn post_attack_ignite_applies_to_direct_melee_victim() {
1330        init_vanilla_registry();
1331
1332        let attacker = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1333        let victim = TestLivingEntity::new(2, &vanilla_entities::ZOMBIE);
1334        let stack = enchanted_item(
1335            &vanilla_items::DIAMOND_SWORD,
1336            Identifier::vanilla_static("fire_aspect"),
1337            2,
1338        );
1339        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK)
1340            .with_causing_entity(attacker.id())
1341            .with_direct_entity(attacker.id());
1342        let context = EnchantmentPostAttackContext::new(
1343            &victim,
1344            Some(&attacker),
1345            Some(&attacker),
1346            &damage_source,
1347        );
1348
1349        do_post_attack_effects_from_item(test_world(), &stack, &context);
1350
1351        assert_eq!(victim.remaining_fire_ticks(), 160);
1352    }
1353
1354    #[test]
1355    fn projectile_spawned_effect_ignites_firework() {
1356        init_vanilla_registry();
1357
1358        let owner = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1359        let projectile = FireworkRocketEntity::new(
1360            &vanilla_entities::FIREWORK_ROCKET,
1361            2,
1362            DVec3::ZERO,
1363            Weak::new(),
1364        );
1365        let mut rocket = enchanted_item(
1366            &vanilla_items::FIREWORK_ROCKET,
1367            Identifier::vanilla_static("flame"),
1368            1,
1369        );
1370
1371        on_projectile_spawned(test_world(), &mut rocket, &projectile, Some(&owner));
1372
1373        assert_eq!(projectile.remaining_fire_ticks(), 2_000);
1374    }
1375
1376    #[test]
1377    fn post_attack_effects_match_enchantment_slot() {
1378        init_vanilla_registry();
1379
1380        let attacker = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1381        let victim = TestLivingEntity::new(2, &vanilla_entities::ZOMBIE);
1382        let mut stack = enchanted_item(
1383            &vanilla_items::DIAMOND_SWORD,
1384            Identifier::vanilla_static("fire_aspect"),
1385            1,
1386        );
1387        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK)
1388            .with_causing_entity(attacker.id())
1389            .with_direct_entity(attacker.id());
1390        let context = EnchantmentPostAttackContext::new(
1391            &victim,
1392            Some(&attacker),
1393            Some(&attacker),
1394            &damage_source,
1395        );
1396
1397        apply_post_attack_effects(
1398            test_world(),
1399            &mut stack,
1400            Some(EquipmentSlot::Head),
1401            EnchantmentTarget::Attacker,
1402            &context,
1403        );
1404        assert_eq!(victim.remaining_fire_ticks(), 0);
1405
1406        apply_post_attack_effects(
1407            test_world(),
1408            &mut stack,
1409            Some(EquipmentSlot::MainHand),
1410            EnchantmentTarget::Attacker,
1411            &context,
1412        );
1413        assert_eq!(victim.remaining_fire_ticks(), 80);
1414    }
1415
1416    #[test]
1417    fn post_attack_change_item_damage_calls_equipped_break_hook() {
1418        init_vanilla_registry();
1419
1420        let attacker = TestLivingEntity::new(1, &vanilla_entities::ZOMBIE);
1421        let victim = TestLivingEntity::new(2, &vanilla_entities::PLAYER);
1422        let mut chestplate = enchanted_item(
1423            &vanilla_items::DIAMOND_CHESTPLATE,
1424            Identifier::vanilla_static("thorns"),
1425            10,
1426        );
1427        chestplate.set_damage_value(chestplate.get_max_damage() - 1);
1428        victim.equip(EquipmentSlot::Chest, chestplate);
1429
1430        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK)
1431            .with_causing_entity(attacker.id())
1432            .with_direct_entity(attacker.id());
1433        let context = EnchantmentPostAttackContext::new(
1434            &victim,
1435            Some(&attacker),
1436            Some(&attacker),
1437            &damage_source,
1438        );
1439        let source = ItemStack::empty();
1440
1441        do_post_attack_effects_with_item_source(test_world(), &victim, &source, &context);
1442
1443        let mut chestplate_broke = false;
1444        victim.with_equipment_slot(EquipmentSlot::Chest, &mut |stack| {
1445            chestplate_broke = stack.is_empty();
1446        });
1447        assert!(chestplate_broke);
1448        assert_eq!(victim.broken_slots(), vec![EquipmentSlot::Chest]);
1449    }
1450
1451    #[test]
1452    fn lunge_post_piercing_requirements_and_effect_are_supported() {
1453        init_vanilla_registry();
1454
1455        let user = TestLivingEntity::new(1, &vanilla_entities::ZOMBIE);
1456        let effects = vanilla_enchantments::LUNGE.effects.post_piercing_attack;
1457        assert_eq!(effects.len(), 1);
1458        assert!(entity_requirements_match(effects[0].requirements, &user, 1));
1459        assert!(post_piercing_entity_effect_is_supported(&effects[0].effect));
1460    }
1461
1462    #[test]
1463    fn post_attack_ignite_skips_indirect_damage_source() {
1464        init_vanilla_registry();
1465
1466        let attacker = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1467        let direct_entity = TestLivingEntity::new(2, &vanilla_entities::PLAYER);
1468        let victim = TestLivingEntity::new(3, &vanilla_entities::ZOMBIE);
1469        let stack = enchanted_item(
1470            &vanilla_items::DIAMOND_SWORD,
1471            Identifier::vanilla_static("fire_aspect"),
1472            2,
1473        );
1474        let damage_source = DamageSource::environment(&vanilla_damage_types::ARROW)
1475            .with_causing_entity(attacker.id())
1476            .with_direct_entity(direct_entity.id());
1477        let context = EnchantmentPostAttackContext::new(
1478            &victim,
1479            Some(&attacker),
1480            Some(&direct_entity),
1481            &damage_source,
1482        );
1483
1484        do_post_attack_effects_from_item(test_world(), &stack, &context);
1485
1486        assert_eq!(victim.remaining_fire_ticks(), 0);
1487    }
1488
1489    #[test]
1490    fn post_attack_mob_effect_matches_victim_predicate() {
1491        init_vanilla_registry();
1492
1493        let attacker = TestLivingEntity::new(1, &vanilla_entities::PLAYER);
1494        let spider = TestLivingEntity::new(2, &vanilla_entities::SPIDER);
1495        let zombie = TestLivingEntity::new(3, &vanilla_entities::ZOMBIE);
1496        let stack = enchanted_item(
1497            &vanilla_items::DIAMOND_SWORD,
1498            Identifier::vanilla_static("bane_of_arthropods"),
1499            1,
1500        );
1501        let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK)
1502            .with_causing_entity(attacker.id())
1503            .with_direct_entity(attacker.id());
1504        let spider_context = EnchantmentPostAttackContext::new(
1505            &spider,
1506            Some(&attacker),
1507            Some(&attacker),
1508            &damage_source,
1509        );
1510        let zombie_context = EnchantmentPostAttackContext::new(
1511            &zombie,
1512            Some(&attacker),
1513            Some(&attacker),
1514            &damage_source,
1515        );
1516
1517        do_post_attack_effects_from_item(test_world(), &stack, &spider_context);
1518        do_post_attack_effects_from_item(test_world(), &stack, &zombie_context);
1519
1520        let Some(slowness) = spider.mob_effect(vanilla_mob_effects::SLOWNESS) else {
1521            panic!("bane of arthropods should apply slowness to spiders");
1522        };
1523        assert_eq!(slowness.duration(), 30);
1524        assert_eq!(slowness.amplifier(), 3);
1525        assert!(zombie.mob_effect(vanilla_mob_effects::SLOWNESS).is_none());
1526    }
1527}