use std::sync::Arc; use std::sync::atomic::AtomicBool; use crate::{ entity::{Entity, EntityBase, EntityBaseFuture, projectile::ThrownItemEntity}, server::Server, }; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::{Block, BlockId}; use pumpkin_protocol::java::client::play::CWorldEvent; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::{boundingbox::BoundingBox, vector2::Vector2}; use pumpkin_util::math::{position::BlockPos, vector2::to_chunk_pos}; use pumpkin_world::world::BlockFlags; use tokio::sync::RwLock; const GRAVITY: f64 = 0.05; pub struct SplashPotionEntity { pub thrown: ThrownItemEntity, pub item_stack: RwLock, } impl SplashPotionEntity { pub fn new(entity: Entity) -> Self { entity.set_velocity(Vector3::new(0.0, 0.1, 0.0)); let thrown = ThrownItemEntity { entity, owner_id: None, collides_with_projectiles: false, has_hit: AtomicBool::new(false), gravity: GRAVITY, }; Self { thrown, item_stack: RwLock::new(ItemStack::new(1, &pumpkin_data::item::Item::SPLASH_POTION)), } } pub fn new_shot(entity: Entity, shooter: &Entity) -> Self { let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY); thrown.entity.set_velocity(Vector3::new(0.0, 0.1, 0.0)); Self { thrown, item_stack: RwLock::new(ItemStack::new(1, &pumpkin_data::item::Item::SPLASH_POTION)), } } pub async fn set_item_stack(&self, item_stack: ItemStack) { let mut write = self.item_stack.write().await; *write = item_stack; } } fn is_water_potion(stack: &ItemStack) -> bool { stack .get_data_component::() .and_then(|pc| pc.potion_id) == Some(pumpkin_data::potion::Potion::WATER.id as i32) } /// Extinguishes fire (including soul fire) at the hit position and its four horizontal neighbors. async fn extinguish_fire(world: &Arc, hit_pos: Vector3) { let air_state_id = Block::AIR.default_state.id; let neighbors = [ hit_pos, Vector3::new(hit_pos.x + 1.0, hit_pos.y, hit_pos.z), Vector3::new(hit_pos.x - 1.0, hit_pos.y, hit_pos.z), Vector3::new(hit_pos.x, hit_pos.y, hit_pos.z + 1.0), Vector3::new(hit_pos.x, hit_pos.y, hit_pos.z - 1.0), ]; for p in neighbors { let pos = BlockPos(Vector3::new( p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32, )); let state_id = world.get_block_state_id(&pos); let raw_block_id = state_id.to_block_id(); if raw_block_id == BlockId::FIRE || raw_block_id == BlockId::SOUL_FIRE { world .set_block_state(&pos, air_state_id, BlockFlags::NOTIFY_ALL) .await; } } } pub(crate) async fn extinguish_fire_if_water_potion( world: &Arc, hit_pos: Vector3, stack: &ItemStack, ) { if is_water_potion(stack) { extinguish_fire(world, hit_pos).await; } } impl EntityBase for SplashPotionEntity { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { let entity = self.get_entity(); let stack = self.item_stack.read().await; // Sync the item stack entity.send_meta_data( &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::splash_potion::ITEM_STACK, &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( stack.clone(), ), )], None, ); }) } fn tick<'a>( &'a self, caller: &'a Arc, server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.thrown.process_tick(caller, server).await }) } fn get_entity(&self) -> &Entity { self.thrown.get_entity() } fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { None } fn cast_any(&self) -> &dyn std::any::Any { self } fn on_hit(&self, hit: crate::entity::projectile::ProjectileHit) -> EntityBaseFuture<'_, ()> { Box::pin(async move { let world = self.get_entity().world.load(); let hit_pos = hit.hit_pos(); // Only extinguish fire for plain water potions let stack = self.item_stack.read().await.clone(); extinguish_fire_if_water_potion(&world, hit_pos, &stack).await; let effects = crate::item::potion::PotionContents::read_potion_effects(&stack); let mut color = 0x385dc6; // Default to water color if no effects/color found if let Some(pc) = stack.get_data_component::() { if let Some(c) = pc.custom_color { color = c; } else if !effects.is_empty() { let mut r_sum = 0.0; let mut g_sum = 0.0; let mut b_sum = 0.0; let count = effects.len() as f32; for (eff, _, _, _, _, _) in &effects { let c = eff.color; r_sum += ((c >> 16) & 0xFF) as f32; g_sum += ((c >> 8) & 0xFF) as f32; b_sum += (c & 0xFF) as f32; } let r = (r_sum / count) as i32; let g = (g_sum / count) as i32; let b = (b_sum / count) as i32; color = (r << 16) | (g << 8) | b; } } else { // Try to guess from effects directly if potion contents missing but effects present if !effects.is_empty() { let mut r_sum = 0.0; let mut g_sum = 0.0; let mut b_sum = 0.0; let count = effects.len() as f32; for (eff, _, _, _, _, _) in &effects { let c = eff.color; r_sum += ((c >> 16) & 0xFF) as f32; g_sum += ((c >> 8) & 0xFF) as f32; b_sum += (c & 0xFF) as f32; } let r = (r_sum / count) as i32; let g = (g_sum / count) as i32; let b = (b_sum / count) as i32; color = (r << 16) | (g << 8) | b; } } // Play splash particles let has_instant = effects.iter().any(|(e, _, _, _, _, _)| { e.id == pumpkin_data::effect::StatusEffect::INSTANT_DAMAGE.id || e.id == pumpkin_data::effect::StatusEffect::INSTANT_HEALTH.id }); let event_id = if has_instant { 2007 } else { 2002 }; // Convert hit_pos to BlockPos let block_pos = BlockPos(Vector3::new( hit_pos.x.floor() as i32, hit_pos.y.floor() as i32, hit_pos.z.floor() as i32, )); world.broadcast_to_chunk( to_chunk_pos(&Vector2::new(block_pos.0.x, block_pos.0.z)), &CWorldEvent::new(event_id, block_pos, color, false), ); // If no effects, just splash (like water bottles) if effects.is_empty() { return; } let radius = 4.0f64; let min = Vector3::new(hit_pos.x - radius, hit_pos.y - radius, hit_pos.z - radius); let max = Vector3::new(hit_pos.x + radius, hit_pos.y + radius, hit_pos.z + radius); let aabb = BoundingBox::new(min, max); // Gather entity and player candidates let mut candidates = world.get_entities_at_box(&aabb); let players = world.get_players_at_box(&aabb); for p in players { candidates.push(p.clone() as Arc); } for cand in candidates { let cand_clone = cand.clone(); let effs_clone: Vec<_> = effects.clone(); let hit_pos_clone = hit_pos; tokio::spawn(async move { if let Some(living) = cand_clone.get_living_entity() { let pos = cand_clone.get_entity().pos.load(); let dx = pos.x - hit_pos_clone.x; let dy = pos.y - hit_pos_clone.y; let dz = pos.z - hit_pos_clone.z; let dist = (dx * dx + dy * dy + dz * dz).sqrt(); if dist > radius { return; } // Distance scaling let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0); crate::item::potion::PotionContents::apply_effects_to( living, effs_clone, scale, crate::item::potion::PotionApplicationSource::Normal, ) .await; } }); } }) } }