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