Skip to main content

steel_core/
enchantment_helper.rs

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