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