use std::collections::HashSet; use std::sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}, }; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::world::WorldEvent; use pumpkin_data::{Block, BlockId, BlockStateId}; use pumpkin_util::Difficulty; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::world::BlockFlags; use rand::RngExt; use crate::block::blocks::fire::FireBlockBase; use crate::entity::player::Player; use crate::entity::{Entity, EntityBase}; use crate::server::Server; use crate::world::World; pub struct LightningBoltEntity { entity: Entity, life: AtomicI32, seed: AtomicI64, flashes: AtomicI32, visual_only: AtomicBool, cause: Mutex>>, hit_entities: Mutex>, blocks_set_on_fire: AtomicI32, } impl LightningBoltEntity { pub fn new(entity: Entity) -> Self { entity.no_physics.store(true, Ordering::Relaxed); let seed = rand::rng().random::(); let flashes = rand::rng().random_range(1..=3); Self { entity, life: AtomicI32::new(2), seed: AtomicI64::new(seed), flashes: AtomicI32::new(flashes), visual_only: AtomicBool::new(false), cause: Mutex::new(None), hit_entities: Mutex::new(HashSet::new()), blocks_set_on_fire: AtomicI32::new(0), } } pub fn set_visual_only(&self, visual_only: bool) { self.visual_only.store(visual_only, Ordering::Relaxed); } pub fn is_visual_only(&self) -> bool { self.visual_only.load(Ordering::Relaxed) } pub fn set_cause(&self, cause: Option>) { if let Ok(mut c) = self.cause.lock() { *c = cause; } } pub fn get_cause(&self) -> Option> { self.cause.lock().ok().and_then(|c| c.clone()) } pub fn get_blocks_set_on_fire(&self) -> i32 { self.blocks_set_on_fire.load(Ordering::Relaxed) } pub fn get_seed(&self) -> i64 { self.seed.load(Ordering::Relaxed) } pub fn set_seed(&self, seed: i64) { self.seed.store(seed, Ordering::Relaxed); } fn get_strike_position(&self) -> BlockPos { let pos = self.entity.pos.load(); Vector3::new(pos.x, pos.y - 1.0e-6, pos.z).to_block_pos() } fn power_lightning_rod(&self, world: &Arc) { let strike_pos = self.get_strike_position(); let block = world.get_block(&strike_pos); if block == &Block::LIGHTNING_ROD { crate::block::blocks::redstone::lightning_rod::LightningRodBlock::trigger( world, &strike_pos, ); } } fn spawn_fire(&self, world: &Arc, additional_sources: i32) { if self.visual_only.load(Ordering::Relaxed) { return; } let pos = self.entity.block_pos.load(); let try_place = |p: BlockPos| { if world.get_block_state(&p).is_air() && FireBlockBase::can_place_at(world, &p) { let fire_block = FireBlockBase::get_fire_type(world, &p); world.set_block_state(&p, fire_block.default_state.id, BlockFlags::NOTIFY_ALL); self.blocks_set_on_fire.fetch_add(1, Ordering::Relaxed); } }; try_place(pos); for _ in 0..additional_sources { let dx = rand::rng().random_range(-1..=1); let dy = rand::rng().random_range(-1..=1); let dz = rand::rng().random_range(-1..=1); let nearby_pos = pos.offset(Vector3::new(dx, dy, dz)); try_place(nearby_pos); } } fn clear_copper_on_lightning_strike(&self, world: &Arc) { let strike_pos = self.get_strike_position(); let struck_state = world.get_block_state(&strike_pos); let struck_block = struck_state.id.to_block(); let is_waxed = is_waxed_copper(struck_block.id); let is_weathering = is_weathering_copper(struck_block.id); if is_weathering || is_waxed { if is_weathering && let Some(first_block_id) = get_first_stage_copper(struck_block.id) { let first_block = first_block_id.to_block(); let offset = struck_state .id .as_u16() .saturating_sub(struck_block.default_state.id.as_u16()); let new_state_id = BlockStateId::new(first_block.default_state.id.as_u16() + offset) .unwrap_or(first_block.default_state.id); world.set_block_state(&strike_pos, new_state_id, BlockFlags::NOTIFY_ALL); } let strikes_count = rand::rng().random_range(3..=5); for _ in 0..strikes_count { let step_count = rand::rng().random_range(1..=8); Self::random_walk_cleaning_copper(world, &strike_pos, step_count); } } } fn random_walk_cleaning_copper( world: &Arc, original_strike_pos: &BlockPos, step_count: i32, ) { let mut work_pos = *original_strike_pos; for _ in 0..step_count { if let Some(next_pos) = Self::random_step_cleaning_copper(world, &work_pos) { work_pos = next_pos; } else { break; } } } fn random_step_cleaning_copper(world: &Arc, pos: &BlockPos) -> Option { let candidates = random_in_cube(10, pos, 1); for candidate in candidates { let state = world.get_block_state(&candidate); let block = state.id.to_block(); if is_weathering_copper(block.id) && let Some(prev_block_id) = get_previous_stage_copper(block.id) { let prev_block = prev_block_id.to_block(); let offset = state .id .as_u16() .saturating_sub(block.default_state.id.as_u16()); let new_state_id = BlockStateId::new(prev_block.default_state.id.as_u16() + offset) .unwrap_or(prev_block.default_state.id); world.set_block_state(&candidate, new_state_id, BlockFlags::NOTIFY_ALL); world.sync_world_event(WorldEvent::ParticlesElectricSpark, candidate, -1); return Some(candidate); } } None } } impl EntityBase for LightningBoltEntity { fn tick(&self, _caller: &Arc, _server: &Server) { let entity = &self.entity; let life = self.life.load(Ordering::Relaxed); if life == 2 { let world = entity.world.load(); let pos = entity.pos.load(); let pitch_thunder = 0.8 + rand::rng().random::() * 0.2; world.play_sound_fine( Sound::EntityLightningBoltThunder, SoundCategory::Weather, &pos, 10000.0, pitch_thunder, ); let pitch_impact = 0.5 + rand::rng().random::() * 0.2; world.play_sound_fine( Sound::EntityLightningBoltImpact, SoundCategory::Weather, &pos, 2.0, pitch_impact, ); let difficulty = world.level_info.load().difficulty; if difficulty == Difficulty::Normal || difficulty == Difficulty::Hard { self.spawn_fire(&world, 4); } self.power_lightning_rod(&world); self.clear_copper_on_lightning_strike(&world); } let new_life = life - 1; self.life.store(new_life, Ordering::Relaxed); if new_life < 0 { let flashes = self.flashes.load(Ordering::Relaxed); if flashes == 0 { entity.remove(); return; } else if new_life < -rand::rng().random_range(0..10) { self.flashes.store(flashes - 1, Ordering::Relaxed); self.life.store(1, Ordering::Relaxed); self.seed.store(rand::random::(), Ordering::Relaxed); let world = entity.world.load(); self.spawn_fire(&world, 0); } } let current_life = self.life.load(Ordering::Relaxed); if current_life >= 0 && !self.visual_only.load(Ordering::Relaxed) { let world = entity.world.load(); let pos = entity.pos.load(); let damage_box = BoundingBox::new( Vector3::new(pos.x - 3.0, pos.y - 3.0, pos.z - 3.0), Vector3::new(pos.x + 3.0, pos.y + 9.0, pos.z + 3.0), ); let entities = world.get_all_at_box(&damage_box); if let Ok(mut hit_guard) = self.hit_entities.lock() { for hit_entity in entities { if hit_entity.get_entity().entity_id == entity.entity_id { continue; } let hit_id = hit_entity.get_entity().entity_id; if hit_guard.insert(hit_id) { hit_entity.on_lightning_strike(hit_entity.as_ref(), self); } } } } } fn init_data_tracker(&self) {} fn get_entity(&self) -> &Entity { &self.entity } fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { None } fn cast_any(&self) -> &dyn std::any::Any { self } } fn random_in_cube(count: usize, center: &BlockPos, radius: i32) -> Vec { let mut list = Vec::with_capacity(count); for _ in 0..count { let dx = rand::rng().random_range(-radius..=radius); let dy = rand::rng().random_range(-radius..=radius); let dz = rand::rng().random_range(-radius..=radius); list.push(center.offset(Vector3::new(dx, dy, dz))); } list } #[must_use] pub const fn is_waxed_copper(id: BlockId) -> bool { matches!( id, BlockId::WAXED_COPPER_BLOCK | BlockId::WAXED_EXPOSED_COPPER | BlockId::WAXED_WEATHERED_COPPER | BlockId::WAXED_OXIDIZED_COPPER | BlockId::WAXED_CHISELED_COPPER | BlockId::WAXED_EXPOSED_CHISELED_COPPER | BlockId::WAXED_WEATHERED_CHISELED_COPPER | BlockId::WAXED_OXIDIZED_CHISELED_COPPER | BlockId::WAXED_COPPER_GRATE | BlockId::WAXED_EXPOSED_COPPER_GRATE | BlockId::WAXED_WEATHERED_COPPER_GRATE | BlockId::WAXED_OXIDIZED_COPPER_GRATE | BlockId::WAXED_CUT_COPPER | BlockId::WAXED_EXPOSED_CUT_COPPER | BlockId::WAXED_WEATHERED_CUT_COPPER | BlockId::WAXED_OXIDIZED_CUT_COPPER | BlockId::WAXED_CUT_COPPER_STAIRS | BlockId::WAXED_EXPOSED_CUT_COPPER_STAIRS | BlockId::WAXED_WEATHERED_CUT_COPPER_STAIRS | BlockId::WAXED_OXIDIZED_CUT_COPPER_STAIRS | BlockId::WAXED_CUT_COPPER_SLAB | BlockId::WAXED_EXPOSED_CUT_COPPER_SLAB | BlockId::WAXED_WEATHERED_CUT_COPPER_SLAB | BlockId::WAXED_OXIDIZED_CUT_COPPER_SLAB | BlockId::WAXED_COPPER_BULB | BlockId::WAXED_EXPOSED_COPPER_BULB | BlockId::WAXED_WEATHERED_COPPER_BULB | BlockId::WAXED_OXIDIZED_COPPER_BULB | BlockId::WAXED_COPPER_DOOR | BlockId::WAXED_EXPOSED_COPPER_DOOR | BlockId::WAXED_WEATHERED_COPPER_DOOR | BlockId::WAXED_OXIDIZED_COPPER_DOOR | BlockId::WAXED_COPPER_TRAPDOOR | BlockId::WAXED_EXPOSED_COPPER_TRAPDOOR | BlockId::WAXED_WEATHERED_COPPER_TRAPDOOR | BlockId::WAXED_OXIDIZED_COPPER_TRAPDOOR ) } #[must_use] pub const fn is_weathering_copper(id: BlockId) -> bool { matches!( id, BlockId::COPPER_BLOCK | BlockId::EXPOSED_COPPER | BlockId::WEATHERED_COPPER | BlockId::OXIDIZED_COPPER | BlockId::CHISELED_COPPER | BlockId::EXPOSED_CHISELED_COPPER | BlockId::WEATHERED_CHISELED_COPPER | BlockId::OXIDIZED_CHISELED_COPPER | BlockId::COPPER_GRATE | BlockId::EXPOSED_COPPER_GRATE | BlockId::WEATHERED_COPPER_GRATE | BlockId::OXIDIZED_COPPER_GRATE | BlockId::CUT_COPPER | BlockId::EXPOSED_CUT_COPPER | BlockId::WEATHERED_CUT_COPPER | BlockId::OXIDIZED_CUT_COPPER | BlockId::CUT_COPPER_STAIRS | BlockId::EXPOSED_CUT_COPPER_STAIRS | BlockId::WEATHERED_CUT_COPPER_STAIRS | BlockId::OXIDIZED_CUT_COPPER_STAIRS | BlockId::CUT_COPPER_SLAB | BlockId::EXPOSED_CUT_COPPER_SLAB | BlockId::WEATHERED_CUT_COPPER_SLAB | BlockId::OXIDIZED_CUT_COPPER_SLAB | BlockId::COPPER_BULB | BlockId::EXPOSED_COPPER_BULB | BlockId::WEATHERED_COPPER_BULB | BlockId::OXIDIZED_COPPER_BULB | BlockId::COPPER_DOOR | BlockId::EXPOSED_COPPER_DOOR | BlockId::WEATHERED_COPPER_DOOR | BlockId::OXIDIZED_COPPER_DOOR | BlockId::COPPER_TRAPDOOR | BlockId::EXPOSED_COPPER_TRAPDOOR | BlockId::WEATHERED_COPPER_TRAPDOOR | BlockId::OXIDIZED_COPPER_TRAPDOOR ) } #[must_use] pub const fn get_first_stage_copper(id: BlockId) -> Option { match id { BlockId::COPPER_BLOCK | BlockId::EXPOSED_COPPER | BlockId::WEATHERED_COPPER | BlockId::OXIDIZED_COPPER => Some(BlockId::COPPER_BLOCK), BlockId::CHISELED_COPPER | BlockId::EXPOSED_CHISELED_COPPER | BlockId::WEATHERED_CHISELED_COPPER | BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER), BlockId::COPPER_GRATE | BlockId::EXPOSED_COPPER_GRATE | BlockId::WEATHERED_COPPER_GRATE | BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::COPPER_GRATE), BlockId::CUT_COPPER | BlockId::EXPOSED_CUT_COPPER | BlockId::WEATHERED_CUT_COPPER | BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::CUT_COPPER), BlockId::CUT_COPPER_STAIRS | BlockId::EXPOSED_CUT_COPPER_STAIRS | BlockId::WEATHERED_CUT_COPPER_STAIRS | BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS), BlockId::CUT_COPPER_SLAB | BlockId::EXPOSED_CUT_COPPER_SLAB | BlockId::WEATHERED_CUT_COPPER_SLAB | BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB), BlockId::COPPER_BULB | BlockId::EXPOSED_COPPER_BULB | BlockId::WEATHERED_COPPER_BULB | BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::COPPER_BULB), BlockId::COPPER_DOOR | BlockId::EXPOSED_COPPER_DOOR | BlockId::WEATHERED_COPPER_DOOR | BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::COPPER_DOOR), BlockId::COPPER_TRAPDOOR | BlockId::EXPOSED_COPPER_TRAPDOOR | BlockId::WEATHERED_COPPER_TRAPDOOR | BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR), _ => None, } } #[must_use] pub const fn get_previous_stage_copper(id: BlockId) -> Option { match id { BlockId::OXIDIZED_COPPER => Some(BlockId::WEATHERED_COPPER), BlockId::WEATHERED_COPPER => Some(BlockId::EXPOSED_COPPER), BlockId::EXPOSED_COPPER => Some(BlockId::COPPER_BLOCK), BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::WEATHERED_CHISELED_COPPER), BlockId::WEATHERED_CHISELED_COPPER => Some(BlockId::EXPOSED_CHISELED_COPPER), BlockId::EXPOSED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER), BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::WEATHERED_COPPER_GRATE), BlockId::WEATHERED_COPPER_GRATE => Some(BlockId::EXPOSED_COPPER_GRATE), BlockId::EXPOSED_COPPER_GRATE => Some(BlockId::COPPER_GRATE), BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::WEATHERED_CUT_COPPER), BlockId::WEATHERED_CUT_COPPER => Some(BlockId::EXPOSED_CUT_COPPER), BlockId::EXPOSED_CUT_COPPER => Some(BlockId::CUT_COPPER), BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::WEATHERED_CUT_COPPER_STAIRS), BlockId::WEATHERED_CUT_COPPER_STAIRS => Some(BlockId::EXPOSED_CUT_COPPER_STAIRS), BlockId::EXPOSED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS), BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::WEATHERED_CUT_COPPER_SLAB), BlockId::WEATHERED_CUT_COPPER_SLAB => Some(BlockId::EXPOSED_CUT_COPPER_SLAB), BlockId::EXPOSED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB), BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::WEATHERED_COPPER_BULB), BlockId::WEATHERED_COPPER_BULB => Some(BlockId::EXPOSED_COPPER_BULB), BlockId::EXPOSED_COPPER_BULB => Some(BlockId::COPPER_BULB), BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::WEATHERED_COPPER_DOOR), BlockId::WEATHERED_COPPER_DOOR => Some(BlockId::EXPOSED_COPPER_DOOR), BlockId::EXPOSED_COPPER_DOOR => Some(BlockId::COPPER_DOOR), BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::WEATHERED_COPPER_TRAPDOOR), BlockId::WEATHERED_COPPER_TRAPDOOR => Some(BlockId::EXPOSED_COPPER_TRAPDOOR), BlockId::EXPOSED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR), _ => None, } }