Skip to main content

steel_core/portal/
end_portal.rs

1//! End portal destination calculation.
2
3use std::sync::Arc;
4
5use glam::DVec3;
6use steel_protocol::packets::game::RelativeMovement;
7use steel_utils::{BlockPos, ChunkPos, Direction, SectionPos};
8
9use crate::{
10    entity::Entity,
11    level_data::RespawnData,
12    portal::{PortalTicketTarget, TeleportPostTransition, TeleportTransition},
13    world::World,
14};
15
16/// Vanilla `ServerLevel.END_SPAWN_POINT`.
17pub(crate) const END_SPAWN_POINT: BlockPos = BlockPos::new(100, 50, 0);
18
19const END_PLATFORM_PREWARM_CHUNK_RADIUS: u8 = 1;
20
21/// Returns the chunks Steel prewarms before creating the End spawn platform.
22#[must_use]
23pub(crate) const fn end_platform_prewarm_center() -> ChunkPos {
24    prewarm_center(END_SPAWN_POINT)
25}
26
27/// Returns the chunk square radius that covers the vanilla 5x5 End platform.
28#[must_use]
29pub(crate) const fn end_platform_prewarm_chunk_radius() -> u8 {
30    END_PLATFORM_PREWARM_CHUNK_RADIUS
31}
32
33/// Returns the chunk centered on a block position for End portal prewarming.
34#[must_use]
35pub(crate) const fn prewarm_center(pos: BlockPos) -> ChunkPos {
36    ChunkPos::new(
37        SectionPos::block_to_section_coord(pos.x()),
38        SectionPos::block_to_section_coord(pos.z()),
39    )
40}
41
42/// Calculates vanilla's non-End -> End portal transition.
43#[must_use]
44pub(crate) fn calculate_entry_transition(
45    target_world: &Arc<World>,
46    entity: &dyn Entity,
47) -> Option<TeleportTransition> {
48    if !target_world.create_end_platform(end_platform_origin()) {
49        log::error!("Unable to create End platform at {}", target_world.key);
50        return None;
51    }
52
53    Some(TeleportTransition {
54        target_world: target_world.clone(),
55        position: end_entry_position(entity.as_player().is_some()),
56        rotation: (Direction::West.to_yaw(), 0.0),
57        velocity: DVec3::ZERO,
58        relatives: RelativeMovement::DELTA.union(RelativeMovement::new(RelativeMovement::X_ROT)),
59        portal_cooldown: entity.dimension_changing_delay(),
60        as_passenger: false,
61        post_transition: portal_sound_then_destination_ticket(),
62    })
63}
64
65/// Calculates vanilla's End -> respawn-world portal transition for non-player entities.
66#[must_use]
67pub(crate) fn calculate_entity_return_transition(
68    target_world: &Arc<World>,
69    entity: &dyn Entity,
70    respawn_data: &RespawnData,
71) -> TeleportTransition {
72    let spawn_pos = target_world.adjust_spawn_location(respawn_data.pos());
73    TeleportTransition {
74        target_world: target_world.clone(),
75        position: block_bottom_center(spawn_pos),
76        rotation: (respawn_data.yaw, respawn_data.pitch),
77        velocity: DVec3::ZERO,
78        relatives: RelativeMovement::DELTA.union(RelativeMovement::ROTATION),
79        portal_cooldown: entity.dimension_changing_delay(),
80        as_passenger: false,
81        post_transition: portal_sound_then_destination_ticket(),
82    }
83}
84
85/// Calculates the currently supported End -> respawn-world transition for players.
86///
87/// Vanilla delegates to `ServerPlayer.findRespawnPositionAndUseSpawnBlock`.
88///
89/// TODO(respawn): replace this with the vanilla personal bed/anchor respawn path once Steel has
90/// that player respawn foundation. This currently covers only the default respawn branch.
91#[must_use]
92pub(crate) fn calculate_player_return_transition(
93    target_world: &Arc<World>,
94    entity: &dyn Entity,
95    position: DVec3,
96    rotation: (f32, f32),
97) -> TeleportTransition {
98    TeleportTransition {
99        target_world: target_world.clone(),
100        position,
101        rotation,
102        velocity: DVec3::ZERO,
103        relatives: RelativeMovement::NONE,
104        portal_cooldown: entity.dimension_changing_delay(),
105        as_passenger: false,
106        post_transition: TeleportPostTransition::do_nothing(),
107    }
108}
109
110const fn end_platform_origin() -> BlockPos {
111    BlockPos::new(
112        END_SPAWN_POINT.x(),
113        END_SPAWN_POINT.y() - 1,
114        END_SPAWN_POINT.z(),
115    )
116}
117
118fn end_entry_position(is_player: bool) -> DVec3 {
119    let mut position = block_bottom_center(END_SPAWN_POINT);
120    if is_player {
121        position.y -= 1.0;
122    }
123    position
124}
125
126fn portal_sound_then_destination_ticket() -> TeleportPostTransition {
127    TeleportPostTransition::play_portal_sound().then(TeleportPostTransition::place_portal_ticket(
128        PortalTicketTarget::Destination,
129    ))
130}
131
132fn block_bottom_center(pos: BlockPos) -> DVec3 {
133    let (x, y, z) = pos.get_bottom_center();
134    DVec3::new(x, y, z)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::{
140        END_SPAWN_POINT, block_bottom_center, end_entry_position, end_platform_origin,
141        end_platform_prewarm_center, end_platform_prewarm_chunk_radius,
142    };
143    use glam::DVec3;
144    use steel_utils::{BlockPos, ChunkPos};
145
146    #[test]
147    fn end_platform_origin_is_below_vanilla_end_spawn() {
148        assert_eq!(END_SPAWN_POINT, BlockPos::new(100, 50, 0));
149        assert_eq!(end_platform_origin(), BlockPos::new(100, 49, 0));
150    }
151
152    #[test]
153    fn end_entry_player_position_is_one_block_below_spawn_center() {
154        assert_eq!(
155            end_entry_position(false),
156            block_bottom_center(BlockPos::new(100, 50, 0))
157        );
158        assert_eq!(end_entry_position(true), DVec3::new(100.5, 49.0, 0.5));
159    }
160
161    #[test]
162    fn end_platform_prewarm_covers_negative_z_edge() {
163        assert_eq!(end_platform_prewarm_center(), ChunkPos::new(6, 0));
164        assert_eq!(end_platform_prewarm_chunk_radius(), 1);
165    }
166}