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
19const 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 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 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 #[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 #[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 #[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 #[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 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 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 pub fn interact_on(
550 &self,
551 entity: &dyn Entity,
552 hand: InteractionHand,
553 location: DVec3,
554 ) -> InteractionResult {
555 if self.is_spectator() {
556 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 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 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 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}