feat: add dragon ai
not perfect, bit buggy, but it works overall
This commit is contained in:
parent
542a5a2103
commit
2c338f468c
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
42
pumpkin/src/entity/boss/ender_dragon/flight_history.rs
Normal file
42
pumpkin/src/entity/boss/ender_dragon/flight_history.rs
Normal 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]
|
||||
}
|
||||
}
|
||||
46
pumpkin/src/entity/boss/ender_dragon/phase/charging.rs
Normal file
46
pumpkin/src/entity/boss/ender_dragon/phase/charging.rs
Normal 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
|
||||
}
|
||||
}
|
||||
92
pumpkin/src/entity/boss/ender_dragon/phase/circling.rs
Normal file
92
pumpkin/src/entity/boss/ender_dragon/phase/circling.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
101
pumpkin/src/entity/boss/ender_dragon/phase/dying.rs
Normal file
101
pumpkin/src/entity/boss/ender_dragon/phase/dying.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
30
pumpkin/src/entity/boss/ender_dragon/phase/fly_to_portal.rs
Normal file
30
pumpkin/src/entity/boss/ender_dragon/phase/fly_to_portal.rs
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
||||
29
pumpkin/src/entity/boss/ender_dragon/phase/hovering.rs
Normal file
29
pumpkin/src/entity/boss/ender_dragon/phase/hovering.rs
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
||||
25
pumpkin/src/entity/boss/ender_dragon/phase/landing.rs
Normal file
25
pumpkin/src/entity/boss/ender_dragon/phase/landing.rs
Normal 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;
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
122
pumpkin/src/entity/boss/ender_dragon/phase/mod.rs
Normal file
122
pumpkin/src/entity/boss/ender_dragon/phase/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
47
pumpkin/src/entity/boss/ender_dragon/phase/sit_attacking.rs
Normal file
47
pumpkin/src/entity/boss/ender_dragon/phase/sit_attacking.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
73
pumpkin/src/entity/boss/ender_dragon/phase/sit_breathing.rs
Normal file
73
pumpkin/src/entity/boss/ender_dragon/phase/sit_breathing.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
168
pumpkin/src/entity/boss/ender_dragon/phase/strafing.rs
Normal file
168
pumpkin/src/entity/boss/ender_dragon/phase/strafing.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
30
pumpkin/src/entity/boss/ender_dragon/phase/taking_off.rs
Normal file
30
pumpkin/src/entity/boss/ender_dragon/phase/taking_off.rs
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,4 +139,8 @@ impl EntityBase for ExperienceOrbEntity {
|
||||
fn get_gravity(&self) -> f64 {
|
||||
0.03
|
||||
}
|
||||
|
||||
fn cast_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,4 +124,8 @@ impl EntityBase for FallingEntity {
|
||||
fn get_gravity(&self) -> f64 {
|
||||
0.04
|
||||
}
|
||||
|
||||
fn cast_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -470,6 +470,10 @@ impl EntityBase for ArrowEntity {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn cast_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrowEntity {
|
||||
|
||||
@ -181,4 +181,8 @@ impl EntityBase for EggEntity {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn cast_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 ¤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<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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user