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