1mod pathfinder;
4
5use crate::entity::leash::{LeashData, Leashable};
6pub use pathfinder::PathfinderMob;
7use pathfinder::tick_path_navigation_target;
8#[cfg(test)]
9use pathfinder::{find_ground_path_target_surface, path_end_node_can_reach_target};
10
11use std::sync::Arc;
12
13use glam::DVec3;
14use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
15use simdnbt::owned::{NbtCompound, NbtTag};
16use steel_math::trig;
17use steel_math::{DEG_TO_RAD, DEGREE_90, DEGREE_360, RAD_TO_DEG, fast_floor, wrap_degrees};
18use steel_protocol::packets::game::CTakeItemEntity;
19use steel_registry::attribute::AttributeRef;
20use steel_registry::blocks::block_state_ext::BlockStateExt as _;
21use steel_registry::data_components::components::ItemEnchantments;
22use steel_registry::data_components::vanilla_components::CUSTOM_NAME;
23use steel_registry::enchantment_effect::EnchantmentEffectComponent;
24use steel_registry::entity_type::EntityTypeRef;
25use steel_registry::item_stack::ItemStack;
26use steel_registry::items::ItemRef;
27use steel_registry::loot_table::LootTableRef;
28use steel_registry::sound_event::SoundEventRef;
29use steel_registry::vanilla_block_tags::BlockTag;
30use steel_registry::{
31 REGISTRY, RegistryExt, TaggedRegistryExt, vanilla_attributes, vanilla_damage_types,
32 vanilla_entities, vanilla_entity_type_tags::EntityTypeTag, vanilla_game_events,
33 vanilla_game_rules, vanilla_items,
34};
35use steel_utils::locks::SyncMutex;
36use steel_utils::nbt::NbtNumeric as _;
37use steel_utils::nbt::NbtValueInput as _;
38use steel_utils::types::{Difficulty, InteractionHand};
39use steel_utils::{
40 BlockPos, ChunkPos, Downcast as _, Identifier, TICKS_PER_SECOND, WorldAabb, axis::Axis,
41};
42
43use crate::behavior::items::SpawnEggItem;
44use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext, ITEM_BEHAVIORS, InteractionResult};
45use crate::enchantment_helper::{self, EnchantmentDamageContext, EnchantmentPostAttackContext};
46use crate::entity::ai::control::{
47 BodyRotationInput, MobControls, MoveControlOperation, rotate_if_necessary, rotate_towards,
48};
49use crate::entity::ai::goal::{GoalControl, GoalSelector};
50use crate::entity::ai::navigation::PathNavigation;
51use crate::entity::ai::path::{PathType, PathfindingContext, PathfindingMalus};
52use crate::entity::ai::sensing::Sensing;
53use crate::entity::ai::walk::WalkPathEvaluator;
54use crate::entity::attribute::{AttributeModifier, AttributeModifierOperation};
55use crate::entity::conversion::{AfterConversion, convert_to as convert_mob};
56use crate::entity::damage::DamageSource;
57use crate::entity::entities::objects::items::ItemEntity;
58use crate::entity::{
59 ConversionParams, Entity, EntitySpawnReason, LivingEntity, LivingTravelInput, RemovalReason,
60 SharedEntity, SpawnGroupData, WeakEntity, aabb_contains_any_liquid,
61};
62use crate::inventory::equipment::EquipmentSlot;
63use crate::physics::MoveResult;
64use crate::player::Player;
65use crate::world::game_event::GameEventContext;
66use crate::world::{LevelReader, World};
67
68const MOB_FLAG_NO_AI: i8 = 1;
69const MOB_FLAG_LEFT_HANDED: i8 = 2;
70const MOB_FLAG_AGGRESSIVE: i8 = 4;
71const MOVE_CONTROL_MIN_SPEED_SQR: f64 = 2.500_000_3e-7;
72const MOVE_CONTROL_MAX_TURN: f32 = DEGREE_90;
73const DEFAULT_EQUIPMENT_DROP_CHANCE: f32 = 0.085;
74const REPLACED_EQUIPMENT_DROP_BIAS: f32 = 0.1;
77const PRESERVE_ITEM_DROP_CHANCE_THRESHOLD: f32 = 1.0;
78const PRESERVE_ITEM_DROP_CHANCE: f32 = 2.0;
79const BODY_ROTATION_MOVING_DISTANCE_SQR: f64 = 2.500_000_3e-7;
80const TARGET_REACH_DISTANCE_SQR: f64 = 2.25;
81const DEFAULT_ATTACK_REACH_BASE: f32 = 2.04;
82const DEFAULT_ATTACK_REACH_OFFSET: f32 = 0.6;
83const RANDOM_SPAWN_BONUS_ID: Identifier = Identifier::vanilla_static("random_spawn_bonus");
84const RANDOM_SPAWN_BONUS_SCALE: f64 = 0.114_850_000_000_000_01;
85const LEFT_HANDED_SPAWN_CHANCE: f32 = 0.05;
86const ITEM_PICKUP_REACH: DVec3 = DVec3::new(1.0, 0.0, 1.0);
89const SUN_BURN_IGNITE_SECONDS: f32 = 8.0;
90const SUN_BURN_IGNITE_TICKS: i32 = (SUN_BURN_IGNITE_SECONDS * TICKS_PER_SECOND as f32) as i32;
91pub(crate) const LIGHT_MAGIC_VALUE_BRIGHTNESS_GATE: f32 = 0.5;
93const SUN_BURN_RANDOM_SCALE: f32 = 30.0;
94const SUN_BURN_LIGHT_OFFSET: f32 = 0.4;
95const SUN_BURN_LIGHT_GAIN: f32 = 2.0;
96const SUN_BURN_DURABILITY_ROLL_BOUND: i32 = 2;
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99struct DropChances {
100 by_equipment: [f32; EquipmentSlot::ALL.len()],
101}
102
103impl DropChances {
104 const DEFAULT: Self = Self {
105 by_equipment: [DEFAULT_EQUIPMENT_DROP_CHANCE; EquipmentSlot::ALL.len()],
106 };
107
108 #[must_use]
109 const fn by_equipment(self, slot: EquipmentSlot) -> f32 {
110 self.by_equipment[slot.index()]
111 }
112
113 const fn set_guaranteed_drop(&mut self, slot: EquipmentSlot) {
114 self.by_equipment[slot.index()] = PRESERVE_ITEM_DROP_CHANCE;
115 }
116
117 fn set_equipment_chance(&mut self, slot: EquipmentSlot, chance: f32) -> bool {
118 if chance < 0.0 {
119 return false;
120 }
121
122 self.by_equipment[slot.index()] = chance;
123 true
124 }
125
126 #[must_use]
127 fn is_preserved(self, slot: EquipmentSlot) -> bool {
128 self.by_equipment(slot) > PRESERVE_ITEM_DROP_CHANCE_THRESHOLD
129 }
130
131 fn save(self, nbt: &mut NbtCompound) {
132 if self == Self::DEFAULT {
133 return;
134 }
135
136 let mut drop_chances = NbtCompound::new();
137 for slot in EquipmentSlot::ALL {
138 let chance = self.by_equipment(slot);
139 if chance.to_bits() != DEFAULT_EQUIPMENT_DROP_CHANCE.to_bits() {
140 drop_chances.insert(slot.name(), chance);
141 }
142 }
143
144 nbt.insert("drop_chances", NbtTag::Compound(drop_chances));
145 }
146
147 fn load(nbt: BorrowedNbtCompoundView<'_, '_>) -> Self {
148 let Some(drop_chances) = nbt.compound("drop_chances") else {
149 return Self::DEFAULT;
150 };
151
152 let mut loaded = Self::DEFAULT;
153 for slot in EquipmentSlot::ALL {
154 let Some(chance) = drop_chances
155 .get(slot.name())
156 .and_then(|tag| tag.codec_f32())
157 else {
158 continue;
159 };
160 if !loaded.set_equipment_chance(slot, chance) {
161 return Self::DEFAULT;
162 }
163 }
164
165 loaded
166 }
167}
168
169#[derive(Debug)]
170pub struct MobBase {
171 goal_selector: SyncMutex<GoalSelector>,
172 target_selector: SyncMutex<GoalSelector>,
173 target: SyncMutex<Option<WeakEntity>>,
174 sensing: SyncMutex<Sensing>,
175 controls: SyncMutex<MobControls>,
176 navigation: SyncMutex<PathNavigation>,
177 pathfinding_malus: SyncMutex<PathfindingMalus>,
178 persistence_required: SyncMutex<bool>,
179 can_pick_up_loot: SyncMutex<bool>,
180 drop_chances: SyncMutex<DropChances>,
181 home_restriction: SyncMutex<MobHomeRestriction>,
182 death_loot_table: SyncMutex<Option<Identifier>>,
183 death_loot_table_seed: SyncMutex<i64>,
184 leash_data: SyncMutex<Option<LeashData>>,
185 ambient_sound_time: SyncMutex<i32>,
186 xp_reward: SyncMutex<i32>,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190struct MobHomeRestriction {
191 position: BlockPos,
192 radius: i32,
193}
194impl MobHomeRestriction {
195 const fn none() -> Self {
196 Self {
197 position: BlockPos::ZERO,
198 radius: -1,
199 }
200 }
201}
202
203impl MobBase {
204 #[must_use]
205 pub fn new() -> Self {
206 Self {
207 goal_selector: SyncMutex::new(GoalSelector::new()),
208 target_selector: SyncMutex::new(GoalSelector::new()),
209 target: SyncMutex::new(None),
210 sensing: SyncMutex::new(Sensing::new()),
211 controls: SyncMutex::new(MobControls::new()),
212 navigation: SyncMutex::new(PathNavigation::new()),
213 pathfinding_malus: SyncMutex::new(PathfindingMalus::new()),
214 persistence_required: SyncMutex::new(false),
215 can_pick_up_loot: SyncMutex::new(false),
216 drop_chances: SyncMutex::new(DropChances::DEFAULT),
217 home_restriction: SyncMutex::new(MobHomeRestriction::none()),
218 death_loot_table: SyncMutex::new(None),
219 death_loot_table_seed: SyncMutex::new(0),
220 leash_data: SyncMutex::new(None),
221 ambient_sound_time: SyncMutex::new(0),
222 xp_reward: SyncMutex::new(0),
223 }
224 }
225
226 #[must_use]
227 pub const fn goal_selector(&self) -> &SyncMutex<GoalSelector> {
228 &self.goal_selector
229 }
230
231 #[must_use]
232 pub const fn target_selector(&self) -> &SyncMutex<GoalSelector> {
233 &self.target_selector
234 }
235
236 #[must_use]
237 pub(crate) const fn sensing(&self) -> &SyncMutex<Sensing> {
238 &self.sensing
239 }
240
241 #[must_use]
242 pub fn target(&self, is_valid: impl Fn(&dyn LivingEntity) -> bool) -> Option<SharedEntity> {
243 let mut target = self.target.lock();
244 let Some(upgraded) = target.as_ref().and_then(WeakEntity::upgrade) else {
245 *target = None;
246 return None;
247 };
248 let living_target = upgraded.as_living_entity()?;
249 if !is_valid(living_target) {
250 return None;
251 }
252 Some(upgraded)
253 }
254
255 pub fn set_target(
256 &self,
257 target: Option<&SharedEntity>,
258 is_valid: impl Fn(&dyn LivingEntity) -> bool,
259 ) -> bool {
260 let Some(target) = target else {
261 *self.target.lock() = None;
262 return true;
263 };
264 if !target.is_living_entity() {
265 return false;
266 }
267 let Some(living_target) = target.as_living_entity() else {
268 return false;
269 };
270 if !is_valid(living_target) {
271 *self.target.lock() = None;
272 return false;
273 }
274
275 *self.target.lock() = Some(Arc::downgrade(target));
276 true
277 }
278
279 #[must_use]
280 pub const fn controls(&self) -> &SyncMutex<MobControls> {
281 &self.controls
282 }
283
284 #[must_use]
285 pub const fn navigation(&self) -> &SyncMutex<PathNavigation> {
286 &self.navigation
287 }
288
289 #[must_use]
290 pub const fn pathfinding_malus(&self) -> &SyncMutex<PathfindingMalus> {
291 &self.pathfinding_malus
292 }
293
294 #[must_use]
295 pub const fn persistence_required(&self) -> &SyncMutex<bool> {
296 &self.persistence_required
297 }
298
299 pub const fn can_pick_up_loot(&self) -> &SyncMutex<bool> {
300 &self.can_pick_up_loot
301 }
302
303 const fn drop_chances(&self) -> &SyncMutex<DropChances> {
304 &self.drop_chances
305 }
306
307 const fn home_restriction(&self) -> &SyncMutex<MobHomeRestriction> {
308 &self.home_restriction
309 }
310
311 const fn death_loot_table(&self) -> &SyncMutex<Option<Identifier>> {
312 &self.death_loot_table
313 }
314
315 const fn death_loot_table_seed(&self) -> &SyncMutex<i64> {
316 &self.death_loot_table_seed
317 }
318
319 const fn leash_data(&self) -> &SyncMutex<Option<LeashData>> {
320 &self.leash_data
321 }
322
323 #[must_use]
324 pub fn ambient_sound_time(&self) -> i32 {
325 *self.ambient_sound_time.lock()
326 }
327
328 pub fn set_ambient_sound_time(&self, ambient_sound_time: i32) {
329 *self.ambient_sound_time.lock() = ambient_sound_time;
330 }
331
332 fn get_and_increment_ambient_sound_time(&self) -> i32 {
333 let mut ambient_sound_time = self.ambient_sound_time.lock();
334 let previous = *ambient_sound_time;
335 *ambient_sound_time += 1;
336 previous
337 }
338
339 #[must_use]
340 pub fn xp_reward(&self) -> i32 {
341 *self.xp_reward.lock()
342 }
343
344 pub fn set_xp_reward(&self, xp_reward: i32) {
345 *self.xp_reward.lock() = xp_reward;
346 }
347}
348
349impl Default for MobBase {
350 fn default() -> Self {
351 Self::new()
352 }
353}
354
355pub trait Mob: LivingEntity + Leashable {
356 fn mob_base(&self) -> &MobBase;
357
358 fn mob_flags(&self) -> i8;
359
360 fn set_mob_flags(&self, flags: i8);
361
362 fn check_spawn_rules(&self, _world: &Arc<World>, _reason: EntitySpawnReason) -> bool {
364 true
365 }
366
367 fn set_baby(&self, baby: bool) {
370 let Some(ageable) = self.as_ageable_mob() else {
371 return;
372 };
373 let age = if baby {
374 ageable.get_baby_start_age()
375 } else {
376 0
377 };
378 ageable.set_age(age);
379 }
380
381 fn is_saddled(&self) -> bool {
383 let mut is_saddled = false;
384 self.with_equipment_slot(EquipmentSlot::Saddle, &mut |item_stack| {
385 is_saddled = self.is_equippable_in_slot(item_stack, EquipmentSlot::Saddle);
386 });
387 is_saddled
388 }
389
390 fn has_line_of_sight_cached(&self, target: &dyn Entity) -> bool {
392 self.mob_base()
393 .sensing()
394 .lock()
395 .has_line_of_sight(target.id(), || self.has_line_of_sight(target))
396 }
397
398 fn custom_server_ai_step(&self) {}
399
400 fn ate(&self) {}
402
403 fn tick_goal_selectors(&self, _entity: &SharedEntity) {}
404
405 fn xp_reward(&self) -> i32 {
406 self.mob_base().xp_reward()
407 }
408
409 fn set_xp_reward(&self, xp_reward: i32) {
410 self.mob_base().set_xp_reward(xp_reward);
411 }
412
413 fn target(&self) -> Option<SharedEntity> {
415 self.mob_base()
416 .target(|target| self.is_valid_target(target))
417 }
418
419 fn set_target(&self, target: Option<&SharedEntity>) -> bool {
423 self.mob_base()
424 .set_target(target, |target| self.is_valid_target(target))
425 }
426
427 fn is_valid_target(&self, target: &dyn LivingEntity) -> bool {
428 if target
429 .as_player()
430 .is_some_and(|player| player.has_infinite_materials() || player.is_spectator())
431 {
432 return false;
433 }
434
435 Mob::can_attack(self, target)
436 }
437
438 fn can_attack(&self, target: &dyn LivingEntity) -> bool {
441 target.entity_type() != &vanilla_entities::GHAST && LivingEntity::can_attack(self, target)
442 }
443
444 fn base_experience_reward_mob(&self) -> i32 {
445 let xp_reward = self.xp_reward();
446 if xp_reward <= 0 {
447 return xp_reward;
448 }
449
450 let mut result = xp_reward;
451 for slot in EquipmentSlot::ALL {
452 if !slot.can_increase_experience() {
453 continue;
454 }
455
456 let should_increase = {
457 let equipment = self.living_base().equipment().lock();
458 !equipment.get_ref(slot).is_empty() && self.equipment_drop_chance(slot) <= 1.0
459 };
460 if should_increase {
461 result += 1 + rand::random_range(0..3);
462 }
463 }
464 result
465 }
466
467 fn ambient_sound_interval(&self) -> i32 {
468 if let Some(animal) = self.as_animal() {
469 return animal.ambient_sound_interval_animal();
470 }
471
472 80
473 }
474
475 fn ambient_sound(&self) -> Option<SoundEventRef> {
476 None
477 }
478
479 fn play_ambient_sound(&self) {
480 self.make_sound(self.ambient_sound());
481 }
482
483 fn reset_ambient_sound_time(&self) {
484 self.mob_base()
485 .set_ambient_sound_time(-self.ambient_sound_interval());
486 }
487
488 fn base_tick_mob(&self) {
490 self.base_tick_living_entity();
491 self.mob_base_tick();
492 }
493
494 fn mob_base_tick(&self) {
496 if !LivingEntity::is_alive(self) {
497 return;
498 }
499
500 let ambient_sound_time = self.mob_base().get_and_increment_ambient_sound_time();
501 if rand::random_range(0..1000) < ambient_sound_time {
502 self.reset_ambient_sound_time();
503 self.play_ambient_sound();
504 }
505 }
506
507 fn finalize_spawn(
508 &self,
509 world: &Arc<World>,
510 spawn_reason: EntitySpawnReason,
511 group_data: Option<SpawnGroupData>,
512 ) -> Option<SpawnGroupData> {
513 self.finalize_spawn_mob_base(world, spawn_reason, group_data)
514 }
515
516 fn finalize_spawn_mob_base(
517 &self,
518 _world: &Arc<World>,
519 _spawn_reason: EntitySpawnReason,
520 group_data: Option<SpawnGroupData>,
521 ) -> Option<SpawnGroupData> {
522 let needs_random_spawn_bonus = !self
523 .attributes()
524 .lock()
525 .has_modifier(vanilla_attributes::FOLLOW_RANGE, &RANDOM_SPAWN_BONUS_ID);
526 let random_spawn_bonus = needs_random_spawn_bonus
527 .then(|| RANDOM_SPAWN_BONUS_SCALE * (rand::random::<f64>() - rand::random::<f64>()));
528 let left_handed = rand::random::<f32>() < LEFT_HANDED_SPAWN_CHANCE;
529
530 if let Some(amount) = random_spawn_bonus {
531 self.attributes().lock().add_modifier(
532 vanilla_attributes::FOLLOW_RANGE,
533 AttributeModifier {
534 id: RANDOM_SPAWN_BONUS_ID,
535 amount,
536 operation: AttributeModifierOperation::AddMultipliedBase,
537 },
538 true,
539 );
540 }
541 self.set_left_handed(left_handed);
542 group_data
543 }
544
545 fn convert_to(
550 &self,
551 entity_type: EntityTypeRef,
552 params: ConversionParams,
553 after_conversion: Option<&AfterConversion<'_>>,
554 ) -> Option<SharedEntity>
555 where
556 Self: Sized,
557 {
558 convert_mob(self, entity_type, params, after_conversion)
559 }
560
561 fn check_mob_spawn_rules(
565 entity_type: EntityTypeRef,
566 level: &dyn LevelReader,
567 spawn_reason: EntitySpawnReason,
568 pos: BlockPos,
569 ) -> bool
570 where
571 Self: Sized,
572 {
573 if spawn_reason.is_spawner() {
574 return true;
575 }
576
577 let below = pos.below();
578 let state = level.get_block_state(below);
579 BLOCK_BEHAVIORS
580 .get_behavior(state.get_block())
581 .is_valid_spawn(state, level, below, entity_type)
582 }
583
584 fn check_spawn_obstruction(&self, world: &Arc<World>) -> bool {
585 let aabb = self.bounding_box();
586 if aabb_contains_any_liquid(world, aabb) {
587 return false;
588 }
589
590 !world.has_entity_in_aabb_matching(&aabb, |entity| {
591 entity.id() != self.id()
592 && !entity.is_removed()
593 && !entity.is_spectator()
594 && entity.blocks_building()
595 && !entity.is_passenger_of_same_vehicle(self.as_entity_event_source())
596 })
597 }
598
599 fn interact_mob(
601 &self,
602 player: &Player,
603 hand: InteractionHand,
604 location: DVec3,
605 ) -> InteractionResult {
606 if !LivingEntity::is_alive(self) {
607 return InteractionResult::Pass;
608 }
609
610 let important_interaction = self.check_and_handle_important_interactions(player, hand);
611 if important_interaction.consumes_action() {
612 if let Some(world) = self.level() {
613 world.game_event(
614 &vanilla_game_events::ENTITY_INTERACT,
615 self.block_position(),
616 &GameEventContext::new(Some(player), None),
617 );
618 }
619 return important_interaction;
620 }
621
622 let interaction_result = self.interact_entity(player, hand, location);
623 if interaction_result != InteractionResult::Pass {
624 return interaction_result;
625 }
626
627 let interaction_result = self.mob_interact(player, hand);
628 if interaction_result.consumes_action() {
629 self.game_event_with_source_entity(&vanilla_game_events::ENTITY_INTERACT, Some(player));
630 }
631
632 interaction_result
633 }
634
635 fn check_and_handle_important_interactions(
638 &self,
639 player: &Player,
640 hand: InteractionHand,
641 ) -> InteractionResult {
642 let Some(living_entity) = self.as_living_entity() else {
643 return InteractionResult::Pass;
644 };
645
646 let item = {
647 let inventory = player.inventory.lock();
648 inventory.get_item_in_hand(hand).item()
649 };
650
651 if item.key == vanilla_items::NAME_TAG.key {
652 let name_tag_result = {
653 let mut inventory = player.inventory.lock();
654 let item_stack = inventory.get_item_in_hand_mut(hand);
655 ITEM_BEHAVIORS.get_behavior(item).interact_living_entity(
656 item_stack,
657 player,
658 living_entity,
659 hand,
660 )
661 };
662 if name_tag_result.consumes_action() {
663 return name_tag_result;
664 }
665 }
666
667 if ITEM_BEHAVIORS.get_behavior(item).as_spawn_egg().is_some() {
668 let spawn_egg_result = {
669 let mut inventory = player.inventory.lock();
670 let item_stack = inventory.get_item_in_hand_mut(hand);
671 SpawnEggItem::interact_with_mob(item_stack, player, self)
672 };
673 if spawn_egg_result.consumes_action() {
674 return spawn_egg_result;
675 }
676 }
677
678 InteractionResult::Pass
679 }
680
681 fn mob_interact(&self, _player: &Player, _hand: InteractionHand) -> InteractionResult {
684 InteractionResult::Pass
685 }
686
687 fn can_shear_equipment(&self, _player: &Player) -> bool {
690 !self.is_vehicle()
691 }
692
693 fn use_player_item(&self, player: &Player, hand: InteractionHand) {
695 player.inventory.lock().shrink_item_in_hand(hand, 1);
696 }
698
699 fn remove_when_far_away(&self, dist_sqr: f64) -> bool {
700 self.as_animal()
701 .is_none_or(|animal| animal.remove_when_far_away_animal(dist_sqr))
702 }
703
704 fn requires_custom_persistence(&self) -> bool {
705 self.is_passenger() || self.is_leashed()
706 }
707
708 fn is_persistence_required(&self) -> bool {
709 *self.mob_base().persistence_required().lock()
710 }
711
712 fn set_persistence_required(&self) {
713 *self.mob_base().persistence_required().lock() = true;
714 }
715
716 fn mob_can_be_leashed(&self) -> bool {
718 !self.is_enemy()
719 }
720
721 fn can_pick_up_loot(&self) -> bool {
723 *self.mob_base().can_pick_up_loot().lock()
724 }
725
726 fn set_can_pick_up_loot(&self, can_pick_up_loot: bool) {
727 *self.mob_base().can_pick_up_loot().lock() = can_pick_up_loot;
728 }
729
730 fn equipment_drop_chance(&self, slot: EquipmentSlot) -> f32 {
731 self.mob_base().drop_chances().lock().by_equipment(slot)
732 }
733
734 fn set_equipment_drop_chance(&self, slot: EquipmentSlot, chance: f32) {
735 self.mob_base()
736 .drop_chances()
737 .lock()
738 .set_equipment_chance(slot, chance);
739 }
740
741 fn is_equipment_drop_preserved(&self, slot: EquipmentSlot) -> bool {
742 self.mob_base().drop_chances().lock().is_preserved(slot)
743 }
744
745 fn set_guaranteed_drop(&self, slot: EquipmentSlot) {
746 self.mob_base()
747 .drop_chances()
748 .lock()
749 .set_guaranteed_drop(slot);
750 }
751
752 fn get_pickup_reach(&self) -> DVec3 {
755 ITEM_PICKUP_REACH
756 }
757
758 fn wants_to_pick_up(&self, item_stack: &ItemStack) -> bool {
761 self.can_hold_item(item_stack)
762 }
763
764 fn can_hold_item(&self, _item_stack: &ItemStack) -> bool {
767 true
768 }
769
770 fn pick_up_item(&self, world: &Arc<World>, item_entity: &ItemEntity) {
777 let equipped = self.equip_item_if_possible(item_entity.get_item());
778 if equipped.is_empty() {
779 return;
780 }
781
782 let count = equipped.count();
783 let chunk_pos = ChunkPos::from_entity_pos(item_entity.position());
787 world.broadcast_to_nearby(
788 chunk_pos,
789 CTakeItemEntity::new(item_entity.id(), self.id(), count),
790 None,
791 );
792
793 let mut remaining = item_entity.get_item();
794 remaining.shrink(count);
795 if remaining.is_empty() {
796 item_entity.set_removed(RemovalReason::Discarded);
797 } else {
798 item_entity.set_item(remaining);
799 }
800 }
801
802 fn equip_item_if_possible(&self, mut item_stack: ItemStack) -> ItemStack {
808 let mut slot = self.get_equipment_slot_for_item(&item_stack);
809 if !self.is_equippable_in_slot(&item_stack, slot) {
810 return ItemStack::empty();
811 }
812
813 let mut current = self.equipment_in_slot(slot);
814 let mut can_replace = self.can_replace_current_item(&item_stack, ¤t, slot);
815 if slot.is_armor() && !can_replace {
818 slot = EquipmentSlot::MainHand;
819 current = self.equipment_in_slot(slot);
820 can_replace = current.is_empty();
821 }
822
823 if !can_replace || !self.can_hold_item(&item_stack) {
824 return ItemStack::empty();
825 }
826
827 let drop_chance = self.equipment_drop_chance(slot);
828 if !current.is_empty()
829 && (rand::random::<f32>() - REPLACED_EQUIPMENT_DROP_BIAS).max(0.0) < drop_chance
830 {
831 self.spawn_at_location(current, 0.0);
832 }
833
834 let to_equip = slot.limit(&mut item_stack);
835 let equipped = to_equip.copy_with_count(to_equip.count());
836 self.living_base().equipment().lock().set(slot, to_equip);
837 self.set_guaranteed_drop(slot);
838 self.set_persistence_required();
839 equipped
840 }
841
842 fn get_equipment_slot_for_item(&self, item_stack: &ItemStack) -> EquipmentSlot {
846 match item_stack.get_equippable() {
847 Some(equippable) if self.can_use_slot(equippable.slot) => equippable.slot,
848 _ => EquipmentSlot::MainHand,
849 }
850 }
851
852 fn equipment_in_slot(&self, slot: EquipmentSlot) -> ItemStack {
854 let equipment = self.living_base().equipment().lock();
855 let stack = equipment.get_ref(slot);
856 stack.copy_with_count(stack.count())
857 }
858
859 fn can_replace_current_item(
862 &self,
863 new_item_stack: &ItemStack,
864 current_item_stack: &ItemStack,
865 slot: EquipmentSlot,
866 ) -> bool {
867 if current_item_stack.is_empty() {
868 true
869 } else if slot.is_armor() {
870 self.compare_armor(new_item_stack, current_item_stack, slot)
871 } else if slot == EquipmentSlot::MainHand {
872 self.compare_weapons(new_item_stack, current_item_stack, slot)
873 } else {
874 false
875 }
876 }
877
878 #[expect(
882 clippy::float_cmp,
883 reason = "vanilla compares approximate attribute values for exact equality"
884 )]
885 fn compare_armor(
886 &self,
887 new_item_stack: &ItemStack,
888 current_item_stack: &ItemStack,
889 slot: EquipmentSlot,
890 ) -> bool {
891 if current_item_stack.has_enchantment_effect(EnchantmentEffectComponent::PreventArmorChange)
892 {
893 return false;
894 }
895
896 let new_defense =
897 self.approximate_attribute_with(new_item_stack, vanilla_attributes::ARMOR, slot);
898 let old_defense =
899 self.approximate_attribute_with(current_item_stack, vanilla_attributes::ARMOR, slot);
900 if new_defense != old_defense {
901 return new_defense > old_defense;
902 }
903
904 let new_toughness = self.approximate_attribute_with(
905 new_item_stack,
906 vanilla_attributes::ARMOR_TOUGHNESS,
907 slot,
908 );
909 let old_toughness = self.approximate_attribute_with(
910 current_item_stack,
911 vanilla_attributes::ARMOR_TOUGHNESS,
912 slot,
913 );
914 if new_toughness == old_toughness {
915 self.can_replace_equal_item(new_item_stack, current_item_stack)
916 } else {
917 new_toughness > old_toughness
918 }
919 }
920
921 fn get_preferred_weapon_type(&self) -> Option<Identifier> {
925 None
926 }
927
928 #[expect(
932 clippy::float_cmp,
933 reason = "vanilla compares approximate attribute values for exact equality"
934 )]
935 fn compare_weapons(
936 &self,
937 new_item_stack: &ItemStack,
938 current_item_stack: &ItemStack,
939 slot: EquipmentSlot,
940 ) -> bool {
941 if let Some(preferred_weapon_type) = self.get_preferred_weapon_type() {
942 let current_is_preferred = REGISTRY
943 .items
944 .is_in_tag(current_item_stack.item(), &preferred_weapon_type);
945 let new_is_preferred = REGISTRY
946 .items
947 .is_in_tag(new_item_stack.item(), &preferred_weapon_type);
948 if current_is_preferred && !new_is_preferred {
949 return false;
950 }
951 if !current_is_preferred && new_is_preferred {
952 return true;
953 }
954 }
955
956 let new_attack_damage = self.approximate_attribute_with(
957 new_item_stack,
958 vanilla_attributes::ATTACK_DAMAGE,
959 slot,
960 );
961 let old_attack_damage = self.approximate_attribute_with(
962 current_item_stack,
963 vanilla_attributes::ATTACK_DAMAGE,
964 slot,
965 );
966 if new_attack_damage == old_attack_damage {
967 self.can_replace_equal_item(new_item_stack, current_item_stack)
968 } else {
969 new_attack_damage > old_attack_damage
970 }
971 }
972
973 fn approximate_attribute_with(
978 &self,
979 item_stack: &ItemStack,
980 attribute: AttributeRef,
981 slot: EquipmentSlot,
982 ) -> f64 {
983 let base_value = self
984 .attributes()
985 .lock()
986 .get_base_value(attribute)
987 .unwrap_or(0.0);
988 item_stack
989 .get_attribute_modifiers()
990 .map_or(base_value, |modifiers| {
991 modifiers.compute(attribute, base_value, slot)
992 })
993 }
994
995 fn can_replace_equal_item(
998 &self,
999 new_item_stack: &ItemStack,
1000 current_item_stack: &ItemStack,
1001 ) -> bool {
1002 let new_enchantments = new_item_stack
1003 .get_enchantments()
1004 .map_or(0, ItemEnchantments::len);
1005 let current_enchantments = current_item_stack
1006 .get_enchantments()
1007 .map_or(0, ItemEnchantments::len);
1008 if new_enchantments != current_enchantments {
1009 return new_enchantments > current_enchantments;
1010 }
1011
1012 let new_damage = new_item_stack.get_damage_value();
1013 let current_damage = current_item_stack.get_damage_value();
1014 if new_damage == current_damage {
1015 new_item_stack.has(CUSTOM_NAME) && !current_item_stack.has(CUSTOM_NAME)
1016 } else {
1017 new_damage < current_damage
1018 }
1019 }
1020
1021 fn tick_looting(&self) {
1024 let Some(world) = self.level() else {
1025 return;
1026 };
1027 if !self.can_pick_up_loot()
1028 || !Entity::is_alive(self)
1029 || !world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING)
1030 {
1031 return;
1032 }
1033
1034 let reach = self.get_pickup_reach();
1035 let search_box = self.bounding_box().inflate_xyz(reach.x, reach.y, reach.z);
1036 for entity in world.get_entities_in_aabb(&search_box) {
1037 let Some(item_entity) = entity.downcast_ref::<ItemEntity>() else {
1038 continue;
1039 };
1040 let item = item_entity.get_item();
1041 if item_entity.is_removed()
1042 || item.is_empty()
1043 || item_entity.has_pickup_delay()
1044 || !self.wants_to_pick_up(&item)
1045 {
1046 continue;
1047 }
1048
1049 self.pick_up_item(&world, item_entity);
1050 }
1051 }
1052
1053 fn drop_custom_death_loot_mob(&self, _source: &DamageSource, killed_by_player: bool) {
1054 if self.level().is_none() {
1055 return;
1056 }
1057
1058 for slot in EquipmentSlot::ALL {
1059 let drop_chance = self.equipment_drop_chance(slot);
1060 let preserve = self.is_equipment_drop_preserved(slot);
1061 if !can_attempt_equipment_drop(drop_chance, preserve, killed_by_player) {
1062 continue;
1063 }
1064
1065 let can_drop_item = {
1066 let equipment = self.living_base().equipment().lock();
1067 let item_stack = equipment.get_ref(slot);
1068 !item_stack.is_empty()
1069 && !item_stack
1070 .has_enchantment_effect(EnchantmentEffectComponent::PreventEquipmentDrop)
1071 };
1072 if !can_drop_item {
1073 continue;
1074 }
1075
1076 let random_roll = rand::random::<f32>();
1079 if random_roll >= drop_chance {
1080 continue;
1081 }
1082
1083 let mut item_stack = {
1084 let mut equipment = self.living_base().equipment().lock();
1085 let item_stack = equipment.get_ref(slot);
1086 if item_stack.is_empty()
1087 || item_stack
1088 .has_enchantment_effect(EnchantmentEffectComponent::PreventEquipmentDrop)
1089 {
1090 continue;
1091 }
1092
1093 equipment.take(slot)
1094 };
1095 if !preserve && item_stack.is_damageable_item() {
1096 let max_damage = item_stack.get_max_damage();
1097 let inner = rand::random_range(0..(max_damage - 3).max(1));
1098 let damage = max_damage - rand::random_range(0..=inner);
1099 item_stack.set_damage_value(damage);
1100 }
1101
1102 self.spawn_at_location(item_stack, 0.0);
1103 }
1104 }
1105
1106 fn save_mob(&self, nbt: &mut NbtCompound) {
1107 self.save_equipment(nbt);
1108 nbt.insert("CanPickUpLoot", i8::from(self.can_pick_up_loot()));
1109 nbt.insert(
1110 "PersistenceRequired",
1111 i8::from(self.is_persistence_required()),
1112 );
1113 self.mob_base().drop_chances().lock().save(nbt);
1114 if let Some(leash_data) = self.mob_base().leash_data().lock().as_ref() {
1115 leash_data.save(nbt);
1116 }
1117
1118 if self.has_home() {
1119 let home = *self.mob_base().home_restriction().lock();
1120 nbt.insert("home_radius", home.radius);
1121 nbt.insert(
1122 "home_pos",
1123 NbtTag::IntArray(vec![
1124 home.position.x(),
1125 home.position.y(),
1126 home.position.z(),
1127 ]),
1128 );
1129 }
1130
1131 nbt.insert("LeftHanded", i8::from(self.is_left_handed()));
1132 if let Some(loot_table) = self.mob_base().death_loot_table().lock().as_ref() {
1133 nbt.insert("DeathLootTable", loot_table.to_string());
1134 }
1135 let loot_table_seed = *self.mob_base().death_loot_table_seed().lock();
1136 if loot_table_seed != 0 {
1137 nbt.insert("DeathLootTableSeed", loot_table_seed);
1138 }
1139 if self.is_no_ai() {
1140 nbt.insert("NoAI", i8::from(true));
1141 }
1142 }
1143
1144 fn load_mob(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
1145 self.load_equipment(nbt);
1146 self.set_can_pick_up_loot(
1147 nbt.byte_value("CanPickUpLoot")
1148 .is_some_and(|value| value != 0),
1149 );
1150 *self.mob_base().persistence_required().lock() = nbt
1151 .byte_value("PersistenceRequired")
1152 .is_some_and(|value| value != 0);
1153 *self.mob_base().drop_chances().lock() = DropChances::load(nbt);
1154 *self.mob_base().leash_data().lock() = LeashData::load(nbt);
1155 let home_radius = nbt.int_value("home_radius").unwrap_or(-1);
1156 if home_radius >= 0 {
1157 let home_position = nbt
1158 .int_array("home_pos")
1159 .filter(|position| position.len() == 3)
1160 .map_or(BlockPos::ZERO, |position| {
1161 BlockPos::new(position[0], position[1], position[2])
1162 });
1163 self.set_home_to(home_position, home_radius);
1164 } else {
1165 self.clear_home();
1166 }
1167
1168 self.set_left_handed(nbt.byte_value("LeftHanded").is_some_and(|value| value != 0));
1169 let death_loot_table = nbt
1170 .string("DeathLootTable")
1171 .and_then(|loot_table| loot_table.to_str().as_ref().parse().ok());
1172 *self.mob_base().death_loot_table().lock() = death_loot_table;
1173 *self.mob_base().death_loot_table_seed().lock() =
1174 nbt.long_value("DeathLootTableSeed").unwrap_or(0);
1175 self.set_no_ai(nbt.byte_value("NoAI").is_some_and(|value| value != 0));
1176 }
1177
1178 fn set_death_loot_table(&self, loot_table: Option<Identifier>) {
1179 *self.mob_base().death_loot_table().lock() = loot_table;
1180 }
1181
1182 fn set_death_loot_table_seed(&self, seed: i64) {
1183 *self.mob_base().death_loot_table_seed().lock() = seed;
1184 }
1185
1186 fn custom_death_loot_table(&self) -> Option<LootTableRef> {
1187 self.mob_base()
1188 .death_loot_table()
1189 .lock()
1190 .as_ref()
1191 .and_then(|key| REGISTRY.loot_tables.by_key(key))
1192 }
1193
1194 fn has_custom_death_loot_table(&self) -> bool {
1195 self.mob_base().death_loot_table().lock().is_some()
1196 }
1197
1198 fn death_loot_table_seed(&self) -> i64 {
1199 *self.mob_base().death_loot_table_seed().lock()
1200 }
1201
1202 fn clear_custom_death_loot_table(&self) {
1203 *self.mob_base().death_loot_table().lock() = None;
1204 }
1205
1206 fn is_within_home(&self) -> bool {
1207 self.is_within_home_pos(self.block_position())
1208 }
1209
1210 fn is_within_home_pos(&self, pos: BlockPos) -> bool {
1211 let home = *self.mob_base().home_restriction().lock();
1212 home.radius == -1
1213 || block_pos_distance_sqr(home.position, pos) < home_radius_sqr(home.radius)
1214 }
1215
1216 fn is_within_home_vec(&self, pos: DVec3) -> bool {
1217 let home = *self.mob_base().home_restriction().lock();
1218 home.radius == -1
1219 || block_center_distance_sqr(home.position, pos) < home_radius_sqr(home.radius)
1220 }
1221
1222 fn set_home_to(&self, position: BlockPos, radius: i32) {
1223 *self.mob_base().home_restriction().lock() = MobHomeRestriction { position, radius };
1224 }
1225
1226 fn home_position(&self) -> BlockPos {
1227 self.mob_base().home_restriction().lock().position
1228 }
1229
1230 fn home_radius(&self) -> i32 {
1231 self.mob_base().home_restriction().lock().radius
1232 }
1233
1234 fn clear_home(&self) {
1235 self.mob_base().home_restriction().lock().radius = -1;
1236 }
1237
1238 fn has_home(&self) -> bool {
1239 self.home_radius() != -1
1240 }
1241
1242 fn check_mob_despawn(&self) {
1243 if self
1244 .level()
1245 .is_some_and(|world| world.difficulty() == Difficulty::Peaceful)
1246 && !self.entity_type().allowed_in_peaceful
1247 {
1248 self.set_removed(RemovalReason::Discarded);
1249 return;
1250 }
1251
1252 if self.is_persistence_required() || self.requires_custom_persistence() {
1253 self.set_no_action_time(0);
1254 return;
1255 }
1256
1257 let Some(nearest_player_dist_sqr) = self.nearest_player_distance_sqr() else {
1258 return;
1259 };
1260
1261 let mob_category = self.entity_type().mob_category;
1262 let despawn_distance = mob_category.despawn_distance();
1263 let despawn_distance_sqr = despawn_distance * despawn_distance;
1264 if nearest_player_dist_sqr > f64::from(despawn_distance_sqr)
1265 && self.remove_when_far_away(nearest_player_dist_sqr)
1266 {
1267 self.set_removed(RemovalReason::Discarded);
1268 return;
1269 }
1270
1271 let no_despawn_distance = mob_category.no_despawn_distance();
1272 let no_despawn_distance_sqr = no_despawn_distance * no_despawn_distance;
1273 if self.no_action_time() > 600
1274 && nearest_player_dist_sqr > f64::from(no_despawn_distance_sqr)
1275 && self.remove_when_far_away(nearest_player_dist_sqr)
1276 {
1277 let should_discard = rand::random_range(0..800) == 0;
1278 if should_discard {
1279 self.set_removed(RemovalReason::Discarded);
1280 }
1281 } else if nearest_player_dist_sqr < f64::from(no_despawn_distance_sqr) {
1282 self.set_no_action_time(0);
1283 }
1284 }
1285
1286 fn nearest_player_distance_sqr(&self) -> Option<f64> {
1287 let world = self.level()?;
1288 world.nearest_player_distance_sqr(self.position())
1289 }
1290
1291 fn controlling_passenger_mob(&self) -> Option<SharedEntity> {
1292 let first_passenger = self.first_passenger()?;
1293 if self.is_no_ai() || !first_passenger.is_mob() || !first_passenger.can_control_vehicle() {
1294 return None;
1295 }
1296
1297 Some(first_passenger)
1298 }
1299
1300 fn get_pathfinding_malus(&self, path_type: PathType) -> f32 {
1301 self.mob_base().pathfinding_malus().lock().get(path_type)
1302 }
1303
1304 fn max_fall_distance(&self) -> i32 {
1306 3
1307 }
1308
1309 fn set_pathfinding_malus(&self, path_type: PathType, malus: f32) {
1310 self.mob_base()
1311 .pathfinding_malus()
1312 .lock()
1313 .set(path_type, malus);
1314 }
1315
1316 fn is_no_ai(&self) -> bool {
1317 self.mob_flags() & MOB_FLAG_NO_AI != 0
1318 }
1319
1320 fn set_no_ai(&self, no_ai: bool) {
1321 self.set_mob_flag(MOB_FLAG_NO_AI, no_ai);
1322 }
1323
1324 fn is_left_handed(&self) -> bool {
1325 self.mob_flags() & MOB_FLAG_LEFT_HANDED != 0
1326 }
1327
1328 fn set_left_handed(&self, left_handed: bool) {
1329 self.set_mob_flag(MOB_FLAG_LEFT_HANDED, left_handed);
1330 }
1331
1332 fn is_aggressive(&self) -> bool {
1333 self.mob_flags() & MOB_FLAG_AGGRESSIVE != 0
1334 }
1335
1336 fn max_head_x_rot(&self) -> f32 {
1338 40.0
1339 }
1340
1341 fn max_head_y_rot(&self) -> f32 {
1343 75.0
1344 }
1345
1346 #[must_use]
1350 fn do_hurt_target(&self, entity: &SharedEntity, world: &World, target: &SharedEntity) -> bool {
1351 let Some(attacker) = self.as_entity_event_source().as_living_entity() else {
1352 return false;
1353 };
1354 let weapon_item = {
1355 let mut main_hand = ItemStack::empty();
1356 self.with_equipment_slot(EquipmentSlot::MainHand, &mut |item_stack| {
1357 main_hand = item_stack.copy_with_count(item_stack.count());
1358 });
1359 main_hand
1360 };
1361 let attack_damage = self
1362 .attributes()
1363 .lock()
1364 .required_value(vanilla_attributes::ATTACK_DAMAGE) as f32;
1365 let damage_source = self.mob_attack_damage_source(&weapon_item, entity);
1366 let enchantment_context = EnchantmentDamageContext::new(
1367 target.entity_type(),
1368 Some(self.entity_type()),
1369 Some(self.entity_type()),
1370 &damage_source,
1371 );
1372 let mut damage =
1373 enchantment_helper::modify_damage(&weapon_item, &enchantment_context, attack_damage);
1374 damage += ITEM_BEHAVIORS
1375 .get_behavior(weapon_item.item())
1376 .get_attack_damage_bonus(attacker, target.as_ref(), damage, &damage_source);
1377
1378 let old_movement = target.velocity();
1379 let was_hurt = target.hurt(world, &damage_source, damage);
1380 if was_hurt {
1381 self.cause_extra_knockback(
1382 target.as_ref(),
1383 self.get_attack_knockback(target.as_ref(), &weapon_item, &damage_source),
1384 old_movement,
1385 );
1386 self.with_equipment_slot_mut(EquipmentSlot::MainHand, &mut |stack| {
1387 if stack.is_empty() {
1388 return;
1389 }
1390 if let Some(living_target) = target.as_living_entity() {
1391 ITEM_BEHAVIORS.get_behavior(stack.item()).hurt_enemy(
1392 stack,
1393 living_target,
1394 attacker,
1395 );
1396 }
1397 });
1398 let post_attack_context = EnchantmentPostAttackContext::new(target, &damage_source);
1399 enchantment_helper::do_post_attack_effects_from_item(
1400 world,
1401 &weapon_item,
1402 &post_attack_context,
1403 );
1404 self.set_last_hurt_mob(Some(target));
1405 self.play_attack_sound();
1406 }
1407
1408 if let Some(user) = self.as_entity_event_source().as_living_entity() {
1409 enchantment_helper::do_post_piercing_attack_effects(world, user);
1410 }
1411 was_hurt
1412 }
1413
1414 fn mob_attack_damage_source(
1418 &self,
1419 weapon_item: &ItemStack,
1420 attacker: &SharedEntity,
1421 ) -> DamageSource {
1422 let damage_source = if let Some(damage_type) = weapon_item.get_damage_type() {
1423 DamageSource::environment(damage_type)
1424 } else {
1425 ITEM_BEHAVIORS
1426 .get_behavior(weapon_item.item())
1427 .get_item_damage_source(attacker)
1428 .unwrap_or_else(|| DamageSource::environment(&vanilla_damage_types::MOB_ATTACK))
1429 };
1430
1431 damage_source
1432 .with_causing_entity(Arc::clone(attacker))
1433 .with_direct_entity(Arc::clone(attacker))
1434 }
1435
1436 fn get_attack_knockback(
1439 &self,
1440 target: &dyn Entity,
1441 weapon_item: &ItemStack,
1442 damage_source: &DamageSource,
1443 ) -> f64 {
1444 let attack_knockback = self
1445 .attributes()
1446 .lock()
1447 .required_value(vanilla_attributes::ATTACK_KNOCKBACK);
1448 let enchantment_context = EnchantmentDamageContext::new(
1449 target.entity_type(),
1450 Some(self.entity_type()),
1451 Some(self.entity_type()),
1452 damage_source,
1453 );
1454 let modified = enchantment_helper::modify_knockback(
1455 weapon_item,
1456 &enchantment_context,
1457 attack_knockback as f32,
1458 );
1459 f64::from(modified) / 2.0
1460 }
1461
1462 fn cause_extra_knockback(
1465 &self,
1466 target: &dyn Entity,
1467 knockback_amount: f64,
1468 _old_movement: DVec3,
1469 ) {
1470 if knockback_amount <= 0.0 {
1471 return;
1472 }
1473 let Some(living_target) = target.as_living_entity() else {
1474 return;
1475 };
1476
1477 let yaw_radians = self.rotation().0.to_radians();
1478 let yaw_sin = f64::from(trig::sin(yaw_radians));
1479 let yaw_cos = f64::from(trig::cos(yaw_radians));
1480 living_target.knockback(knockback_amount, yaw_sin, -yaw_cos);
1481
1482 let velocity = self.velocity();
1483 self.set_velocity(DVec3::new(velocity.x * 0.6, velocity.y, velocity.z * 0.6));
1484 }
1485
1486 fn play_attack_sound(&self) {}
1488
1489 fn is_within_melee_attack_range(&self, target: &dyn LivingEntity) -> bool {
1491 let max_range = default_attack_reach();
1493 let min_range = 0.0;
1494 let target_hitbox = target.bounding_box();
1495 self.attack_bounding_box(max_range)
1496 .intersects(target_hitbox)
1497 && (min_range <= 0.0
1498 || !self
1499 .attack_bounding_box(min_range)
1500 .intersects(target_hitbox))
1501 }
1502
1503 fn attack_bounding_box(&self, horizontal_expansion: f64) -> WorldAabb {
1506 let own_aabb = self.bounding_box();
1507 let base = if let Some(vehicle) = self.vehicle() {
1508 let mount_aabb = vehicle.bounding_box();
1509 WorldAabb::new(
1510 own_aabb.min_x().min(mount_aabb.min_x()),
1511 own_aabb.min_y(),
1512 own_aabb.min_z().min(mount_aabb.min_z()),
1513 own_aabb.max_x().max(mount_aabb.max_x()),
1514 own_aabb.max_y(),
1515 own_aabb.max_z().max(mount_aabb.max_z()),
1516 )
1517 } else {
1518 own_aabb
1519 };
1520
1521 base.inflate_xyz(horizontal_expansion, 0.0, horizontal_expansion)
1522 }
1523
1524 fn set_aggressive(&self, aggressive: bool) {
1525 self.set_mob_flag(MOB_FLAG_AGGRESSIVE, aggressive);
1526 }
1527
1528 fn set_mob_flag(&self, flag: i8, enabled: bool) {
1529 let flags = self.mob_flags();
1530 let next = if enabled { flags | flag } else { flags & !flag };
1531 self.set_mob_flags(next);
1532 }
1533
1534 fn controlled_mob_vehicle(&self) -> Option<SharedEntity> {
1535 let vehicle = self.vehicle()?;
1536 if vehicle
1537 .controlling_passenger()
1538 .is_none_or(|passenger| passenger.id() != self.id())
1539 {
1540 return None;
1541 }
1542 vehicle.as_mob()?;
1543 Some(vehicle)
1544 }
1545
1546 fn set_wanted_position(&self, position: DVec3, speed_modifier: f64) {
1547 if let Some(vehicle) = self.controlled_mob_vehicle()
1548 && let Some(mob) = vehicle.as_mob()
1549 {
1550 mob.set_wanted_position(position, speed_modifier);
1551 return;
1552 }
1553
1554 self.mob_base()
1555 .controls()
1556 .lock()
1557 .move_control
1558 .set_wanted_position(position, speed_modifier);
1559 }
1560
1561 fn jump_control_jump(&self) {
1562 self.mob_base().controls().lock().jump_control.jump();
1563 }
1564
1565 fn set_mob_speed(&self, speed: f32) {
1567 self.set_speed(speed);
1568 let input = self.travel_input();
1569 self.set_travel_input(LivingTravelInput::new(
1570 input.sideways(),
1571 input.vertical(),
1572 speed,
1573 ));
1574 }
1575
1576 fn mob_server_ai_step(&self, entity: &SharedEntity) {
1577 self.increment_no_action_time();
1578 self.mob_base().sensing().lock().tick();
1579 if self.tick_count() % 5 == 0 {
1580 self.update_control_flags();
1581 }
1582 self.tick_goal_selectors(entity);
1583 self.tick_path_navigation();
1584 self.custom_server_ai_step();
1585 self.tick_move_control();
1586 self.tick_look_control();
1587 self.tick_jump_control();
1588 }
1589
1590 fn is_sun_burn_tick(&self) -> bool {
1593 let Some(world) = self.level() else {
1594 return false;
1595 };
1596 if !world.monsters_burn() {
1597 return false;
1598 }
1599
1600 let eye_pos = BlockPos::new(
1601 fast_floor(self.position().x),
1602 fast_floor(self.get_eye_y()),
1603 fast_floor(self.position().z),
1604 );
1605 let brightness = world.light_level_dependent_magic_value(eye_pos);
1606 let feet_pos = self.block_position();
1608 let in_rain = world.is_raining_at(feet_pos)
1609 || world.is_raining_at(BlockPos::new(
1610 feet_pos.x(),
1611 fast_floor(self.bounding_box().max_y()),
1612 feet_pos.z(),
1613 ));
1614 let in_non_burnable_block =
1615 self.is_in_water() || in_rain || self.is_in_powder_snow() || self.was_in_powder_snow();
1616 brightness > LIGHT_MAGIC_VALUE_BRIGHTNESS_GATE
1617 && rand::random::<f32>() * SUN_BURN_RANDOM_SCALE
1618 < (brightness - SUN_BURN_LIGHT_OFFSET) * SUN_BURN_LIGHT_GAIN
1619 && !in_non_burnable_block
1620 && world.can_see_sky(eye_pos)
1621 }
1622
1623 fn sun_protection_slot(&self) -> EquipmentSlot {
1624 EquipmentSlot::Head
1625 }
1626
1627 fn tick_daylight_burning(&self) {
1630 if !LivingEntity::is_alive(self) || !self.is_sun_burn_tick() {
1631 return;
1632 }
1633
1634 let slot = self.sun_protection_slot();
1635 let has_infinite_materials = self.has_infinite_materials();
1636 let mut broke_sun_blocker: Option<ItemRef> = None;
1637 {
1638 let mut equipment = self.living_base().equipment().lock();
1639 if equipment.get_ref(slot).is_empty() {
1640 drop(equipment);
1641 self.ignite_for_ticks(SUN_BURN_IGNITE_TICKS);
1642 return;
1643 }
1644 if equipment.get_ref(slot).is_damageable_item() {
1645 let item = equipment.get_ref(slot).item();
1646 let sun_blocker = equipment.get_mut(slot);
1647 if sun_blocker.hurt_and_break(
1648 rand::random_range(0..SUN_BURN_DURABILITY_ROLL_BOUND),
1649 has_infinite_materials,
1650 ) {
1651 broke_sun_blocker = Some(item);
1652 }
1653 }
1654 }
1655
1656 let Some(item) = broke_sun_blocker else {
1657 return;
1658 };
1659 self.on_equipped_item_broken(item, slot);
1660 }
1661
1662 fn mob_ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
1667 let result = self.default_ai_step(entity);
1668 if REGISTRY
1669 .entity_types
1670 .is_in_tag(self.entity_type(), &EntityTypeTag::BURN_IN_DAYLIGHT)
1671 {
1672 self.tick_daylight_burning();
1673 }
1674 self.tick_looting();
1675 result
1676 }
1677
1678 fn tick_path_navigation(&self) {
1679 let Some(world) = self.level() else {
1680 return;
1681 };
1682 let game_time = world.game_time();
1683 self.mob_base().navigation().lock().tick();
1684 tick_path_navigation_target(self, &world, game_time, true);
1685 }
1686
1687 fn tick_move_control(&self) {
1688 let move_control = {
1689 let mut controls = self.mob_base().controls().lock();
1690 let move_control = controls.move_control;
1691 if matches!(move_control.operation(), MoveControlOperation::MoveTo) {
1692 controls.move_control.set_wait();
1693 }
1694 move_control
1695 };
1696
1697 match move_control.operation() {
1698 MoveControlOperation::Wait => {
1699 let input = self.travel_input();
1700 self.set_travel_input(LivingTravelInput::new(
1701 input.sideways(),
1702 input.vertical(),
1703 0.0,
1704 ));
1705 }
1706 MoveControlOperation::MoveTo => self.tick_move_to_control(
1707 move_control.wanted_position(),
1708 move_control.speed_modifier(),
1709 ),
1710 MoveControlOperation::Strafe => {
1711 self.tick_strafe_control(
1712 move_control.strafe_forward(),
1713 move_control.strafe_right(),
1714 );
1715 }
1716 MoveControlOperation::Jumping => {
1717 self.tick_jumping_control(move_control.speed_modifier());
1718 }
1719 }
1720 }
1721
1722 fn tick_move_to_control(&self, wanted_position: DVec3, speed_modifier: f64) {
1723 let position = self.position();
1724 let xd = wanted_position.x - position.x;
1725 let yd = wanted_position.y - position.y;
1726 let zd = wanted_position.z - position.z;
1727 let dd = xd * xd + yd * yd + zd * zd;
1728 if dd < MOVE_CONTROL_MIN_SPEED_SQR {
1729 let input = self.travel_input();
1730 self.set_travel_input(LivingTravelInput::new(
1731 input.sideways(),
1732 input.vertical(),
1733 0.0,
1734 ));
1735 return;
1736 }
1737
1738 let y_rot = zd.atan2(xd) as f32 * RAD_TO_DEG - DEGREE_90;
1739 let (_, pitch) = self.rotation();
1740 self.set_rotation((
1741 rotlerp(self.rotation().0, y_rot, MOVE_CONTROL_MAX_TURN),
1742 pitch,
1743 ));
1744 let movement_speed = self
1745 .attributes()
1746 .lock()
1747 .required_value(vanilla_attributes::MOVEMENT_SPEED);
1748 self.set_mob_speed((speed_modifier * movement_speed) as f32);
1749
1750 if should_jump_to_wanted_position(self, xd, yd, zd) {
1751 self.jump_control_jump();
1752 self.mob_base().controls().lock().move_control.set_jumping();
1753 }
1754 }
1755
1756 fn tick_strafe_control(&self, forward: f32, right: f32) {
1757 let movement_speed = self
1758 .attributes()
1759 .lock()
1760 .required_value(vanilla_attributes::MOVEMENT_SPEED) as f32;
1761 let speed = movement_speed * 0.25;
1762 let mut strafe_forward = forward;
1763 let mut strafe_right = right;
1764
1765 let mut distance = strafe_forward
1766 .mul_add(strafe_forward, strafe_right * strafe_right)
1767 .sqrt();
1768 if distance < 1.0 {
1769 distance = 1.0;
1770 }
1771 distance = speed / distance;
1772 let xa = strafe_forward * distance;
1773 let za = strafe_right * distance;
1774 let yaw_radians = self.rotation().0 * DEG_TO_RAD;
1775 let sin = trig::sin(yaw_radians);
1776 let cos = trig::cos(yaw_radians);
1777 let dx = xa.mul_add(cos, -(za * sin));
1778 let dz = za.mul_add(cos, xa * sin);
1779 if !self.is_strafe_walkable(dx, dz) {
1780 strafe_forward = 1.0;
1781 strafe_right = 0.0;
1782 }
1783
1784 self.set_speed(speed);
1785 self.set_travel_input(LivingTravelInput::new(strafe_right, 0.0, strafe_forward));
1786 self.mob_base().controls().lock().move_control.set_wait();
1787 }
1788
1789 fn is_strafe_walkable(&self, dx: f32, dz: f32) -> bool {
1790 let Some(world) = self.level() else {
1791 return true;
1792 };
1793 let position = self.position();
1794 let pos = BlockPos::new(
1795 fast_floor(position.x + f64::from(dx)),
1796 fast_floor(position.y),
1797 fast_floor(position.z + f64::from(dz)),
1798 );
1799 let mut context = PathfindingContext::new(world.as_ref(), self.block_position());
1800 WalkPathEvaluator::path_type_static(&mut context, pos) == PathType::Walkable
1801 }
1802
1803 fn tick_jumping_control(&self, speed_modifier: f64) {
1804 let movement_speed = self
1805 .attributes()
1806 .lock()
1807 .required_value(vanilla_attributes::MOVEMENT_SPEED);
1808 self.set_mob_speed((speed_modifier * movement_speed) as f32);
1809 if self.on_ground()
1810 || (self.is_in_water() || self.is_in_lava()) && self.is_affected_by_fluids()
1811 {
1812 self.mob_base().controls().lock().move_control.set_wait();
1813 }
1814 }
1815
1816 fn tick_look_control(&self) {
1817 let look_control = {
1818 let mut controls = self.mob_base().controls().lock();
1819 let look_control = controls.look_control;
1820 controls.look_control.tick_cooldown();
1821 look_control
1822 };
1823
1824 let mut rotation = self.rotation();
1825 rotation.1 = 0.0;
1826 if look_control.is_looking_at_target() {
1827 let position = self.position();
1828 let wanted_position = look_control.wanted_position();
1829 let xd = wanted_position.x - position.x;
1830 let yd = wanted_position.y - self.get_eye_y();
1831 let zd = wanted_position.z - position.z;
1832 let horizontal = xd.hypot(zd);
1833 if horizontal.abs() > 1.0e-5 || yd.abs() > 1.0e-5 {
1834 let target_pitch = -(yd.atan2(horizontal)) as f32 * RAD_TO_DEG;
1835 rotation.1 =
1836 rotate_towards(rotation.1, target_pitch, look_control.x_max_rot_angle());
1837 }
1838 if zd.abs() > 1.0e-5 || xd.abs() > 1.0e-5 {
1839 let target_yaw = zd.atan2(xd) as f32 * RAD_TO_DEG - DEGREE_90;
1840 self.set_y_head_rot(rotate_towards(
1841 self.y_head_rot(),
1842 target_yaw,
1843 look_control.y_max_rot_speed(),
1844 ));
1845 }
1846 } else {
1847 self.set_y_head_rot(rotate_towards(self.y_head_rot(), self.y_body_rot(), 10.0));
1848 }
1849
1850 self.set_rotation(rotation);
1851 self.clamp_head_rotation_to_body_when_pathing();
1852 }
1853
1854 fn clamp_head_rotation_to_body_when_pathing(&self) {
1855 if self.mob_base().navigation().lock().is_done() {
1856 return;
1857 }
1858
1859 self.set_y_head_rot(rotate_if_necessary(
1860 self.y_head_rot(),
1861 self.y_body_rot(),
1862 self.max_head_y_rot(),
1863 ));
1864 }
1865
1866 fn tick_jump_control(&self) {
1867 let jumping = self.mob_base().controls().lock().jump_control.tick();
1868 self.set_jumping(jumping);
1869 }
1870
1871 fn update_control_flags(&self) {
1872 let no_controller = self
1873 .controlling_passenger()
1874 .is_none_or(|passenger| !passenger.is_mob());
1875 let not_in_boat = self
1876 .vehicle()
1877 .is_none_or(|vehicle| !vehicle.entity_type().is_abstract_boat);
1878
1879 let mut selector = self.mob_base().goal_selector().lock();
1880 selector.set_control(GoalControl::Move, no_controller);
1881 selector.set_control(GoalControl::Jump, no_controller && not_in_boat);
1882 selector.set_control(GoalControl::Look, no_controller);
1883 }
1884
1885 fn tick_body_rotation_control(&self) {
1886 let moving = {
1887 let delta = self.position() - self.old_position();
1888 delta.x.mul_add(delta.x, delta.z * delta.z) > BODY_ROTATION_MOVING_DISTANCE_SQR
1889 };
1890 let carrying_mob_passenger = self
1891 .first_passenger()
1892 .is_some_and(|passenger| passenger.is_mob());
1893 let input = BodyRotationInput::new(
1894 moving,
1895 carrying_mob_passenger,
1896 self.rotation().0,
1897 self.y_body_rot(),
1898 self.y_head_rot(),
1899 self.max_head_y_rot(),
1900 );
1901 let update = self
1902 .mob_base()
1903 .controls()
1904 .lock()
1905 .body_rotation_control
1906 .tick(input);
1907 self.set_y_body_rot(update.y_body_rot());
1908 self.set_y_head_rot(update.y_head_rot());
1909 }
1910}
1911
1912impl<T: Mob> Leashable for T {
1914 fn leash_data(&self) -> &SyncMutex<Option<LeashData>> {
1915 self.mob_base().leash_data()
1916 }
1917
1918 fn can_be_leashed(&self) -> bool {
1920 self.mob_can_be_leashed()
1921 }
1922
1923 fn on_leash_removed(&self) {
1924 if self.leash_holder().is_none() {
1925 self.clear_home();
1926 }
1927 }
1928
1929 fn leash_too_far_behaviour(&self) {
1931 self.drop_leash();
1932 self.mob_base()
1933 .goal_selector()
1934 .lock()
1935 .disable_control(GoalControl::Move);
1936 }
1937
1938 fn when_leashed_to(&self, holder: &dyn Entity) {
1939 if let Some(pathfinder) = self.as_pathfinder_mob() {
1940 pathfinder.when_leashed_to_pathfinder(holder);
1941 } else {
1942 holder.notify_leash_holder(self.as_entity_event_source());
1943 }
1944 }
1945
1946 fn close_range_leash_behaviour(&self, holder: &dyn Entity) {
1947 if let Some(pathfinder) = self.as_pathfinder_mob() {
1948 pathfinder.close_range_leash_behaviour_pathfinder(holder);
1949 }
1950 }
1951}
1952
1953fn can_attempt_equipment_drop(drop_chance: f32, preserve: bool, killed_by_player: bool) -> bool {
1954 drop_chance != 0.0 && (killed_by_player || preserve)
1955}
1956
1957fn default_attack_reach() -> f64 {
1958 f64::from(DEFAULT_ATTACK_REACH_BASE).sqrt() - f64::from(DEFAULT_ATTACK_REACH_OFFSET)
1959}
1960
1961fn should_jump_to_wanted_position<M: Mob + ?Sized>(mob: &M, xd: f64, yd: f64, zd: f64) -> bool {
1962 let max_up_step = f64::from(mob.max_up_step());
1963 if yd > max_up_step && xd * xd + zd * zd < mob.bounding_box().width().max(1.0) {
1964 return true;
1965 }
1966
1967 let Some(world) = mob.level() else {
1968 return false;
1969 };
1970 let pos = mob.block_position();
1971 let block_state = world.get_block_state(pos);
1972 let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
1973 let shape = behavior.get_collision_shape(
1974 block_state,
1975 world.as_ref(),
1976 pos,
1977 BlockCollisionContext::empty(),
1978 );
1979 let shape_top = position_shape_top(pos, shape.max(Axis::Y));
1980 let block = block_state.get_block();
1981 !shape.is_empty()
1982 && mob.position().y < shape_top
1983 && !block.has_tag(&BlockTag::DOORS)
1984 && !block.has_tag(&BlockTag::FENCES)
1985}
1986
1987fn position_shape_top(pos: BlockPos, local_y: f64) -> f64 {
1988 f64::from(pos.y()) + local_y
1989}
1990
1991pub fn block_pos_distance_sqr(a: BlockPos, b: BlockPos) -> f64 {
1992 let dx = f64::from(a.x() - b.x());
1993 let dy = f64::from(a.y() - b.y());
1994 let dz = f64::from(a.z() - b.z());
1995 dx.mul_add(dx, dy.mul_add(dy, dz * dz))
1996}
1997
1998pub fn block_center_distance_sqr(pos: BlockPos, target: DVec3) -> f64 {
1999 let (x, y, z) = pos.get_center();
2000 DVec3::new(x, y, z).distance_squared(target)
2001}
2002
2003fn home_radius_sqr(radius: i32) -> f64 {
2004 let radius = f64::from(radius);
2005 radius * radius
2006}
2007
2008fn rotlerp(a: f32, b: f32, max: f32) -> f32 {
2009 let mut diff = wrap_degrees(b - a);
2010 if diff > max {
2011 diff = max;
2012 }
2013 if diff < -max {
2014 diff = -max;
2015 }
2016
2017 let mut result = a + diff;
2018 if result < 0.0 {
2019 result += DEGREE_360;
2020 } else if result > DEGREE_360 {
2021 result -= DEGREE_360;
2022 }
2023 result
2024}
2025
2026#[cfg(test)]
2027mod tests;