From 3c43f9715e8b4d63c17dc81ba82bd4e802113aaf Mon Sep 17 00:00:00 2001 From: RB007 <36972202+RoosterBooster007@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:21:11 -0400 Subject: [PATCH] fix: end portal crash and island setup (#1945) * fix: use noise_sampler * fix: end portal teleportation and generation * feature: dragon fight start (improved main end island) --- pumpkin-codegen/src/configured_feature.rs | 16 +- pumpkin-codegen/src/placed_feature.rs | 16 +- .../generated/placed_features_generated.rs | 2 +- .../src/chunk_system/generation_cache.rs | 14 +- .../feature/features/end_platform.rs | 19 +- .../generation/feature/features/end_spike.rs | 72 +++- .../src/generation/feature/placed_features.rs | 4 +- pumpkin-world/src/generation/proto_chunk.rs | 5 +- pumpkin/src/entity/mod.rs | 21 +- pumpkin/src/world/dragon_fight.rs | 391 ++++++++++++++++++ pumpkin/src/world/end_podium.rs | 95 +++++ pumpkin/src/world/mod.rs | 11 + 12 files changed, 639 insertions(+), 27 deletions(-) create mode 100644 pumpkin/src/world/dragon_fight.rs create mode 100644 pumpkin/src/world/end_podium.rs diff --git a/pumpkin-codegen/src/configured_feature.rs b/pumpkin-codegen/src/configured_feature.rs index 00dc1983e..44f82fcb3 100644 --- a/pumpkin-codegen/src/configured_feature.rs +++ b/pumpkin-codegen/src/configured_feature.rs @@ -1233,7 +1233,21 @@ fn value_to_placement_modifier_cf(v: &Value) -> TokenStream { match type_str { "minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) }, "minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) }, - "minecraft:fixed_placement" => quote! { PlacementModifier::FixedPlacement }, + "minecraft:fixed_placement" => { + let positions = v["positions"] + .as_array() + .map(|arr| { + arr.iter().map(|p| { + let coords = p.as_array().unwrap(); + let x = coords[0].as_i64().unwrap_or(0) as i32; + let y = coords[1].as_i64().unwrap_or(0) as i32; + let z = coords[2].as_i64().unwrap_or(0) as i32; + quote! { BlockPos::new(#x, #y, #z) } + }).collect::>() + }) + .unwrap_or_default(); + quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) } + }, "minecraft:heightmap" => { let hm = crate::placed_feature::value_to_height_map( v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING"), diff --git a/pumpkin-codegen/src/placed_feature.rs b/pumpkin-codegen/src/placed_feature.rs index e8b6569d6..41c78208e 100644 --- a/pumpkin-codegen/src/placed_feature.rs +++ b/pumpkin-codegen/src/placed_feature.rs @@ -108,7 +108,21 @@ fn value_to_placement_modifier(v: &Value) -> TokenStream { match type_str { "minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) }, "minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) }, - "minecraft:fixed_placement" => quote! { PlacementModifier::FixedPlacement }, + "minecraft:fixed_placement" => { + let positions = v["positions"] + .as_array() + .map(|arr| { + arr.iter().map(|p| { + let coords = p.as_array().unwrap(); + let x = coords[0].as_i64().unwrap_or(0) as i32; + let y = coords[1].as_i64().unwrap_or(0) as i32; + let z = coords[2].as_i64().unwrap_or(0) as i32; + quote! { BlockPos::new(#x, #y, #z) } + }).collect::>() + }) + .unwrap_or_default(); + quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) } + }, "minecraft:heightmap" => { let heightmap = value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING")); diff --git a/pumpkin-data/src/generated/placed_features_generated.rs b/pumpkin-data/src/generated/placed_features_generated.rs index 61e301d33..fd7c371f5 100644 --- a/pumpkin-data/src/generated/placed_features_generated.rs +++ b/pumpkin-data/src/generated/placed_features_generated.rs @@ -841,7 +841,7 @@ fn build_placed_features() -> std::collections::HashMap { PlacedFeature { feature: Feature::Named("end_platform".to_string()), placement: vec![ - PlacementModifier::FixedPlacement, + PlacementModifier::FixedPlacement(vec![BlockPos::new(100, 50, 0)]), PlacementModifier::Biome(BiomePlacementModifier), ], }, diff --git a/pumpkin-world/src/chunk_system/generation_cache.rs b/pumpkin-world/src/chunk_system/generation_cache.rs index a50f499b3..f19214252 100644 --- a/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/pumpkin-world/src/chunk_system/generation_cache.rs @@ -289,8 +289,9 @@ impl GenerationCache for Cache { fn ocean_floor_height_exclusive(&self, x: i32, z: i32) -> i32 { let dx = (x >> 4) - self.x; let dy = (z >> 4) - self.z; - debug_assert!(dx < self.size && dy < self.size); - debug_assert!(dx >= 0 && dy >= 0); + if dx < 0 || dy < 0 || dx >= self.size || dy >= self.size { + return 0; + } match &self.chunks[(dx * self.size + dy) as usize] { Chunk::Level(_data) => { 0 // todo missing @@ -302,8 +303,13 @@ impl GenerationCache for Cache { fn get_biome_for_terrain_gen(&self, x: i32, y: i32, z: i32) -> &'static Biome { let dx = (x >> 4) - self.x; let dy = (z >> 4) - self.z; - debug_assert!(dx < self.size && dy < self.size); - debug_assert!(dx >= 0 && dy >= 0); + let (dx, dy) = if dx < 0 || dy < 0 || dx >= self.size || dy >= self.size { + // Position is outside the cache — fall back to the centre chunk's biome + let mid = self.size / 2; + (mid, mid) + } else { + (dx, dy) + }; match &self.chunks[(dx * self.size + dy) as usize] { Chunk::Level(data) => { // Could this happen? diff --git a/pumpkin-world/src/generation/feature/features/end_platform.rs b/pumpkin-world/src/generation/feature/features/end_platform.rs index c74f9181d..acdb5803e 100644 --- a/pumpkin-world/src/generation/feature/features/end_platform.rs +++ b/pumpkin-world/src/generation/feature/features/end_platform.rs @@ -1,5 +1,8 @@ use pumpkin_data::Block; -use pumpkin_util::{math::position::BlockPos, random::RandomGenerator}; +use pumpkin_util::{ + math::{position::BlockPos, vector3::Vector3}, + random::RandomGenerator, +}; use crate::generation::proto_chunk::GenerationCache; use crate::world::BlockRegistryExt; @@ -18,18 +21,16 @@ impl EndPlatformFeature { _random: &mut RandomGenerator, pos: BlockPos, ) -> bool { - for _ in -2..2 { - for _ in -2..2 { - for t in -1..3 { - let state = if t == -1 { + for dx in -2i32..=2 { + for dz in -2i32..=2 { + for dy in -1i32..3 { + let state = if dy == -1 { Block::OBSIDIAN.default_state } else { Block::AIR.default_state }; - if GenerationCache::get_block_state(chunk, &pos.0).0 == state.id { - continue; - } - chunk.set_block_state(&pos.0, state); + let target = Vector3::new(pos.0.x + dx, pos.0.y + dy, pos.0.z + dz); + chunk.set_block_state(&target, state); } } } diff --git a/pumpkin-world/src/generation/feature/features/end_spike.rs b/pumpkin-world/src/generation/feature/features/end_spike.rs index f38ecafb2..ae645716f 100644 --- a/pumpkin-world/src/generation/feature/features/end_spike.rs +++ b/pumpkin-world/src/generation/feature/features/end_spike.rs @@ -1,4 +1,7 @@ -use pumpkin_data::Block; +use pumpkin_data::{ + Block, BlockState, + block_properties::{BlockProperties, OakFenceLikeProperties}, +}; use pumpkin_util::{ math::position::BlockPos, random::{RandomGenerator, RandomImpl}, @@ -43,12 +46,17 @@ impl EndSpikeFeature { ) -> bool { let mut spikes = self.spikes.clone(); if spikes.is_empty() { - for i in 0..10 { + let mut sizes: Vec = (0..10).collect(); + for i in (1..10_usize).rev() { + let j = random.next_bounded_i32(i as i32 + 1) as usize; + sizes.swap(i, j); + } + + for (i, &l) in sizes.iter().enumerate() { let angle = 2.0 * (-std::f64::consts::PI + 0.3141592653589793 * i as f64); let center_x = (42.0 * angle.cos()).floor() as i32; let center_z = (42.0 * angle.sin()).floor() as i32; - let l = random.next_bounded_i32(10); // TODO let radius = 2 + l / 3; let height = 76 + l * 3; let guarded = l == 1 || l == 2; @@ -82,7 +90,7 @@ impl EndSpikeFeature { ), BlockPos::new( spike.center_x + radius, - chunk.height() as i32 + 10, + spike.height + 10, spike.center_z + radius, ), ) { @@ -100,6 +108,60 @@ impl EndSpikeFeature { } chunk.set_block_state(&pos.0, Block::AIR.default_state); } - // TODO + + // Bedrock cap serves as the crystal base, fire sits on top of it + chunk.set_block_state( + &pumpkin_util::math::vector3::Vector3::new( + spike.center_x, + spike.height, + spike.center_z, + ), + Block::BEDROCK.default_state, + ); + chunk.set_block_state( + &pumpkin_util::math::vector3::Vector3::new( + spike.center_x, + spike.height + 1, + spike.center_z, + ), + Block::FIRE.default_state, + ); + + // Iron-bar cage for guarded spikes: 5x5 walls + open top frame at dy=3. + if spike.guarded { + for dy in 0_i32..=3 { + for dx in -2_i32..=2 { + for dz in -2_i32..=2 { + // Only place on perimeter walls and the top frame + let on_x_wall = dx.abs() == 2; + let on_z_wall = dz.abs() == 2; + let on_top = dy == 3; + if !on_x_wall && !on_z_wall && !on_top { + continue; + } + + // Connectivity rules + let x_edge = on_x_wall || on_top; + let z_edge = on_z_wall || on_top; + + let mut props = OakFenceLikeProperties::default(&Block::IRON_BARS); + props.north = x_edge && dz != 2; + props.south = x_edge && dz != -2; + props.west = z_edge && dx != 2; + props.east = z_edge && dx != -2; + + let bar_state = BlockState::from_id(props.to_state_id(&Block::IRON_BARS)); + chunk.set_block_state( + &pumpkin_util::math::vector3::Vector3::new( + spike.center_x + dx, + spike.height + dy, + spike.center_z + dz, + ), + bar_state, + ); + } + } + } + } } } diff --git a/pumpkin-world/src/generation/feature/placed_features.rs b/pumpkin-world/src/generation/feature/placed_features.rs index f6fe244e9..98ec8d25f 100644 --- a/pumpkin-world/src/generation/feature/placed_features.rs +++ b/pumpkin-world/src/generation/feature/placed_features.rs @@ -121,7 +121,7 @@ pub enum PlacementModifier { HeightRange(HeightRangePlacementModifier), InSquare(SquarePlacementModifier), RandomOffset(RandomOffsetPlacementModifier), - FixedPlacement, + FixedPlacement(Vec), } impl PlacementModifier { @@ -161,7 +161,7 @@ impl PlacementModifier { Self::HeightRange(modifier) => modifier.get_positions(min_y, height, random, pos), Self::InSquare(_) => SquarePlacementModifier::get_positions(random, pos), Self::RandomOffset(modifier) => modifier.get_positions(random, pos), - Self::FixedPlacement => Box::new(iter::empty()), + Self::FixedPlacement(positions) => Box::new(positions.clone().into_iter()), } } } diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 075526136..57132f8ea 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -713,9 +713,8 @@ impl ProtoChunk { let v_count = noise_sampler.vertical_cell_block_count() as i32; let horizontal_cells = CHUNK_DIM as i32 / h_count; - let min_y = self.bottom_y(); - let minimum_cell_y = min_y / v_count as i8; - let cell_height = self.height() / v_count as u16; + let minimum_cell_y = noise_sampler.min_y() / v_count as i8; + let cell_height = noise_sampler.height() / v_count as u16; let delta_y_step = 1.0 / v_count as f64; let delta_x_z_step = 1.0 / h_count as f64; diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index aa0eef964..236281f68 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -1599,7 +1599,26 @@ impl Entity { let source_axis = source_portal.as_ref().map(|p| p.axis); drop(portal_manager); - let (teleport_pos, new_yaw) = if let Some(dest_result) = + let is_end_portal = dest_world.dimension == Dimension::THE_END + || self.world.load().dimension == Dimension::THE_END; + + let (teleport_pos, new_yaw) = if is_end_portal { + if dest_world.dimension == Dimension::THE_END { + // Entering the End: spawn on the obsidian platform at (100, 50, 0) + (Vector3::new(100.5f64, 50.0f64, 0.5f64), None) + } else { + // Leaving the End through the exit portal: return to overworld spawn + let info = dest_world.level_info.load(); + ( + Vector3::new( + f64::from(info.spawn_x) + 0.5, + f64::from(info.spawn_y), + f64::from(info.spawn_z) + 0.5, + ), + None, + ) + } + } else if let Some(dest_result) = NetherPortal::search_for_portal(&dest_world, target_pos).await { let base_pos = source_portal.as_ref().map_or_else( diff --git a/pumpkin/src/world/dragon_fight.rs b/pumpkin/src/world/dragon_fight.rs new file mode 100644 index 000000000..242bc0055 --- /dev/null +++ b/pumpkin/src/world/dragon_fight.rs @@ -0,0 +1,391 @@ +//! End Dragon fight manager. Handles the dragon lifecycle, end crystal +//! spawning, boss bar, and exit portal. + +use std::sync::Arc; + +use tracing::info; +use uuid::Uuid; + +use pumpkin_data::{Block, entity::EntityType}; +use pumpkin_util::{ + math::{position::BlockPos, vector2::Vector2, vector3::Vector3}, + text::TextComponent, +}; +use pumpkin_world::world::BlockFlags; + +use super::{ + World, + bossbar::{Bossbar, BossbarColor, BossbarDivisions, BossbarFlags}, +}; +use crate::entity::{Entity, decoration::end_crystal::EndCrystalEntity}; + +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; + +pub struct DragonFight { + pub dragon_killed: bool, + pub previously_killed: bool, + pub needs_state_scanning: bool, + pub dragon_uuid: Option, + pub portal_location: Option, + pub crystals_alive: u32, + + ticks_since_dragon_seen: i32, + ticks_since_crystals_scanned: i32, + ticks_since_last_player_scan: i32, + bossbar_uuid: Uuid, + bossbar_players: Vec, +} + +impl Default for DragonFight { + fn default() -> Self { + Self::new() + } +} + +impl DragonFight { + #[must_use] + pub fn new() -> Self { + Self { + dragon_killed: false, + previously_killed: false, + needs_state_scanning: true, + dragon_uuid: None, + portal_location: None, + crystals_alive: 0, + ticks_since_dragon_seen: 0, + ticks_since_crystals_scanned: 0, + // Start over the threshold so the scan runs on the first tick with players + ticks_since_last_player_scan: PLAYER_SCAN_INTERVAL + 1, + bossbar_uuid: Uuid::new_v4(), + bossbar_players: Vec::new(), + } + } + + pub async fn tick(&mut self, world: &Arc) { + 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 + if self.bossbar_players.is_empty() { + return; + } + + if self.needs_state_scanning { + self.scan_state(world).await; + self.needs_state_scanning = false; + } + + if !self.dragon_killed { + self.ticks_since_dragon_seen += 1; + if self.dragon_uuid.is_none() + || self.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN + { + self.find_or_create_dragon(world).await; + self.ticks_since_dragon_seen = 0; + } + + self.ticks_since_crystals_scanned += 1; + if self.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL { + self.update_crystal_count(world); + self.ticks_since_crystals_scanned = 0; + } + } + } +} + +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. + async fn scan_state(&mut self, world: &Arc) { + info!("Scanning End fight state..."); + + let has_active_portal = self.has_active_exit_portal(world).await; + + if has_active_portal { + info!("Exit portal found – dragon has been killed previously."); + self.previously_killed = true; + } 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 = { + let entities = world.entities.load(); + entities + .iter() + .find(|e| e.get_entity().entity_type == &EntityType::ENDER_DRAGON) + .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 + if let Some(e) = world + .entities + .load() + .iter() + .find(|e| e.get_entity().entity_uuid == uuid) + { + e.get_entity().remove().await; + } + self.dragon_uuid = None; + self.dragon_killed = true; + } else { + info!("Found existing dragon entity."); + self.dragon_uuid = Some(uuid); + self.dragon_killed = false; + } + } else { + self.dragon_killed = true; + } + + // If the world looks fresh but the dragon appears dead, reset so one gets spawned + 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 + async fn has_active_exit_portal(&self, world: &Arc) -> bool { + for cx in -8i32..=8 { + for cz in -8i32..=8 { + let bx = cx * 16; + let bz = cz * 16; + for y in 30i32..=80 { + if world.get_block(&BlockPos::new(bx, y, bz)).await == &Block::END_PORTAL { + return true; + } + } + } + } + false + } +} + +impl DragonFight { + async fn find_or_create_dragon(&mut self, world: &Arc) { + let uuid = { + let entities = world.entities.load(); + entities + .iter() + .find(|e| e.get_entity().entity_type == &EntityType::ENDER_DRAGON) + .map(|e| e.get_entity().entity_uuid) + }; + + if let Some(u) = uuid { + info!("Re-acquired existing dragon."); + self.dragon_uuid = Some(u); + } else { + info!("No dragon found \u{2013} spawning one."); + self.create_new_dragon(world).await; + } + } + + async fn create_new_dragon(&mut self, world: &Arc) { + let entity = Entity::new( + world.clone(), + Vector3::new(0.5, DRAGON_SPAWN_Y, 0.5), + &EntityType::ENDER_DRAGON, + ); + let uuid = entity.entity_uuid; + world.spawn_entity(Arc::new(entity)).await; + self.dragon_uuid = Some(uuid); + info!("Spawned ender dragon."); + } + + fn update_crystal_count(&mut self, world: &Arc) { + self.crystals_alive = world + .entities + .load() + .iter() + .filter(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL) + .count() as u32; + } + + /// Called when the dragon dies. Activates the exit portal, places the + /// dragon egg on a first kill, and hides the boss bar. + pub async fn set_dragon_killed(&mut self, world: &Arc, killed_uuid: Uuid) { + if Some(killed_uuid) != self.dragon_uuid { + return; + } + + self.update_bossbar_health(world, 0.0).await; + self.remove_all_bossbar(world).await; + + self.spawn_exit_portal(world, true).await; + + 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( + &egg_pos, + Block::DRAGON_EGG.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .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. + pub async fn spawn_crystals(&mut self, world: &Arc) { + // Don't re-spawn if crystals are already present (resumed world). + if world + .entities + .load() + .iter() + .any(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL) + { + return; + } + + for i in 0..10usize { + let angle = 2.0 * (-std::f64::consts::PI + std::f64::consts::PI * 0.1 * i as f64); + let cx = (42.0f64 * angle.cos()).floor() as i32; + let cz = (42.0f64 * angle.sin()).floor() as i32; + + let mut crystal_y: Option = None; + for y in (70..=115i32).rev() { + if world.get_block(&BlockPos::new(cx, y, cz)).await == &Block::BEDROCK { + crystal_y = Some(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), + &EntityType::END_CRYSTAL, + ); + let crystal = Arc::new(EndCrystalEntity::new(entity)); + crystal.set_show_bottom(true).await; + world.spawn_entity(crystal).await; + } + 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. + pub async fn spawn_exit_portal(&mut self, world: &Arc, active: bool) { + 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; + } + portal_y -= 1; + } + portal_y = portal_y.max(world.min_y + 1); + self.portal_location = Some(BlockPos::new(0, portal_y, 0)); + } + + if let Some(loc) = self.portal_location { + super::end_podium::place(world, loc, active).await; + } + } +} + +impl DragonFight { + fn make_bossbar(&self) -> Bossbar { + Bossbar { + uuid: self.bossbar_uuid, + title: TextComponent::translate("entity.minecraft.ender_dragon", []), + health: 1.0, + color: BossbarColor::Pink, + division: BossbarDivisions::NoDivision, + flags: BossbarFlags::DragonBar, + } + } + + async fn update_bossbar_health(&self, world: &Arc, health: f32) { + for player in world.players.load().iter() { + if self.bossbar_players.contains(&player.gameprofile.id) { + player + .update_bossbar_health(&self.bossbar_uuid, health) + .await; + } + } + } + + async fn remove_all_bossbar(&mut self, world: &Arc) { + for player in world.players.load().iter() { + if self.bossbar_players.contains(&player.gameprofile.id) { + player.remove_bossbar(self.bossbar_uuid).await; + } + } + self.bossbar_players.clear(); + } + + /// Syncs the boss bar recipient list + async fn update_players(&mut self, world: &Arc) { + let players = world.players.load(); + + let current: Vec = players + .iter() + .filter(|p| { + let pos = p.living_entity.entity.pos.load(); + let dx = pos.x; + let dy = pos.y - DRAGON_SPAWN_Y; + let dz = pos.z; + dx * dx + dy * dy + dz * dz < ARENA_RADIUS * ARENA_RADIUS + }) + .map(|p| p.gameprofile.id) + .collect(); + + for &uid in ¤t { + if !self.bossbar_players.contains(&uid) { + if !self.dragon_killed + && let Some(p) = players.iter().find(|p| p.gameprofile.id == uid) + { + p.send_bossbar(&self.make_bossbar()).await; + } + self.bossbar_players.push(uid); + } + } + + let to_remove: Vec = self + .bossbar_players + .iter() + .filter(|uid| !current.contains(uid)) + .copied() + .collect(); + for uid in &to_remove { + if let Some(p) = players.iter().find(|p| &p.gameprofile.id == uid) { + p.remove_bossbar(self.bossbar_uuid).await; + } + self.bossbar_players.retain(|u| u != uid); + } + } +} diff --git a/pumpkin/src/world/end_podium.rs b/pumpkin/src/world/end_podium.rs new file mode 100644 index 000000000..6f7248c8b --- /dev/null +++ b/pumpkin/src/world/end_podium.rs @@ -0,0 +1,95 @@ +//! Places the End exit podium structure (the beacon-like structure at (0, y, 0)). +//! +//! `active = false` = inactive podium: bedrock rim + air centre, placed before the dragon is killed. +//! `active = true` = active podium: bedrock rim + `END_PORTAL` tiles in the centre, placed after kill. + +use std::sync::Arc; + +use pumpkin_data::{ + Block, + block_properties::{BlockProperties, HorizontalFacing, WallTorchLikeProperties}, +}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::world::BlockFlags; + +use super::World; + +/// Place the podium structure centred on `origin` into `world`. +pub async fn place(world: &Arc, origin: BlockPos, active: bool) { + let ox = origin.0.x; + let oy = origin.0.y; + let oz = origin.0.z; + + for y in (oy - 4)..=(oy + 32) { + for x in (ox - 4)..=(ox + 4) { + for z in (oz - 4)..=(oz + 4) { + let dx = (x - ox) as f64; + let dy = (y - oy) as f64; + let dz = (z - oz) as f64; + let dist_sq = dx * dx + dy * dy + dz * dz; + + let inside_rim = dist_sq < 2.5 * 2.5; // inner 2-block disc + let near = inside_rim || dist_sq < 3.5 * 3.5; // outer rim up to 3.5 + + if !near { + continue; + } + + let pos = BlockPos::new(x, y, z); + let state_id = match y.cmp(&oy) { + std::cmp::Ordering::Less => { + if inside_rim { + Block::BEDROCK.default_state.id + } else { + Block::END_STONE.default_state.id + } + } + std::cmp::Ordering::Greater => Block::AIR.default_state.id, + std::cmp::Ordering::Equal => { + if !inside_rim { + Block::BEDROCK.default_state.id + } else if active { + Block::END_PORTAL.default_state.id + } else { + Block::AIR.default_state.id + } + } + }; + + world + .set_block_state(&pos, state_id, BlockFlags::NOTIFY_ALL) + .await; + } + } + } + + for y in oy..=(oy + 3) { + world + .set_block_state( + &BlockPos::new(ox, y, oz), + Block::BEDROCK.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + + // Wall torches on N/S/E/W faces at pillar height 2 + let torch_y = oy + 2; + // Torch position relative to origin and facing direction: (dx, dz, facing) + for (dx, dz, facing) in [ + (0i32, -1i32, HorizontalFacing::North), + (0, 1, HorizontalFacing::South), + (-1, 0, HorizontalFacing::West), + (1, 0, HorizontalFacing::East), + ] { + let props = WallTorchLikeProperties { facing }; + let state_id = props.to_state_id(&Block::WALL_TORCH); + world + .set_block_state( + &BlockPos::new(ox + dx, torch_y, oz + dz), + state_id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f7587fb17..d4800bc5d 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -135,6 +135,8 @@ use tokio::sync::Mutex; pub mod border; pub mod bossbar; pub mod custom_bossbar; +pub mod dragon_fight; +pub mod end_podium; pub mod natural_spawner; pub mod scoreboard; pub mod weather; @@ -202,6 +204,8 @@ pub struct World { unsent_block_changes: Mutex>, /// POI storage for fast portal lookups pub portal_poi: Mutex, + /// End Dragon fight manager (only present in `THE_END` dimension). + pub dragon_fight: Option>, } impl PartialEq for World { @@ -244,6 +248,8 @@ impl World { synced_block_event_queue: Mutex::new(Vec::new()), unsent_block_changes: Mutex::new(HashMap::new()), portal_poi: Mutex::new(portal_poi), + dragon_fight: (dimension == Dimension::THE_END) + .then(|| Mutex::new(dragon_fight::DragonFight::new())), server, } } @@ -781,6 +787,11 @@ impl World { //self.level.chunk_loading.lock().unwrap().send_change(); + // Tick the End dragon fight (only on THE_END worlds). + if let Some(ref fight_mutex) = self.dragon_fight { + fight_mutex.lock().await.tick(self).await; + } + let total_elapsed = start.elapsed(); if total_elapsed.as_millis() > 50 { debug!(