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