Skip to main content

steel_core/entity/mob/
mod.rs

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