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 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 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 #[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 #[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 #[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 #[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 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 pub fn interact_on(
503 &self,
504 entity: &dyn Entity,
505 hand: InteractionHand,
506 location: DVec3,
507 ) -> InteractionResult {
508 if self.is_spectator() {
509 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 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 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}