use super::{Entity, EntityBase, NBTStorage, living::LivingEntity}; use crate::server::Server; use pumpkin_data::BlockDirection; use pumpkin_data::entity::EntityType; use pumpkin_protocol::java::client::play::CEntityVelocity; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; use std::{ sync::Arc, sync::atomic::{AtomicBool, Ordering}, }; pub mod arrow; pub mod egg; pub mod ender_pearl; pub mod eye_of_ender; pub mod firework_rocket; pub mod lingering_potion; pub mod snowball; pub mod splash_potion; pub mod wind_charge; #[must_use] pub fn is_projectile(entity_type: &EntityType) -> bool { *entity_type == EntityType::ARROW || *entity_type == EntityType::EGG || *entity_type == EntityType::SNOWBALL || *entity_type == EntityType::FIREWORK_ROCKET || *entity_type == EntityType::WIND_CHARGE || *entity_type == EntityType::SPLASH_POTION || *entity_type == EntityType::LINGERING_POTION || *entity_type == EntityType::ENDER_PEARL } pub struct ThrownItemEntity { pub entity: Entity, pub owner_id: Option, pub collides_with_projectiles: bool, pub has_hit: AtomicBool, } impl ThrownItemEntity { pub fn new(entity: Entity, owner: &Entity) -> Self { let mut owner_pos = owner.pos.load(); owner_pos.y += owner.get_eye_height() - 0.1; entity.pos.store(owner_pos); Self { entity, owner_id: Some(owner.entity_id), collides_with_projectiles: false, has_hit: AtomicBool::new(false), } } pub fn set_velocity_from( &self, _shooter: &Entity, pitch: f32, yaw: f32, roll: f32, speed: f32, divergence: f32, ) { let yaw_rad = yaw.to_radians(); let pitch_rad = pitch.to_radians(); let roll_rad = (pitch + roll).to_radians(); let x = -yaw_rad.sin() * pitch_rad.cos(); let y = -roll_rad.sin(); let z = yaw_rad.cos() * pitch_rad.cos(); self.set_velocity( f64::from(x), f64::from(y), f64::from(z), f64::from(speed), f64::from(divergence), ); } pub fn set_velocity(&self, x: f64, y: f64, z: f64, power: f64, uncertainty: f64) { fn next_triangular(mode: f64, deviation: f64) -> f64 { deviation.mul_add(rand::random::() - rand::random::(), mode) } let velocity = Vector3::new(x, y, z) .normalize() .add_raw( next_triangular(0.0, 0.017_227_5 * uncertainty), next_triangular(0.0, 0.017_227_5 * uncertainty), next_triangular(0.0, 0.017_227_5 * uncertainty), ) .multiply(power, power, power); self.entity.velocity.store(velocity); let len = velocity.horizontal_length(); self.entity.set_rotation( velocity.x.atan2(velocity.z) as f32 * 57.295_776, velocity.y.atan2(len) as f32 * 57.295_776, ); } } impl NBTStorage for ThrownItemEntity {} impl ThrownItemEntity { /// Process a tick for projectile movement and collisions pub async fn process_tick<'a>(&'a self, caller: &'a Arc, _server: &'a Server) { let entity = self.get_entity(); let world = entity.world.load(); // Apply gravity and inertia let mut velocity = entity.velocity.load(); velocity.y -= self.get_gravity(); let inertia = if entity.touching_water.load(Ordering::Relaxed) { 0.8 } else { 0.99 }; velocity = velocity.multiply(inertia, inertia, inertia); // Store velocity entity.velocity.store(velocity); let start_pos = entity.pos.load(); let delta = velocity; // Update position let new_pos = start_pos.add(&delta); entity.set_pos(new_pos); // Send updated velocity to clients let packet = CEntityVelocity::new(entity.entity_id.into(), velocity); world.broadcast_packet_all(&packet).await; // Calculate search box for collisions 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); let mut closest_t = 1.0f64; let mut hit = None; // Block collisions let (block_cols, block_positions) = world .get_block_collisions(search_box, caller.as_ref()) .await; for (idx, bb) in block_cols.iter().enumerate() { if let Some(t) = calculate_ray_intersection(&start_pos, &delta, bb) && t < closest_t { closest_t = t; // Map back to block pos let mut curr = 0; for (len, pos) in &block_positions { curr += len; if idx < curr { let hit_pos = start_pos.add(&delta.multiply(t, t, t)); hit = Some(ProjectileHit::Block { pos: *pos, face: get_hit_face(hit_pos, *pos), hit_pos, normal: delta.normalize().multiply(-1.0, -1.0, -1.0), }); break; } } } } // Entity collisions let candidates = world.get_entities_at_box(&search_box); for cand in candidates { if self.should_skip_collision(entity, &cand) { continue; } let ebb = cand.get_entity().bounding_box.load().expand(0.3, 0.3, 0.3); if let Some(t) = calculate_ray_intersection(&start_pos, &delta, &ebb) && t < closest_t { closest_t = t; let hit_pos = start_pos.add(&delta.multiply(t, t, t)); hit = Some(ProjectileHit::Entity { entity: cand.clone(), hit_pos, normal: delta.normalize().multiply(-1.0, -1.0, -1.0), }); } } // Handle hit or continue if let Some(h) = hit { // Ensure hit is only processed once per projectile if self.has_hit.swap(true, Ordering::SeqCst) { return; } // Just trigger hit effects and remove caller.on_hit(h).await; entity.remove().await; } } /// Returns if collision should be skipped (e.g. owner or projectile vs projectile) fn should_skip_collision(&self, self_ent: &Entity, other: &Arc) -> bool { let other_ent = other.get_entity(); if other_ent.entity_id == self_ent.entity_id { return true; } // Skip owner for initial frames if Some(other_ent.entity_id) == self.owner_id && self_ent.age.load(Ordering::Relaxed) < 5 { return true; } // Projectiles should pass through lingering clouds if *other_ent.entity_type == EntityType::AREA_EFFECT_CLOUD { return true; } // Projectile vs projectile logic if !self.collides_with_projectiles && is_projectile(other_ent.entity_type) { return true; } false } const fn get_entity(&self) -> &Entity { &self.entity } #[allow(dead_code, clippy::unused_self)] const fn get_living_entity(&self) -> Option<&LivingEntity> { None } #[allow(dead_code, clippy::unused_self)] const fn as_nbt_storage(&self) -> &dyn NBTStorage { self } #[allow(clippy::unused_self)] const fn get_gravity(&self) -> f64 { 0.03 } } /// Ray intersection algorithm for AABBs, returning a t value fn calculate_ray_intersection( start: &Vector3, dir: &Vector3, bb: &BoundingBox, ) -> Option { let mut t_min = 0.0f64; let mut t_max = 1.0f64; let b_min = [bb.min.x, bb.min.y, bb.min.z]; let b_max = [bb.max.x, bb.max.y, bb.max.z]; let s = [start.x, start.y, start.z]; let d = [dir.x, dir.y, dir.z]; for i in 0..3 { if d[i].abs() < 1e-9 { if s[i] < b_min[i] || s[i] > b_max[i] { return None; } } else { let t1 = (b_min[i] - s[i]) / d[i]; let t2 = (b_max[i] - s[i]) / d[i]; t_min = t_min.max(t1.min(t2)); t_max = t_max.min(t1.max(t2)); } } (0.0..=1.0).contains(&t_min).then_some(t_min) } /// Get the face of the block that was hit fn get_hit_face(hit_pos: Vector3, block_pos: BlockPos) -> BlockDirection { let local = hit_pos.sub(&block_pos.0.to_f64()); let eps = 1.0e-4; if local.x <= eps { BlockDirection::West } else if local.x >= 1.0 - eps { BlockDirection::East } else if local.y <= eps { BlockDirection::Down } else if local.y >= 1.0 - eps { BlockDirection::Up } else if local.z <= eps { BlockDirection::North } else { BlockDirection::South } } pub enum ProjectileHit { Block { pos: BlockPos, face: BlockDirection, hit_pos: Vector3, normal: Vector3, }, Entity { entity: Arc, hit_pos: Vector3, normal: Vector3, }, } impl ProjectileHit { /// Returns the exact impact coordinates regardless of what was hit. #[must_use] pub const fn hit_pos(&self) -> Vector3 { match self { Self::Block { hit_pos, .. } | Self::Entity { hit_pos, .. } => *hit_pos, } } /// Returns the surface normal of the impact. #[must_use] pub const fn normal(&self) -> Vector3 { match self { Self::Block { normal, .. } | Self::Entity { normal, .. } => *normal, } } /// Safely returns the face hit if it was a block, otherwise None. #[must_use] pub const fn face(&self) -> Option { match self { Self::Block { face, .. } => Some(*face), Self::Entity { .. } => None, } } }