feat: add dragon ai

not perfect, bit buggy, but it works overall
This commit is contained in:
Alexander Medvedev 2026-04-12 19:10:14 +02:00
parent 542a5a2103
commit 2c338f468c
48 changed files with 2198 additions and 117 deletions

35
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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"] }

View File

@ -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::<Aes128>::new_from_slices(key, iv).expect("Invalid key/iv");
let mut encryptor =
Cfb8Encryptor::<Aes128>::new_from_slices(key, iv).expect("Invalid key/iv");
encryptor.encrypt(data);
}

View File

@ -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::<Aes128>::new_from_slices(key, iv).expect("Invalid key/iv");
let mut decryptor =
Cfb8Decryptor::<Aes128>::new_from_slices(key, iv).expect("Invalid key/iv");
decryptor.decrypt(encrypted_data);
}

View File

@ -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<R: AsyncRead + Unpin> AsyncRead for StreamDecryptor<R> {
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<W: AsyncWrite + Unpin> AsyncWrite for StreamEncryptor<W> {
// 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<u8, U1> = (&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);

View File

@ -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<SyncChunk> = 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;

View File

@ -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 }

View File

@ -68,7 +68,7 @@ impl CommandExecutor for GiveExecutor {
let amplifier: u8 = match self.1 {
Amplifier::Base => 0,
Amplifier::Specified => BoundedNumArgumentConsumer::new()
Amplifier::Specified => BoundedNumArgumentConsumer::<i32>::new()
.name("amplifier")
.min(0)
.max(255)

View File

@ -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
}
}

View File

@ -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>) -> 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<f64> {
Vector3::new(self.x, self.y, self.z)
}
}
#[must_use]
pub fn find_path(
nodes: &[Option<DragonNode>; NODE_COUNT],
from: usize,
to: usize,
final_node: Option<DragonNode>,
) -> Vec<usize> {
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<f64>, 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<Arc<EnderDragonPart>>,
pub flight_history: Mutex<DragonFlightHistory>,
pub flap_time: Mutex<f32>,
pub o_flap_time: Mutex<f32>,
pub in_wall: Mutex<bool>,
pub dragon_death_time: Mutex<i32>,
pub sitting_damage_received: Mutex<f32>,
pub nodes_initialized: Mutex<bool>,
pub fight_origin: Mutex<BlockPos>,
pub phase_manager: PhaseManager,
pub phase: Mutex<EnderDragonPhase>,
pub growl_time: Mutex<i32>,
pub yaw_rot_accel: Mutex<f32>,
pub holding_pattern_clockwise: Mutex<bool>,
pub target_location: Mutex<Option<Vector3<f64>>>,
pub fireball_charge: Mutex<i32>,
pub nodes: Mutex<[Option<DragonNode>; NODE_COUNT]>,
pub path: Mutex<Vec<usize>>,
pub target_node: Mutex<usize>,
pub strafe_target: Mutex<Option<Vector3<f64>>>,
pub target_player: Mutex<Option<uuid::Uuid>>,
pub ticks_sitting: Mutex<i32>,
pub sit_attack_timer: Mutex<i32>,
pub breathing_timer: Mutex<i32>,
}
impl EnderDragonEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let dragon = Self { mob_entity };
Arc::new(dragon)
pub async fn new(entity: Entity) -> Arc<Self> {
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<dyn EntityBase>)
.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<f64>) -> usize {
self.ensure_nodes_initialized().await;
let nodes = self.nodes.lock().await;
Self::nearest_node_in(&nodes, pos)
}
fn nearest_node_in(nodes: &[Option<DragonNode>; NODE_COUNT], pos: Vector3<f64>) -> 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<f64>, to: Vector3<f64>) -> 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<f64>) {
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<f64>,
speed: f32,
yaw: f32,
) -> Vector3<f64> {
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<f64>,
target: Vector3<f64>,
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<Arc<Player>> {
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<dyn EntityBase>) -> 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<f64> {
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
}
}

View File

@ -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]
}
}

View File

@ -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
}
}

View File

@ -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;
}
}
})
}
}

View File

@ -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::<f32>() - 0.5) * 8.0;
let yo = (rand::random::<f32>() - 0.5) * 4.0;
let zo = (rand::random::<f32>() - 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;
}
})
}
}

View File

@ -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);
})
}
}

View File

@ -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);
})
}
}

View File

@ -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;
})
}
}

View File

@ -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);
})
}
}

View File

@ -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<Vector3<f64>> {
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<dyn Phase>; 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<dyn Phase> {
self.phases[phase_type as usize].clone()
}
}

View File

@ -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;
}
})
}
}

View File

@ -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;
}
})
}
}

View File

@ -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;
}
}
})
}
}

View File

@ -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);
})
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -139,4 +139,8 @@ impl EntityBase for ExperienceOrbEntity {
fn get_gravity(&self) -> f64 {
0.03
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -124,4 +124,8 @@ impl EntityBase for FallingEntity {
fn get_gravity(&self) -> f64 {
0.04
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -391,6 +391,14 @@ impl<T: Mob + Send + 'static> EntityBase for T {
})
}
fn is_collidable(&self, _entity: Option<Box<dyn EntityBase>>) -> bool {
true
}
fn can_hit(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
caller: &'a dyn EntityBase,
@ -433,6 +441,10 @@ impl<T: Mob + Send + 'static> 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()
}

View File

@ -108,7 +108,7 @@ pub type EntityBaseFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub type TeleportFuture = Pin<Box<dyn Future<Output = ()> + 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<Self>) -> Option<Arc<ItemEntity>> {
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<World>,
position: Vector3<f64>,
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<World>,
position: Vector3<f64>,
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
}

View File

@ -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
}

View File

@ -470,6 +470,10 @@ impl EntityBase for ArrowEntity {
}
})
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}
impl ArrowEntity {

View File

@ -181,4 +181,8 @@ impl EntityBase for EggEntity {
}
})
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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<Self>) {
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();

View File

@ -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<BlockPos>,
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<usize>,
/// Active respawn stage, or `None` when not respawning.
pub respawn_stage: Option<DragonRespawnStage>,
/// 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<Uuid>,
// ── 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<Uuid>,
}
@ -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<usize> = (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<World>) {
// 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<World>) {
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<World>) -> 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<World>) {
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<World>) {
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<World>) {
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<World>, 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::<crate::entity::boss::ender_dragon::EnderDragonEntity>()
{
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<World>, 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<World>) {
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<World>, 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<World>) {
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<World>, crystal_uuids: Vec<Uuid>) {
// 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<World>, 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<World>) {
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<World>) {
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<World>) {
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<World>) {
// 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<i32> = 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<World>, 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<World>) {
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 &current {
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<Uuid> = 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()
}
}

View File

@ -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<dyn EntityBase>) {
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;
{