1435 lines
45 KiB
Rust
1435 lines
45 KiB
Rust
use super::{Entity, EntityBase, ai::pathfinder::Navigator, living::LivingEntity};
|
||
use crate::entity::ai::control::MoveControlTrait;
|
||
use crate::entity::ai::control::look_control::LookControl;
|
||
use crate::entity::ai::control::move_control::MoveControl;
|
||
use crate::entity::ai::goal::goal_selector::GoalSelector;
|
||
use crate::entity::player::Player;
|
||
use crate::server::Server;
|
||
use crate::world::World;
|
||
use crossbeam::atomic::AtomicCell;
|
||
use pumpkin_data::attributes::Attributes;
|
||
use pumpkin_data::damage::DamageType;
|
||
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||
use pumpkin_data::item::Item;
|
||
use pumpkin_data::item_stack::ItemStack;
|
||
use pumpkin_data::tag::{self, Taggable};
|
||
use pumpkin_data::tracked_data;
|
||
use pumpkin_data::{Block, BlockDirection};
|
||
use pumpkin_nbt::compound::NbtCompound;
|
||
use pumpkin_protocol::java::client::play::{CHeadRot, CUpdateEntityRot};
|
||
use pumpkin_util::Difficulty;
|
||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||
use pumpkin_util::math::position::BlockPos;
|
||
use pumpkin_util::math::vector2::Vector2;
|
||
use pumpkin_util::math::vector3::Vector3;
|
||
use pumpkin_util::random::xoroshiro128::Xoroshiro;
|
||
use pumpkin_util::random::{RandomGenerator, get_seed};
|
||
use pumpkin_util::version::JavaMinecraftVersion;
|
||
use rand::RngExt;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::Ordering::Relaxed;
|
||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
|
||
use uuid::Uuid;
|
||
|
||
pub mod bat;
|
||
pub mod blaze;
|
||
pub mod breeze;
|
||
pub mod cave_spider;
|
||
pub mod creaking;
|
||
pub mod creeper;
|
||
pub mod crossbow_attack_mob;
|
||
pub mod elder_guardian;
|
||
pub mod enderman;
|
||
pub mod endermite;
|
||
pub mod equipment;
|
||
pub mod evoker;
|
||
pub mod ghast;
|
||
pub mod giant;
|
||
pub mod guardian;
|
||
pub mod hoglin;
|
||
pub mod illusioner;
|
||
pub mod magma_cube;
|
||
pub mod patrol;
|
||
pub mod phantom;
|
||
pub mod piglin;
|
||
pub mod piglin_ai;
|
||
pub mod piglin_brute;
|
||
pub mod pillager;
|
||
pub mod raider;
|
||
pub mod ravager;
|
||
pub mod shulker;
|
||
pub mod silverfish;
|
||
pub mod skeleton;
|
||
pub mod slime;
|
||
pub mod spider;
|
||
pub mod vex;
|
||
pub mod vindicator;
|
||
pub mod warden;
|
||
pub mod witch;
|
||
pub mod zoglin;
|
||
pub mod zombie;
|
||
pub mod zombified_piglin;
|
||
|
||
pub struct MobEntity {
|
||
pub living_entity: LivingEntity,
|
||
pub goals_selector: std::sync::Mutex<GoalSelector>,
|
||
pub target_selector: std::sync::Mutex<GoalSelector>,
|
||
pub navigator: std::sync::Mutex<Navigator>,
|
||
pub target: std::sync::Mutex<Option<Arc<dyn EntityBase>>>,
|
||
pub look_control: std::sync::Mutex<LookControl>,
|
||
pub move_control: std::sync::Mutex<Box<dyn MoveControlTrait>>,
|
||
pub position_target: AtomicCell<BlockPos>,
|
||
pub position_target_range: AtomicI32,
|
||
pub love_ticks: AtomicI32,
|
||
pub breeding_cooldown: AtomicI32,
|
||
pub breeder: AtomicCell<Option<Uuid>>,
|
||
pub persistence_required: AtomicBool,
|
||
mob_flags: AtomicU8,
|
||
last_sent_yaw: AtomicU8,
|
||
last_sent_pitch: AtomicU8,
|
||
last_sent_head_yaw: AtomicU8,
|
||
}
|
||
impl MobEntity {
|
||
const AI_DISABLED_FLAG: u8 = 1;
|
||
const LEFT_HANDED_FLAG: u8 = 2;
|
||
const ATTACKING_FLAG: u8 = 4;
|
||
const CAN_PICK_UP_LOOT_FLAG: u8 = 8;
|
||
|
||
pub const MAX_WEARING_ARMOR_CHANCE: f32 = 0.15;
|
||
pub const WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE: f32 = 0.1087;
|
||
pub const WEARING_ARMOR_UPGRADE_MATERIAL_ATTEMPTS: f32 = 3.0;
|
||
pub const MAX_PICKUP_LOOT_CHANCE: f32 = 0.55;
|
||
pub const MAX_ENCHANTED_ARMOR_CHANCE: f32 = 0.5;
|
||
pub const MAX_ENCHANTED_WEAPON_CHANCE: f32 = 0.25;
|
||
pub const EQUIPMENT_POPULATION_ORDER: [EquipmentSlot; 4] = [
|
||
EquipmentSlot::HEAD,
|
||
EquipmentSlot::CHEST,
|
||
EquipmentSlot::LEGS,
|
||
EquipmentSlot::FEET,
|
||
];
|
||
|
||
#[must_use]
|
||
pub const fn get_equipment_for_slot(
|
||
slot: &EquipmentSlot,
|
||
armor_type: i32,
|
||
) -> Option<&'static Item> {
|
||
match slot {
|
||
EquipmentSlot::Head(_) => match armor_type {
|
||
0 => Some(&Item::LEATHER_HELMET),
|
||
1 => Some(&Item::COPPER_HELMET),
|
||
2 => Some(&Item::GOLDEN_HELMET),
|
||
3 => Some(&Item::CHAINMAIL_HELMET),
|
||
4 => Some(&Item::IRON_HELMET),
|
||
5 => Some(&Item::DIAMOND_HELMET),
|
||
_ => None,
|
||
},
|
||
EquipmentSlot::Chest(_) => match armor_type {
|
||
0 => Some(&Item::LEATHER_CHESTPLATE),
|
||
1 => Some(&Item::COPPER_CHESTPLATE),
|
||
2 => Some(&Item::GOLDEN_CHESTPLATE),
|
||
3 => Some(&Item::CHAINMAIL_CHESTPLATE),
|
||
4 => Some(&Item::IRON_CHESTPLATE),
|
||
5 => Some(&Item::DIAMOND_CHESTPLATE),
|
||
_ => None,
|
||
},
|
||
EquipmentSlot::Legs(_) => match armor_type {
|
||
0 => Some(&Item::LEATHER_LEGGINGS),
|
||
1 => Some(&Item::COPPER_LEGGINGS),
|
||
2 => Some(&Item::GOLDEN_LEGGINGS),
|
||
3 => Some(&Item::CHAINMAIL_LEGGINGS),
|
||
4 => Some(&Item::IRON_LEGGINGS),
|
||
5 => Some(&Item::DIAMOND_LEGGINGS),
|
||
_ => None,
|
||
},
|
||
EquipmentSlot::Feet(_) => match armor_type {
|
||
0 => Some(&Item::LEATHER_BOOTS),
|
||
1 => Some(&Item::COPPER_BOOTS),
|
||
2 => Some(&Item::GOLDEN_BOOTS),
|
||
3 => Some(&Item::CHAINMAIL_BOOTS),
|
||
4 => Some(&Item::IRON_BOOTS),
|
||
5 => Some(&Item::DIAMOND_BOOTS),
|
||
_ => None,
|
||
},
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
#[must_use]
|
||
pub fn new(entity: Entity) -> Self {
|
||
Self {
|
||
living_entity: LivingEntity::new(entity),
|
||
goals_selector: std::sync::Mutex::new(GoalSelector::default()),
|
||
target_selector: std::sync::Mutex::new(GoalSelector::default()),
|
||
navigator: std::sync::Mutex::new(Navigator::default()),
|
||
target: std::sync::Mutex::new(None),
|
||
look_control: std::sync::Mutex::new(LookControl::default()),
|
||
move_control: std::sync::Mutex::new(Box::new(MoveControl::default())),
|
||
position_target: AtomicCell::new(BlockPos::ZERO),
|
||
position_target_range: AtomicI32::new(-1),
|
||
love_ticks: AtomicI32::new(0),
|
||
breeding_cooldown: AtomicI32::new(0),
|
||
breeder: AtomicCell::new(None),
|
||
persistence_required: AtomicBool::new(false),
|
||
mob_flags: AtomicU8::new(0),
|
||
last_sent_yaw: AtomicU8::new(0),
|
||
last_sent_pitch: AtomicU8::new(0),
|
||
last_sent_head_yaw: AtomicU8::new(0),
|
||
}
|
||
}
|
||
|
||
pub fn has_position_target(&self) -> bool {
|
||
self.position_target_range.load(Relaxed) != -1
|
||
}
|
||
|
||
pub fn is_in_position_target_range(&self) -> bool {
|
||
self.is_in_position_target_range_pos(&self.living_entity.entity.block_pos.load())
|
||
}
|
||
|
||
pub fn is_in_position_target_range_pos(&self, block_pos: &BlockPos) -> bool {
|
||
let position_target_range = self.position_target_range.load(Relaxed);
|
||
if position_target_range == -1 {
|
||
true
|
||
} else {
|
||
self.position_target.load().squared_distance(block_pos)
|
||
< position_target_range * position_target_range
|
||
}
|
||
}
|
||
|
||
pub fn set_attacking(&self, attacking: bool) {
|
||
self.set_mob_flag(Self::ATTACKING_FLAG, attacking);
|
||
}
|
||
|
||
pub fn is_attacking(&self) -> bool {
|
||
(self.mob_flags.load(Relaxed) & Self::ATTACKING_FLAG) != 0
|
||
}
|
||
|
||
pub fn set_left_handed(&self, left_handed: bool) {
|
||
self.set_mob_flag(Self::LEFT_HANDED_FLAG, left_handed);
|
||
}
|
||
|
||
pub fn can_pick_up_loot(&self) -> bool {
|
||
(self.mob_flags.load(Relaxed) & Self::CAN_PICK_UP_LOOT_FLAG) != 0
|
||
}
|
||
|
||
pub fn set_can_pick_up_loot(&self, value: bool) {
|
||
self.set_mob_flag(Self::CAN_PICK_UP_LOOT_FLAG, value);
|
||
}
|
||
|
||
pub fn is_left_handed(&self) -> bool {
|
||
(self.mob_flags.load(Relaxed) & Self::LEFT_HANDED_FLAG) != 0
|
||
}
|
||
|
||
pub fn set_no_ai(&self, no_ai: bool) {
|
||
self.set_mob_flag(Self::AI_DISABLED_FLAG, no_ai);
|
||
}
|
||
|
||
pub fn is_no_ai(&self) -> bool {
|
||
(self.mob_flags.load(Relaxed) & Self::AI_DISABLED_FLAG) != 0
|
||
}
|
||
|
||
pub fn clear_ai_goals(&self, mob: &dyn Mob) {
|
||
let running_goals = self
|
||
.goals_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.clear();
|
||
for mut goal in running_goals {
|
||
goal.goal.stop(mob);
|
||
}
|
||
|
||
let running_target_goals = self
|
||
.target_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.clear();
|
||
for mut goal in running_target_goals {
|
||
goal.goal.stop(mob);
|
||
}
|
||
}
|
||
|
||
pub fn write_mob_nbt(&self, nbt: &mut NbtCompound) {
|
||
if self.is_no_ai() {
|
||
nbt.put_bool("NoAI", true);
|
||
}
|
||
if self.is_left_handed() {
|
||
nbt.put_bool("LeftHanded", true);
|
||
}
|
||
if self.can_pick_up_loot() {
|
||
nbt.put_bool("CanPickUpLoot", true);
|
||
}
|
||
if self.persistence_required.load(Relaxed) {
|
||
nbt.put_bool("PersistenceRequired", true);
|
||
}
|
||
}
|
||
|
||
pub fn read_mob_nbt(&self, nbt: &NbtCompound) {
|
||
if let Some(no_ai) = nbt.get_bool("NoAI") {
|
||
self.set_no_ai(no_ai);
|
||
}
|
||
if let Some(left_handed) = nbt.get_bool("LeftHanded") {
|
||
self.set_left_handed(left_handed);
|
||
}
|
||
if let Some(can_pick_up_loot) = nbt.get_bool("CanPickUpLoot") {
|
||
self.set_can_pick_up_loot(can_pick_up_loot);
|
||
}
|
||
if let Some(persistence_required) = nbt.get_bool("PersistenceRequired") {
|
||
self.persistence_required
|
||
.store(persistence_required, Relaxed);
|
||
}
|
||
}
|
||
|
||
pub fn add_goal<G: crate::entity::ai::goal::Goal + 'static>(&self, priority: u8, goal: G) {
|
||
self.goals_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.add_goal(priority, Box::new(goal));
|
||
}
|
||
|
||
pub fn add_target_goal<G: crate::entity::ai::goal::Goal + 'static>(
|
||
&self,
|
||
priority: u8,
|
||
goal: G,
|
||
) {
|
||
self.target_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.add_goal(priority, Box::new(goal));
|
||
}
|
||
|
||
pub fn set_target(&self, target: Option<Arc<dyn EntityBase>>) {
|
||
let mut t = self
|
||
.target
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
*t = target;
|
||
}
|
||
|
||
pub fn get_target(&self) -> Option<Arc<dyn EntityBase>> {
|
||
self.target
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.clone()
|
||
}
|
||
|
||
fn set_mob_flag(&self, flag: u8, value: bool) {
|
||
let old_b = self.mob_flags.load(Ordering::Relaxed);
|
||
|
||
let new_b = if value { old_b | flag } else { old_b & !flag };
|
||
|
||
if new_b != old_b {
|
||
self.mob_flags.store(new_b, Ordering::Relaxed);
|
||
|
||
self.living_entity
|
||
.entity
|
||
.set_synced_data(tracked_data::mob::DATA_MOB_FLAGS_ID, new_b);
|
||
}
|
||
}
|
||
|
||
pub fn is_in_love(&self) -> bool {
|
||
self.love_ticks.load(Relaxed) > 0
|
||
}
|
||
|
||
pub fn set_love_ticks(&self, ticks: i32, breeder: Option<Uuid>) {
|
||
self.love_ticks.store(ticks, Relaxed);
|
||
self.breeder.store(breeder);
|
||
}
|
||
|
||
pub fn reset_love_ticks(&self) {
|
||
self.love_ticks.store(0, Relaxed);
|
||
}
|
||
|
||
pub fn is_breeding_ready(&self) -> bool {
|
||
self.living_entity.entity.age.load(Relaxed) >= 0
|
||
&& self.breeding_cooldown.load(Relaxed) <= 0
|
||
}
|
||
|
||
pub fn is_in_attack_range(&self, target: &dyn EntityBase) -> bool {
|
||
const DEFAULT_ATTACK_RANGE: f64 = 0.828_427_12; // sqrt(2.04) - 0.6
|
||
|
||
// TODO: Implement DataComponent lookup for ATTACK_RANGE when components are ready
|
||
let max_range = DEFAULT_ATTACK_RANGE;
|
||
let min_range = 0.0;
|
||
|
||
let target_hitbox = target.get_entity().bounding_box.load();
|
||
|
||
if !self.get_attack_box(max_range).intersects(&target_hitbox) {
|
||
return false;
|
||
}
|
||
|
||
min_range <= 0.0 || !self.get_attack_box(min_range).intersects(&target_hitbox)
|
||
}
|
||
|
||
pub fn is_dark_enough_to_spawn(world: &World, pos: &BlockPos, is_thundering: bool) -> bool {
|
||
let sky_light = world.get_sky_light_level(pos);
|
||
if sky_light > rand::random_range(0..32) {
|
||
return false;
|
||
}
|
||
|
||
let dimension = &world.dimension;
|
||
let block_light_limit = dimension.monster_spawn_block_light_limit;
|
||
|
||
let block_light = world.get_block_light_level(pos).unwrap_or(0);
|
||
if block_light_limit < 15 && block_light > block_light_limit {
|
||
return false;
|
||
}
|
||
|
||
let current_brightness = if is_thundering {
|
||
(sky_light - 10).max(block_light)
|
||
} else {
|
||
sky_light.max(block_light)
|
||
};
|
||
|
||
// TODO
|
||
let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(get_seed()));
|
||
current_brightness <= dimension.monster_spawn_light_level.get(&mut random) as u8
|
||
}
|
||
|
||
pub fn check_mob_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||
let below = pos.down();
|
||
let state = world.get_block_state(&below);
|
||
state.is_side_solid(BlockDirection::Up)
|
||
}
|
||
|
||
pub fn check_monster_spawn_rules(world: &World, pos: &BlockPos, is_thundering: bool) -> bool {
|
||
if world.level_info.load().difficulty == Difficulty::Peaceful {
|
||
return false;
|
||
}
|
||
|
||
if !Self::is_dark_enough_to_spawn(world, pos, is_thundering) {
|
||
return false;
|
||
}
|
||
|
||
Self::check_mob_spawn_rules(world, pos)
|
||
}
|
||
|
||
pub fn check_any_light_monster_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||
if world.level_info.load().difficulty == Difficulty::Peaceful {
|
||
return false;
|
||
}
|
||
|
||
Self::check_mob_spawn_rules(world, pos)
|
||
}
|
||
|
||
pub fn check_surface_monsters_spawn_rules(
|
||
world: &World,
|
||
pos: &BlockPos,
|
||
is_thundering: bool,
|
||
) -> bool {
|
||
Self::check_monster_spawn_rules(world, pos, is_thundering) && world.can_see_sky(pos)
|
||
}
|
||
|
||
pub fn check_animal_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||
let below = pos.down();
|
||
world
|
||
.get_block(&below)
|
||
.has_tag(&tag::Block::MINECRAFT_ANIMALS_SPAWNABLE_ON)
|
||
&& Self::is_bright_enough_to_spawn(world, pos)
|
||
}
|
||
|
||
pub fn is_bright_enough_to_spawn(world: &World, pos: &BlockPos) -> bool {
|
||
world.get_max_local_raw_brightness(pos) > 8
|
||
}
|
||
|
||
pub fn check_surface_water_animal_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||
let sea_level = world.sea_level;
|
||
let min_spawn_level = sea_level - 13;
|
||
pos.0.y >= min_spawn_level
|
||
&& pos.0.y <= sea_level
|
||
&& world
|
||
.get_fluid(&pos.down())
|
||
.has_tag(&tag::Fluid::MINECRAFT_WATER)
|
||
&& (world.get_block(&pos.up()) == &Block::WATER
|
||
|| world
|
||
.get_fluid(&pos.up())
|
||
.has_tag(&tag::Fluid::MINECRAFT_WATER))
|
||
}
|
||
|
||
pub fn check_surface_ageable_water_creature_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||
Self::check_surface_water_animal_spawn_rules(world, pos)
|
||
}
|
||
|
||
pub fn try_attack(&self, caller: &dyn EntityBase, target: &dyn EntityBase) {
|
||
if self.living_entity.dead.load(Relaxed) {
|
||
return;
|
||
}
|
||
|
||
let attack_damage: f32 =
|
||
self.living_entity
|
||
.get_attribute_value(&Attributes::ATTACK_DAMAGE) as f32;
|
||
|
||
let damaged = target.damage_with_context(
|
||
target,
|
||
attack_damage,
|
||
DamageType::MOB_ATTACK,
|
||
None,
|
||
Some(caller),
|
||
Some(caller),
|
||
);
|
||
|
||
if damaged {
|
||
self.living_entity
|
||
.last_attacking_id
|
||
.store(target.get_entity().entity_id, Relaxed);
|
||
self.living_entity
|
||
.last_attack_time
|
||
.store(self.living_entity.entity.age.load(Relaxed), Relaxed);
|
||
}
|
||
}
|
||
|
||
fn get_attack_box(&self, attack_range: f64) -> BoundingBox {
|
||
let vehicle_opt = self.living_entity.entity.get_vehicle();
|
||
|
||
let base_box = vehicle_opt.as_ref().map_or_else(
|
||
|| self.living_entity.entity.bounding_box.load(),
|
||
|vehicle| {
|
||
let vehicle_box = vehicle.get_entity().bounding_box.load();
|
||
let my_box = self.living_entity.entity.bounding_box.load();
|
||
|
||
BoundingBox {
|
||
min: Vector3::new(
|
||
my_box.min.x.min(vehicle_box.min.x),
|
||
my_box.min.y,
|
||
my_box.min.z.min(vehicle_box.min.z),
|
||
),
|
||
max: Vector3::new(
|
||
my_box.max.x.max(vehicle_box.max.x),
|
||
my_box.max.y,
|
||
my_box.max.z.max(vehicle_box.max.z),
|
||
),
|
||
}
|
||
},
|
||
);
|
||
|
||
base_box.expand(attack_range, 0.0, attack_range)
|
||
}
|
||
|
||
pub fn tick_sun_burn(&self) {
|
||
if !self
|
||
.living_entity
|
||
.entity
|
||
.entity_type
|
||
.has_tag(&tag::EntityType::MINECRAFT_BURN_IN_DAYLIGHT)
|
||
{
|
||
return;
|
||
}
|
||
if !self.is_sun_burn_tick() {
|
||
return;
|
||
}
|
||
self.apply_sun_burn();
|
||
}
|
||
|
||
fn is_sun_burn_tick(&self) -> bool {
|
||
let entity = &self.living_entity.entity;
|
||
|
||
let world_arc = entity.world.load();
|
||
let world = world_arc.as_ref();
|
||
|
||
let eye_block_pos = entity.get_eye_pos().to_block_pos();
|
||
if !world.monsters_burn(&eye_block_pos) {
|
||
return false;
|
||
}
|
||
|
||
// Vanilla: getLightLevelDependentMagicValue() — sky light at eye pos, scaled 0–1.
|
||
let brightness = world
|
||
.level
|
||
.light_engine
|
||
.get_sky_light_level(&world.level, &eye_block_pos) as f32
|
||
/ 15.0;
|
||
|
||
if brightness <= 0.5 {
|
||
return false;
|
||
}
|
||
|
||
let is_in_non_burnable = entity.touching_water.load(Relaxed)
|
||
|| world.is_raining()
|
||
|| entity.is_in_powder_snow()
|
||
|| entity.was_in_powder_snow.load(Relaxed);
|
||
|
||
if is_in_non_burnable {
|
||
return false;
|
||
}
|
||
|
||
let pos = entity.pos.load();
|
||
let top_y = world.get_top_block(Vector2::new(pos.x as i32, pos.z as i32));
|
||
if (entity.get_eye_y() as i32) < top_y {
|
||
return false;
|
||
}
|
||
|
||
let mut rng = rand::rng();
|
||
rng.random::<f32>() * 30.0 < (brightness - 0.4) * 2.0
|
||
}
|
||
|
||
fn apply_sun_burn(&self) {
|
||
let entity = &self.living_entity.entity;
|
||
entity.set_on_fire_for(8.0);
|
||
}
|
||
|
||
pub fn mob_interact(&self, player: &Arc<Player>, item_stack: &mut ItemStack) -> bool {
|
||
let entity = &self.living_entity.entity;
|
||
|
||
// If already leashed to player, right-clicking unleashes the mob
|
||
let currently_leashed = entity
|
||
.leashed_to
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
.is_some();
|
||
|
||
if currently_leashed {
|
||
entity.unleash();
|
||
let lead_item =
|
||
pumpkin_data::item_stack::ItemStack::new(1, &pumpkin_data::item::Item::LEAD);
|
||
entity
|
||
.world
|
||
.load()
|
||
.drop_stack(&entity.block_pos.load(), lead_item);
|
||
return true;
|
||
}
|
||
|
||
// If holding a lead, leash the mob to the player
|
||
if item_stack.item.registry_key == "lead"
|
||
|| item_stack.item.registry_key == "minecraft:lead"
|
||
{
|
||
let diff = entity.pos.load() - player.get_entity().pos.load();
|
||
let dist_sq = diff.length_squared();
|
||
if dist_sq <= Entity::LEASH_SNAP_DISTANCE * Entity::LEASH_SNAP_DISTANCE {
|
||
entity.leash_to(player.clone() as Arc<dyn EntityBase>);
|
||
if player.gamemode.load() != pumpkin_util::GameMode::Creative {
|
||
item_stack.decrement(1);
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
false
|
||
}
|
||
|
||
pub fn check_despawn(&self, mob: &dyn Mob) {
|
||
let entity = &self.living_entity.entity;
|
||
|
||
if self.persistence_required.load(Relaxed) {
|
||
return;
|
||
}
|
||
|
||
if (**entity.custom_name.load()).is_some() {
|
||
return;
|
||
}
|
||
|
||
let world = entity.world.load();
|
||
let pos = entity.pos.load();
|
||
let players = world.players.load();
|
||
|
||
let nearest_dist_sq = players
|
||
.iter()
|
||
.filter(|p| p.gamemode.load() != pumpkin_util::GameMode::Spectator)
|
||
.map(|p| {
|
||
let pp = p.get_entity().pos.load();
|
||
let dx = pp.x - pos.x;
|
||
let dy = pp.y - pos.y;
|
||
let dz = pp.z - pos.z;
|
||
dx * dx + dy * dy + dz * dz
|
||
})
|
||
.fold(f64::MAX, f64::min);
|
||
|
||
if nearest_dist_sq == f64::MAX {
|
||
mob.get_entity().remove();
|
||
return;
|
||
}
|
||
|
||
if nearest_dist_sq > 128.0 * 128.0 {
|
||
mob.get_entity().remove();
|
||
return;
|
||
}
|
||
|
||
if nearest_dist_sq > 32.0 * 32.0 && rand::random::<i32>().wrapping_abs() % 800 == 0 {
|
||
mob.get_entity().remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
pub trait Mob: EntityBase + Send + Sync {
|
||
fn get_random(&self) -> rand::rngs::ThreadRng {
|
||
rand::rng()
|
||
}
|
||
|
||
fn requires_custom_persistence(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn remove_when_far_away(&self, _distance_sq: f64) -> bool {
|
||
true
|
||
}
|
||
|
||
fn get_max_look_yaw_change(&self) -> f32 {
|
||
10.0
|
||
}
|
||
|
||
fn get_max_look_pitch_change(&self) -> f32 {
|
||
40.0
|
||
}
|
||
|
||
fn get_max_head_rotation(&self) -> f32 {
|
||
75.0
|
||
}
|
||
|
||
fn get_mob_entity(&self) -> &MobEntity;
|
||
|
||
fn mob_bedrock_identifier(&self) -> Option<&'static str> {
|
||
None
|
||
}
|
||
|
||
/// Metadata which must accompany this mob whenever it is spawned for a Java client.
|
||
fn mob_java_spawn_metadata(&self, _version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
|
||
None
|
||
}
|
||
|
||
/// Metadata which must accompany this mob whenever it is spawned for a Bedrock client.
|
||
fn mob_bedrock_spawn_metadata(
|
||
&self,
|
||
) -> Option<pumpkin_protocol::bedrock::client::set_actor_data::SyncedActorDataList> {
|
||
None
|
||
}
|
||
|
||
fn get_job_site(&self) -> Option<BlockPos> {
|
||
None
|
||
}
|
||
|
||
fn is_job_site_pending(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn release_pending_job_site(&self, _position: BlockPos) {}
|
||
|
||
fn get_trading_player(&self) -> Option<Arc<Player>> {
|
||
None
|
||
}
|
||
|
||
fn get_home(&self) -> Option<BlockPos> {
|
||
None
|
||
}
|
||
|
||
fn get_path_aware_entity(&self) -> Option<&dyn PathAwareEntity> {
|
||
None
|
||
}
|
||
|
||
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
|
||
None
|
||
}
|
||
|
||
fn is_saddled(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn can_be_saddled(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn set_saddled(&self, _saddled: bool) {}
|
||
|
||
/// Per-mob tick hook called each tick before AI runs. Override for mob-specific logic.
|
||
fn mob_tick(&self, _caller: &dyn EntityBase) {}
|
||
|
||
fn post_tick(&self) {}
|
||
|
||
/// Called before damage is applied. Return `false` to cancel the damage entirely.
|
||
/// Used by endermen to dodge projectiles via teleportation.
|
||
fn pre_damage(&self, _damage_type: DamageType, _source: Option<&dyn EntityBase>) -> bool {
|
||
true
|
||
}
|
||
|
||
fn on_damage(&self, _damage_type: DamageType, _source: Option<&dyn EntityBase>) {}
|
||
|
||
fn on_attack(&self, _target: &dyn EntityBase) {}
|
||
|
||
fn on_eating_grass(&self) {}
|
||
|
||
fn modify_incoming_damage(&self, amount: f32, _damage_type: DamageType) -> f32 {
|
||
amount
|
||
}
|
||
|
||
fn can_attack_with_owner(&self, _target: &dyn EntityBase, _owner: &dyn EntityBase) -> bool {
|
||
true
|
||
}
|
||
|
||
fn get_mob_gravity(&self) -> f64 {
|
||
self.get_mob_entity().living_entity.get_gravity()
|
||
}
|
||
|
||
fn get_mob_y_velocity_drag(&self) -> Option<f64> {
|
||
None
|
||
}
|
||
|
||
fn as_ageable(&self) -> Option<&dyn crate::entity::ageable::AgeableMob> {
|
||
None
|
||
}
|
||
|
||
fn as_animal(&self) -> Option<&dyn crate::entity::passive::animal::Animal> {
|
||
None
|
||
}
|
||
|
||
fn as_tamable(&self) -> Option<&dyn crate::entity::passive::tamable::TamableAnimal> {
|
||
None
|
||
}
|
||
|
||
fn as_patrolling_monster(&self) -> Option<&dyn patrol::PatrollingMonster> {
|
||
None
|
||
}
|
||
|
||
fn as_raider(&self) -> Option<&dyn raider::Raider> {
|
||
None
|
||
}
|
||
|
||
fn as_iron_golem(&self) -> Option<&crate::entity::passive::iron_golem::IronGolemEntity> {
|
||
None
|
||
}
|
||
|
||
fn as_crossbow_attack_mob(&self) -> Option<&dyn crossbow_attack_mob::CrossbowAttackMob> {
|
||
None
|
||
}
|
||
|
||
fn populate_default_equipment_slots(
|
||
&self,
|
||
_world: &Arc<World>,
|
||
difficulty: &crate::entity::mob::equipment::RegionalDifficulty,
|
||
) {
|
||
if rand::random::<f32>()
|
||
< MobEntity::MAX_WEARING_ARMOR_CHANCE * difficulty.special_multiplier
|
||
{
|
||
let mut armor_type = rand::random_range(0..3);
|
||
for _ in 1..=3 {
|
||
if rand::random::<f32>() < MobEntity::WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE {
|
||
armor_type += 1;
|
||
}
|
||
}
|
||
|
||
let partial_chance = if difficulty.base_difficulty == Difficulty::Hard {
|
||
0.1f32
|
||
} else {
|
||
0.25f32
|
||
};
|
||
|
||
let living = &self.get_mob_entity().living_entity;
|
||
let mut equipment = living
|
||
.entity_equipment
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
let mut first = true;
|
||
|
||
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||
let current = equipment.get(slot);
|
||
if !first && rand::random::<f32>() < partial_chance {
|
||
break;
|
||
}
|
||
first = false;
|
||
if current.is_empty()
|
||
&& let Some(item) = MobEntity::get_equipment_for_slot(slot, armor_type)
|
||
{
|
||
equipment.put(slot, ItemStack::new(1, item));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn populate_default_equipment_enchantments(
|
||
&self,
|
||
difficulty: &crate::entity::mob::equipment::RegionalDifficulty,
|
||
) {
|
||
self.enchant_spawned_weapon(difficulty);
|
||
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||
self.enchant_spawned_armor(slot, difficulty);
|
||
}
|
||
}
|
||
|
||
fn enchant_spawned_weapon(
|
||
&self,
|
||
difficulty: &crate::entity::mob::equipment::RegionalDifficulty,
|
||
) {
|
||
self.enchant_spawned_equipment(
|
||
&EquipmentSlot::MAIN_HAND,
|
||
MobEntity::MAX_ENCHANTED_WEAPON_CHANCE,
|
||
difficulty,
|
||
);
|
||
}
|
||
|
||
fn enchant_spawned_armor(
|
||
&self,
|
||
slot: &EquipmentSlot,
|
||
difficulty: &crate::entity::mob::equipment::RegionalDifficulty,
|
||
) {
|
||
self.enchant_spawned_equipment(slot, MobEntity::MAX_ENCHANTED_ARMOR_CHANCE, difficulty);
|
||
}
|
||
|
||
fn enchant_spawned_equipment(
|
||
&self,
|
||
slot: &EquipmentSlot,
|
||
chance: f32,
|
||
difficulty: &crate::entity::mob::equipment::RegionalDifficulty,
|
||
) {
|
||
let living = &self.get_mob_entity().living_entity;
|
||
let mut equipment = living
|
||
.entity_equipment
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
if let Some(stack) = equipment.equipment.get_mut(slot)
|
||
&& !stack.is_empty()
|
||
&& rand::random::<f32>() < chance * difficulty.special_multiplier
|
||
{
|
||
crate::entity::mob::equipment::apply_vanilla_enchantments(
|
||
stack,
|
||
slot,
|
||
difficulty.special_multiplier,
|
||
);
|
||
}
|
||
}
|
||
|
||
fn mob_write_nbt(&self, _nbt: &mut NbtCompound) {}
|
||
|
||
fn mob_read_nbt(&self, _nbt: &NbtCompound) {}
|
||
|
||
/// Set or clear the mob's target. Override to add side effects when targeting changes.
|
||
fn set_mob_target(&self, target: Option<Arc<dyn EntityBase>>) {
|
||
let mob = self.get_mob_entity();
|
||
let target_id = target.as_ref().map(|t| t.get_entity().entity_id);
|
||
*mob.target
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner) = target;
|
||
let world = mob.living_entity.entity.world.load_full();
|
||
let entity_id = mob.living_entity.entity.entity_id;
|
||
if let Some(server) = world.server.upgrade() {
|
||
let mut event =
|
||
crate::plugin::api::events::entity::entity_target::EntityTargetEvent::new(
|
||
entity_id, target_id,
|
||
);
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn mob_interact(&self, player: &Arc<Player>, item_stack: &mut ItemStack) -> bool {
|
||
self.get_mob_entity().mob_interact(player, item_stack)
|
||
}
|
||
|
||
fn tame(&self, player: &Arc<Player>) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event = crate::plugin::api::events::entity::entity_tame::EntityTameEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
player.clone(),
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn breed(&self, father_id: i32, mother_id: i32, child_id: i32) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event = crate::plugin::api::events::entity::entity_breed::EntityBreedEvent::new(
|
||
father_id, mother_id, child_id,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn dye(
|
||
&self,
|
||
color: crate::plugin::api::events::entity::entity_dye::DyeColor,
|
||
player: Option<&Arc<Player>>,
|
||
) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event = crate::plugin::api::events::entity::entity_dye::EntityDyeEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
color,
|
||
player.cloned(),
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn enter_love_mode(&self, human_entity_id: Option<i32>, ticks_in_love: i32) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event = crate::plugin::api::events::entity::entity_enter_love_mode::EntityEnterLoveModeEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
human_entity_id,
|
||
ticks_in_love,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn transform(&self, new_entity_id: i32, transform_reason: String) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event =
|
||
crate::plugin::api::events::entity::entity_transform::EntityTransformEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
new_entity_id,
|
||
transform_reason,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn break_door(&self, block_pos: BlockPos) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event =
|
||
crate::plugin::api::events::entity::entity_break_door::EntityBreakDoorEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
block_pos,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn enter_block(&self, block_pos: BlockPos) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event =
|
||
crate::plugin::api::events::entity::entity_enter_block::EntityEnterBlockEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
block_pos,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn interact(&self, block_pos: BlockPos) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event =
|
||
crate::plugin::api::events::entity::entity_interact::EntityInteractEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
block_pos,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn place_block(&self, block_pos: BlockPos, block_name: String) {
|
||
let mob = self.get_mob_entity();
|
||
let mut event = crate::plugin::api::events::entity::entity_place::EntityPlaceEvent::new(
|
||
mob.living_entity.entity.entity_id,
|
||
block_pos,
|
||
block_name,
|
||
);
|
||
if let Some(server) = mob.living_entity.entity.world.load().server.upgrade() {
|
||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||
}
|
||
}
|
||
|
||
fn mob_player_collision(&self, _player: &Arc<Player>) {}
|
||
|
||
fn get_owner_uuid(&self) -> Option<Uuid> {
|
||
self.as_tamable()
|
||
.and_then(crate::entity::passive::tamable::TamableAnimal::get_owner)
|
||
}
|
||
|
||
fn is_sitting(&self) -> bool {
|
||
self.as_tamable()
|
||
.is_some_and(crate::entity::passive::tamable::TamableAnimal::is_in_sitting_pose)
|
||
}
|
||
|
||
fn is_tamed(&self) -> bool {
|
||
self.as_tamable()
|
||
.is_some_and(crate::entity::passive::tamable::TamableAnimal::is_tame)
|
||
}
|
||
|
||
fn get_base_experience_reward(&self) -> u32 {
|
||
self.get_entity().entity_type.experience_reward
|
||
}
|
||
|
||
fn mob_init_data_tracker(&self) {
|
||
let entity = self.get_entity();
|
||
let is_baby = entity.age.load(std::sync::atomic::Ordering::Relaxed) < 0;
|
||
if is_baby {
|
||
entity.set_synced_data(tracked_data::ageable_mob::DATA_BABY_ID, true);
|
||
}
|
||
}
|
||
|
||
fn mob_set_variant_name(&self, _name: &str) {}
|
||
|
||
fn mob_on_lightning_strike(
|
||
&self,
|
||
caller: &dyn EntityBase,
|
||
lightning: &crate::entity::lightning::LightningBoltEntity,
|
||
) {
|
||
self.get_mob_entity()
|
||
.living_entity
|
||
.on_lightning_strike(caller, lightning);
|
||
}
|
||
}
|
||
impl<T: Mob + Send + 'static> EntityBase for T {
|
||
fn get_mob(&self) -> Option<&dyn Mob> {
|
||
Some(self)
|
||
}
|
||
|
||
fn on_lightning_strike(
|
||
&self,
|
||
caller: &dyn EntityBase,
|
||
lightning: &crate::entity::lightning::LightningBoltEntity,
|
||
) {
|
||
self.mob_on_lightning_strike(caller, lightning);
|
||
}
|
||
|
||
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
|
||
Mob::get_item_steerable(self)
|
||
}
|
||
|
||
fn init_data_tracker(&self) {
|
||
self.mob_init_data_tracker();
|
||
let world = self.get_mob_entity().living_entity.entity.world.load();
|
||
crate::entity::mob::equipment::equip_mob_on_spawn(self as &dyn EntityBase, &world);
|
||
|
||
let entity_name = self.get_entity().entity_type.resource_name;
|
||
if let Some(def) = crate::entity::mob::equipment::EQUIPMENT_REGISTRY.get(entity_name)
|
||
&& def.can_pick_up_loot
|
||
{
|
||
let difficulty = crate::entity::mob::equipment::RegionalDifficulty::at(
|
||
&world,
|
||
self.get_entity().pos.load(),
|
||
);
|
||
let pickup_chance = 0.55 * difficulty.special_multiplier;
|
||
self.get_mob_entity()
|
||
.set_can_pick_up_loot(rand::random::<f32>() < pickup_chance);
|
||
}
|
||
}
|
||
|
||
fn set_variant_name(&self, name: &str) {
|
||
self.mob_set_variant_name(name);
|
||
}
|
||
|
||
#[allow(clippy::too_many_lines)]
|
||
fn tick(&self, caller: &dyn EntityBase, server: &Server) {
|
||
let mob_entity = self.get_mob_entity();
|
||
mob_entity.living_entity.entity.tick_leash();
|
||
mob_entity.tick_sun_burn();
|
||
|
||
if mob_entity.breeding_cooldown.load(Relaxed) > 0 {
|
||
mob_entity.breeding_cooldown.fetch_sub(1, Relaxed);
|
||
}
|
||
|
||
if mob_entity.love_ticks.load(Relaxed) > 0 {
|
||
let ticks = mob_entity.love_ticks.fetch_sub(1, Relaxed);
|
||
if ticks % 10 == 0 {
|
||
let entity = &mob_entity.living_entity.entity;
|
||
let pos = entity.pos.load();
|
||
let world = entity.world.load();
|
||
world.spawn_particle(
|
||
pos + Vector3::new(0.0, f64::from(entity.height()) + 0.5, 0.0),
|
||
Vector3::new(0.5, 0.5, 0.5),
|
||
1.0,
|
||
1,
|
||
pumpkin_data::particle::Particle::Heart,
|
||
);
|
||
}
|
||
}
|
||
|
||
mob_entity.check_despawn(self);
|
||
|
||
self.mob_tick(caller);
|
||
|
||
let age = mob_entity.living_entity.entity.age.load(Relaxed);
|
||
let entity_id = mob_entity.living_entity.entity.entity_id;
|
||
|
||
// 1. "Take" selectors out of the mutexes
|
||
let mut target_selector = {
|
||
let mut guard = mob_entity
|
||
.target_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
std::mem::take(&mut *guard)
|
||
};
|
||
let mut goals_selector = {
|
||
let mut guard = mob_entity
|
||
.goals_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
std::mem::take(&mut *guard)
|
||
};
|
||
|
||
// 2. Perform AI logic
|
||
if (age + entity_id) % 2 != 0 && age > 1 {
|
||
target_selector.tick_goals(self, false);
|
||
goals_selector.tick_goals(self, false);
|
||
} else {
|
||
target_selector.tick(self);
|
||
goals_selector.tick(self);
|
||
}
|
||
|
||
// 3. "Put back" selectors
|
||
{
|
||
*mob_entity
|
||
.target_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner) = target_selector;
|
||
*mob_entity
|
||
.goals_selector
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner) = goals_selector;
|
||
};
|
||
|
||
// 4. Repeat for Navigator
|
||
let mut navigator = {
|
||
let mut guard = mob_entity
|
||
.navigator
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
std::mem::take(&mut *guard)
|
||
};
|
||
|
||
navigator.tick(&mob_entity.living_entity);
|
||
|
||
{
|
||
*mob_entity
|
||
.navigator
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner) = navigator;
|
||
};
|
||
|
||
// Controllers are synchronous, so we can just use normal blocks
|
||
{
|
||
let mut look_control = mob_entity
|
||
.look_control
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
look_control.tick(self);
|
||
};
|
||
|
||
{
|
||
let mut move_control = mob_entity
|
||
.move_control
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
move_control.tick(self);
|
||
};
|
||
|
||
mob_entity.living_entity.tick(caller, server);
|
||
self.post_tick();
|
||
|
||
// --- Packet logic remains the same ---
|
||
let entity = &mob_entity.living_entity.entity;
|
||
let yaw = (entity.yaw.load() * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
||
let pitch = (entity.pitch.load() * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
||
let head_yaw = (entity.head_yaw.load() * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
||
|
||
let last_yaw = mob_entity.last_sent_yaw.load(Relaxed);
|
||
let last_pitch = mob_entity.last_sent_pitch.load(Relaxed);
|
||
let last_head_yaw = mob_entity.last_sent_head_yaw.load(Relaxed);
|
||
|
||
let chunk_pos = entity.chunk_pos.load();
|
||
if yaw.abs_diff(last_yaw) >= 1 || pitch.abs_diff(last_pitch) >= 1 {
|
||
let world = entity.world.load();
|
||
world.broadcast_to_chunk(
|
||
chunk_pos,
|
||
&CUpdateEntityRot::new(
|
||
entity.entity_id.into(),
|
||
yaw,
|
||
pitch,
|
||
entity.on_ground.load(Relaxed),
|
||
),
|
||
);
|
||
mob_entity.last_sent_yaw.store(yaw, Relaxed);
|
||
mob_entity.last_sent_pitch.store(pitch, Relaxed);
|
||
}
|
||
|
||
if head_yaw.abs_diff(last_head_yaw) >= 1 {
|
||
let world = entity.world.load();
|
||
|
||
world.broadcast_to_chunk(chunk_pos, &CHeadRot::new(entity.entity_id.into(), head_yaw));
|
||
mob_entity.last_sent_head_yaw.store(head_yaw, Relaxed);
|
||
}
|
||
}
|
||
|
||
fn is_collidable(&self, _entity: Option<Box<dyn EntityBase>>) -> bool {
|
||
true
|
||
}
|
||
|
||
fn can_hit(&self) -> bool {
|
||
true
|
||
}
|
||
|
||
fn damage_with_context(
|
||
&self,
|
||
caller: &dyn EntityBase,
|
||
amount: f32,
|
||
damage_type: DamageType,
|
||
position: Option<Vector3<f64>>,
|
||
source: Option<&dyn EntityBase>,
|
||
cause: Option<&dyn EntityBase>,
|
||
) -> bool {
|
||
// pre_damage hook: allows mobs to dodge/cancel damage (e.g. enderman projectile dodge)
|
||
if !self.pre_damage(damage_type, source) {
|
||
return false;
|
||
}
|
||
// Mob-specific damage modifier (e.g. shulker armor when closed).
|
||
let amount = self.modify_incoming_damage(amount, damage_type);
|
||
let damaged = self.get_mob_entity().living_entity.damage_with_context(
|
||
caller,
|
||
amount,
|
||
damage_type,
|
||
position,
|
||
source,
|
||
cause,
|
||
);
|
||
if damaged {
|
||
self.on_damage(damage_type, source);
|
||
}
|
||
damaged
|
||
}
|
||
|
||
fn interact(&self, player: &Arc<Player>, item_stack: &mut ItemStack) -> bool {
|
||
self.mob_interact(player, item_stack)
|
||
}
|
||
|
||
fn on_player_collision(&self, player: &Arc<Player>) {
|
||
self.mob_player_collision(player);
|
||
}
|
||
|
||
fn get_entity(&self) -> &Entity {
|
||
&self.get_mob_entity().living_entity.entity
|
||
}
|
||
|
||
fn get_living_entity(&self) -> Option<&LivingEntity> {
|
||
Some(&self.get_mob_entity().living_entity)
|
||
}
|
||
|
||
fn cast_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
|
||
fn is_in_love(&self) -> bool {
|
||
self.get_mob_entity().is_in_love()
|
||
}
|
||
|
||
fn is_breeding_ready(&self) -> bool {
|
||
self.get_mob_entity().is_breeding_ready()
|
||
}
|
||
|
||
fn reset_love(&self) {
|
||
self.get_mob_entity().reset_love_ticks();
|
||
}
|
||
|
||
fn set_breeding_cooldown(&self, ticks: i32) {
|
||
self.get_mob_entity()
|
||
.breeding_cooldown
|
||
.store(ticks, Relaxed);
|
||
}
|
||
|
||
fn is_panicking(&self) -> bool {
|
||
self.get_path_aware_entity()
|
||
.is_some_and(PathAwareEntity::is_panicking)
|
||
}
|
||
|
||
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||
<T as Mob>::get_job_site(self)
|
||
}
|
||
|
||
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||
<T as Mob>::get_home(self)
|
||
}
|
||
|
||
fn write_custom_nbt(&self, nbt: &mut NbtCompound) {
|
||
self.get_mob_entity().write_mob_nbt(nbt);
|
||
if let Some(ageable) = self.as_ageable() {
|
||
ageable.write_ageable_nbt(nbt);
|
||
}
|
||
if let Some(animal) = self.as_animal() {
|
||
animal.write_animal_nbt(nbt);
|
||
}
|
||
if let Some(tamable) = self.as_tamable() {
|
||
tamable.write_tamable_nbt(nbt);
|
||
}
|
||
self.mob_write_nbt(nbt);
|
||
}
|
||
|
||
fn read_custom_nbt(&self, nbt: &NbtCompound) {
|
||
self.get_mob_entity().read_mob_nbt(nbt);
|
||
if let Some(ageable) = self.as_ageable() {
|
||
ageable.read_ageable_nbt(nbt);
|
||
}
|
||
if let Some(animal) = self.as_animal() {
|
||
animal.read_animal_nbt(nbt);
|
||
}
|
||
if let Some(tamable) = self.as_tamable() {
|
||
tamable.read_tamable_nbt(nbt);
|
||
}
|
||
self.mob_read_nbt(nbt);
|
||
}
|
||
|
||
fn get_gravity(&self) -> f64 {
|
||
self.get_mob_gravity()
|
||
}
|
||
|
||
fn get_y_velocity_drag(&self) -> Option<f64> {
|
||
self.get_mob_y_velocity_drag()
|
||
}
|
||
|
||
fn get_experience_reward(&self, _killer: Option<&dyn EntityBase>) -> u32 {
|
||
if self
|
||
.get_entity()
|
||
.age
|
||
.load(std::sync::atomic::Ordering::Relaxed)
|
||
< 0
|
||
{
|
||
return 0;
|
||
}
|
||
// TODO: apply enchantment processing like in vanilla
|
||
Mob::get_base_experience_reward(self)
|
||
}
|
||
|
||
fn get_base_experience_reward(&self) -> u32 {
|
||
Mob::get_base_experience_reward(self)
|
||
}
|
||
}
|
||
|
||
#[expect(dead_code)]
|
||
const DEFAULT_PATHFINDING_FAVOR: f32 = 0.0;
|
||
|
||
pub trait PathAwareEntity: Mob + Send + Sync {
|
||
fn get_pathfinding_favor(&self, _block_pos: BlockPos, _world: Arc<World>) -> f32 {
|
||
0.0
|
||
}
|
||
|
||
// TODO: missing SpawnReason attribute
|
||
fn can_spawn(&self, world: Arc<World>) -> bool {
|
||
self.get_pathfinding_favor(
|
||
self.get_mob_entity().living_entity.entity.block_pos.load(),
|
||
world,
|
||
) >= 0.0
|
||
}
|
||
|
||
fn is_navigation(&self) -> bool {
|
||
let navigator = self
|
||
.get_mob_entity()
|
||
.navigator
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
!navigator.is_idle()
|
||
}
|
||
|
||
// TODO: implement
|
||
fn is_panicking(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn should_follow_leash(&self) -> bool {
|
||
true
|
||
}
|
||
|
||
fn on_short_leash_tick(&self) {
|
||
// TODO: implement
|
||
}
|
||
|
||
fn before_leash_tick(&self) {
|
||
// TODO: implement
|
||
}
|
||
|
||
fn get_follow_leash_speed(&self) -> f32 {
|
||
1.0
|
||
}
|
||
}
|
||
|
||
pub trait RangedAttackMob: Mob + Send + Sync {
|
||
fn perform_ranged_attack(&self, target: &Arc<dyn EntityBase>, power: f32);
|
||
}
|