Skip to main content

steel_core/entity/consume_effect/
teleport_randomly.rs

1//! `TeleportRandomlyConsumeEffect` behavior (chorus fruit).
2
3use std::sync::Arc;
4
5use steel_protocol::packets::game::SoundSource;
6use steel_registry::consume_effect::{ConsumeEffectData, TeleportRandomlyConsumeEffect};
7use steel_registry::{sound_events, vanilla_game_events};
8
9use super::ConsumeEffectBehavior;
10use crate::entity::LivingEntity;
11use crate::world::World;
12use crate::world::game_event::GameEventContext;
13
14/// Mirrors vanilla `TeleportRandomlyConsumeEffect.apply`.
15pub struct TeleportRandomlyBehavior;
16
17impl ConsumeEffectBehavior for TeleportRandomlyBehavior {
18    fn apply(&self, effect: &ConsumeEffectData, world: &Arc<World>, user: &dyn LivingEntity) {
19        let Some(teleport) = effect.downcast_ref::<TeleportRandomlyConsumeEffect>() else {
20            return;
21        };
22        teleport_randomly(*teleport, world, user);
23    }
24}
25
26/// Tries up to 16 random nearby positions, delegating each attempt to
27/// `LivingEntity::random_teleport` (vanilla `Entity.randomTeleport`), and
28/// stops at the first one that lands.
29///
30// TODO(26.3): Vanilla snapshot 26.3 adds the block tag
31// `#consumable_does_not_teleport_to` — "blocks that entities do not
32// teleport to when they consume food that teleports randomly when eaten"
33// (empty by default). Once that tag exists in the registry extraction, a
34// landing block tagged with it must be rejected here, the same way an
35// unloaded/non-solid candidate already is — this is specific to this
36// consume-effect path, not the shared `Entity.randomTeleport` primitive
37// (Enderman's own random teleport is unaffected by this tag).
38fn teleport_randomly(
39    effect: TeleportRandomlyConsumeEffect,
40    world: &Arc<World>,
41    user: &dyn LivingEntity,
42) {
43    let diameter = f64::from(effect.diameter());
44    let min_y = f64::from(world.get_min_y());
45    let max_y = f64::from(world.get_min_y() + world.dimension_type.logical_height - 1);
46
47    for _ in 0..16 {
48        let origin = user.position();
49        let x = origin.x + (rand::random::<f64>() - 0.5) * diameter;
50        let y = (origin.y + (rand::random::<f64>() - 0.5) * diameter).clamp(min_y, max_y);
51        let z = origin.z + (rand::random::<f64>() - 0.5) * diameter;
52
53        if user.is_passenger() {
54            user.stop_riding();
55        }
56
57        let old_pos = user.position();
58        if !user.random_teleport(world, x, y, z, true) {
59            continue;
60        }
61
62        world.game_event_at(
63            &vanilla_game_events::TELEPORT,
64            old_pos,
65            &GameEventContext::new(Some(user.as_entity_event_source()), None),
66        );
67        // TODO: Play `FOX_TELEPORT` on `SoundSource::Neutral` instead once Fox
68        // is implemented, mirroring vanilla `TeleportRandomlyConsumeEffect.apply`.
69        world.play_sound_at(
70            &sound_events::ITEM_CHORUS_FRUIT_TELEPORT,
71            SoundSource::Players,
72            user.position(),
73            1.0,
74            1.0,
75            None,
76        );
77        user.reset_fall_distance();
78        user.reset_current_impulse_context();
79        return;
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use steel_registry::consume_effect::TeleportRandomlyConsumeEffect;
86    use steel_registry::{init_vanilla_registry, vanilla_blocks};
87    use steel_utils::types::UpdateFlags;
88    use steel_utils::{BlockPos, ChunkPos};
89
90    use super::teleport_randomly;
91    use crate::behavior::init_behaviors;
92    use crate::entity::Entity;
93    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
94
95    /// With no solid ground anywhere in range, every landing attempt must
96    /// fail and the player must stay exactly where they started — mirroring
97    /// vanilla `Entity.randomTeleport` reverting to the original position
98    /// when no candidate lands.
99    #[test]
100    fn teleport_randomly_leaves_the_player_in_place_with_no_valid_landing() {
101        init_vanilla_registry();
102        let world = fresh_test_world("teleport_randomly_no_valid_landing");
103        for x in -1..=0 {
104            for z in -1..=0 {
105                insert_ready_full_chunk(&world, ChunkPos::new(x, z));
106            }
107        }
108        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
109        let origin = player.position();
110
111        teleport_randomly(
112            TeleportRandomlyConsumeEffect::default_value(),
113            &world,
114            player.as_ref(),
115        );
116
117        assert_eq!(player.position(), origin);
118    }
119
120    /// With solid ground everywhere in range, the player must land on top of
121    /// it, within the effect's diameter of the origin. Mirrors vanilla
122    /// `TeleportRandomlyConsumeEffect.apply` picking the first safe landing.
123    #[test]
124    fn teleport_randomly_lands_on_solid_ground_within_diameter() {
125        init_vanilla_registry();
126        init_behaviors();
127        let world = fresh_test_world("teleport_randomly_valid_landing");
128        for x in -1..=0 {
129            for z in -1..=0 {
130                insert_ready_full_chunk(&world, ChunkPos::new(x, z));
131            }
132        }
133        for x in -8..8 {
134            for z in -8..8 {
135                world.set_block(
136                    BlockPos::new(x, -1, z),
137                    vanilla_blocks::STONE.default_state(),
138                    UpdateFlags::UPDATE_ALL,
139                );
140            }
141        }
142        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
143        let origin = player.position();
144
145        teleport_randomly(
146            TeleportRandomlyConsumeEffect::default_value(),
147            &world,
148            player.as_ref(),
149        );
150
151        let landed = player.position();
152        assert_ne!(landed, origin);
153        assert!((landed.x - origin.x).abs() <= 8.0);
154        assert!((landed.z - origin.z).abs() <= 8.0);
155        // The landing loop preserves the fractional part of the candidate Y
156        // (see `find_ground_y`), so a floor at block Y = -1 always lands the
157        // player somewhere in [0, 1) above it.
158        assert!((0.0..1.0).contains(&landed.y));
159    }
160}