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
402pub(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}