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