Skip to main content

steel_core/player/game_mode/
entity_interaction.rs

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