use std::sync::{ Arc, Weak, atomic::{AtomicI32, Ordering}, }; use pumpkin_data::entity::{EntityStatus, EntityType}; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag::{self, Taggable}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use crate::entity::{ Entity, EntityBase, ageable::{AgeableData, AgeableMob}, ai::goal::{ breed::BreedGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, }, item::ItemEntity, mob::{Mob, MobEntity}, passive::animal::Animal, player::Player, }; pub const SNIFFER_FOOD: &[&Item] = &[&Item::TORCHFLOWER_SEEDS, &Item::PITCHER_POD]; pub const SNIFFER_BABY_START_AGE: i32 = -48000; pub const DIGGING_DROP_SEED_OFFSET_TICKS: i32 = 120; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[repr(i32)] pub enum SnifferState { #[default] Idling = 0, FeelingHappy = 1, Scenting = 2, Sniffing = 3, Searching = 4, Digging = 5, Rising = 6, } impl SnifferState { #[must_use] pub const fn from_id(id: i32) -> Self { match id { 1 => Self::FeelingHappy, 2 => Self::Scenting, 3 => Self::Sniffing, 4 => Self::Searching, 5 => Self::Digging, 6 => Self::Rising, _ => Self::Idling, } } #[must_use] pub const fn id(self) -> i32 { self as i32 } } /// Represents a Sniffer, a large passive mob that digs up ancient seeds. /// /// Wiki: pub struct SnifferEntity { pub mob_entity: MobEntity, pub ageable_data: AgeableData, pub state: AtomicI32, pub drop_seed_at_tick: AtomicI32, pub explored_positions: std::sync::Mutex>, } impl SnifferEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); let sniffer = Self { mob_entity, ageable_data: AgeableData::default(), state: AtomicI32::new(SnifferState::Idling.id()), drop_seed_at_tick: AtomicI32::new(0), explored_positions: std::sync::Mutex::new(Vec::new()), }; let mob_arc = Arc::new(sniffer); 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); goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, BreedGoal::new(1.0)); goal_selector.add_goal(2, Box::new(TemptGoal::new(1.2, SNIFFER_FOOD))); goal_selector.add_goal(3, Box::new(FollowParentGoal::new(1.1))); goal_selector.add_goal(4, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( 5, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default())); }; mob_arc } #[must_use] pub fn get_state(&self) -> SnifferState { SnifferState::from_id(self.state.load(Ordering::Relaxed)) } pub fn set_state(&self, state: SnifferState) { self.state.store(state.id(), Ordering::Relaxed); let entity = self.get_entity(); entity.set_synced_data( pumpkin_data::tracked_data::sniffer::STATE, VarInt(state.id()), ); } pub fn transition_to(&self, state: SnifferState) { let entity = self.get_entity(); let world = entity.world.load(); let pos = entity.pos.load(); match state { SnifferState::Idling => { self.set_state(SnifferState::Idling); } SnifferState::FeelingHappy => { world.play_sound(Sound::EntitySnifferHappy, SoundCategory::Neutral, &pos); self.set_state(SnifferState::FeelingHappy); } SnifferState::Scenting => { self.set_state(SnifferState::Scenting); self.on_scenting_start(); } SnifferState::Sniffing => { world.play_sound(Sound::EntitySnifferSniffing, SoundCategory::Neutral, &pos); self.set_state(SnifferState::Sniffing); } SnifferState::Searching => { self.set_state(SnifferState::Searching); } SnifferState::Digging => { self.set_state(SnifferState::Digging); self.on_digging_start(); } SnifferState::Rising => { world.play_sound( Sound::EntitySnifferDiggingStop, SoundCategory::Neutral, &pos, ); self.set_state(SnifferState::Rising); } } } fn on_scenting_start(&self) { let entity = self.get_entity(); let world = entity.world.load(); let pos = entity.pos.load(); let pitch = if self.is_baby() { 1.3 } else { 1.0 }; world.play_sound_fine( Sound::EntitySnifferScenting, SoundCategory::Neutral, &pos, 1.0, pitch, ); } fn on_digging_start(&self) { let entity = self.get_entity(); let world = entity.world.load(); let current_ticks = world.level_time.try_lock().map_or(0, |t| t.world_age); let drop_tick = current_ticks as i32 + DIGGING_DROP_SEED_OFFSET_TICKS; self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed); entity.set_synced_data( pumpkin_data::tracked_data::sniffer::DROP_SEED_AT_TICK, VarInt(drop_tick), ); world.send_entity_status(entity, EntityStatus::SnifferDiggingSound, None); } pub fn on_digging_complete(&self, success: bool) { if success { let head_block = self.get_head_block().down(); self.store_explored_position(head_block); } } #[must_use] pub fn get_head_position(&self) -> Vector3 { let entity = self.get_entity(); let pos = entity.pos.load(); let yaw = f64::from(entity.yaw.load()); let yaw_rad = yaw.to_radians(); let forward = Vector3::new(-yaw_rad.sin(), 0.0, yaw_rad.cos()); pos + forward * 2.25 } #[must_use] pub fn get_head_block(&self) -> BlockPos { let head_pos = self.get_head_position(); let entity = self.get_entity(); let pos = entity.pos.load(); BlockPos::floored(head_pos.x, pos.y + 0.2, head_pos.z) } #[must_use] pub fn can_dig(&self) -> bool { let entity = self.get_entity(); !self.is_panicking() && !self.is_baby() && !self.mob_entity.living_entity.is_in_water() && entity.on_ground.load(Ordering::Relaxed) && self.can_dig_at(self.get_head_block().down()) } fn can_dig_at(&self, pos: BlockPos) -> bool { let entity = self.get_entity(); let world = entity.world.load(); let block_state = world.get_block_state(&pos); let block = pumpkin_data::Block::from_state_id(block_state.id); if !block.has_tag(&tag::Block::MINECRAFT_SNIFFER_DIGGABLE_BLOCK) { return false; } let explored = self .explored_positions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); !explored.contains(&pos) } fn store_explored_position(&self, pos: BlockPos) { let mut explored = self .explored_positions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if explored.len() >= 20 { explored.pop(); } explored.insert(0, pos); } pub fn drop_seed(&self) { let entity = self.get_entity(); let world = entity.world.load(); let current_tick = world.get_world_age() as i32; if self.drop_seed_at_tick.load(Ordering::Relaxed) == current_tick { let head_pos = self.get_head_position(); let seed_item = if rand::random::() { &Item::TORCHFLOWER_SEEDS } else { &Item::PITCHER_POD }; let item_stack = ItemStack::new(1, seed_item); let item_entity = Entity::new(world.clone(), head_pos, &EntityType::ITEM); let item_arc = Arc::new(ItemEntity::new(item_entity, item_stack)); world.spawn_entity_non_save(item_arc as Arc); world.play_sound( Sound::EntitySnifferDropSeed, SoundCategory::Neutral, &head_pos, ); } } pub fn spawn_child_from_breeding(&self, partner: &dyn EntityBase) { let entity = self.get_entity(); let world = entity.world.load(); let pos = entity.pos.load(); let item_stack = ItemStack::new(1, &Item::SNIFFER_EGG); let egg_entity = Entity::new(world.clone(), pos, &EntityType::ITEM); let egg_arc = Arc::new(ItemEntity::new(egg_entity, item_stack)); world.spawn_entity_non_save(egg_arc as Arc); world.play_sound(Sound::BlockSnifferEggPlop, SoundCategory::Neutral, &pos); self.mob_entity.reset_love_ticks(); self.mob_entity .breeding_cooldown .store(6000, Ordering::Relaxed); partner.reset_love(); partner.set_breeding_cooldown(6000); } } impl AgeableMob for SnifferEntity { fn get_ageable_data(&self) -> &AgeableData { &self.ageable_data } fn get_baby_start_age(&self) -> i32 { SNIFFER_BABY_START_AGE } } impl Animal for SnifferEntity { fn is_food(&self, item_stack: &ItemStack) -> bool { item_stack.item.has_tag(&tag::Item::MINECRAFT_SNIFFER_FOOD) || SNIFFER_FOOD.iter().any(|i| i.id == item_stack.item.id) } } impl Mob for SnifferEntity { fn as_ageable(&self) -> Option<&dyn AgeableMob> { Some(self) } fn as_animal(&self) -> Option<&dyn Animal> { Some(self) } fn mob_write_nbt(&self, nbt: &mut NbtCompound) { nbt.put_int("State", self.get_state().id()); nbt.put_int( "DropSeedAtTick", self.drop_seed_at_tick.load(Ordering::Relaxed), ); } fn mob_read_nbt(&self, nbt: &NbtCompound) { if let Some(state_id) = nbt.get_int("State") { self.state.store(state_id, Ordering::Relaxed); } if let Some(drop_tick) = nbt.get_int("DropSeedAtTick") { self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed); } } fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } fn mob_tick(&self, _caller: &dyn EntityBase) { self.ageable_ai_step(); let state = self.get_state(); match state { SnifferState::Searching => { let entity = self.get_entity(); let world = entity.world.load(); let ticks = world.get_world_age(); if ticks % 20 == 0 { world.play_sound( Sound::EntitySnifferSearching, SoundCategory::Neutral, &entity.pos.load(), ); } } SnifferState::Digging => { self.drop_seed(); } _ => {} } } fn mob_init_data_tracker(&self) { let entity = self.get_entity(); let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { entity.set_synced_data(pumpkin_data::tracked_data::sniffer::BABY_ID, true); } entity.set_synced_data( pumpkin_data::tracked_data::sniffer::STATE, VarInt(self.get_state().id()), ); entity.set_synced_data( pumpkin_data::tracked_data::sniffer::DROP_SEED_AT_TICK, VarInt(self.drop_seed_at_tick.load(Ordering::Relaxed)), ); } fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { self.animal_interact(player, item_stack, Sound::EntitySnifferEat) } }