Skip to main content

steel_core/entity/mob/
mod.rs

1//! Vanilla-shaped mob foundations.
2
3mod 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    /// Returns vanilla `Mob.isSaddled`.
340    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    /// Returns vanilla `Mob.getTarget`.
361    fn target(&self) -> Option<SharedEntity> {
362        self.mob_base()
363            .target(|target| self.is_valid_target(target))
364    }
365
366    /// Sets vanilla `Mob.target`.
367    ///
368    /// Returns `false` when the supplied entity is not a living entity.
369    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    /// Returns vanilla `Mob.canAttack`.
386    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    /// Runs vanilla `Mob.baseTick`.
435    fn base_tick_mob(&self) {
436        self.base_tick_living_entity();
437        self.mob_base_tick();
438    }
439
440    /// Runs the mob-owned portion of vanilla `Mob.baseTick`.
441    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    /// Handles vanilla `Mob.interact`.
492    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        // TODO: Handle name tags and spawn eggs once item-on-entity behavior exists.
503        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    /// Handles vanilla `Mob.mobInteract`.
523    fn mob_interact(&self, _player: &Player, _hand: InteractionHand) -> InteractionResult {
524        InteractionResult::Pass
525    }
526
527    /// Returns vanilla `Mob.canShearEquipment`.
528    fn can_shear_equipment(&self, _player: &Player) -> bool {
529        !self.is_vehicle()
530    }
531
532    /// Applies vanilla `Mob.usePlayerItem`.
533    fn use_player_item(&self, player: &Player, hand: InteractionHand) {
534        player.inventory.lock().shrink_item_in_hand(hand, 1);
535        // TODO: Apply USE_REMAINDER components once item use-remainder support exists.
536    }
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    /// Returns vanilla `Mob.canPickUpLoot`.
556    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            // TODO: Apply EquipmentDrops enchantment value effects once damage
603            // sources can resolve their living attacker context.
604            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        // TODO: Return false for enemy mobs once hostile mob foundations exist.
757        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    /// Vanilla `Entity.getMaxFallDistance` baseline.
1098    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    /// Returns vanilla `Mob.getMaxHeadXRot`.
1130    fn max_head_x_rot(&self) -> f32 {
1131        40.0
1132    }
1133
1134    /// Returns vanilla `Mob.getMaxHeadYRot`.
1135    fn max_head_y_rot(&self) -> f32 {
1136        75.0
1137    }
1138
1139    /// Handles vanilla `Mob.doHurtTarget`.
1140    #[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    /// Returns the damage source used by vanilla `ItemStack.getDamageSource`.
1211    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    /// Returns vanilla `LivingEntity.getKnockback` for mob attacks.
1232    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    /// Applies vanilla `LivingEntity.causeExtraKnockback`.
1257    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    /// Plays vanilla `LivingEntity.playAttackSound`.
1280    fn play_attack_sound(&self) {}
1281
1282    /// Returns vanilla `Mob.isWithinMeleeAttackRange`.
1283    fn is_within_melee_attack_range(&self, target: &dyn LivingEntity) -> bool {
1284        // TODO: Use the held item's ATTACK_RANGE component once it has typed component data.
1285        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    /// Returns vanilla `Mob.getAttackBoundingBox`.
1297    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    /// Mirrors vanilla `Mob.setSpeed`: update cached speed and forward AI input.
1358    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;