diff --git a/Cargo.lock b/Cargo.lock index 14b0f90be..5691250b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,13 +19,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ - "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] @@ -375,9 +375,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfb8" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "014c0a0e1ad0dae6a86c082db2f9bd7fe8c2c734227047d0d8b4d4a3a094a1e1" +checksum = "c22e6368a74b7dd7039e10da9f0300ab4520584f2960048a5a4a4e51f8e4e183" dependencies = [ "cipher", ] @@ -447,11 +447,11 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" dependencies = [ - "crypto-common 0.1.7", + "crypto-common 0.2.1", "inout", ] @@ -1892,11 +1892,11 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -2350,6 +2350,15 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + [[package]] name = "p384" version = "0.13.1" @@ -2756,6 +2765,7 @@ dependencies = [ "hmac 0.13.0", "libloading", "num-bigint", + "ordered-float", "pkcs8 0.11.0-rc.11", "postcard", "pumpkin-config", @@ -2893,6 +2903,7 @@ dependencies = [ "bytes", "cfb8", "flate2", + "hybrid-array", "pumpkin-data", "pumpkin-macros", "pumpkin-nbt", diff --git a/Cargo.toml b/Cargo.toml index 9c4edada1..1738cb113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,12 +110,12 @@ serde_json = "1.0" tempfile = { version = "3.27.0", default-features = false } proc-macro-error2 = "2" -aes = "0.8" +aes = "0.9" async-compression = "0.4.41" base64 = "0.22.1" bitflags = "2.11.0" cesu8 = "1.1" -cfb8 = "0.8" +cfb8 = "0.9" colored = "3.1" console-subscriber = { version = "0.5.0", default-features = false } crc-fast = "1.10.0" @@ -155,6 +155,7 @@ pumpkin-world = { path = "pumpkin-world" } quote = "1.0" rand = "0.10.0" rsa = "=0.10.0-rc.17" +cipher = "0.5.1" rustc-hash = "2.1.2" rustyline = "18.0.0" ruzstd = "0.8.2" diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index 20276a407..131ed6822 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -27,6 +27,7 @@ bytes.workspace = true # encryption aes.workspace = true cfb8.workspace = true +hybrid-array = "0.4" # compression async-compression = { workspace = true, features = ["tokio", "zlib"] } diff --git a/pumpkin-protocol/src/java/packet_decoder.rs b/pumpkin-protocol/src/java/packet_decoder.rs index b9a0240b7..3b6a27f4d 100644 --- a/pumpkin-protocol/src/java/packet_decoder.rs +++ b/pumpkin-protocol/src/java/packet_decoder.rs @@ -195,7 +195,6 @@ mod tests { use super::*; use aes::Aes128; use cfb8::Encryptor as Cfb8Encryptor; - use cfb8::cipher::AsyncStreamCipher; use flate2::Compression; use flate2::write::ZlibEncoder; @@ -210,7 +209,8 @@ mod tests { /// Helper function to encrypt data using AES-128 CFB-8 mode fn encrypt_aes128(data: &mut [u8], key: &[u8; 16], iv: &[u8; 16]) { - let encryptor = Cfb8Encryptor::::new_from_slices(key, iv).expect("Invalid key/iv"); + let mut encryptor = + Cfb8Encryptor::::new_from_slices(key, iv).expect("Invalid key/iv"); encryptor.encrypt(data); } diff --git a/pumpkin-protocol/src/java/packet_encoder.rs b/pumpkin-protocol/src/java/packet_encoder.rs index d097f9834..cc0efe263 100644 --- a/pumpkin-protocol/src/java/packet_encoder.rs +++ b/pumpkin-protocol/src/java/packet_encoder.rs @@ -331,7 +331,6 @@ mod tests { use crate::{ClientPacket, ReadingError}; use aes::Aes128; use cfb8::Decryptor as Cfb8Decryptor; - use cfb8::cipher::AsyncStreamCipher; use flate2::read::ZlibDecoder; use pumpkin_data::packet::clientbound::STATUS_STATUS_RESPONSE; use pumpkin_macros::java_packet; @@ -368,7 +367,8 @@ mod tests { /// Helper function to decrypt data using AES-128 CFB-8 mode fn decrypt_aes128(encrypted_data: &mut [u8], key: &[u8; 16], iv: &[u8; 16]) { - let decryptor = Cfb8Decryptor::::new_from_slices(key, iv).expect("Invalid key/iv"); + let mut decryptor = + Cfb8Decryptor::::new_from_slices(key, iv).expect("Invalid key/iv"); decryptor.decrypt(encrypted_data); } diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index e73947480..3c0833764 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -5,9 +5,10 @@ use std::{ task::{Context, Poll}, }; -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, BlockSizeUser, generic_array::GenericArray}; +use aes::cipher::BlockSizeUser; use bytes::Bytes; use codec::var_int::VarInt; +use hybrid_array::{Array, sizes::U1}; use pumpkin_util::{ resource_location::ResourceLocation, text::{TextComponent, style::Style}, @@ -216,7 +217,7 @@ impl AsyncRead for StreamDecryptor { if matches!(internal_poll, Poll::Ready(Ok(()))) { // Decrypt the raw data in-place, note that our block size is 1 byte, so this is always safe for block in buf.filled_mut()[original_fill..].chunks_mut(Aes128Cfb8Dec::block_size()) { - cipher.decrypt_block_mut(block.into()); + cipher.decrypt(block); } } @@ -265,8 +266,10 @@ impl AsyncWrite for StreamEncryptor { // This should never panic out[0] = out_to_use; } else { - let out_block = GenericArray::from_mut_slice(&mut out); - cipher.encrypt_block_b2b_mut(block.into(), out_block); + let out_block: &mut Array = (&mut out[..]) + .try_into() + .expect("Output slice size does not match block size"); + cipher.encrypt_b2b(block, out_block).unwrap(); } let write = Pin::new(&mut ref_self.write); diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index ef6d4f214..e85d2bb5a 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -15,7 +15,7 @@ use crate::{ world::BlockRegistryExt, }; use crossbeam::channel::Sender; -use dashmap::DashMap; +use dashmap::{DashMap, Entry}; use pumpkin_config::{chunk::ChunkConfig, lighting::LightingEngineConfig, world::LevelConfig}; use pumpkin_data::biome::Biome; use pumpkin_data::dimension::Dimension; @@ -215,7 +215,7 @@ impl Level { // TODO let total_cores = num_cpus::get().saturating_sub(2).max(1); let threads_per_dimension = (total_cores / 2).max(1); - let entity_threads = (threads_per_dimension / 2).max(1); + let entity_threads = 1; GenerationSchedule::create( 2, @@ -395,19 +395,13 @@ impl Level { let mut chunks_to_clean = Vec::new(); for chunk in chunks { - let mut should_remove = false; - - if let Some(mut count) = self.chunk_watchers.get_mut(chunk) { - *count = count.saturating_sub(1); - if *count == 0 { - should_remove = true; + if let Entry::Occupied(mut entry) = self.chunk_watchers.entry(*chunk) { + *entry.get_mut() = entry.get().saturating_sub(1); + if *entry.get() == 0 { + entry.remove(); + chunks_to_clean.push(*chunk); } } - - if should_remove { - self.chunk_watchers.remove(chunk); - chunks_to_clean.push(*chunk); - } } self.entity_saver @@ -461,7 +455,14 @@ impl Level { block_entities: Vec::new(), }; - for chunk in self.loaded_chunks.iter() { + // Clone the Arcs quickly to release map shard locks! + let chunks_to_tick: Vec = self + .loaded_chunks + .iter() + .map(|entry| entry.value().clone()) + .collect(); + + for chunk in chunks_to_tick { let chunk_x_base = chunk.x * 16; let chunk_z_base = chunk.z * 16; let section_count = chunk.section.count; diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index a3799bad0..70f07cdf3 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -97,6 +97,8 @@ wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } wasmparser = { workspace = true } +ordered-float = "5.3.0" + postcard = { workspace = true } tracing-serde-structured = { workspace = true } diff --git a/pumpkin/src/command/commands/effect.rs b/pumpkin/src/command/commands/effect.rs index 19bbbdc4b..78facc3dc 100644 --- a/pumpkin/src/command/commands/effect.rs +++ b/pumpkin/src/command/commands/effect.rs @@ -68,7 +68,7 @@ impl CommandExecutor for GiveExecutor { let amplifier: u8 = match self.1 { Amplifier::Base => 0, - Amplifier::Specified => BoundedNumArgumentConsumer::new() + Amplifier::Specified => BoundedNumArgumentConsumer::::new() .name("amplifier") .min(0) .max(255) diff --git a/pumpkin/src/entity/area_effect_cloud.rs b/pumpkin/src/entity/area_effect_cloud.rs index 5765a6e5f..71fd12414 100644 --- a/pumpkin/src/entity/area_effect_cloud.rs +++ b/pumpkin/src/entity/area_effect_cloud.rs @@ -418,4 +418,8 @@ impl EntityBase for AreaEffectCloudEntity { fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { None } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/boss/ender_dragon.rs b/pumpkin/src/entity/boss/ender_dragon.rs index 1ebc046de..09506f2d5 100644 --- a/pumpkin/src/entity/boss/ender_dragon.rs +++ b/pumpkin/src/entity/boss/ender_dragon.rs @@ -1,19 +1,783 @@ +use pumpkin_data::Block; +use pumpkin_data::damage::DamageType; +use pumpkin_data::entity::EntityType; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; use std::sync::Arc; +use std::sync::atomic::Ordering; +use tokio::sync::Mutex; use crate::entity::{ - Entity, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, + living::LivingEntity, mob::{Mob, MobEntity}, + player::Player, }; +pub mod flight_history; +pub mod phase; + +use flight_history::DragonFlightHistory; +use phase::{EnderDragonPhase, PhaseManager}; + +pub const NODE_COUNT: usize = 24; +pub const NODE_Y: i32 = 105; +pub const NODE_REACH_SQ: f64 = 64.0; +pub const DEATH_TIMER_MAX: i32 = 200; +const GROWL_COOLDOWN_BASE: i32 = 200; +const FLAP_SPEED: f32 = 0.2; +pub const FLY_ACCEL: f64 = 0.1; +pub const FLY_SPEED: f64 = 0.6; +const ARENA_RADIUS: f64 = 192.0; + +const NODE_ADJACENCY: [u32; NODE_COUNT] = [ + 6146, 8197, 8202, 16404, 32808, 32848, 65696, 131392, 131712, 263424, 526848, 525313, 1581057, + 3166214, 2138120, 6373424, 4358208, 12910976, 9044480, 9706496, 15216640, 13688832, 11763712, + 8257536, +]; + +#[derive(Clone, Copy, Default, Debug)] +pub struct DragonNode { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl DragonNode { + #[must_use] + pub const fn new(x: f64, y: f64, z: f64) -> Self { + Self { x, y, z } + } + + #[inline] + #[must_use] + pub fn dist_sq(&self, other: &Self) -> f64 { + let (dx, dy, dz) = (self.x - other.x, self.y - other.y, self.z - other.z); + dx * dx + dy * dy + dz * dz + } + + #[inline] + #[must_use] + pub fn dist_sq_vec(&self, v: Vector3) -> f64 { + let (dx, dy, dz) = (self.x - v.x, self.y - v.y, self.z - v.z); + dx * dx + dy * dy + dz * dz + } + + #[inline] + #[must_use] + pub const fn as_vec3(&self) -> Vector3 { + Vector3::new(self.x, self.y, self.z) + } +} + +#[must_use] +pub fn find_path( + nodes: &[Option; NODE_COUNT], + from: usize, + to: usize, + final_node: Option, +) -> Vec { + if from == to && final_node.is_none() { + return vec![]; + } + + let mut g = [f64::MAX; NODE_COUNT]; + let mut came_from = [NODE_COUNT; NODE_COUNT]; + let mut closed = [false; NODE_COUNT]; + let mut open: std::collections::BinaryHeap<(ordered_float::OrderedFloat, usize)> = + std::collections::BinaryHeap::new(); + + let h = |n: usize| -> f64 { + if let Some(target) = final_node + && let Some(node) = nodes[n] + { + return node.dist_sq(&target).sqrt(); + } + match (nodes[n], nodes[to]) { + (Some(a), Some(b)) => a.dist_sq(&b).sqrt(), + _ => 0.0, + } + }; + + g[from] = 0.0; + open.push((ordered_float::OrderedFloat(-h(from)), from)); + + 'outer: while let Some((_, cur)) = open.pop() { + if closed[cur] { + continue; + } + if cur == to { + break 'outer; + } + closed[cur] = true; + + let adj = NODE_ADJACENCY[cur]; + for nbr in 0..NODE_COUNT { + if (adj >> nbr) & 1 == 0 || closed[nbr] { + continue; + } + let (Some(cn), Some(nn)) = (nodes[cur], nodes[nbr]) else { + continue; + }; + let ng = g[cur] + cn.dist_sq(&nn).sqrt(); + if ng < g[nbr] { + g[nbr] = ng; + came_from[nbr] = cur; + open.push((ordered_float::OrderedFloat(-(ng + h(nbr))), nbr)); + } + } + } + + let mut path = Vec::new(); + let mut cur = to; + while cur != NODE_COUNT { + path.push(cur); + cur = came_from[cur]; + } + path.reverse(); + if path.first() == Some(&from) { + path.remove(0); + } + path +} + +pub struct EnderDragonPart { + pub entity: Entity, + pub dragon_uuid: uuid::Uuid, +} + +impl EnderDragonPart { + pub const fn new(entity: Entity, dragon_uuid: uuid::Uuid) -> Self { + Self { + entity, + dragon_uuid, + } + } +} + +impl NBTStorage for EnderDragonPart {} + +impl EntityBase for EnderDragonPart { + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn damage<'a>( + &'a self, + source: &'a dyn EntityBase, + amount: f32, + damage_type: DamageType, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let world = self.entity.world.load(); + if let Some(dragon_base) = world + .entities + .load() + .iter() + .find(|e| e.get_entity().entity_uuid == self.dragon_uuid) + { + return dragon_base.damage(source, amount, damage_type).await; + } + false + }) + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn can_hit(&self) -> bool { + true + } +} + pub struct EnderDragonEntity { pub mob_entity: MobEntity, + pub parts: Vec>, + + pub flight_history: Mutex, + pub flap_time: Mutex, + pub o_flap_time: Mutex, + + pub in_wall: Mutex, + pub dragon_death_time: Mutex, + pub sitting_damage_received: Mutex, + + pub nodes_initialized: Mutex, + + pub fight_origin: Mutex, + + pub phase_manager: PhaseManager, + pub phase: Mutex, + pub growl_time: Mutex, + pub yaw_rot_accel: Mutex, + pub holding_pattern_clockwise: Mutex, + pub target_location: Mutex>>, + pub fireball_charge: Mutex, + + pub nodes: Mutex<[Option; NODE_COUNT]>, + pub path: Mutex>, + pub target_node: Mutex, + + pub strafe_target: Mutex>>, + pub target_player: Mutex>, + pub ticks_sitting: Mutex, + pub sit_attack_timer: Mutex, + pub breathing_timer: Mutex, } impl EnderDragonEntity { - pub fn new(entity: Entity) -> Arc { - let mob_entity = MobEntity::new(entity); - let dragon = Self { mob_entity }; - Arc::new(dragon) + pub async fn new(entity: Entity) -> Arc { + entity.no_clip.store(true, Ordering::Relaxed); + let base_id = entity.entity_id; + let dragon_uuid = entity.entity_uuid; + let world = entity.world.load(); + + let _ = Entity::reserve_ids(8); + + let mut parts = Vec::new(); + for i in 1..=8 { + let part_entity = Entity::from_uuid_with_id( + base_id + i, + uuid::Uuid::new_v4(), + world.clone(), + entity.pos.load(), + &EntityType::ENDER_DRAGON, + ); + let part = Arc::new(EnderDragonPart::new(part_entity, dragon_uuid)); + world + .add_entity_silent(part.clone() as Arc) + .await; + parts.push(part); + } + + Arc::new(Self { + mob_entity: MobEntity::new(entity), + parts, + flight_history: Mutex::new(DragonFlightHistory::default()), + flap_time: Mutex::new(0.0), + o_flap_time: Mutex::new(0.0), + in_wall: Mutex::new(false), + dragon_death_time: Mutex::new(0), + sitting_damage_received: Mutex::new(0.0), + nodes_initialized: Mutex::new(false), + fight_origin: Mutex::new(BlockPos::new(0, 128, 0)), + phase_manager: PhaseManager::new(), + phase: Mutex::new(EnderDragonPhase::Circling), + growl_time: Mutex::new(GROWL_COOLDOWN_BASE), + yaw_rot_accel: Mutex::new(0.0), + holding_pattern_clockwise: Mutex::new(true), + target_location: Mutex::new(None), + fireball_charge: Mutex::new(0), + nodes: Mutex::new([None; NODE_COUNT]), + path: Mutex::new(Vec::new()), + target_node: Mutex::new(0), + strafe_target: Mutex::new(None), + target_player: Mutex::new(None), + ticks_sitting: Mutex::new(0), + sit_attack_timer: Mutex::new(0), + breathing_timer: Mutex::new(0), + }) + } + + pub async fn set_fight_origin(&self, pos: BlockPos) { + let mut initialized = self.nodes_initialized.lock().await; + let mut origin = self.fight_origin.lock().await; + if *origin != pos { + *origin = pos; + *initialized = false; + } + } + + pub async fn set_phase(&self, phase_type: EnderDragonPhase) { + let mut phase_lock = self.phase.lock().await; + if *phase_lock == phase_type { + return; + } + + let old_phase = self.phase_manager.get_phase(*phase_lock); + old_phase.end(self).await; + + *phase_lock = phase_type; + + let new_phase = self.phase_manager.get_phase(phase_type); + new_phase.begin(self).await; + } + + async fn ensure_nodes_initialized(&self) { + let mut initialized = self.nodes_initialized.lock().await; + if *initialized { + return; + } + + let world = self.mob_entity.living_entity.entity.world.load(); + let fight_origin = self.fight_origin.lock().await; + + let mut nodes = self.nodes.lock().await; + for i in 0..NODE_COUNT { + let mut y_adjustment = 5; + let node_x; + let node_z; + + if i < 12 { + node_x = fight_origin.0.x + + (60.0 * ((i as f32) * std::f32::consts::TAU / 12.0).cos()) as i32; + node_z = fight_origin.0.z + + (60.0 * ((i as f32) * std::f32::consts::TAU / 12.0).sin()) as i32; + } else if i < 20 { + let multiplier = i - 12; + node_x = fight_origin.0.x + + (40.0 * ((multiplier as f32) * std::f32::consts::TAU / 8.0).cos()) as i32; + node_z = fight_origin.0.z + + (40.0 * ((multiplier as f32) * std::f32::consts::TAU / 8.0).sin()) as i32; + y_adjustment += 10; + } else { + let multiplier = i - 20; + node_x = fight_origin.0.x + + (20.0 * ((multiplier as f32) * std::f32::consts::TAU / 4.0).cos()) as i32; + node_z = fight_origin.0.z + + (20.0 * ((multiplier as f32) * std::f32::consts::TAU / 4.0).sin()) as i32; + } + + let height = world.get_motion_blocking_height(node_x, node_z).await; + let node_y = 73.max(height + y_adjustment); + nodes[i] = Some(DragonNode::new(node_x as f64, node_y as f64, node_z as f64)); + } + + let pos = self.mob_entity.living_entity.entity.pos.load(); + let nearest = Self::nearest_node_in(&nodes, pos); + let dest = Self::random_node_idx(); + let new_path = find_path(&nodes, nearest, dest, None); + drop(nodes); + + *self.target_node.lock().await = dest; + *self.path.lock().await = new_path; + *initialized = true; + } + + pub async fn find_closest_node(&self) -> usize { + let pos = self.mob_entity.living_entity.entity.pos.load(); + self.find_closest_node_to(pos).await + } + + pub async fn find_closest_node_to(&self, pos: Vector3) -> usize { + self.ensure_nodes_initialized().await; + let nodes = self.nodes.lock().await; + Self::nearest_node_in(&nodes, pos) + } + + fn nearest_node_in(nodes: &[Option; NODE_COUNT], pos: Vector3) -> usize { + nodes + .iter() + .enumerate() + .filter_map(|(i, n)| n.map(|n| (i, n.dist_sq_vec(pos)))) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) + .map_or(0, |(i, _)| i) + } + + fn random_node_idx() -> usize { + rand::random_range(0..NODE_COUNT) + } + + #[inline] + #[must_use] + pub fn yaw_toward(from: Vector3, to: Vector3) -> f32 { + (-(to.z - from.z)).atan2(to.x - from.x).to_degrees() as f32 - 90.0 + } + + pub fn move_relative(&self, speed: f32, relative_movement: Vector3) { + let yaw = self.mob_entity.living_entity.entity.yaw.load(); + let movement = Self::get_relative_movement(relative_movement, speed, yaw); + let entity = &self.mob_entity.living_entity.entity; + let vel = entity.velocity.load(); + entity.velocity.store(vel + movement); + } + + fn get_relative_movement( + relative_movement: Vector3, + speed: f32, + yaw: f32, + ) -> Vector3 { + let dist = relative_movement.length_squared(); + if dist < 1.0E-7 { + return Vector3::new(0.0, 0.0, 0.0); + } + let vec = if dist > 1.0 { + relative_movement.normalize() + } else { + relative_movement + } * (speed as f64); + let sin = (yaw * (std::f32::consts::PI / 180.0)).sin() as f64; + let cos = (yaw * (std::f32::consts::PI / 180.0)).cos() as f64; + Vector3::new(vec.x * cos - vec.z * sin, vec.y, vec.z * cos + vec.x * sin) + } + + pub async fn steer_toward( + &self, + pos: Vector3, + target: Vector3, + fly_speed: f32, + turn_speed: f32, + _y_scale: f64, + ) { + let xdd = target.x - pos.x; + let mut ydd = target.y - pos.y; + let zdd = target.z - pos.z; + let dist_sq = xdd * xdd + ydd * ydd + zdd * zdd; + + let horizontal_dist = xdd.hypot(zdd); + if horizontal_dist > 0.0 { + ydd = (ydd / horizontal_dist).clamp(-(fly_speed as f64), fly_speed as f64); + } + + let entity = &self.mob_entity.living_entity.entity; + let mut vel = entity.velocity.load(); + vel.y += ydd * 0.01; + entity.velocity.store(vel); + + let yaw = entity.yaw.load(); + let aim_diff = target - pos; + let aim = if aim_diff.length_squared() > 1e-6 { + aim_diff.normalize() + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + + let dir_vec = Vector3::new( + (yaw * (std::f32::consts::PI / 180.0)).sin() as f64, + vel.y, + -(yaw * (std::f32::consts::PI / 180.0)).cos() as f64, + ); + let dir = if dir_vec.length_squared() > 1e-6 { + dir_vec.normalize() + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + + let dot = (dir.dot(&aim) as f32 + 0.5).max(0.0) / 1.5; + if xdd.abs() > 1.0E-5 || zdd.abs() > 1.0E-5 { + let mut y_rot_d = + (180.0 - (xdd.atan2(zdd).to_degrees() as f32) - yaw).rem_euclid(360.0); + if y_rot_d > 180.0 { + y_rot_d -= 360.0; + } + y_rot_d = y_rot_d.clamp(-50.0, 50.0); + + let mut y_rot_a = self.yaw_rot_accel.lock().await; + *y_rot_a *= 0.8; + *y_rot_a += y_rot_d * turn_speed; + entity.yaw.store(yaw + *y_rot_a * 0.1); + } + + let span = (2.0 / (dist_sq + 1.0)) as f32; + self.move_relative( + 0.06 * (dot * span + (1.0 - span)), + Vector3::new(0.0, 0.0, -1.0), + ); + + let actual_vel = entity.velocity.load(); + let actual_dir = if actual_vel.length_squared() > 1e-6 { + actual_vel.normalize() + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + let slide = 0.8 + 0.15 * (actual_dir.dot(&dir) + 1.0) / 2.0; + entity.velocity.store(Vector3::new( + actual_vel.x * slide, + actual_vel.y * 0.91, + actual_vel.z * slide, + )); + } + + async fn update_flap_time(&self) { + let sitting = self.phase.lock().await.is_sitting(); + let in_wall = *self.in_wall.lock().await; + let mut flap = self.flap_time.lock().await; + let mut o_flap = self.o_flap_time.lock().await; + + *o_flap = *flap; + *flap += if sitting { + 0.1 + } else if in_wall { + FLAP_SPEED * 0.5 + } else { + FLAP_SPEED + }; + if *flap > std::f32::consts::TAU { + *flap -= std::f32::consts::TAU; + } + } + + async fn tick_growl(&self) { + if self.phase.lock().await.is_sitting() { + return; + } + let mut t = self.growl_time.lock().await; + if *t > 0 { + *t -= 1; + } else { + *t = GROWL_COOLDOWN_BASE + rand::random_range(0..100); + } + } + + fn find_nearest_player(&self) -> Option> { + let world = self.mob_entity.living_entity.entity.world.load(); + let pos = self.mob_entity.living_entity.entity.pos.load(); + world + .players + .load() + .iter() + .filter(|p| { + let p_pos = p.living_entity.entity.pos.load(); + p_pos.distance_squared(pos) < ARENA_RADIUS * ARENA_RADIUS + }) + .min_by(|a, b| { + let a_pos = a.living_entity.entity.pos.load(); + let b_pos = b.living_entity.entity.pos.load(); + a_pos + .distance_squared(pos) + .partial_cmp(&b_pos.distance_squared(pos)) + .unwrap() + }) + .cloned() + } + + async fn handle_player_collisions(&self) { + let world = self.mob_entity.living_entity.entity.world.load(); + + let dragon_bbox = self.mob_entity.living_entity.entity.bounding_box.load(); + let xm = f64::midpoint(dragon_bbox.min.x, dragon_bbox.max.x); + let zm = f64::midpoint(dragon_bbox.min.z, dragon_bbox.max.z); + + for player in world.players.load().iter() { + if player + .living_entity + .entity + .bounding_box + .load() + .intersects(&dragon_bbox) + { + let player_pos = player.living_entity.entity.pos.load(); + let xd = player_pos.x - xm; + let zd = player_pos.z - zm; + let dd = (xd * xd + zd * zd).max(0.1); + + player + .living_entity + .entity + .apply_knockback(4.0, xd / dd, zd / dd); + player.living_entity.entity.send_velocity().await; + + if !self.phase.lock().await.is_sitting() { + player.damage(self, 5.0, DamageType::MOB_ATTACK).await; + } + } + } + } + + async fn tick_crystal_healing(&self) { + let world = self.mob_entity.living_entity.entity.world.load(); + let pos = self.mob_entity.living_entity.entity.pos.load(); + + let mut nearest_crystal = None; + let mut min_dist_sq = 1024.0; // 32 blocks + + for entity in world.entities.load().iter() { + if entity.get_entity().entity_type == &EntityType::END_CRYSTAL { + let crystal_pos = entity.get_entity().pos.load(); + let dist_sq = pos.distance_squared(crystal_pos); + if dist_sq < min_dist_sq { + min_dist_sq = dist_sq; + nearest_crystal = Some(entity.clone()); + } + } + } + + if let Some(_crystal) = nearest_crystal { + let living = &self.mob_entity.living_entity; + if living.health.load() < living.get_max_health() { + living.heal(1.0).await; + } + } + } + + async fn tick_block_breaking(&self) { + let phase_type = *self.phase.lock().await; + if phase_type.is_sitting() || phase_type == EnderDragonPhase::Dying { + return; + } + + let world = self.mob_entity.living_entity.entity.world.load(); + let bbox = self.mob_entity.living_entity.entity.bounding_box.load(); + + let min = bbox.min_block_pos(); + let max = bbox.max_block_pos(); + + for pos in BlockPos::iterate(min, max) { + let block = world.get_block(&pos).await; + if block != &Block::BEDROCK + && block != &Block::END_STONE + && block != &Block::OBSIDIAN + && block != &Block::IRON_BARS + && block != &Block::END_PORTAL + && block != &Block::END_PORTAL_FRAME + { + world.set_block_state(&pos, 0, BlockFlags::NOTIFY_ALL).await; + } + } + } + + async fn tick_parts(&self) { + let history = self.flight_history.lock().await; + let p5 = history.get(5); + let p10 = history.get(10); + let p0 = history.get(0); + + let tilt = (p5.y - p10.y) as f32 * 10.0 * (std::f32::consts::PI / 180.0); + let cc_tilt = tilt.cos() as f64; + let ss_tilt = tilt.sin() as f64; + + let yaw = self.mob_entity.living_entity.entity.yaw.load(); + let rot1 = yaw * (std::f32::consts::PI / 180.0); + let ss1 = rot1.sin() as f64; + let cc1 = rot1.cos() as f64; + + let pos = self.mob_entity.living_entity.entity.pos.load(); + + // Body + self.parts[2] + .entity + .set_pos(Vector3::new(pos.x + ss1 * 0.5, pos.y, pos.z - cc1 * 0.5)); + + // Wings + self.parts[6].entity.set_pos(Vector3::new( + pos.x + cc1 * 4.5, + pos.y + 2.0, + pos.z + ss1 * 4.5, + )); + self.parts[7].entity.set_pos(Vector3::new( + pos.x - cc1 * 4.5, + pos.y + 2.0, + pos.z - ss1 * 4.5, + )); + + let head_y_offset = if self.phase.lock().await.is_sitting() { + -1.0 + } else { + (p5.y - p0.y) as f64 + }; + + let yaw_accel = *self.yaw_rot_accel.lock().await; + let rot2 = (yaw - yaw_accel * 0.01) * (std::f32::consts::PI / 180.0); + let ss2 = rot2.sin() as f64; + let cc2 = rot2.cos() as f64; + + // Head & Neck + self.parts[0].entity.set_pos(Vector3::new( + pos.x + ss2 * 6.5 * cc_tilt, + pos.y + head_y_offset + ss_tilt * 6.5, + pos.z - cc2 * 6.5 * cc_tilt, + )); + self.parts[1].entity.set_pos(Vector3::new( + pos.x + ss2 * 5.5 * cc_tilt, + pos.y + head_y_offset + ss_tilt * 5.5, + pos.z - cc2 * 5.5 * cc_tilt, + )); + + // Tails + for i in 0..3 { + let pi = history.get(12 + i * 2); + let rot = yaw * (std::f32::consts::PI / 180.0) + + (pi.y_rot - p5.y_rot).rem_euclid(360.0).to_radians(); + let ss = rot.sin() as f64; + let cc = rot.cos() as f64; + let dd = (i + 1) as f64 * 2.0; + + self.parts[3 + i as usize].entity.set_pos(Vector3::new( + pos.x - (ss1 * 1.5 + ss * dd) * cc_tilt, + pos.y + (pi.y - p5.y) - (dd + 1.5) * ss_tilt + 1.5, + pos.z + (cc1 * 1.5 + cc * dd) * cc_tilt, + )); + } + + for part in &self.parts { + part.entity.send_pos_rot().await; + } + } + + pub async fn ai_step(&self) { + self.mob_entity.living_entity.entity.update_last_pos(); + self.ensure_nodes_initialized().await; + self.update_flap_time().await; + + { + let y = self.mob_entity.living_entity.entity.pos.load().y; + let yaw = self.mob_entity.living_entity.entity.yaw.load(); + self.flight_history.lock().await.record(y, yaw); + }; + + self.tick_growl().await; + self.tick_crystal_healing().await; + + { + let world = self.mob_entity.living_entity.entity.world.load(); + if let Some(ref fight_mutex) = world.dragon_fight { + let living = &self.mob_entity.living_entity; + fight_mutex + .lock() + .await + .update_dragon(&world, living.health.load(), living.get_max_health()) + .await; + } + } + + let phase_type: EnderDragonPhase = *self.phase.lock().await; + let phase = self.phase_manager.get_phase(phase_type); + + if phase_type.is_sitting() { + *self.ticks_sitting.lock().await += 1; + } else { + self.handle_player_collisions().await; + self.tick_block_breaking().await; + } + + phase.tick(self).await; + + if phase_type == EnderDragonPhase::Dying { + return; + } + + let target_location = *self.target_location.lock().await; + if let Some(target) = target_location { + let pos = self.mob_entity.living_entity.entity.pos.load(); + self.steer_toward( + pos, + target, + phase.get_fly_speed(), + phase.get_turn_speed(), + 0.5, + ) + .await; + } + + self.mob_entity.living_entity.entity.send_pos_rot().await; + self.tick_parts().await; + } + + pub async fn hurt(&self, damage: f32) { + let phase_type: EnderDragonPhase = *self.phase.lock().await; + if phase_type.is_sitting() { + *self.sitting_damage_received.lock().await += damage; + } } } @@ -23,4 +787,38 @@ impl Mob for EnderDragonEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ai_step().await; + }) + } + + fn on_damage<'a>( + &'a self, + _damage_type: DamageType, + _source: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let living = &self.mob_entity.living_entity; + if living.health.load() <= 0.0 { + self.set_phase(EnderDragonPhase::Dying).await; + } + }) + } + + fn get_mob_gravity(&self) -> f64 { + 0.0 + } +} + +pub trait Vector3Ext { + fn distance_squared(self, other: Self) -> f64; +} + +impl Vector3Ext for Vector3 { + fn distance_squared(self, other: Self) -> f64 { + let (dx, dy, dz) = (self.x - other.x, self.y - other.y, self.z - other.z); + dx * dx + dy * dy + dz * dz + } } diff --git a/pumpkin/src/entity/boss/ender_dragon/flight_history.rs b/pumpkin/src/entity/boss/ender_dragon/flight_history.rs new file mode 100644 index 000000000..7596cfb34 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/flight_history.rs @@ -0,0 +1,42 @@ +#[derive(Clone, Copy, Default, Debug)] +pub struct Sample { + pub y: f64, + pub y_rot: f32, +} + +#[derive(Clone, Copy, Debug)] +pub struct DragonFlightHistory { + samples: [Sample; 64], + head: i32, +} + +impl Default for DragonFlightHistory { + fn default() -> Self { + Self { + samples: [Sample::default(); 64], + head: -1, + } + } +} + +impl DragonFlightHistory { + pub const fn record(&mut self, y: f64, y_rot: f32) { + self.head += 1; + if self.head >= 64 { + self.head = 0; + } + self.samples[self.head as usize] = Sample { y, y_rot }; + } + + #[must_use] + pub fn get(&self, offset: i32) -> Sample { + if self.head < 0 { + return Sample::default(); + } + let mut index = (self.head - offset) & 63; + if index < 0 { + index += 64; + } + self.samples[index as usize] + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/charging.rs b/pumpkin/src/entity/boss/ender_dragon/phase/charging.rs new file mode 100644 index 000000000..5c75e0cdf --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/charging.rs @@ -0,0 +1,46 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, NODE_Y, Vector3Ext}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; + +pub struct ChargingPhase; + +impl super::Phase for ChargingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Charging + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let target_id = { + let guard = dragon.target_player.lock().await; + *guard + }; + let world = dragon.mob_entity.living_entity.entity.world.load(); + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + + let target_pos = if let Some(id) = target_id + && let Some(player) = world.players.load().iter().find(|p| p.gameprofile.id == id) + { + player.living_entity.entity.pos.load() + } else { + let origin = { + let guard = dragon.fight_origin.lock().await; + guard.0 + }; + Vector3::new(origin.x as f64, NODE_Y as f64 - 20.0, origin.z as f64) + }; + + if pos.distance_squared(target_pos) < 25.0 { + dragon.set_phase(EnderDragonPhase::Hovering).await; + return; + } + + *dragon.target_location.lock().await = Some(target_pos); + }) + } + + fn get_fly_speed(&self) -> f32 { + 3.0 + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/circling.rs b/pumpkin/src/entity/boss/ender_dragon/phase/circling.rs new file mode 100644 index 000000000..626a53d75 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/circling.rs @@ -0,0 +1,92 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, Vector3Ext, find_path}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; +use std::sync::atomic::Ordering; + +pub struct CirclingPhase; + +impl super::Phase for CirclingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Circling + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + let mut target_location = dragon.target_location.lock().await; + + let d0 = target_location.map_or(0.0, |loc| pos.distance_squared(loc)); + + if target_location.is_none() + || !(100.0..=22500.0).contains(&d0) + || dragon + .mob_entity + .living_entity + .entity + .horizontal_collision + .load(Ordering::Relaxed) + { + let mut path = dragon.path.lock().await; + if path.is_empty() { + drop(path); + let i = dragon.find_closest_node().await; + let mut j = i as i32; + + let mut clockwise = dragon.holding_pattern_clockwise.lock().await; + if rand::random_range(0..8) == 0 { + *clockwise = !*clockwise; + j = i as i32 + 6; + } + if *clockwise { + j += 1; + } else { + j -= 1; + } + drop(clockwise); + + let world = dragon.mob_entity.living_entity.entity.world.load(); + let crystals_alive = if let Some(ref fight) = world.dragon_fight { + fight.lock().await.alive_crystals() > 0 + } else { + false + }; + + let j = if crystals_alive { + j.rem_euclid(12) as usize + } else { + (j - 12).rem_euclid(8) as usize + 12 + }; + + let mut path_lock = dragon.path.lock().await; + let nodes = dragon.nodes.lock().await; + *path_lock = find_path(&nodes, i, j, None); + drop(nodes); + path = path_lock; + } + + if let Some(next_node_idx) = path.first().copied() { + path.remove(0); + let nodes = dragon.nodes.lock().await; + if let Some(node) = nodes[next_node_idx] { + let mut y_target = node.y + rand::random_range(0.0..20.0); + while y_target < node.y { + y_target = node.y + rand::random_range(0.0..20.0); + } + *target_location = Some(Vector3::new(node.x, y_target, node.z)); + } + } + } + drop(target_location); + + if rand::random_range(0..64) == 0 { + if rand::random_bool(0.5) { + dragon.set_phase(EnderDragonPhase::FlyToPortal).await; + } else if let Some(player) = dragon.find_nearest_player() { + *dragon.target_player.lock().await = Some(player.gameprofile.id); + dragon.set_phase(EnderDragonPhase::Strafing).await; + } + } + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/dying.rs b/pumpkin/src/entity/boss/ender_dragon/phase/dying.rs new file mode 100644 index 000000000..02662577e --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/dying.rs @@ -0,0 +1,101 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{DEATH_TIMER_MAX, EnderDragonEntity}; +use crate::entity::experience_orb::ExperienceOrbEntity; +use futures::future::BoxFuture; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_util::math::vector3::Vector3; + +pub struct DyingPhase; + +impl super::Phase for DyingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Dying + } + + fn begin<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + *dragon.target_location.lock().await = None; + }) + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let mut t = dragon.dragon_death_time.lock().await; + *t += 1; + + let entity = &dragon.mob_entity.living_entity.entity; + let world = entity.world.load(); + + if *t == 1 { + world + .play_sound( + Sound::EntityEnderDragonDeath, + SoundCategory::Hostile, + &entity.pos.load(), + ) + .await; + } + + if *t >= 180 && *t <= 200 { + let xo = (rand::random::() - 0.5) * 8.0; + let yo = (rand::random::() - 0.5) * 4.0; + let zo = (rand::random::() - 0.5) * 8.0; + let pos = entity.pos.load(); + world + .spawn_particle( + Vector3::new( + pos.x + xo as f64, + pos.y + 2.0 + yo as f64, + pos.z + zo as f64, + ), + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + Particle::ExplosionEmitter, + ) + .await; + } + + let xp_count = if let Some(ref fight_mutex) = world.dragon_fight + && !fight_mutex.lock().await.has_previously_killed_dragon() + { + 12000 + } else { + 500 + }; + + if *t > 150 && *t % 5 == 0 { + ExperienceOrbEntity::spawn( + &world, + entity.pos.load(), + (xp_count as f32 * 0.08) as u32, + ) + .await; + } + + entity.velocity.store(Vector3::new(0.0, 0.1, 0.0)); + + if *t >= DEATH_TIMER_MAX { + ExperienceOrbEntity::spawn( + &world, + entity.pos.load(), + (xp_count as f32 * 0.2) as u32, + ) + .await; + + if let Some(ref fight_mutex) = world.dragon_fight { + fight_mutex + .lock() + .await + .set_dragon_killed(&world, entity.entity_uuid) + .await; + } + for part in &dragon.parts { + part.entity.remove().await; + } + entity.remove().await; + } + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/fly_to_portal.rs b/pumpkin/src/entity/boss/ender_dragon/phase/fly_to_portal.rs new file mode 100644 index 000000000..09a335d01 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/fly_to_portal.rs @@ -0,0 +1,30 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, Vector3Ext}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; + +pub struct FlyToPortalPhase; + +impl super::Phase for FlyToPortalPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::FlyToPortal + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let origin = { + let guard = dragon.fight_origin.lock().await; + guard.0 + }; + let target = Vector3::new(origin.x as f64, origin.y as f64 + 10.0, origin.z as f64); + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + + if pos.distance_squared(target) < 4.0 { + dragon.set_phase(EnderDragonPhase::LandingApproach).await; + return; + } + + *dragon.target_location.lock().await = Some(target); + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/hovering.rs b/pumpkin/src/entity/boss/ender_dragon/phase/hovering.rs new file mode 100644 index 000000000..96f65fe08 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/hovering.rs @@ -0,0 +1,29 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, NODE_Y}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; + +pub struct HoveringPhase; + +impl super::Phase for HoveringPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Hovering + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let origin = { + let guard = dragon.fight_origin.lock().await; + guard.0 + }; + let target = Vector3::new(origin.x as f64, NODE_Y as f64 + 10.0, origin.z as f64); + + if rand::random_bool(0.01) { + dragon.set_phase(EnderDragonPhase::TakingOff).await; + return; + } + + *dragon.target_location.lock().await = Some(target); + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/landing.rs b/pumpkin/src/entity/boss/ender_dragon/phase/landing.rs new file mode 100644 index 000000000..637c51760 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/landing.rs @@ -0,0 +1,25 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::EnderDragonEntity; +use futures::future::BoxFuture; + +pub struct LandingPhase; + +impl super::Phase for LandingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Landing + } + + fn begin<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + *dragon.target_location.lock().await = None; + }) + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + dragon.set_phase(EnderDragonPhase::SitAttacking).await; + *dragon.ticks_sitting.lock().await = 0; + *dragon.sit_attack_timer.lock().await = 0; + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/landing_approach.rs b/pumpkin/src/entity/boss/ender_dragon/phase/landing_approach.rs new file mode 100644 index 000000000..abc03ec6a --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/landing_approach.rs @@ -0,0 +1,30 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, Vector3Ext}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; + +pub struct LandingApproachPhase; + +impl super::Phase for LandingApproachPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::LandingApproach + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let origin = { + let guard = dragon.fight_origin.lock().await; + guard.0 + }; + let target = Vector3::new(origin.x as f64, origin.y as f64, origin.z as f64); + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + + if pos.distance_squared(target) < 1.0 { + dragon.set_phase(EnderDragonPhase::Landing).await; + return; + } + + *dragon.target_location.lock().await = Some(target); + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/mod.rs b/pumpkin/src/entity/boss/ender_dragon/phase/mod.rs new file mode 100644 index 000000000..772f12d1d --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/mod.rs @@ -0,0 +1,122 @@ +use crate::entity::boss::ender_dragon::EnderDragonEntity; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; +use std::sync::Arc; + +mod charging; +mod circling; +mod dying; +mod fly_to_portal; +mod hovering; +mod landing; +mod landing_approach; +mod sit_attacking; +mod sit_breathing; +mod strafing; +mod taking_off; + +pub use charging::ChargingPhase; +pub use circling::CirclingPhase; +pub use dying::DyingPhase; +pub use fly_to_portal::FlyToPortalPhase; +pub use hovering::HoveringPhase; +pub use landing::LandingPhase; +pub use landing_approach::LandingApproachPhase; +pub use sit_attacking::SitAttackingPhase; +pub use sit_breathing::SitBreathingPhase; +pub use strafing::StrafingPhase; +pub use taking_off::TakingOffPhase; + +pub trait Phase: Send + Sync { + fn get_type(&self) -> EnderDragonPhase; + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()>; + fn begin<'a>(&'a self, _dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async {}) + } + fn end<'a>(&'a self, _dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async {}) + } + fn is_sitting(&self) -> bool { + false + } + fn get_fly_speed(&self) -> f32 { + 0.6 + } + fn get_turn_speed(&self) -> f32 { + 0.1 + } + fn get_fly_target_location(&self) -> Option> { + None + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum EnderDragonPhase { + #[default] + Circling = 0, + Strafing = 1, + Charging = 2, + FlyToPortal = 3, + LandingApproach = 4, + Landing = 5, + SitAttacking = 6, + SitBreathing = 7, + TakingOff = 8, + Hovering = 9, + Dying = 10, +} + +impl EnderDragonPhase { + #[must_use] + pub const fn is_sitting(self) -> bool { + matches!( + self, + Self::LandingApproach + | Self::Landing + | Self::SitAttacking + | Self::SitBreathing + | Self::TakingOff + ) + } + + #[must_use] + pub const fn network_id(self) -> i32 { + self as i32 + } +} + +pub struct PhaseManager { + phases: [Arc; 11], +} + +impl Default for PhaseManager { + fn default() -> Self { + Self::new() + } +} + +impl PhaseManager { + #[must_use] + pub fn new() -> Self { + Self { + phases: [ + Arc::new(CirclingPhase), + Arc::new(StrafingPhase), + Arc::new(ChargingPhase), + Arc::new(FlyToPortalPhase), + Arc::new(LandingApproachPhase), + Arc::new(LandingPhase), + Arc::new(SitAttackingPhase), + Arc::new(SitBreathingPhase), + Arc::new(TakingOffPhase), + Arc::new(HoveringPhase), + Arc::new(DyingPhase), + ], + } + } + + #[must_use] + pub fn get_phase(&self, phase_type: EnderDragonPhase) -> Arc { + self.phases[phase_type as usize].clone() + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/sit_attacking.rs b/pumpkin/src/entity/boss/ender_dragon/phase/sit_attacking.rs new file mode 100644 index 000000000..d317912c4 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/sit_attacking.rs @@ -0,0 +1,47 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::EnderDragonEntity; +use futures::future::BoxFuture; + +pub struct SitAttackingPhase; + +impl super::Phase for SitAttackingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::SitAttacking + } + + fn begin<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + *dragon.target_location.lock().await = None; + }) + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let mut timer = dragon.sit_attack_timer.lock().await; + *timer += 1; + + if *timer > 40 { + *timer = 0; + let should_breathe = rand::random_bool(0.5); + let should_take_off = *dragon.ticks_sitting.lock().await > 200; + drop(timer); + + if should_breathe { + dragon.set_phase(EnderDragonPhase::SitBreathing).await; + *dragon.breathing_timer.lock().await = 0; + } else if should_take_off { + dragon.set_phase(EnderDragonPhase::TakingOff).await; + } + } else { + drop(timer); + } + + let mut dmg = dragon.sitting_damage_received.lock().await; + if *dmg > 150.0 { + *dmg = 0.0; + drop(dmg); + dragon.set_phase(EnderDragonPhase::TakingOff).await; + } + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/sit_breathing.rs b/pumpkin/src/entity/boss/ender_dragon/phase/sit_breathing.rs new file mode 100644 index 000000000..6d5f9b9f0 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/sit_breathing.rs @@ -0,0 +1,73 @@ +use super::EnderDragonPhase; +use crate::entity::{ + Entity, area_effect_cloud::AreaEffectCloudEntity, boss::ender_dragon::EnderDragonEntity, +}; +use futures::future::BoxFuture; +use pumpkin_data::entity::EntityType; +use pumpkin_util::math::vector3::Vector3; + +pub struct SitBreathingPhase; + +impl super::Phase for SitBreathingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::SitBreathing + } + + fn begin<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + *dragon.target_location.lock().await = None; + }) + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let mut timer = dragon.breathing_timer.lock().await; + *timer += 1; + + if *timer > 100 { + *timer = 0; + drop(timer); + dragon.set_phase(EnderDragonPhase::SitAttacking).await; + return; + } + drop(timer); + + let timer_val = *dragon.breathing_timer.lock().await; + if timer_val == 1 { + let entity = &dragon.mob_entity.living_entity.entity; + let pos = entity.pos.load(); + let yaw = entity.yaw.load().to_radians() as f64; + let world = entity.world.load(); + + // Spawn the lingering cloud at the dragon's head position + let offset = Vector3::new(-yaw.sin() * 2.0, 0.5, yaw.cos() * 2.0); + let cloud_pos = pos.add(&offset); + + let cloud_entity = + Entity::new(world.clone(), cloud_pos, &EntityType::AREA_EFFECT_CLOUD); + let cloud = AreaEffectCloudEntity::create( + cloud_entity, + pumpkin_data::item_stack::ItemStack::new( + 0, + &pumpkin_data::item::Item::DRAGON_BREATH, + ), + vec![( + &pumpkin_data::effect::StatusEffect::INSTANT_DAMAGE, + 1, + 0, + false, + true, + true, + )], + 600, // duration + 3.0, // radius + 20, // reapplication delay + 20, // wait time + 0.5, // radius on use + -100, // duration on use + ); + world.spawn_entity(cloud).await; + } + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/strafing.rs b/pumpkin/src/entity/boss/ender_dragon/phase/strafing.rs new file mode 100644 index 000000000..e631144b3 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/strafing.rs @@ -0,0 +1,168 @@ +use super::EnderDragonPhase; +use crate::entity::{ + Entity, + area_effect_cloud::AreaEffectCloudEntity, + boss::ender_dragon::{EnderDragonEntity, Vector3Ext, find_path}, +}; +use futures::future::BoxFuture; +use pumpkin_data::entity::EntityType; +use pumpkin_util::math::vector3::Vector3; +use std::sync::atomic::Ordering; + +pub struct StrafingPhase; + +impl super::Phase for StrafingPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::Strafing + } + + fn begin<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + *dragon.fireball_charge.lock().await = 0; + *dragon.target_location.lock().await = None; + }) + } + + #[expect(clippy::too_many_lines)] + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let target_id = *dragon.target_player.lock().await; + let world = dragon.mob_entity.living_entity.entity.world.load(); + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + + let player_target = if let Some(id) = target_id { + world + .players + .load() + .iter() + .find(|p| p.gameprofile.id == id) + .cloned() + } else { + None + }; + + let Some(player) = player_target else { + dragon.set_phase(EnderDragonPhase::Circling).await; + return; + }; + + let player_pos = player.living_entity.entity.pos.load(); + let mut path = dragon.path.lock().await; + let mut target_location = dragon.target_location.lock().await; + + if path.is_empty() { + let d2 = player_pos.x - pos.x; + let d3 = player_pos.z - pos.z; + let d4 = d2.hypot(d3); + let d5 = (0.4 + d4 / 80.0 - 1.0).clamp(0.0, 10.0); + *target_location = + Some(Vector3::new(player_pos.x, player_pos.y + d5, player_pos.z)); + } + + let d11 = target_location + .map(|loc| pos.distance_squared(loc)) + .unwrap_or(0.0); + if !(100.0..=22500.0).contains(&d11) + || dragon + .mob_entity + .living_entity + .entity + .horizontal_collision + .load(Ordering::Relaxed) + { + if path.is_empty() { + drop(path); + let i = dragon.find_closest_node().await; + let j = dragon.find_closest_node_to(player_pos).await; + + let mut path_lock = dragon.path.lock().await; + let nodes = dragon.nodes.lock().await; + *path_lock = find_path(&nodes, i, j, None); + drop(nodes); + path = path_lock; + } + + if let Some(next_node_idx) = path.first().copied() { + path.remove(0); + let nodes = dragon.nodes.lock().await; + if let Some(node) = nodes[next_node_idx] { + let mut y_target = node.y + rand::random_range(0.0..20.0); + while y_target < node.y { + y_target = node.y + rand::random_range(0.0..20.0); + } + *target_location = Some(Vector3::new(node.x, y_target, node.z)); + } + } + } + drop(path); + drop(target_location); + + if player_pos.distance_squared(pos) < 4096.0 { + let mut charge = dragon.fireball_charge.lock().await; + + let aim_diff = player_pos - pos; + let aim = if aim_diff.length_squared() > 1e-6 { + aim_diff.normalize() + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + + let yaw = dragon.mob_entity.living_entity.entity.yaw.load(); + let dir = Vector3::new( + (yaw * (std::f32::consts::PI / 180.0)).sin() as f64, + 0.0, + -(yaw * (std::f32::consts::PI / 180.0)).cos() as f64, + ); + + let dir_norm = if dir.length_squared() > 1e-6 { + dir.normalize() + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + + let dot = dir_norm.dot(&aim) as f32; + let angle_degs = dot.acos().to_degrees() + 0.5; + + *charge += 1; + + if *charge >= 5 && angle_degs < 10.0 { + *charge = 0; + drop(charge); + + let cloud_entity = + Entity::new(world.clone(), player_pos, &EntityType::AREA_EFFECT_CLOUD); + let cloud = AreaEffectCloudEntity::create( + cloud_entity, + pumpkin_data::item_stack::ItemStack::new( + 0, + &pumpkin_data::item::Item::DRAGON_BREATH, + ), + vec![( + &pumpkin_data::effect::StatusEffect::INSTANT_DAMAGE, + 1, + 0, + false, + true, + true, + )], + 600, + 3.0, + 20, + 20, + 0.5, + -100, + ); + world.spawn_entity(cloud).await; + + dragon.path.lock().await.clear(); + dragon.set_phase(EnderDragonPhase::Circling).await; + } + } else { + let mut charge = dragon.fireball_charge.lock().await; + if *charge > 0 { + *charge -= 1; + } + } + }) + } +} diff --git a/pumpkin/src/entity/boss/ender_dragon/phase/taking_off.rs b/pumpkin/src/entity/boss/ender_dragon/phase/taking_off.rs new file mode 100644 index 000000000..4fec74bf4 --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon/phase/taking_off.rs @@ -0,0 +1,30 @@ +use super::EnderDragonPhase; +use crate::entity::boss::ender_dragon::{EnderDragonEntity, NODE_Y, Vector3Ext}; +use futures::future::BoxFuture; +use pumpkin_util::math::vector3::Vector3; + +pub struct TakingOffPhase; + +impl super::Phase for TakingOffPhase { + fn get_type(&self) -> EnderDragonPhase { + EnderDragonPhase::TakingOff + } + + fn tick<'a>(&'a self, dragon: &'a EnderDragonEntity) -> BoxFuture<'a, ()> { + Box::pin(async move { + let origin = { + let guard = dragon.fight_origin.lock().await; + guard.0 + }; + let target = Vector3::new(origin.x as f64, NODE_Y as f64, origin.z as f64); + let pos = dragon.mob_entity.living_entity.entity.pos.load(); + + if pos.distance_squared(target) < 16.0 { + dragon.set_phase(EnderDragonPhase::Circling).await; + return; + } + + *dragon.target_location.lock().await = Some(target); + }) + } +} diff --git a/pumpkin/src/entity/decoration/armor_stand.rs b/pumpkin/src/entity/decoration/armor_stand.rs index bb083a010..d1253fce5 100644 --- a/pumpkin/src/entity/decoration/armor_stand.rs +++ b/pumpkin/src/entity/decoration/armor_stand.rs @@ -439,6 +439,10 @@ impl EntityBase for ArmorStandEntity { fn get_gravity(&self) -> f64 { 0.08 } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } pub enum ArmorStandFlags { diff --git a/pumpkin/src/entity/decoration/end_crystal.rs b/pumpkin/src/entity/decoration/end_crystal.rs index 3633ff045..67b58b36f 100644 --- a/pumpkin/src/entity/decoration/end_crystal.rs +++ b/pumpkin/src/entity/decoration/end_crystal.rs @@ -70,4 +70,8 @@ impl EntityBase for EndCrystalEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/decoration/painting.rs b/pumpkin/src/entity/decoration/painting.rs index d69b0256e..0c1b6888e 100644 --- a/pumpkin/src/entity/decoration/painting.rs +++ b/pumpkin/src/entity/decoration/painting.rs @@ -61,4 +61,8 @@ impl EntityBase for PaintingEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/experience_orb.rs b/pumpkin/src/entity/experience_orb.rs index c0648f4bc..8cf3613c3 100644 --- a/pumpkin/src/entity/experience_orb.rs +++ b/pumpkin/src/entity/experience_orb.rs @@ -139,4 +139,8 @@ impl EntityBase for ExperienceOrbEntity { fn get_gravity(&self) -> f64 { 0.03 } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/falling.rs b/pumpkin/src/entity/falling.rs index 1a29b0157..f55abb585 100644 --- a/pumpkin/src/entity/falling.rs +++ b/pumpkin/src/entity/falling.rs @@ -124,4 +124,8 @@ impl EntityBase for FallingEntity { fn get_gravity(&self) -> f64 { 0.04 } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index f53b794a8..962bd868d 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -497,4 +497,8 @@ impl EntityBase for ItemEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 9cd185faa..e5f34fe1c 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -2237,6 +2237,10 @@ impl EntityBase for LivingEntity { Some(self) } + fn cast_any(&self) -> &dyn std::any::Any { + self + } + fn as_nbt_storage(&self) -> &dyn NBTStorage { self } diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 3c0e55fd9..2d90785c5 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -391,6 +391,14 @@ impl EntityBase for T { }) } + fn is_collidable(&self, _entity: Option>) -> bool { + true + } + + fn can_hit(&self) -> bool { + true + } + fn damage_with_context<'a>( &'a self, caller: &'a dyn EntityBase, @@ -433,6 +441,10 @@ impl EntityBase for T { Some(&self.get_mob_entity().living_entity) } + fn cast_any(&self) -> &dyn std::any::Any { + self + } + fn is_in_love(&self) -> bool { self.get_mob_entity().is_in_love() } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 80d8f6baf..ca66795b7 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -108,7 +108,7 @@ pub type EntityBaseFuture<'a, T> = Pin + Send + 'a>>; pub type TeleportFuture = Pin + Send>>; -pub trait EntityBase: Send + Sync + NBTStorage { +pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { /// Called every tick for this entity. /// /// The `caller` parameter is a reference to the entity that initiated the tick. @@ -130,6 +130,13 @@ pub trait EntityBase: Send + Sync + NBTStorage { }) } + fn as_any(&self) -> &dyn std::any::Any + where + Self: Sized, + { + self + } + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { let entity = self.get_entity(); @@ -279,8 +286,11 @@ pub trait EntityBase: Send + Sync + NBTStorage { } fn get_entity(&self) -> &Entity; + fn get_living_entity(&self) -> Option<&LivingEntity>; + fn cast_any(&self) -> &dyn std::any::Any; + fn get_item_entity(self: Arc) -> Option> { None } @@ -498,11 +508,31 @@ impl Entity { Self::from_uuid(Uuid::new_v4(), world, position, entity_type) } + pub fn reserve_ids(count: i32) -> i32 { + CURRENT_ID.fetch_add(count, Relaxed) + } + pub fn from_uuid( entity_uuid: uuid::Uuid, world: Arc, position: Vector3, entity_type: &'static EntityType, + ) -> Self { + Self::from_uuid_with_id( + CURRENT_ID.fetch_add(1, Relaxed), + entity_uuid, + world, + position, + entity_type, + ) + } + + pub fn from_uuid_with_id( + entity_id: i32, + entity_uuid: uuid::Uuid, + world: Arc, + position: Vector3, + entity_type: &'static EntityType, ) -> Self { let floor_x = position.x.floor() as i32; let floor_y = position.y.floor() as i32; @@ -515,7 +545,7 @@ impl Entity { }; Self { - entity_id: CURRENT_ID.fetch_add(1, Relaxed), + entity_id, entity_uuid, entity_type, on_ground: AtomicBool::new(false), @@ -2795,6 +2825,10 @@ impl EntityBase for Entity { None } + fn cast_any(&self) -> &dyn std::any::Any { + self + } + fn as_nbt_storage(&self) -> &dyn NBTStorage { self } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 353cd0924..ce9dcf656 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -3562,6 +3562,10 @@ impl EntityBase for Player { Box::pin(async move { name.insertion(self.gameprofile.name.clone()) }) } + fn cast_any(&self) -> &dyn std::any::Any { + self + } + fn as_nbt_storage(&self) -> &dyn NBTStorage { self } diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 0045caad6..68406d9f1 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -470,6 +470,10 @@ impl EntityBase for ArrowEntity { } }) } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } impl ArrowEntity { diff --git a/pumpkin/src/entity/projectile/egg.rs b/pumpkin/src/entity/projectile/egg.rs index 7cac9774b..a2b64644f 100644 --- a/pumpkin/src/entity/projectile/egg.rs +++ b/pumpkin/src/entity/projectile/egg.rs @@ -181,4 +181,8 @@ impl EntityBase for EggEntity { } }) } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/projectile/eye_of_ender.rs b/pumpkin/src/entity/projectile/eye_of_ender.rs index f688cb572..27f8cedbc 100644 --- a/pumpkin/src/entity/projectile/eye_of_ender.rs +++ b/pumpkin/src/entity/projectile/eye_of_ender.rs @@ -219,4 +219,8 @@ impl EntityBase for EyeOfEnder { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/projectile/firework_rocket.rs b/pumpkin/src/entity/projectile/firework_rocket.rs index 8bd5a1159..ad0d811a9 100644 --- a/pumpkin/src/entity/projectile/firework_rocket.rs +++ b/pumpkin/src/entity/projectile/firework_rocket.rs @@ -159,4 +159,8 @@ impl EntityBase for FireworkRocketEntity { fn as_nbt_storage(&self) -> &dyn crate::entity::NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/projectile/lingering_potion.rs b/pumpkin/src/entity/projectile/lingering_potion.rs index 6a5530044..248fe4f96 100644 --- a/pumpkin/src/entity/projectile/lingering_potion.rs +++ b/pumpkin/src/entity/projectile/lingering_potion.rs @@ -100,6 +100,10 @@ impl EntityBase for LingeringPotionEntity { self } + 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(); diff --git a/pumpkin/src/entity/projectile/snowball.rs b/pumpkin/src/entity/projectile/snowball.rs index 9a1c24ca5..5b89bffa9 100644 --- a/pumpkin/src/entity/projectile/snowball.rs +++ b/pumpkin/src/entity/projectile/snowball.rs @@ -63,6 +63,10 @@ impl EntityBase for SnowballEntity { self } + 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(); diff --git a/pumpkin/src/entity/projectile/splash_potion.rs b/pumpkin/src/entity/projectile/splash_potion.rs index 65bd1541a..846393b4a 100644 --- a/pumpkin/src/entity/projectile/splash_potion.rs +++ b/pumpkin/src/entity/projectile/splash_potion.rs @@ -141,6 +141,10 @@ impl EntityBase for SplashPotionEntity { self } + 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(); diff --git a/pumpkin/src/entity/projectile/wind_charge.rs b/pumpkin/src/entity/projectile/wind_charge.rs index 469c2bcba..cd819fa07 100644 --- a/pumpkin/src/entity/projectile/wind_charge.rs +++ b/pumpkin/src/entity/projectile/wind_charge.rs @@ -98,4 +98,8 @@ impl EntityBase for WindChargeEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/entity/tnt.rs b/pumpkin/src/entity/tnt.rs index e7daf0a78..a5b894bb0 100644 --- a/pumpkin/src/entity/tnt.rs +++ b/pumpkin/src/entity/tnt.rs @@ -119,6 +119,10 @@ impl EntityBase for TNTEntity { self } + fn cast_any(&self) -> &dyn std::any::Any { + self + } + fn is_immune_to_explosion(&self) -> bool { true } diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index f56266371..0ed6ca10b 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -72,7 +72,7 @@ pub async fn from_type( id if id == EntityType::WOLF.id => WolfEntity::new(entity).await, id if id == EntityType::WITHER.id => WitherEntity::new(entity).await, - id if id == EntityType::ENDER_DRAGON.id => EnderDragonEntity::new(entity), + id if id == EntityType::ENDER_DRAGON.id => EnderDragonEntity::new(entity).await, id if id == EntityType::AREA_EFFECT_CLOUD.id => { crate::entity::area_effect_cloud::AreaEffectCloudEntity::new(entity) diff --git a/pumpkin/src/entity/vehicle/boat.rs b/pumpkin/src/entity/vehicle/boat.rs index 6f9f0f7ae..f1153ab9a 100644 --- a/pumpkin/src/entity/vehicle/boat.rs +++ b/pumpkin/src/entity/vehicle/boat.rs @@ -231,4 +231,8 @@ impl EntityBase for BoatEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index e6b86e6de..f98d9e64b 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -723,23 +723,26 @@ impl Server { /// Ticks essential server functions that must run even when the game is frozen. /// This includes player ticking (network, keep-alives) and flushing world updates to clients. pub async fn tick_players_and_network(&self) { - // First, flush pending block updates and synced block events to clients - for world in self.worlds.load().iter() { + let worlds = self.worlds.load(); + + for world in worlds.iter() { world.flush_block_updates().await; world.flush_synced_block_events().await; } - let players_to_tick: Vec<_> = self.get_all_players(); - for player in players_to_tick { - player.tick(self).await; + for world in worlds.iter() { + let players = world.players.load(); + for player in players.iter() { + player.tick(self).await; + } } } /// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`. pub async fn tick_worlds(self: &Arc) { - let mut set = JoinSet::new(); - self.task_scheduler.tick(self).await; + let mut set = JoinSet::new(); + for world in self.worlds.load().iter() { let world = world.clone(); let server = self.clone(); diff --git a/pumpkin/src/world/dragon_fight.rs b/pumpkin/src/world/dragon_fight.rs index 242bc0055..45d0365ff 100644 --- a/pumpkin/src/world/dragon_fight.rs +++ b/pumpkin/src/world/dragon_fight.rs @@ -1,12 +1,15 @@ //! End Dragon fight manager. Handles the dragon lifecycle, end crystal -//! spawning, boss bar, and exit portal. +//! spawning, boss bar, gateway spawning, respawn sequence, and exit portal. +//! +//! Matches vanilla `EnderDragonFight` behaviour as closely as `PumpkinMC`'s +//! current API allows. use std::sync::Arc; -use tracing::info; +use tracing::{debug, info}; use uuid::Uuid; -use pumpkin_data::{Block, entity::EntityType}; +use pumpkin_data::{Block, entity::EntityType, world::WorldEvent}; use pumpkin_util::{ math::{position::BlockPos, vector2::Vector2, vector3::Vector3}, text::TextComponent, @@ -19,13 +22,51 @@ use super::{ }; use crate::entity::{Entity, decoration::end_crystal::EndCrystalEntity}; +// ── Constants (match vanilla exactly) ──────────────────────────────────────── + const MAX_TICKS_BEFORE_DRAGON_RESPAWN: i32 = 1200; const CRYSTAL_SCAN_INTERVAL: i32 = 100; const PLAYER_SCAN_INTERVAL: i32 = 20; const DRAGON_SPAWN_Y: f64 = 128.0; const ARENA_RADIUS: f64 = 192.0; +/// Number of end gateways that can be unlocked (one per dragon kill after the first). +const GATEWAY_COUNT: usize = 20; +/// Radius at which gateways are placed (blocks from origin). +const GATEWAY_DISTANCE: f64 = 96.0; +/// Y level at which gateways are placed. +const GATEWAY_Y: i32 = 75; + +// ── Respawn stage ───────────────────────────────────────────────────────────── + +/// Mirrors vanilla `DragonRespawnStage`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DragonRespawnStage { + /// Crystals are charging up (0-99 ticks). + Start, + /// Summoning beam is active (100-179 ticks). + Preparing, + /// Dragon is being summoned (180-199 ticks). + End, +} + +impl DragonRespawnStage { + /// Advance through the respawn animation. Returns `true` when the stage + /// has completed and the caller should advance to the next one. + #[must_use] + pub const fn tick(&self, time: i32) -> bool { + match self { + Self::Start => time >= 100, + Self::Preparing => time >= 80, + Self::End => time >= 20, + } + } +} + +// ── DragonFight ─────────────────────────────────────────────────────────────── + pub struct DragonFight { + // ── Persistent state (would be saved to disk in vanilla) ────────────────── pub dragon_killed: bool, pub previously_killed: bool, pub needs_state_scanning: bool, @@ -33,9 +74,23 @@ pub struct DragonFight { pub portal_location: Option, pub crystals_alive: u32, + /// Gateways not yet unlocked, stored as angle indices 0-19. + /// Vanilla shuffles these at world creation and pops one per dragon kill. + pub pending_gateways: Vec, + + /// Active respawn stage, or `None` when not respawning. + pub respawn_stage: Option, + /// Ticks elapsed in the current respawn stage. + pub respawn_time: i32, + /// UUIDs of the four crystals being used for the respawn ritual. + pub respawn_crystal_uuids: Vec, + + // ── Transient counters ──────────────────────────────────────────────────── ticks_since_dragon_seen: i32, ticks_since_crystals_scanned: i32, ticks_since_last_player_scan: i32, + + // ── Boss bar ────────────────────────────────────────────────────────────── bossbar_uuid: Uuid, bossbar_players: Vec, } @@ -49,6 +104,11 @@ impl Default for DragonFight { impl DragonFight { #[must_use] pub fn new() -> Self { + // Build the gateway queue: indices 0-19 in a fixed (non-shuffled for + // now) order. Vanilla shuffles by world seed — shuffle here if your + // server exposes a seed. + let pending_gateways: Vec = (0..GATEWAY_COUNT).collect(); + Self { dragon_killed: false, previously_killed: false, @@ -56,32 +116,47 @@ impl DragonFight { dragon_uuid: None, portal_location: None, crystals_alive: 0, + pending_gateways, + respawn_stage: None, + respawn_time: 0, + respawn_crystal_uuids: Vec::new(), ticks_since_dragon_seen: 0, ticks_since_crystals_scanned: 0, - // Start over the threshold so the scan runs on the first tick with players + // Start above the threshold so the first tick with players triggers a scan. ticks_since_last_player_scan: PLAYER_SCAN_INTERVAL + 1, bossbar_uuid: Uuid::new_v4(), bossbar_players: Vec::new(), } } + // ── Main tick ───────────────────────────────────────────────────────────── + pub async fn tick(&mut self, world: &Arc) { + // 1. Update boss-bar recipients every 20 ticks. self.ticks_since_last_player_scan += 1; if self.ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL { self.update_players(world).await; self.ticks_since_last_player_scan = 0; } - // Nothing to do without players nearby + // Nothing to do without nearby players. if self.bossbar_players.is_empty() { return; } + // 2. One-time state scan on the first populated tick. if self.needs_state_scanning { self.scan_state(world).await; self.needs_state_scanning = false; } + // 3. Respawn sequence (takes priority over normal dragon-missing logic). + if self.respawn_stage.is_some() { + self.tick_respawn(world).await; + return; + } + + // 4. Normal fight ticking. if !self.dragon_killed { self.ticks_since_dragon_seen += 1; if self.dragon_uuid.is_none() @@ -98,12 +173,11 @@ impl DragonFight { } } } -} -impl DragonFight { - /// Runs once on the first tick that has players. Figures out whether this - /// is a fresh world or a resumed one and brings the podium/dragon into a - /// consistent state. + // ── State scanning ──────────────────────────────────────────────────────── + + /// Runs once on the first tick with nearby players. Determines whether + /// this is a fresh fight or a resumed one and reconciles the entity list. async fn scan_state(&mut self, world: &Arc) { info!("Scanning End fight state..."); @@ -115,16 +189,14 @@ impl DragonFight { } else { info!("No exit portal – fight is fresh or in progress."); self.previously_killed = false; - // Place the inactive podium if it hasn't been set yet if self.portal_location.is_none() { self.spawn_exit_portal(world, false).await; } - // Spawn crystals on the spike caps self.spawn_crystals(world).await; } - // Check for a live dragon entity. - let existing_dragon = { + // Reconcile any live dragon entity. + let existing = { let entities = world.entities.load(); entities .iter() @@ -132,9 +204,9 @@ impl DragonFight { .map(|e| e.get_entity().entity_uuid) }; - if let Some(uuid) = existing_dragon { - if has_active_portal { - // The portal is active but a dragon entity still exists... so clean it up + match existing { + Some(uuid) if has_active_portal => { + // Stale dragon after a completed fight — remove it. if let Some(e) = world .entities .load() @@ -145,23 +217,25 @@ impl DragonFight { } self.dragon_uuid = None; self.dragon_killed = true; - } else { - info!("Found existing dragon entity."); + } + Some(uuid) => { + info!("Found existing dragon entity {:?}.", uuid); self.dragon_uuid = Some(uuid); self.dragon_killed = false; } - } else { - self.dragon_killed = true; + None => { + self.dragon_killed = true; + } } - // If the world looks fresh but the dragon appears dead, reset so one gets spawned + // Fresh world where the dragon appears dead → force a spawn next tick. if !self.previously_killed && self.dragon_killed { self.dragon_killed = false; } } - /// Returns true if an active `END_PORTAL` block exists within an 8-chunk - /// radius of the origin. Samples one column per chunk for performance + /// Checks whether a `END_PORTAL` block exists within an 8-chunk radius + /// of the origin, matching vanilla's `hasActiveExitPortal`. async fn has_active_exit_portal(&self, world: &Arc) -> bool { for cx in -8i32..=8 { for cz in -8i32..=8 { @@ -176,9 +250,9 @@ impl DragonFight { } false } -} -impl DragonFight { + // ── Dragon lifecycle ────────────────────────────────────────────────────── + async fn find_or_create_dragon(&mut self, world: &Arc) { let uuid = { let entities = world.entities.load(); @@ -189,37 +263,57 @@ impl DragonFight { }; if let Some(u) = uuid { - info!("Re-acquired existing dragon."); + debug!("Re-acquired existing dragon {:?}.", u); self.dragon_uuid = Some(u); + self.ticks_since_dragon_seen = 0; } else { - info!("No dragon found \u{2013} spawning one."); + debug!("No dragon found – spawning one."); self.create_new_dragon(world).await; } } async fn create_new_dragon(&mut self, world: &Arc) { - let entity = Entity::new( - world.clone(), - Vector3::new(0.5, DRAGON_SPAWN_Y, 0.5), - &EntityType::ENDER_DRAGON, - ); - let uuid = entity.entity_uuid; - world.spawn_entity(Arc::new(entity)).await; + let uuid = Uuid::new_v4(); + let position = Vector3::new(0.5, DRAGON_SPAWN_Y, 0.5); + let dragon = + crate::entity::r#type::from_type(&EntityType::ENDER_DRAGON, position, world, uuid) + .await; + + world.spawn_entity(dragon).await; self.dragon_uuid = Some(uuid); - info!("Spawned ender dragon."); + info!("Spawned ender dragon {:?}.", uuid); } - fn update_crystal_count(&mut self, world: &Arc) { - self.crystals_alive = world - .entities - .load() - .iter() - .filter(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL) - .count() as u32; + /// Called every tick while the dragon is alive. Updates the boss-bar + /// health fraction, matching vanilla `EnderDragonFight.updateDragon`. + pub async fn update_dragon(&mut self, world: &Arc, health: f32, max_health: f32) { + self.ticks_since_dragon_seen = 0; + let fraction = if max_health > 0.0 { + (health / max_health).clamp(0.0, 1.0) + } else { + 0.0 + }; + self.update_bossbar_health(world, fraction).await; + + // Sync fight origin to the dragon so its pathfinding nodes are correctly placed. + if let Some(loc) = self.portal_location + && let Some(dragon_uuid) = self.dragon_uuid + && let Some(e) = world + .entities + .load() + .iter() + .find(|e| e.get_entity().entity_uuid == dragon_uuid) + && let Some(dragon) = e + .cast_any() + .downcast_ref::() + { + dragon.set_fight_origin(loc).await; + } } - /// Called when the dragon dies. Activates the exit portal, places the - /// dragon egg on a first kill, and hides the boss bar. + /// Called by the dragon entity when it dies. Activates the portal, places + /// the egg on a first kill, spawns a gateway, and hides the boss bar. + /// Matches vanilla `EnderDragonFight.setDragonKilled`. pub async fn set_dragon_killed(&mut self, world: &Arc, killed_uuid: Uuid) { if Some(killed_uuid) != self.dragon_uuid { return; @@ -228,12 +322,13 @@ impl DragonFight { self.update_bossbar_health(world, 0.0).await; self.remove_all_bossbar(world).await; + // Activate the exit portal. self.spawn_exit_portal(world, true).await; + // Place the dragon egg on the first kill. if !self.previously_killed && let Some(loc) = self.portal_location { - // The bedrock pillar is 4 blocks tall, so the egg sits at podium_y + 4 let egg_pos = BlockPos::new(loc.0.x, loc.0.y + 4, loc.0.z); world .set_block_state( @@ -244,18 +339,202 @@ impl DragonFight { .await; } + // Spawn a new end gateway. + self.spawn_new_gateway(world).await; + self.previously_killed = true; self.dragon_killed = true; self.dragon_uuid = None; } -} -impl DragonFight { - /// Spawns end crystals on each obsidian spike. Scans downward from y=115 - /// for the bedrock cap placed by the spike generator and places the crystal - /// one block above it. Falls back to y=78 if the chunk hasn't loaded yet. + fn update_crystal_count(&mut self, world: &Arc) { + self.crystals_alive = world + .entities + .load() + .iter() + .filter(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL) + .count() as u32; + debug!("Found {} end crystals still alive.", self.crystals_alive); + } + + // ── Crystal destruction ─────────────────────────────────────────────────── + + /// Called when an end crystal is destroyed. If a respawn is in progress + /// and this was one of the ritual crystals, the respawn is aborted. + /// Matches vanilla `EnderDragonFight.onCrystalDestroyed`. + pub async fn on_crystal_destroyed(&mut self, world: &Arc, crystal_uuid: Uuid) { + if self.respawn_stage.is_some() && self.respawn_crystal_uuids.contains(&crystal_uuid) { + self.abort_respawn(world).await; + } else { + self.update_crystal_count(world); + // The dragon entity itself handles the visual beam-break logic; + // we just keep the count accurate here. + } + } + + // ── Respawn sequence ────────────────────────────────────────────────────── + + /// Attempt to begin a respawn. Requires four end crystals placed on the + /// cardinal sides of the portal, exactly as in vanilla `tryRespawn`. + pub async fn try_respawn(&mut self, world: &Arc) { + if !self.dragon_killed || self.respawn_stage.is_some() { + return; + } + + // Ensure we know where the portal is. + if self.portal_location.is_none() { + info!("Tried to respawn but no portal location – placing one."); + self.spawn_exit_portal(world, true).await; + } + + let Some(portal_loc) = self.portal_location else { + return; + }; + + // Scan the four cardinal directions one block above the portal centre + // for end crystals, matching vanilla's cardinal check. + let centre = BlockPos::new(portal_loc.0.x, portal_loc.0.y + 1, portal_loc.0.z); + let offsets: [(i32, i32); 4] = [(3, 0), (-3, 0), (0, 3), (0, -3)]; + let entities = world.entities.load(); + + let mut ritual_uuids = Vec::new(); + for (dx, dz) in offsets { + let check = BlockPos::new(centre.0.x + dx, centre.0.y, centre.0.z + dz); + // Find a crystal near this position (within 2 blocks). + let found = entities.iter().find(|e| { + if e.get_entity().entity_type != &EntityType::END_CRYSTAL { + return false; + } + let p = e.get_entity().pos.load(); + let cx = check.0.x as f64 + 0.5; + let cz = check.0.z as f64 + 0.5; + (p.x - cx).abs() < 2.0 && (p.z - cz).abs() < 2.0 + }); + if let Some(e) = found { + ritual_uuids.push(e.get_entity().entity_uuid) + } else { + debug!("Respawn attempt failed – missing crystal near {:?}.", check); + return; + } + } + + debug!("Found all four ritual crystals – beginning respawn."); + self.begin_respawn(world, ritual_uuids).await; + } + + async fn begin_respawn(&mut self, world: &Arc, crystal_uuids: Vec) { + // Tear down the active portal (replace END_PORTAL/BEDROCK with END_STONE) + // so the podium resets, matching vanilla. + if let Some(loc) = self.portal_location { + self.clear_portal_blocks(world, loc).await; + } + + self.respawn_stage = Some(DragonRespawnStage::Start); + self.respawn_time = 0; + self.respawn_crystal_uuids = crystal_uuids; + self.spawn_exit_portal(world, false).await; + } + + /// Replace the bedrock/portal blocks of the current podium with end-stone, + /// matching the vanilla portal-reset done during respawn. + async fn clear_portal_blocks(&self, world: &Arc, loc: BlockPos) { + // The podium is 7×6×7 centred on loc; just scan a generous volume. + for dy in -1i32..=5 { + for dx in -4i32..=4 { + for dz in -4i32..=4 { + let pos = BlockPos::new(loc.0.x + dx, loc.0.y + dy, loc.0.z + dz); + let block = world.get_block(&pos).await; + if block == &Block::BEDROCK || block == &Block::END_PORTAL { + world + .set_block_state( + &pos, + Block::END_STONE.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } + } + } + + async fn abort_respawn(&mut self, world: &Arc) { + debug!("Aborting dragon respawn sequence."); + self.respawn_stage = None; + self.respawn_time = 0; + self.respawn_crystal_uuids.clear(); + // Re-activate the portal so the world remains in a valid state. + self.spawn_exit_portal(world, true).await; + } + + /// Drive the respawn animation forward by one tick. + async fn tick_respawn(&mut self, world: &Arc) { + let Some(stage) = self.respawn_stage else { + return; + }; + + // Verify all ritual crystals still exist. + { + let entities = world.entities.load(); + let all_alive = self + .respawn_crystal_uuids + .iter() + .all(|uid| entities.iter().any(|e| e.get_entity().entity_uuid == *uid)); + if !all_alive { + self.abort_respawn(world).await; + return; + } + } + + self.respawn_time += 1; + + if stage.tick(self.respawn_time) { + match stage { + DragonRespawnStage::Start => { + self.respawn_stage = Some(DragonRespawnStage::Preparing); + self.respawn_time = 0; + } + DragonRespawnStage::Preparing => { + self.respawn_stage = Some(DragonRespawnStage::End); + self.respawn_time = 0; + } + DragonRespawnStage::End => { + // Spawn the new dragon. + self.respawn_stage = None; + self.respawn_time = 0; + self.respawn_crystal_uuids.clear(); + self.dragon_killed = false; + self.create_new_dragon(world).await; + } + } + } + } + + // ── Gateway spawning ────────────────────────────────────────────────────── + + /// Spawn the next end gateway, consuming one index from `pending_gateways`. + /// Matches vanilla `spawnNewGateway`. + async fn spawn_new_gateway(&mut self, world: &Arc) { + let Some(idx) = self.pending_gateways.pop() else { + return; + }; + + let angle = 2.0 * (-std::f64::consts::PI + std::f64::consts::PI * 0.1 * idx as f64); + let x = (GATEWAY_DISTANCE * angle.cos()).floor() as i32; + let z = (GATEWAY_DISTANCE * angle.sin()).floor() as i32; + let pos = BlockPos::new(x, GATEWAY_Y, z); + + world + .sync_world_event(WorldEvent::AnimationEndGatewaySpawn, pos, 0) + .await; + info!("Spawned end gateway #{} at {:?}.", idx, pos); + } + + // ── Crystal spawning ────────────────────────────────────────────────────── + + /// Spawn end crystals on the obsidian spike tops. Skips if any crystal + /// already exists (resumed world). Matches vanilla `respawnCrystals`. pub async fn spawn_crystals(&mut self, world: &Arc) { - // Don't re-spawn if crystals are already present (resumed world). if world .entities .load() @@ -270,18 +549,18 @@ impl DragonFight { let cx = (42.0f64 * angle.cos()).floor() as i32; let cz = (42.0f64 * angle.sin()).floor() as i32; - let mut crystal_y: Option = None; + // Find the top of the spike by scanning down from y=115 for bedrock. + let mut crystal_y = 78i32; for y in (70..=115i32).rev() { if world.get_block(&BlockPos::new(cx, y, cz)).await == &Block::BEDROCK { - crystal_y = Some(y + 1); + crystal_y = y + 1; break; } } - let y = crystal_y.unwrap_or(78); let entity = Entity::new( world.clone(), - Vector3::new(cx as f64 + 0.5, y as f64, cz as f64 + 0.5), + Vector3::new(cx as f64 + 0.5, crystal_y as f64, cz as f64 + 0.5), &EntityType::END_CRYSTAL, ); let crystal = Arc::new(EndCrystalEntity::new(entity)); @@ -290,17 +569,16 @@ impl DragonFight { } info!("Spawned end crystals on spike tops."); } -} -impl DragonFight { - /// Places the exit podium centred at column (0, 0). Pass `active = true` - /// to fill the portal disc with `END_PORTAL` blocks after the dragon dies. + // ── Exit portal ─────────────────────────────────────────────────────────── + + /// Place (or activate) the exit podium. `active = true` fills the portal + /// disc with `END_PORTAL` blocks after the dragon dies. pub async fn spawn_exit_portal(&mut self, world: &Arc, active: bool) { + // Determine location once and cache it. if self.portal_location.is_none() { let top_y = world.get_top_block(Vector2::new(0, 0)).await; let mut portal_y = top_y; - - // Walk down past any bedrock already at the centre (e.g. spike cap). while portal_y > 63 { if world.get_block(&BlockPos::new(0, portal_y, 0)).await != &Block::BEDROCK { break; @@ -315,9 +593,9 @@ impl DragonFight { super::end_podium::place(world, loc, active).await; } } -} -impl DragonFight { + // ── Boss bar ────────────────────────────────────────────────────────────── + fn make_bossbar(&self) -> Bossbar { Bossbar { uuid: self.bossbar_uuid, @@ -348,7 +626,8 @@ impl DragonFight { self.bossbar_players.clear(); } - /// Syncs the boss bar recipient list + /// Sync the boss-bar recipient list with nearby players. + /// Matches vanilla `updatePlayers`. async fn update_players(&mut self, world: &Arc) { let players = world.players.load(); @@ -364,6 +643,7 @@ impl DragonFight { .map(|p| p.gameprofile.id) .collect(); + // Add newly-in-range players. for &uid in ¤t { if !self.bossbar_players.contains(&uid) { if !self.dragon_killed @@ -375,12 +655,14 @@ impl DragonFight { } } + // Remove out-of-range players. let to_remove: Vec = self .bossbar_players .iter() .filter(|uid| !current.contains(uid)) .copied() .collect(); + for uid in &to_remove { if let Some(p) = players.iter().find(|p| &p.gameprofile.id == uid) { p.remove_bossbar(self.bossbar_uuid).await; @@ -388,4 +670,21 @@ impl DragonFight { self.bossbar_players.retain(|u| u != uid); } } + + // ── Public queries ──────────────────────────────────────────────────────── + + #[must_use] + pub const fn alive_crystals(&self) -> u32 { + self.crystals_alive + } + + #[must_use] + pub const fn has_previously_killed_dragon(&self) -> bool { + self.previously_killed + } + + #[must_use] + pub const fn is_respawning(&self) -> bool { + self.respawn_stage.is_some() + } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 85d32239d..c42881b93 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -2986,6 +2986,11 @@ impl World { .await; entity.init_data_tracker().await; + self.add_entity_silent(entity).await; + } + + pub async fn add_entity_silent(&self, entity: Arc) { + let base_entity = entity.get_entity(); let chunk_coordinate = base_entity.block_pos.load().chunk_position(); let chunk = self.level.get_entity_chunk(chunk_coordinate).await; {