use std::sync::atomic::{AtomicI32, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; use crossbeam::atomic::AtomicCell; use pumpkin_data::BlockDirection; use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::CEntityPositionSync; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use rand::RngExt; use crate::entity::ai::goal::active_target::ActiveTargetGoal; use crate::entity::ai::goal::look_around::RandomLookAroundGoal; use crate::entity::ai::goal::look_at_entity::LookAtEntityGoal; use crate::entity::ai::goal::revenge::RevengeGoal; use crate::entity::ai::goal::{Controls, Goal}; use crate::entity::mob::{Mob, MobEntity}; use crate::entity::projectile::shulker_bullet::ShulkerBulletEntity; use crate::entity::{Entity, EntityBase}; const DEFAULT_ATTACH_FACE: BlockDirection = BlockDirection::Down; const NO_COLOR: u8 = 16; const PEEK_PER_TICK: f32 = 0.05; const MAX_TELEPORT_DISTANCE: i32 = 8; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Axis { X = 0, Y = 1, Z = 2, } trait BlockDirectionExt { fn axis_of(self) -> Axis; } impl BlockDirectionExt for BlockDirection { fn axis_of(self) -> Axis { match self { Self::East | Self::West => Axis::X, Self::Up | Self::Down => Axis::Y, Self::North | Self::South => Axis::Z, } } } pub struct ShulkerEntity { pub mob_entity: MobEntity, attach_face: AtomicU8, peek_amount: AtomicU8, color: AtomicU8, current_peek_amount: AtomicCell, prev_peek_amount: AtomicCell, } impl ShulkerEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); let shulker = Self { mob_entity, attach_face: AtomicU8::new(DEFAULT_ATTACH_FACE as u8), peek_amount: AtomicU8::new(0), color: AtomicU8::new(NO_COLOR), current_peek_amount: AtomicCell::new(0.0), prev_peek_amount: AtomicCell::new(0.0), }; let mob_arc = Arc::new(shulker); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); Arc::downgrade(&mob_arc) }; { 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( 1, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), ); goal_selector.add_goal(4, Box::new(ShulkerAttackGoal::new(mob_arc.clone()))); goal_selector.add_goal(7, Box::new(ShulkerPeekGoal::new(mob_arc.clone()))); goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); target_selector.add_goal( 2, ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), ); }; mob_arc } pub fn get_attach_face(&self) -> BlockDirection { BlockDirection::try_from(i32::from(self.attach_face.load(Ordering::Relaxed))) .unwrap_or(DEFAULT_ATTACH_FACE) } pub fn set_attach_face(&self, face: BlockDirection) { self.attach_face.store(face as u8, Ordering::Relaxed); let entity = &self.mob_entity.living_entity.entity; entity.set_synced_data( pumpkin_data::tracked_data::shulker::ATTACH_FACE_ID, VarInt(face as i32), ); } pub fn get_color(&self) -> Option { let c = self.color.load(Ordering::Relaxed); if c == NO_COLOR { None } else { Some(c) } } pub fn set_color(&self, color: Option) { let val = color.unwrap_or(NO_COLOR); self.color.store(val, Ordering::Relaxed); let entity = &self.mob_entity.living_entity.entity; entity.set_synced_data(pumpkin_data::tracked_data::shulker::COLOR, val as i8); } pub fn get_raw_peek(&self) -> u8 { self.peek_amount.load(Ordering::Relaxed) } pub fn set_raw_peek(&self, amount: u8) { let entity = &self.mob_entity.living_entity.entity; let world = entity.world.load(); let pos = entity.pos.load(); if amount == 0 { world.play_sound_fine( Sound::EntityShulkerClose, SoundCategory::Hostile, &pos, 1.0, 1.0, ); } else if self.peek_amount.load(Ordering::Relaxed) == 0 { world.play_sound_fine( Sound::EntityShulkerOpen, SoundCategory::Hostile, &pos, 1.0, 1.0, ); } self.peek_amount.store(amount, Ordering::Relaxed); entity.set_synced_data(pumpkin_data::tracked_data::shulker::PEEK_ID, amount); } pub fn is_closed(&self) -> bool { self.peek_amount.load(Ordering::Relaxed) == 0 } fn update_peek_amount(&self) -> bool { let current = self.current_peek_amount.load(); self.prev_peek_amount.store(current); let target = f32::from(self.get_raw_peek()) * 0.01; if (current - target).abs() < 1e-6 { return false; } let new = if current > target { (current - PEEK_PER_TICK).max(target) } else { (current + PEEK_PER_TICK).min(target) }; self.current_peek_amount.store(new); true } fn find_attachable_face(&self, pos: &BlockPos) -> Option { let entity = &self.mob_entity.living_entity.entity; let world = entity.world.load(); for dir in BlockDirection::all() { let neighbour = pos.offset_direction(dir); let state = world.get_block_state(&neighbour); if state.is_solid() { return Some(dir); } } None } fn can_stay_at(&self, pos: &BlockPos, face: BlockDirection) -> bool { let entity = &self.mob_entity.living_entity.entity; let world = entity.world.load(); let own_state = world.get_block_state(pos); if !own_state.is_air() { return false; } let neighbour = pos.offset_direction(face); let state = world.get_block_state(&neighbour); state.is_solid() } fn find_new_attachment(&self) { let pos = self.mob_entity.living_entity.entity.block_pos.load(); if let Some(dir) = self.find_attachable_face(&pos) { self.set_attach_face(dir); } else { self.teleport_somewhere(); } } pub fn teleport_somewhere(&self) -> bool { let entity = &self.mob_entity.living_entity.entity; let base_pos = entity.block_pos.load(); let world = entity.world.load(); let candidates: Vec<(i32, i32, i32)> = { let mut rng = rand::rng(); (0..20) .map(|_| { ( rng.random_range(-MAX_TELEPORT_DISTANCE..=MAX_TELEPORT_DISTANCE), rng.random_range(-MAX_TELEPORT_DISTANCE..=MAX_TELEPORT_DISTANCE), rng.random_range(-MAX_TELEPORT_DISTANCE..=MAX_TELEPORT_DISTANCE), ) }) .collect() }; for (dx, dy, dz) in candidates { let candidate = BlockPos::new(base_pos.0.x + dx, base_pos.0.y + dy, base_pos.0.z + dz); let candidate_state = world.get_block_state(&candidate); if !candidate_state.is_air() { continue; } if let Some(dir) = self.find_attachable_face(&candidate) { let new_pos = Vector3::new( f64::from(candidate.0.x) + 0.5, f64::from(candidate.0.y), f64::from(candidate.0.z) + 0.5, ); self.set_attach_face(dir); 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), ), ); entity.last_sent_pos.store(new_pos); world.play_sound_fine( Sound::EntityShulkerTeleport, SoundCategory::Hostile, &new_pos, 1.0, 1.0, ); self.set_raw_peek(0); self.mob_entity.set_target(None); return true; } } false } pub fn on_shulker_damage(&self) { let living = &self.mob_entity.living_entity; let health = living.health.load(); let max = living.get_max_health(); if health < max * 0.5 && rand::rng().random_range(0..4) == 0 { self.teleport_somewhere(); } } } impl Mob for ShulkerEntity { fn mob_write_nbt(&self, nbt: &mut NbtCompound) { nbt.put_byte("AttachFace", self.attach_face.load(Ordering::Relaxed) as i8); nbt.put_byte("PeekAmount", self.peek_amount.load(Ordering::Relaxed) as i8); nbt.put_byte("Color", self.color.load(Ordering::Relaxed) as i8); } fn mob_read_nbt(&self, nbt: &NbtCompound) { if let Some(face) = nbt.get_byte("AttachFace") { self.attach_face.store(face as u8, Ordering::Relaxed); } if let Some(peek) = nbt.get_byte("PeekAmount") { self.peek_amount.store(peek as u8, Ordering::Relaxed); } if let Some(color) = nbt.get_byte("Color") { self.color.store(color as u8, Ordering::Relaxed); } } fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } fn get_mob_gravity(&self) -> f64 { 0.0 } fn mob_tick(&self, _caller: &dyn EntityBase) { let entity = &self.mob_entity.living_entity.entity; if !entity.is_alive() { return; } entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); self.update_peek_amount(); let pos = entity.block_pos.load(); let face = self.get_attach_face(); if !self.can_stay_at(&pos, face) { self.find_new_attachment(); } } fn on_damage(&self, _damage_type: DamageType, _source: Option<&dyn EntityBase>) { self.on_shulker_damage(); } fn pre_damage(&self, damage_type: DamageType, _source: Option<&dyn EntityBase>) -> bool { if self.is_closed() && damage_type == DamageType::ARROW { return false; } true } fn modify_incoming_damage(&self, amount: f32, damage_type: DamageType) -> f32 { if self.is_closed() && !crate::entity::living::bypasses_armor_durability(&damage_type) { const ARMOR: f32 = 20.0; let f1 = (ARMOR - amount * 0.5f32).clamp(ARMOR * 0.2f32, 20.0f32); (amount * (1.0f32 - f1 / 25.0f32)).max(0.0) } else { amount } } } struct ShulkerAttackGoal { shulker: Arc, attack_cooldown: AtomicI32, } impl ShulkerAttackGoal { const fn new(shulker: Arc) -> Self { Self { shulker, attack_cooldown: AtomicI32::new(20), } } } impl Goal for ShulkerAttackGoal { fn controls(&self) -> Controls { Controls::MOVE | Controls::LOOK } fn can_start(&mut self, mob: &dyn Mob) -> bool { let target = mob.get_mob_entity().get_target(); target .as_ref() .is_some_and(|t| t.get_living_entity().is_some_and(|l| l.entity.is_alive())) } fn should_continue(&self, mob: &dyn Mob) -> bool { let target = mob.get_mob_entity().get_target(); target .as_ref() .is_some_and(|t| t.get_living_entity().is_some_and(|l| l.entity.is_alive())) } fn start(&mut self, _mob: &dyn Mob) { self.attack_cooldown.store(20, Ordering::Relaxed); self.shulker.set_raw_peek(100); } fn stop(&mut self, _mob: &dyn Mob) { self.shulker.set_raw_peek(0); } fn should_run_every_tick(&self) -> bool { true } fn tick(&mut self, mob: &dyn Mob) { let mob_entity = mob.get_mob_entity(); let target_arc = mob_entity.get_target(); let Some(target) = target_arc else { return; }; if !target.get_entity().is_alive() { return; } let entity = &mob_entity.living_entity.entity; let shulker_pos = entity.pos.load(); let target_pos = target.get_entity().pos.load(); let dist_sq = shulker_pos.squared_distance_to_vec(&target_pos); if dist_sq > 400.0 { mob_entity.set_target(None); return; } let cooldown = self.attack_cooldown.fetch_sub(1, Ordering::Relaxed) - 1; if cooldown <= 0 { let new_cd = 20 + mob.get_random().random_range(0..5) * 10; self.attack_cooldown.store(new_cd, Ordering::Relaxed); let world = entity.world.load(); let target_pos = target.get_entity().pos.load(); let bullet = ShulkerBulletEntity::new( entity, target.get_entity().entity_id, target_pos, self.shulker.get_attach_face().axis_of(), ); world.spawn_entity_non_save(Arc::new(bullet)); let pitch = 1.0 + (mob.get_random().random::() - mob.get_random().random::()) * 0.2; world.play_sound_fine( Sound::EntityShulkerShoot, SoundCategory::Hostile, &shulker_pos, 2.0, pitch, ); } } } struct ShulkerPeekGoal { shulker: Arc, peek_time: AtomicI32, } impl ShulkerPeekGoal { const fn new(shulker: Arc) -> Self { Self { shulker, peek_time: AtomicI32::new(0), } } } impl Goal for ShulkerPeekGoal { fn can_start(&mut self, mob: &dyn Mob) -> bool { let has_target = mob.get_mob_entity().get_target().is_some(); if has_target { return false; } if mob.get_random().random_range(0..40) != 0 { return false; } let pos = mob.get_mob_entity().living_entity.entity.block_pos.load(); let face = self.shulker.get_attach_face(); self.shulker.can_stay_at(&pos, face) } fn should_continue(&self, mob: &dyn Mob) -> bool { let has_target = mob.get_mob_entity().get_target().is_some(); !has_target && self.peek_time.load(Ordering::Relaxed) > 0 } fn start(&mut self, mob: &dyn Mob) { let duration = 20 * (1 + mob.get_random().random_range(0..3)); self.peek_time.store(duration, Ordering::Relaxed); self.shulker.set_raw_peek(30); } fn stop(&mut self, _mob: &dyn Mob) { let has_target = self.shulker.mob_entity.get_target().is_some(); if !has_target { self.shulker.set_raw_peek(0); } } fn tick(&mut self, _mob: &dyn Mob) { self.peek_time.fetch_sub(1, Ordering::Relaxed); } } trait BlockPosExt { fn offset_direction(&self, dir: BlockDirection) -> Self; } impl BlockPosExt for BlockPos { fn offset_direction(&self, dir: BlockDirection) -> Self { let offset = dir.to_offset(); Self::new( self.0.x + offset.x, self.0.y + offset.y, self.0.z + offset.z, ) } }