steel_core/entity/consume_effect/
teleport_randomly.rs1use 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
14pub 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
26fn 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 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 #[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 #[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 assert!((0.0..1.0).contains(&landed.y));
159 }
160}