use std::sync::{ Arc, Weak, atomic::{AtomicBool, Ordering}, }; use crate::entity::attributes::Modifier; use crate::entity::attributes::ModifierOperation; use pumpkin_data::{BlockStateId, attributes::Attributes}; use crossbeam::atomic::AtomicCell; use pumpkin_data::{ damage::DamageType, data_component_impl::EquipmentSlot, entity::EntityType, item::Item, particle::Particle, sound::{Sound, SoundCategory}, tag, tag::Taggable, }; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::{ codec::var_int::VarInt, java::client::play::{CEntityPositionSync, Metadata}, }; use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use rand::RngExt; use crate::entity::{ Entity, EntityBase, ai::{ goal::{ active_target::ActiveTargetGoal, chase_player::ChasePlayerGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, pick_up_block::PickUpBlockGoal, place_block::PlaceBlockGoal, revenge::RevengeGoal, swim::SwimGoal, teleport_towards_player::TeleportTowardsPlayerGoal, wander_around::WanderAroundGoal, }, pathfinder::node::PathType, }, mob::{Mob, MobEntity}, player::Player, }; const SPEED_BOOST: f64 = 0.15; const ENDERMAN_SPEED_BOOST_ID: &str = "minecraft:attacking"; pub const ENDERMAN_EYE_HEIGHT: f64 = 2.55; pub const ENDERMAN_BODY_Y_OFFSET: f64 = 1.45; pub const PLAYER_EYE_HEIGHT: f64 = 1.62; fn is_projectile_damage(dt: DamageType) -> bool { let (names, _) = pumpkin_data::tag::DamageType::MINECRAFT_IS_PROJECTILE; names.contains(&dt.message_id) } pub struct EndermanEntity { pub mob_entity: MobEntity, carried_block: AtomicCell>, angry: AtomicBool, provoked: AtomicBool, speed_boosted: AtomicBool, } impl EndermanEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); let entity = Self { mob_entity, carried_block: AtomicCell::new(None), angry: AtomicBool::new(false), provoked: AtomicBool::new(false), speed_boosted: AtomicBool::new(false), }; let mob_arc = Arc::new(entity); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); Arc::downgrade(&mob_arc) }; let mut navigator = mob_arc .mob_entity .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); navigator.set_mob_dimensions(0.6, 2.9); navigator.set_pathfinding_malus(PathType::Water, -1.0); drop(navigator); { let mut goal_selector = mob_arc .mob_entity .goals_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let mut target_selector = mob_arc .mob_entity .target_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, Box::new(ChasePlayerGoal::new(mob_arc.clone()))); goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, false))); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( 8, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), ); goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); goal_selector.add_goal(10, Box::new(PlaceBlockGoal::new(mob_arc.clone()))); goal_selector.add_goal(11, Box::new(PickUpBlockGoal::new(mob_arc.clone()))); target_selector.add_goal(1, Box::new(TeleportTowardsPlayerGoal::new(mob_arc.clone()))); target_selector.add_goal(2, Box::new(RevengeGoal::new(true))); target_selector.add_goal( 3, ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::ENDERMITE, true), ); }; mob_arc } pub fn teleport_randomly(&self) -> bool { let entity = &self.mob_entity.living_entity.entity; let pos = entity.pos.load(); let (x, y, z) = { let mut rng = self.get_random(); ( pos.x + (rng.random_range(0.0..1.0) - 0.5) * 64.0, pos.y + (rng.random_range(0i32..64) - 32) as f64, pos.z + (rng.random_range(0.0..1.0) - 0.5) * 64.0, ) }; self.teleport_to(x, y, z) } pub fn teleport_towards(&self, target: &dyn EntityBase) -> bool { let entity = &self.mob_entity.living_entity.entity; let pos = entity.pos.load(); let target_pos = target.get_entity().pos.load(); let dx = pos.x - target_pos.x; let dy = (pos.y + ENDERMAN_BODY_Y_OFFSET) - (target_pos.y + PLAYER_EYE_HEIGHT); let dz = pos.z - target_pos.z; let dist = (dx * dx + dy * dy + dz * dz).sqrt(); if dist < 1e-6 { return false; } let nx = dx / dist; let ny = dy / dist; let nz = dz / dist; let (x, y, z) = { let mut rng = self.get_random(); ( pos.x + (rng.random_range(0.0..1.0) - 0.5) * 8.0 - nx * 16.0, pos.y + (rng.random_range(0i32..16) - 8) as f64 - ny * 16.0, pos.z + (rng.random_range(0.0..1.0) - 0.5) * 8.0 - nz * 16.0, ) }; self.teleport_to(x, y, z) } pub fn teleport_to(&self, x: f64, y: f64, z: f64) -> bool { let entity = &self.mob_entity.living_entity.entity; let origin = entity.pos.load(); let world = entity.world.load(); let min_y = f64::from(world.dimension.min_y); let max_y = f64::from(world.dimension.min_y + world.dimension.height - 1); let mut target_y = y.clamp(min_y, max_y); let block_x = x.floor() as i32; let mut block_y = target_y.floor() as i32; let block_z = z.floor() as i32; let mut found_ground = false; loop { let below_pos = BlockPos::new(block_x, block_y - 1, block_z); let below_state = world.get_block_state(&below_pos); if below_state.is_solid() { found_ground = true; break; } if block_y <= world.dimension.min_y { break; } block_y -= 1; target_y = block_y as f64; } if !found_ground { return false; } let dest_pos = BlockPos::new(block_x, block_y, block_z); let dest_fluid = world.get_fluid(&dest_pos); if dest_fluid.has_tag(&tag::Fluid::MINECRAFT_WATER) { return false; } let half_width = 0.3; let height = 2.9; let bb = BoundingBox::new( Vector3::new(x - half_width, target_y, z - half_width), Vector3::new(x + half_width, target_y + height, z + half_width), ); if !world.is_space_empty(bb) { return false; } let new_pos = Vector3::new(x, target_y, z); for pos in &[origin, new_pos] { world.spawn_particle( *pos, Vector3::new(0.0, 0.0, 0.0), 0.0, 128, Particle::Portal, ); world.play_sound(Sound::EntityEndermanTeleport, SoundCategory::Hostile, pos); } if let Some(server) = world.server.upgrade() { let mut event = crate::plugin::api::events::entity::entity_teleport::EntityTeleportEvent::new( entity.entity_id, origin, new_pos, ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return false; } } entity.set_pos(new_pos); let chunk_pos = entity.chunk_pos.load(); world.broadcast_to_chunk( chunk_pos, &CEntityPositionSync::new( entity.entity_id.into(), new_pos, Vector3::new(0.0, 0.0, 0.0), entity.yaw.load(), entity.pitch.load(), entity.on_ground.load(Ordering::Relaxed), ), ); self.mob_entity .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .stop(); true } pub fn set_target(&self, target: Option>) { let is_some = target.is_some(); let mut mob_target = self .mob_entity .target .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *mob_target = target; drop(mob_target); if is_some { self.set_angry(true); // Use attribute modifier instead of direct speed arithmetic if !self.speed_boosted.swap(true, Ordering::Relaxed) { let living = &self.mob_entity.living_entity; let modifier = Modifier { id: ENDERMAN_SPEED_BOOST_ID.to_string(), amount: SPEED_BOOST, operation: ModifierOperation::Add, }; living.update_attribute(&Attributes::MOVEMENT_SPEED, |inst| { inst.add_or_replace_modifier(modifier); }); crate::entity::attributes::send_attribute_updates_for_living( living, vec![Attributes::MOVEMENT_SPEED], ); } } else { self.set_angry(false); self.set_provoked(false); if self.speed_boosted.swap(false, Ordering::Relaxed) { let living = &self.mob_entity.living_entity; living.update_attribute(&Attributes::MOVEMENT_SPEED, |inst| { inst.remove_modifier(ENDERMAN_SPEED_BOOST_ID); }); crate::entity::attributes::send_attribute_updates_for_living( living, vec![Attributes::MOVEMENT_SPEED], ); } } } pub fn set_angry(&self, angry: bool) { self.angry.store(angry, Ordering::Relaxed); self.mob_entity.living_entity.entity.send_meta_data( &[Metadata::new( pumpkin_data::tracked_data::enderman::CREEPY, angry, )], None, ); } pub fn is_angry(&self) -> bool { self.angry.load(Ordering::Relaxed) } pub fn set_provoked(&self, provoked: bool) { self.provoked.store(provoked, Ordering::Relaxed); self.mob_entity.living_entity.entity.send_meta_data( &[Metadata::new( pumpkin_data::tracked_data::enderman::STARED_AT, provoked, )], None, ); } pub fn set_carried_block(&self, block_state: Option) { self.carried_block.store(block_state); let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32)); self.mob_entity.living_entity.entity.send_meta_data( &[Metadata::new( pumpkin_data::tracked_data::enderman::CARRY_STATE, value, )], None, ); } pub fn get_carried_block(&self) -> Option { self.carried_block.load() } pub fn is_player_staring(&self, player: &Player) -> bool { let equipment = player.living_entity.entity_equipment.try_lock(); if let Ok(equipment) = equipment && let Some(head_stack) = equipment.equipment.get(&EquipmentSlot::HEAD) && !head_stack.is_empty() && head_stack.item == &Item::CARVED_PUMPKIN { return false; } let entity = &self.mob_entity.living_entity.entity; let enderman_pos = entity.pos.load(); let enderman_eye_y = enderman_pos.y + ENDERMAN_EYE_HEIGHT; let player_entity = player.get_entity(); let player_pos = player_entity.pos.load(); let player_eye_y = player_pos.y + PLAYER_EYE_HEIGHT; let pitch = player_entity.pitch.load().to_radians(); let yaw = -player_entity.yaw.load().to_radians(); let cos_pitch = pitch.cos(); let look_dir = Vector3::new( (yaw.sin() * cos_pitch) as f64, (-pitch.sin()) as f64, (yaw.cos() * cos_pitch) as f64, ); let dx = enderman_pos.x - player_pos.x; let dy = enderman_eye_y - player_eye_y; let dz = enderman_pos.z - player_pos.z; let distance = (dx * dx + dy * dy + dz * dz).sqrt(); if distance < 0.1 { return false; } let dir_x = dx / distance; let dir_y = dy / distance; let dir_z = dz / distance; let dot = look_dir.x * dir_x + look_dir.y * dir_y + look_dir.z * dir_z; if dot <= 1.0 - 0.025 / distance { return false; } let enderman_eye_pos = Vector3::new(enderman_pos.x, enderman_eye_y, enderman_pos.z); let player_eye_pos = Vector3::new(player_pos.x, player_eye_y, player_pos.z); let world = entity.world.load(); world .raycast(enderman_eye_pos, player_eye_pos, |block_pos, w| { let state = w.get_block_state(block_pos); state.is_solid() }) .is_none() } } impl Mob for EndermanEntity { fn mob_write_nbt(&self, nbt: &mut NbtCompound) { if let Some(block_state) = self.carried_block.load() { nbt.put_int("carriedBlockState", block_state.as_u16() as i32); } } fn mob_read_nbt(&self, nbt: &NbtCompound) { if let Some(block_state) = nbt.get_int("carriedBlockState") { self.set_carried_block(BlockStateId::new(block_state as u16)); } } fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } fn set_mob_target(&self, target: Option>) { self.set_target(target); } // TODO: sunlight avoidance, carried block drop on death, angerable system, ambient sound override fn mob_tick<'a>(&'a self, caller: &'a Arc) { let entity = &self.mob_entity.living_entity.entity; if !entity.is_alive() { return; } let world = entity.world.load(); let raining_at_feet = world.is_raining_at(&entity.block_pos.load()); let raining_at_head = world.is_raining_at(&entity.bounding_box.load().max_block_pos()); if entity.touching_water.load(Ordering::SeqCst) || raining_at_feet || raining_at_head { caller.damage(caller.as_ref(), 1.0, DamageType::DROWN); } } fn pre_damage(&self, damage_type: DamageType, _source: Option<&dyn EntityBase>) -> bool { let is_projectile = is_projectile_damage(damage_type); if is_projectile { for _ in 0..64 { if self.teleport_randomly() { return false; } } } true } fn on_damage(&self, _damage_type: DamageType, source: Option<&dyn EntityBase>) { if source.is_some_and(|s| s.get_living_entity().is_some()) { return; } let should_teleport = self.get_random().random_range(0..10) != 0; if should_teleport { self.teleport_randomly(); } } }