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