mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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)
This commit is contained in:
@@ -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::<Vec<_>>()
|
||||
})
|
||||
.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"),
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) }
|
||||
},
|
||||
"minecraft:heightmap" => {
|
||||
let heightmap =
|
||||
value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING"));
|
||||
|
||||
@@ -841,7 +841,7 @@ fn build_placed_features() -> std::collections::HashMap<String, PlacedFeature> {
|
||||
PlacedFeature {
|
||||
feature: Feature::Named("end_platform".to_string()),
|
||||
placement: vec![
|
||||
PlacementModifier::FixedPlacement,
|
||||
PlacementModifier::FixedPlacement(vec![BlockPos::new(100, 50, 0)]),
|
||||
PlacementModifier::Biome(BiomePlacementModifier),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<i32> = (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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ pub enum PlacementModifier {
|
||||
HeightRange(HeightRangePlacementModifier),
|
||||
InSquare(SquarePlacementModifier),
|
||||
RandomOffset(RandomOffsetPlacementModifier),
|
||||
FixedPlacement,
|
||||
FixedPlacement(Vec<BlockPos>),
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
391
pumpkin/src/world/dragon_fight.rs
Normal file
391
pumpkin/src/world/dragon_fight.rs
Normal file
@@ -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<Uuid>,
|
||||
pub portal_location: Option<BlockPos>,
|
||||
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<Uuid>,
|
||||
}
|
||||
|
||||
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<World>) {
|
||||
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<World>) {
|
||||
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<World>) -> 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<World>) {
|
||||
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<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;
|
||||
self.dragon_uuid = Some(uuid);
|
||||
info!("Spawned ender dragon.");
|
||||
}
|
||||
|
||||
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 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<World>, 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<World>) {
|
||||
// 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<i32> = 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<World>, 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<World>, 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<World>) {
|
||||
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<World>) {
|
||||
let players = world.players.load();
|
||||
|
||||
let current: Vec<Uuid> = 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<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;
|
||||
}
|
||||
self.bossbar_players.retain(|u| u != uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
pumpkin/src/world/end_podium.rs
Normal file
95
pumpkin/src/world/end_podium.rs
Normal file
@@ -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<World>, 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;
|
||||
}
|
||||
}
|
||||
@@ -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<HashMap<BlockPos, u16>>,
|
||||
/// POI storage for fast portal lookups
|
||||
pub portal_poi: Mutex<portal::PortalPoiStorage>,
|
||||
/// End Dragon fight manager (only present in `THE_END` dimension).
|
||||
pub dragon_fight: Option<Mutex<dragon_fight::DragonFight>>,
|
||||
}
|
||||
|
||||
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!(
|
||||
|
||||
Reference in New Issue
Block a user