use std::sync::atomic::AtomicBool; use std::sync::{Arc, RwLock}; use crate::{ entity::{Entity, EntityBase, 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; 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 fn set_item_stack(&self, item_stack: ItemStack) { let mut write = self .item_stack .write() .unwrap_or_else(std::sync::PoisonError::into_inner); *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. 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); } } } pub(crate) fn extinguish_fire_if_water_potion( world: &Arc, hit_pos: Vector3, stack: &ItemStack, ) { if is_water_potion(stack) { extinguish_fire(world, hit_pos); } } impl EntityBase for SplashPotionEntity { fn init_data_tracker(&self) { let entity = self.get_entity(); let stack = self .item_stack .read() .unwrap_or_else(std::sync::PoisonError::into_inner); // 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(&self, caller: &dyn EntityBase, _server: &Server) { self.thrown.process_tick(caller); } 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) { let world = self.get_entity().world.load(); let hit_pos = hit.hit_pos(); // Extinguish fire if it's a water potion let stack = self .item_stack .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); extinguish_fire_if_water_potion(&world, hit_pos, &stack); // Send impact entity status (plays break particles) world.send_entity_status( self.get_entity(), pumpkin_data::entity::EntityStatus::Death, Some(pumpkin_protocol::bedrock::server::actor_event::ActorEventID::Death), ); let effects = crate::item::potion::PotionContents::read_potion_effects(&stack); // Calculate color: custom_color if present, else blend of effects, else default water color let mut color = 0x385dc6; // default water-like color 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 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 { if let Some(living) = cand.get_living_entity() { let pos = cand.get_entity().pos.load(); let dx = pos.x - hit_pos.x; let dy = pos.y - hit_pos.y; let dz = pos.z - hit_pos.z; let dist = (dx * dx + dy * dy + dz * dz).sqrt(); if dist > radius { continue; } // Distance scaling let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0); crate::item::potion::PotionContents::apply_effects_to( living, effects.clone(), scale, crate::item::potion::PotionApplicationSource::Normal, ); } } } }