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