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