use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use crate::entity::projectile::{ProjectileHit, is_projectile}; use crate::{ entity::{ Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, }, server::Server, }; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_protocol::java::client::play::Metadata; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::vector3::Vector3; pub struct FishingBobberEntity { pub entity: Entity, pub owner_id: i32, pub hooked_entity_id: AtomicI32, pub in_ground: AtomicBool, pub has_hit: AtomicBool, pub wait_countdown: AtomicI32, pub bite_countdown: AtomicI32, } impl FishingBobberEntity { const WATER_INERTIA: f64 = 0.8; const AIR_INERTIA: f64 = 0.92; const GRAVITY: f64 = 0.03; pub fn new(entity: Entity, owner: &Player) -> Self { let mut owner_pos = owner.living_entity.entity.pos.load(); owner_pos.y += owner.living_entity.entity.get_eye_height() - 0.1; entity.pos.store(owner_pos); Self { entity, owner_id: owner.living_entity.entity.entity_id, hooked_entity_id: AtomicI32::new(0), in_ground: AtomicBool::new(false), has_hit: AtomicBool::new(false), wait_countdown: AtomicI32::new(rand::random::().abs() % 600 + 100), bite_countdown: AtomicI32::new(0), } } pub async fn reel_in(&self, player: &Player) -> i32 { use pumpkin_data::item::Item; let world = self.entity.world.load(); let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed); if hooked_id != 0 && let Some(hooked) = world.get_entity_by_id(hooked_id) { let player_pos = player.get_entity().pos.load(); let hooked_pos = hooked.get_entity().pos.load(); let delta = player_pos - hooked_pos; let motion = delta .multiply(0.1, 0.1, 0.1) .add_raw(0.0, delta.length().sqrt() * 0.08, 0.0); hooked.get_entity().add_velocity(motion); return 1; } if self.bite_countdown.load(Ordering::Relaxed) > 0 { // Caught something! player .increment_stat( pumpkin_data::statistic::StatisticCategory::Custom, pumpkin_data::statistic::CustomStatistic::FishCaught as i32, 1, ) .await; // TODO: Use actual loot tables. For now, just give a raw cod. let _item_stack = ItemStack::new(1, &Item::COD); // player.inventory().add_item(item_stack).await; // Need public add_item world.play_sound( Sound::EntityExperienceOrbPickup, SoundCategory::Neutral, &player.position(), ); return 1; } 0 } #[expect(clippy::too_many_lines)] pub async fn process_tick<'a>(&'a self, caller: &'a Arc, _server: &'a Server) { let entity = self.get_entity(); let world = entity.world.load(); if self.in_ground.load(Ordering::Relaxed) { return; } let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed); if hooked_id != 0 { if let Some(hooked) = world.get_entity_by_id(hooked_id) { if hooked.get_entity().removed.load(Ordering::Relaxed) { self.hooked_entity_id.store(0, Ordering::Relaxed); } else { let mut hooked_pos = hooked.get_entity().pos.load(); hooked_pos.y += hooked.get_entity().get_eye_height() * 0.8; entity.set_pos(hooked_pos); return; } } else { self.hooked_entity_id.store(0, Ordering::Relaxed); } } let mut velocity = entity.velocity.load(); let start_pos = entity.pos.load(); if entity.touching_water.load(Ordering::Relaxed) { velocity.y += 0.02; // Buoyancy let bite = self.bite_countdown.load(Ordering::Relaxed); if bite > 0 { self.bite_countdown.store(bite - 1, Ordering::Relaxed); if bite % 5 == 0 { world.spawn_particle( entity.pos.load(), Vector3::new(0.1f32, 0.1f32, 0.1f32), 0.0, 5, pumpkin_data::particle::Particle::Bubble, ); } } else { let wait = self.wait_countdown.load(Ordering::Relaxed); if wait > 0 { self.wait_countdown.store(wait - 1, Ordering::Relaxed); } else { // Start bite self.bite_countdown.store(40, Ordering::Relaxed); self.wait_countdown .store(rand::random::().abs() % 600 + 100, Ordering::Relaxed); world.play_sound( Sound::EntityFishingBobberSplash, SoundCategory::Neutral, &entity.pos.load(), ); } } } else { velocity.y -= Self::GRAVITY; } let inertia = if entity.touching_water.load(Ordering::Relaxed) { Self::WATER_INERTIA } else { Self::AIR_INERTIA }; velocity = velocity.multiply(inertia, inertia, inertia); entity.velocity.store(velocity); let new_pos = start_pos.add(&velocity); let search_box = BoundingBox::new( Vector3::new( start_pos.x.min(new_pos.x), start_pos.y.min(new_pos.y), start_pos.z.min(new_pos.z), ), Vector3::new( start_pos.x.max(new_pos.x), start_pos.y.max(new_pos.y), start_pos.z.max(new_pos.z), ), ) .expand(0.3, 0.3, 0.3); // Basic block collision to stop bobber let (block_cols, _) = world .get_block_collisions(search_box, caller.as_ref()) .await; if !block_cols.is_empty() { self.in_ground.store(true, Ordering::Relaxed); entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); return; } entity.set_pos(new_pos); let candidates = world.get_entities_at_box(&search_box); for cand in candidates { if cand.get_entity().entity_id == self.owner_id || cand.get_entity().entity_id == entity.entity_id { continue; } if is_projectile(cand.get_entity().entity_type) { continue; } let ebb = cand.get_entity().bounding_box.load().expand(0.3, 0.3, 0.3); if ebb.intersects(&search_box) { self.hooked_entity_id .store(cand.get_entity().entity_id, Ordering::Relaxed); entity.send_meta_data( &[Metadata::new( TrackedData::HOOKED_ENTITY, MetaDataType::INT, cand.get_entity().entity_id + 1, )], None, ); return; } } } } impl NBTStorage for FishingBobberEntity {} impl EntityBase for FishingBobberEntity { fn get_entity(&self) -> &Entity { &self.entity } fn get_living_entity(&self) -> Option<&LivingEntity> { None } fn cast_any(&self) -> &dyn std::any::Any { self } fn as_nbt_storage(&self) -> &dyn NBTStorage { self } fn on_hit(&self, _hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { Box::pin(async move { self.has_hit.store(true, Ordering::Relaxed); }) } fn tick<'a>( &'a self, caller: &'a Arc, server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.process_tick(caller, server).await; }) } }