Pumpkin/pumpkin/src/entity/projectile/fishing_bobber.rs
2026-07-13 12:53:33 +02:00

259 lines
8.4 KiB
Rust

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::<i32>().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<dyn EntityBase>, _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::<i32>().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<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.process_tick(caller, server).await;
})
}
}