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