Skip to main content

steel_core/player/game_mode/
entity_interaction.rs

1use super::{
2    ATTACK_RANGE_BUFFER, CSetEntityMotion, ClipBlockShape, ClipFluid, DVec3, DamageSource,
3    DamageType, ENTITY_INTERACTION_RANGE_BUFFER, EnchantmentDamageContext,
4    EnchantmentPostAttackContext, Entity, EntityTypeRef, GameType, ITEM_BEHAVIORS, InteractionHand,
5    InteractionResult, InventoryAccess, ItemStack, LivingEntity, PiercingWeapon, Player, SAttack,
6    SInteract, SharedEntity, SoundEventHolder, SoundEventRef, TextComponent, TranslatedMessage,
7    World, WorldAabb, enchantment_helper, piercing_ray_hit_t, vanilla_attributes,
8    vanilla_damage_types, vanilla_entities,
9};
10use crate::player::food_data::food_constants;
11use std::ops::Add;
12use steel_registry::particle_type::ParticleData;
13use steel_registry::{vanilla_custom_stats, vanilla_particle_types};
14
15const fn sound_holder_ref(holder: &SoundEventHolder) -> Option<SoundEventRef> {
16    match holder {
17        SoundEventHolder::Registry(sound) => Some(*sound),
18        SoundEventHolder::Direct { .. } => {
19            // TODO: Support direct sound holders when entity sound playback can send them.
20            None
21        }
22    }
23}
24impl Player {
25    fn invalid_entity_attacked_message() -> TextComponent {
26        TranslatedMessage {
27            key: "multiplayer.disconnect.invalid_entity_attacked".into(),
28            fallback: None,
29            args: None,
30        }
31        .component()
32    }
33
34    fn eye_position(&self) -> DVec3 {
35        let position = self.position();
36        DVec3::new(position.x, self.get_eye_y(), position.z)
37    }
38
39    fn damage_source_for_attack_type(&self, damage_type: &'static DamageType) -> DamageSource {
40        DamageSource::environment(damage_type)
41            .with_causing_entity(self.id())
42            .with_direct_entity(self.id())
43            .with_source_position(self.position())
44    }
45
46    fn attack_damage_source(&self, attacking_item: &ItemStack) -> DamageSource {
47        if let Some(damage_type) = attacking_item.get_damage_type() {
48            return self.damage_source_for_attack_type(damage_type);
49        }
50        if let Some(source) = ITEM_BEHAVIORS
51            .get_behavior(attacking_item.item())
52            .get_item_damage_source(self)
53        {
54            return source;
55        }
56        self.damage_source_for_attack_type(&vanilla_damage_types::PLAYER_ATTACK)
57    }
58
59    /// Ticks vanilla attack-strength recovery and resets it on main-hand item changes.
60    pub(in crate::player) fn tick_attack_strength(&self) {
61        self.tick_state.lock().advance_attack_strength_ticker();
62
63        let main_hand_item = {
64            let inventory = self.inventory.lock();
65            let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
66            stack.copy_with_count(stack.count())
67        };
68
69        let mut last_item = self.last_item_in_main_hand.lock();
70        if ItemStack::matches(&last_item, &main_hand_item) {
71            return;
72        }
73
74        if !ItemStack::is_same_item(&last_item, &main_hand_item) {
75            self.reset_attack_strength_ticker();
76        }
77
78        *last_item = main_hand_item;
79    }
80
81    fn reset_attack_strength_ticker(&self) {
82        self.tick_state.lock().reset_attack_strength_ticker();
83    }
84
85    fn current_item_attack_strength_delay(&self) -> f32 {
86        let attack_speed = self
87            .attributes()
88            .lock()
89            .required_value(vanilla_attributes::ATTACK_SPEED);
90        Self::attack_strength_delay_from_speed(attack_speed)
91    }
92
93    fn attack_strength_delay_from_speed(attack_speed: f64) -> f32 {
94        (1.0 / attack_speed * 20.0) as f32
95    }
96
97    /// Returns vanilla `Player.getAttackStrengthScale`.
98    #[must_use]
99    pub fn attack_strength_scale(&self, partial_tick: f32) -> f32 {
100        let attack_strength_delay = self.current_item_attack_strength_delay();
101        self.attack_strength_scale_for_delay(partial_tick, attack_strength_delay)
102    }
103
104    fn attack_strength_scale_for_delay(
105        &self,
106        partial_tick: f32,
107        attack_strength_delay: f32,
108    ) -> f32 {
109        let ticker = self.tick_state.lock().attack_strength_ticker() as f32;
110        ((ticker + partial_tick) / attack_strength_delay).clamp(0.0, 1.0)
111    }
112
113    fn base_damage_scale_factor(attack_strength_scale: f32) -> f32 {
114        0.2 + attack_strength_scale * attack_strength_scale * 0.8
115    }
116
117    fn get_knockback(
118        attack_knockback: f64,
119        weapon: &ItemStack,
120        enchantment_context: &EnchantmentDamageContext<'_>,
121    ) -> f64 {
122        let modified = enchantment_helper::modify_knockback(
123            weapon,
124            enchantment_context,
125            attack_knockback as f32,
126        );
127        f64::from(modified) / 2.0
128    }
129
130    fn cause_extra_knockback(
131        &self,
132        entity: &dyn Entity,
133        knockback_amount: f64,
134        old_movement: DVec3,
135    ) {
136        if knockback_amount > 0.0 {
137            let yaw_radians = self.rotation().0.to_radians();
138            let yaw_sin = f64::from(yaw_radians.sin());
139            let yaw_cos = f64::from(yaw_radians.cos());
140            if let Some(living_target) = entity.as_living_entity() {
141                living_target.knockback(knockback_amount, yaw_sin, -yaw_cos);
142            } else {
143                entity.push_impulse(DVec3::new(
144                    -yaw_sin * knockback_amount,
145                    0.1,
146                    yaw_cos * knockback_amount,
147                ));
148            }
149
150            let velocity = self.velocity();
151            self.set_velocity(DVec3::new(velocity.x * 0.6, velocity.y, velocity.z * 0.6));
152            self.set_sprinting(false);
153        }
154
155        if entity.entity_type() == &vanilla_entities::PLAYER
156            && entity.hurt_marked()
157            && let Some(player) = self.get_world().players.get_by_entity_id(entity.id())
158        {
159            let velocity = entity.velocity();
160            player.send_packet(CSetEntityMotion::new(entity.id(), velocity));
161            entity.clear_hurt_mark();
162            entity.set_velocity(old_movement);
163        }
164    }
165
166    fn entity_interaction_range(&self) -> f64 {
167        self.attributes()
168            .lock()
169            .required_value(vanilla_attributes::ENTITY_INTERACTION_RANGE)
170    }
171
172    /// Returns true if the target box is within the player's attack range for `item_stack`.
173    #[must_use]
174    pub fn is_within_attack_range_with_buffer(
175        &self,
176        item_stack: &ItemStack,
177        aabb: WorldAabb,
178        buffer: f64,
179    ) -> bool {
180        let distance = aabb.distance_to_sqr(self.eye_position()).sqrt();
181        let (min_reach, max_reach, hitbox_margin) =
182            if let Some(attack_range) = item_stack.get_attack_range() {
183                if self.game_mode() == GameType::Creative {
184                    (
185                        attack_range.min_creative_reach,
186                        attack_range.max_creative_reach,
187                        attack_range.hitbox_margin,
188                    )
189                } else {
190                    (
191                        attack_range.min_reach,
192                        attack_range.max_reach,
193                        attack_range.hitbox_margin,
194                    )
195                }
196            } else {
197                (0.0, self.entity_interaction_range() as f32, 0.0)
198            };
199        let min_reach = f64::from(min_reach) - f64::from(hitbox_margin) - buffer;
200        let max_reach = f64::from(max_reach) + f64::from(hitbox_margin) + buffer;
201        distance >= min_reach && distance <= max_reach
202    }
203
204    /// Returns true if the target box is within the player's entity interaction range.
205    #[must_use]
206    pub fn is_within_entity_interaction_range_with_buffer(
207        &self,
208        aabb: WorldAabb,
209        buffer: f64,
210    ) -> bool {
211        let max_range = self.entity_interaction_range() + buffer;
212        aabb.distance_to_sqr(self.eye_position()) <= max_range * max_range
213    }
214
215    fn attack_range_for_item(&self, item_stack: &ItemStack) -> (f64, f64, f64) {
216        let Some(attack_range) = item_stack.get_attack_range() else {
217            return (0.0, self.entity_interaction_range(), 0.0);
218        };
219
220        let (min_reach, max_reach) = if self.game_mode() == GameType::Creative {
221            (
222                attack_range.min_creative_reach,
223                attack_range.max_creative_reach,
224            )
225        } else {
226            (attack_range.min_reach, attack_range.max_reach)
227        };
228        (
229            f64::from(min_reach),
230            f64::from(max_reach),
231            f64::from(attack_range.hitbox_margin),
232        )
233    }
234
235    fn piercing_hit_entities(&self, item_stack: &ItemStack, world: &World) -> Vec<SharedEntity> {
236        let look = self.look_angle();
237        if look.length_squared() <= f64::EPSILON {
238            return Vec::new();
239        }
240
241        let (min_reach, max_reach, hitbox_margin) = self.attack_range_for_item(item_stack);
242        let eye_position = self.eye_position();
243        let from = eye_position + look * min_reach;
244        let movement_extension = self.known_movement().dot(look).max(0.0);
245        let mut to = eye_position + look * (max_reach + movement_extension);
246
247        let block_hit = world.clip(eye_position, to, ClipBlockShape::Collider, ClipFluid::None);
248        if !block_hit.is_miss() {
249            to = block_hit.location;
250            if eye_position.distance_squared(to) < eye_position.distance_squared(from) {
251                return Vec::new();
252            }
253        }
254
255        let search_area = WorldAabb::new(from.x, from.y, from.z, from.x, from.y, from.z)
256            .inflate_xyz(hitbox_margin, hitbox_margin, hitbox_margin)
257            .expand_towards(to - from)
258            .inflate(1.0);
259        let mut hits = world
260            .get_entities_in_aabb_matching(&search_area, |entity| {
261                self.can_piercing_hit_entity(entity)
262            })
263            .into_iter()
264            .filter_map(|entity| {
265                piercing_ray_hit_t(world, entity.bounding_box(), from, to, hitbox_margin)
266                    .map(|hit_t| (hit_t, entity))
267            })
268            .collect::<Vec<_>>();
269        hits.sort_by(|(left, _), (right, _)| left.total_cmp(right));
270        hits.into_iter().map(|(_, entity)| entity).collect()
271    }
272
273    fn can_piercing_hit_entity(&self, target: &dyn Entity) -> bool {
274        target.id() != self.id()
275            && !target.is_invulnerable()
276            && target.is_alive()
277            && target.can_be_hit_by_projectile()
278            && !self.is_passenger_of_same_vehicle(target)
279    }
280
281    pub(super) fn piercing_attack(&self, item_stack: &ItemStack, piercing_weapon: &PiercingWeapon) {
282        let world = self.get_world();
283        let base_damage = self
284            .attributes()
285            .lock()
286            .required_value(vanilla_attributes::ATTACK_DAMAGE) as f32;
287        let mut hit_something = false;
288        for target in self.piercing_hit_entities(item_stack, &world) {
289            hit_something |= self.stab_attack(
290                &target,
291                base_damage,
292                true,
293                piercing_weapon.deals_knockback,
294                piercing_weapon.dismounts,
295            );
296        }
297
298        self.reset_attack_strength_ticker();
299        enchantment_helper::do_post_piercing_attack_effects(&world, self);
300        if hit_something {
301            self.play_sound_holder(piercing_weapon.hit_sound.as_ref());
302        }
303        self.play_sound_holder(piercing_weapon.sound.as_ref());
304        self.swing(InteractionHand::MainHand, false);
305    }
306
307    fn stab_attack(
308        &self,
309        target: &SharedEntity,
310        base_damage: f32,
311        deals_damage: bool,
312        deals_knockback: bool,
313        dismounts: bool,
314    ) -> bool {
315        let entity = target.as_ref();
316        if self.cannot_attack(entity) {
317            return false;
318        }
319
320        let attacking_item = {
321            let inventory = self.inventory.lock();
322            let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
323            stack.copy_with_count(stack.count())
324        };
325        let damage_source = self.attack_damage_source(&attacking_item);
326        let enchantment_context = EnchantmentDamageContext::new(
327            entity.entity_type(),
328            Some(self.entity_type()),
329            Some(self.entity_type()),
330            &damage_source,
331        );
332        let enchanted_damage =
333            enchantment_helper::modify_damage(&attacking_item, &enchantment_context, base_damage);
334        let attack_strength_scale = self.attack_strength_scale(0.5);
335        let magic_boost = attack_strength_scale * (enchanted_damage - base_damage);
336        let base_damage = base_damage * Self::base_damage_scale_factor(attack_strength_scale);
337        let damage = base_damage + magic_boost;
338        let old_movement = entity.velocity();
339        let mut affected = deals_knockback;
340        let damage_dealt = deals_damage
341            && entity
342                .level()
343                .is_some_and(|world| entity.hurt(&world, &damage_source, damage));
344        affected |= damage_dealt;
345        if deals_knockback {
346            self.cause_extra_knockback(
347                entity,
348                0.4 + Self::get_knockback(0.0, &attacking_item, &enchantment_context),
349                old_movement,
350            );
351        }
352        if dismounts && entity.is_passenger() {
353            affected = true;
354            entity.stop_riding();
355        }
356
357        if !affected {
358            return false;
359        }
360
361        self.item_attack_interaction(entity, &damage_source, damage_dealt);
362        self.set_last_hurt_mob(Some(target));
363        self.cause_food_exhaustion(food_constants::EXHAUSTION_ATTACK);
364        true
365    }
366
367    fn play_sound_holder(&self, holder: Option<&SoundEventHolder>) {
368        let Some(sound) = holder.and_then(sound_holder_ref) else {
369            return;
370        };
371        self.play_sound(sound, 1.0, 1.0);
372    }
373
374    fn cannot_attack(&self, entity: &dyn Entity) -> bool {
375        !entity.attackable() || entity.skip_attack_interaction(self)
376    }
377
378    /// Attacks an entity with the player's main-hand base damage.
379    ///
380    /// Returns `true` if the target accepted damage.
381    #[must_use]
382    pub fn attack(&self, target: &SharedEntity) -> bool {
383        let entity = target.as_ref();
384        if self.cannot_attack(entity) {
385            return false;
386        }
387
388        let attacking_item = {
389            let inventory = self.inventory.lock();
390            let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
391            stack.copy_with_count(stack.count())
392        };
393        let (attack_damage, attack_speed, attack_knockback) = {
394            let attributes = self.attributes().lock();
395            (
396                attributes.required_value(vanilla_attributes::ATTACK_DAMAGE) as f32,
397                attributes.required_value(vanilla_attributes::ATTACK_SPEED),
398                attributes.required_value(vanilla_attributes::ATTACK_KNOCKBACK),
399            )
400        };
401        let attack_strength_delay = Self::attack_strength_delay_from_speed(attack_speed);
402        let attack_strength_scale =
403            self.attack_strength_scale_for_delay(0.5, attack_strength_delay);
404        let damage_source = self.attack_damage_source(&attacking_item);
405        let enchantment_context = EnchantmentDamageContext::new(
406            entity.entity_type(),
407            Some(self.entity_type()),
408            Some(self.entity_type()),
409            &damage_source,
410        );
411        let enchanted_damage =
412            enchantment_helper::modify_damage(&attacking_item, &enchantment_context, attack_damage);
413        let magic_boost = attack_strength_scale * (enchanted_damage - attack_damage);
414        let mut base_damage = attack_damage * Self::base_damage_scale_factor(attack_strength_scale);
415        base_damage += ITEM_BEHAVIORS
416            .get_behavior(attacking_item.item())
417            .get_attack_damage_bonus(self, entity, base_damage, &damage_source);
418        let total_damage = base_damage + magic_boost;
419        let full_strength_attack = attack_strength_scale > 0.9;
420        let knockback_attack = self.is_sprinting() && full_strength_attack;
421        self.reset_attack_strength_ticker();
422
423        if total_damage <= 0.0 {
424            return false;
425        }
426
427        let old_entity_living_health = entity
428            .as_living_entity()
429            .map_or(0.0, LivingEntity::get_health);
430
431        // TODO: Apply crits, sweep attacks, and sounds.
432        let old_movement = entity.velocity();
433        let Some(target_world) = entity.level() else {
434            return false;
435        };
436        let was_hurt = entity.hurt(&target_world, &damage_source, total_damage);
437        if was_hurt {
438            self.set_last_hurt_mob(Some(target));
439            let sprint_knockback = if knockback_attack { 0.5 } else { 0.0 };
440            self.cause_extra_knockback(
441                entity,
442                Self::get_knockback(attack_knockback, &attacking_item, &enchantment_context)
443                    + sprint_knockback,
444                old_movement,
445            );
446            self.item_attack_interaction(entity, &damage_source, true);
447            self.damage_stats_and_hearts(entity, old_entity_living_health);
448            self.cause_food_exhaustion(food_constants::EXHAUSTION_ATTACK);
449        }
450
451        let world = self.get_world();
452        enchantment_helper::do_post_piercing_attack_effects(&world, self);
453        was_hurt
454    }
455
456    fn item_attack_interaction(
457        &self,
458        entity: &dyn Entity,
459        damage_source: &DamageSource,
460        apply_to_target: bool,
461    ) {
462        let post_attack_context =
463            EnchantmentPostAttackContext::new(entity, Some(self), Some(self), damage_source);
464        let (source_item, item_hurt_enemy) = {
465            let mut inventory = self.inventory.lock();
466            inventory.mutate_item_in_hand(InteractionHand::MainHand, |stack| {
467                if stack.is_empty() {
468                    return (ItemStack::empty(), false);
469                }
470                let behavior = ITEM_BEHAVIORS.get_behavior(stack.item());
471                if let Some(living_target) = entity.as_living_entity() {
472                    behavior.hurt_enemy(stack, living_target, self);
473                }
474                let source_item = stack.copy_with_count(stack.count());
475                (source_item, stack.get_weapon().is_some())
476            })
477        };
478
479        if apply_to_target {
480            let world = self.get_world();
481            enchantment_helper::do_post_attack_effects_with_item_source(
482                &world,
483                entity,
484                &source_item,
485                &post_attack_context,
486            );
487        }
488
489        if !item_hurt_enemy {
490            return;
491        }
492
493        let Some(living_target) = entity.as_living_entity() else {
494            return;
495        };
496        let has_infinite_materials = self.has_infinite_materials();
497        let mut inventory = self.inventory.lock();
498        inventory.mutate_item_in_hand(InteractionHand::MainHand, |stack| {
499            if stack.is_empty() {
500                return;
501            }
502            let behavior = ITEM_BEHAVIORS.get_behavior(stack.item());
503            behavior.post_hurt_enemy(stack, living_target, self);
504            if let Some(damage) = behavior.item_damage_per_attack(stack) {
505                stack.hurt_and_break(damage, has_infinite_materials);
506            }
507        });
508    }
509
510    /// Interacts with an entity using the held item.
511    pub fn interact_on(
512        &self,
513        entity: &dyn Entity,
514        hand: InteractionHand,
515        location: DVec3,
516    ) -> InteractionResult {
517        if self.is_spectator() {
518            // TODO: Open entity menu providers in spectator once that foundation exists.
519            return InteractionResult::Pass;
520        }
521
522        let inventory_access = InventoryAccess::new(self.inventory.clone(), hand);
523        let original_count = inventory_access.with_item(|item| item.count);
524        let result = entity.interact(self, hand, location);
525
526        if self.has_infinite_materials() {
527            inventory_access.with_item(|item| {
528                if item.count < original_count {
529                    item.count = original_count;
530                }
531            });
532        }
533
534        if result.consumes_action() {
535            return result;
536        }
537
538        if inventory_access.with_item(|item| item.is_empty()) {
539            return InteractionResult::Pass;
540        }
541        let Some(living_entity) = entity.as_living_entity() else {
542            return InteractionResult::Pass;
543        };
544        let result = living_entity.interact_living_entity_with_equippable(self, hand);
545        if self.has_infinite_materials() {
546            inventory_access.with_item(|item| {
547                if item.count < original_count {
548                    item.count = original_count;
549                }
550            });
551        }
552        if result.consumes_action() {
553            return result;
554        }
555
556        let item_ref = inventory_access.with_item(|item| item.item());
557        let item_behavior = ITEM_BEHAVIORS.get_behavior(item_ref);
558        let result = inventory_access.with_item(|item| {
559            item_behavior.interact_living_entity(item, self, living_entity, hand)
560        });
561        if self.has_infinite_materials() {
562            inventory_access.with_item(|item| {
563                if item.count < original_count {
564                    item.count = original_count;
565                }
566            });
567        }
568        result
569    }
570
571    /// Handles a client request to attack an entity.
572    pub fn handle_attack(&self, packet: SAttack) {
573        if !self.has_client_loaded() || self.is_spectator() {
574            return;
575        }
576
577        let world = self.get_world();
578        let Some(target) = world.get_accessible_entity_by_id(packet.entity_id) else {
579            return;
580        };
581
582        self.reset_last_action_time();
583
584        let target_pos = target.block_position();
585        if !world.world_border_snapshot().is_within_bounds_with_margin(
586            f64::from(target_pos.x()),
587            f64::from(target_pos.z()),
588            0.0,
589        ) {
590            return;
591        }
592
593        let main_hand_item = {
594            let inventory = self.inventory.lock();
595            let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
596            stack.copy_with_count(stack.count())
597        };
598
599        if !self.is_within_attack_range_with_buffer(
600            &main_hand_item,
601            target.bounding_box(),
602            ATTACK_RANGE_BUFFER,
603        ) {
604            return;
605        }
606
607        if main_hand_item.get_piercing_weapon().is_some() {
608            return;
609        }
610
611        if Self::is_invalid_attack_target(self.id(), target.id(), target.entity_type()) {
612            self.disconnect(Self::invalid_entity_attacked_message());
613            log::warn!(
614                "Player {} tried to attack an invalid entity",
615                self.gameprofile.name
616            );
617            return;
618        }
619
620        if self.cannot_attack_with_item(&main_hand_item, 5) {
621            return;
622        }
623
624        let _ = self.attack(&target);
625    }
626
627    pub(super) fn cannot_attack_with_item(&self, item_stack: &ItemStack, tolerance: i32) -> bool {
628        let required_strength = item_stack.minimum_attack_charge();
629        if required_strength <= 0.0 {
630            return false;
631        }
632
633        let optimistic_strength = {
634            let ticker = self.tick_state.lock().attack_strength_ticker() + tolerance;
635            ticker as f32 / self.current_item_attack_strength_delay()
636        };
637        optimistic_strength < required_strength
638    }
639
640    pub(super) fn is_invalid_attack_target(
641        player_id: i32,
642        target_id: i32,
643        target_type: EntityTypeRef,
644    ) -> bool {
645        target_id == player_id
646            || target_type == &vanilla_entities::ITEM
647            || target_type == &vanilla_entities::EXPERIENCE_ORB
648    }
649
650    /// Handles a client request to interact with an entity.
651    pub fn handle_interact(&self, packet: SInteract) {
652        if !self.has_client_loaded() {
653            return;
654        }
655
656        let world = self.get_world();
657        self.reset_last_action_time();
658        let target = world.get_accessible_entity_by_id(packet.entity_id);
659        self.set_crouching(packet.using_secondary_action);
660        let Some(target) = target else {
661            return;
662        };
663
664        let target_pos = target.block_position();
665        if !world.world_border_snapshot().is_within_bounds_with_margin(
666            f64::from(target_pos.x()),
667            f64::from(target_pos.z()),
668            0.0,
669        ) {
670            return;
671        }
672
673        if !self.is_within_entity_interaction_range_with_buffer(
674            target.bounding_box(),
675            ENTITY_INTERACTION_RANGE_BUFFER,
676        ) {
677            return;
678        }
679
680        let result = self.interact_on(target.as_ref(), packet.hand, packet.location);
681        if result.should_swing_server() {
682            self.swing(packet.hand, true);
683        }
684        self.broadcast_inventory_changes();
685    }
686
687    /// Awards stats and sends particles when an entity gets attacked by this player.
688    pub fn damage_stats_and_hearts(&self, entity: &dyn Entity, old_entity_living_health: f32) {
689        const PARTICLES_PER_HEALTH: f32 = 0.5;
690        const PARTICLE_SPREAD_XZ: f64 = 0.1;
691        const PARTICLE_SPEED: f64 = 0.2;
692
693        if let Some(entity) = entity.as_living_entity() {
694            let actual_damage = old_entity_living_health - entity.get_health();
695            self.award_custom_stat_with_count(
696                &vanilla_custom_stats::DAMAGE_DEALT,
697                (actual_damage * 10.0).round() as i32,
698            );
699
700            let count = (actual_damage * 0.5).round() as i32;
701            let offset = DVec3::new(
702                0.0,
703                f64::from(entity.base().dimensions().height * PARTICLES_PER_HEALTH),
704                0.0,
705            );
706            self.get_world().send_particles(
707                ParticleData::simple(&vanilla_particle_types::DAMAGE_INDICATOR),
708                entity.position().add(offset),
709                count,
710                DVec3::new(PARTICLE_SPREAD_XZ, 0.0, PARTICLE_SPREAD_XZ),
711                PARTICLE_SPEED,
712            );
713        }
714    }
715}