steel_core/entity/entities/
ender_pearl.rs1use std::sync::{Arc, Weak};
14
15use glam::DVec3;
16use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
17use simdnbt::owned::NbtCompound;
18use steel_macros::entity_behavior;
19use steel_protocol::packets::game::{RelativeMovement, SoundSource};
20use steel_registry::entity_type::EntityTypeRef;
21use steel_registry::item_stack::ItemStack;
22use steel_registry::items::ItemRef;
23use steel_registry::vanilla_entity_data::EnderPearlEntityData;
24use steel_registry::vanilla_game_rules::ENDER_PEARLS_VANISH_ON_DEATH;
25use steel_registry::{sound_events, vanilla_damage_types, vanilla_items};
26use steel_utils::ChunkPos;
27use steel_utils::locks::SyncMutex;
28use steel_utils::{DowncastType, DowncastTypeKey};
29
30use crate::chunk::chunk_map::ENDER_PEARL_TICKET_TIMEOUT;
31use crate::entity::damage::DamageSource;
32use crate::entity::{
33 Entity, EntityBase, EntityBaseLoad, EntitySyncedData, LivingEntity, Projectile, ProjectileBase,
34 ProjectileHit, RemovalReason, SharedEntity, ThrowableItemProjectile, ThrowableProjectile,
35 change_entity_world,
36};
37use crate::player::Player;
38use crate::portal::{TeleportPostTransition, TeleportTransition};
39use crate::world::World;
40
41const TELEPORT_DAMAGE: f32 = 5.0;
43
44#[entity_behavior(class = "ThrownEnderpearl")]
46pub struct EnderPearlEntity {
47 base: EntityBase,
49 entity_type: EntityTypeRef,
51 entity_data: SyncMutex<EnderPearlEntityData>,
53 projectile_base: ProjectileBase,
55 ticket_timer: SyncMutex<i32>,
57}
58
59unsafe impl DowncastType for EnderPearlEntity {
61 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/ender_pearl");
62}
63
64impl EnderPearlEntity {
65 #[must_use]
67 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
68 Self {
69 base: EntityBase::new(id, position, entity_type.dimensions, world),
70 entity_type,
71 entity_data: SyncMutex::new(EnderPearlEntityData::new()),
72 projectile_base: ProjectileBase::new(),
73 ticket_timer: SyncMutex::new(0),
74 }
75 }
76
77 #[must_use]
79 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
80 Self {
81 base: EntityBase::from_load(load, entity_type.dimensions),
82 entity_type,
83 entity_data: SyncMutex::new(EnderPearlEntityData::new()),
84 projectile_base: ProjectileBase::new(),
85 ticket_timer: SyncMutex::new(0),
86 }
87 }
88
89 fn owner_player(&self) -> Option<SharedEntity> {
91 let owner = self.get_owner()?;
92 owner.as_player()?;
93 Some(owner)
94 }
95
96 fn deregister_from_owner(&self) {
99 if let Some(owner) = self.owner_player()
100 && let Some(player) = owner.as_player()
101 {
102 player.deregister_ender_pearl(self.uuid());
103 }
104 }
105
106 fn update_ender_pearl_ticket(&self, world: &Arc<World>) {
113 let current_chunk = ChunkPos::from_entity_pos(self.position());
114 let crossed_border = ChunkPos::from_entity_pos(self.old_position()) != current_chunk;
115
116 let mut timer = self.ticket_timer.lock();
117 *timer -= 1;
118 if (*timer > 0 && !crossed_border) || self.owner_player().is_none() {
119 return;
120 }
121
122 world.chunk_map.place_ender_pearl_ticket(current_chunk);
123 *timer = ENDER_PEARL_TICKET_TIMEOUT as i32 - 1;
125 }
126
127 fn should_vanish_on_owner_death(&self, world: &Arc<World>) -> bool {
130 let Some(owner) = self.owner_player() else {
131 return false;
132 };
133 let Some(player) = owner.as_player() else {
134 return false;
135 };
136 Self::should_vanish_for_owner_state(
139 LivingEntity::is_alive(player),
140 player.has_won_game(),
141 world.get_game_rule(&ENDER_PEARLS_VANISH_ON_DEATH),
142 )
143 }
144
145 const fn should_vanish_for_owner_state(
146 owner_alive: bool,
147 owner_won_game: bool,
148 vanish_on_death_rule: bool,
149 ) -> bool {
150 !owner_alive && !owner_won_game && vanish_on_death_rule
151 }
152
153 fn is_allowed_to_teleport_owner(world: &Arc<World>, player: &Player) -> bool {
155 let player_world = player.get_world();
156 if Arc::ptr_eq(&player_world, world) {
157 return LivingEntity::is_alive(player) && !player.is_sleeping();
158 }
159
160 player.can_use_portal(true)
161 }
162
163 fn teleport_owner(
167 &self,
168 world: &Arc<World>,
169 owner: &SharedEntity,
170 player: &Player,
171 teleport_pos: DVec3,
172 ) {
173 if self.is_on_portal_cooldown() {
175 player.reset_portal_cooldown();
176 }
177
178 let transition = TeleportTransition {
179 target_world: Arc::clone(world),
180 position: teleport_pos,
181 rotation: (0.0, 0.0),
182 velocity: DVec3::ZERO,
183 relatives: RelativeMovement::ROTATION.union(RelativeMovement::DELTA),
184 portal_cooldown: player.portal_cooldown(),
185 as_passenger: false,
186 post_transition: TeleportPostTransition::do_nothing(),
187 };
188 let Some(new_owner) = change_entity_world(Arc::clone(owner), &transition) else {
189 log::debug!("failed to teleport ender pearl owner {}", self.id());
190 return;
191 };
192 let Some(new_player) = new_owner.as_player() else {
193 return;
194 };
195
196 new_player.reset_fall_distance();
197 new_player.reset_current_impulse_context();
198
199 let damage = DamageSource::environment(&vanilla_damage_types::ENDER_PEARL);
200 new_player.hurt(world, &damage, TELEPORT_DAMAGE);
201
202 world.play_sound_at(
203 &sound_events::ENTITY_PLAYER_TELEPORT,
204 SoundSource::Players,
205 teleport_pos,
206 1.0,
207 1.0,
208 None,
209 );
210 }
211}
212
213impl Entity for EnderPearlEntity {
214 fn base(&self) -> &EntityBase {
215 &self.base
216 }
217
218 fn entity_type(&self) -> EntityTypeRef {
219 self.entity_type
220 }
221
222 fn tick(&self) {
223 let Some(world) = self.level() else {
227 self.throwable_projectile_tick();
228 return;
229 };
230
231 if self.should_vanish_on_owner_death(&world) {
232 self.deregister_from_owner();
233 self.set_removed(RemovalReason::Discarded);
234 return;
235 }
236
237 self.throwable_projectile_tick();
238
239 if self.is_alive() {
240 self.update_ender_pearl_ticket(&world);
241 }
242 }
243
244 fn get_default_gravity(&self) -> f64 {
245 self.throwable_default_gravity()
246 }
247
248 fn sound_source(&self) -> SoundSource {
249 SoundSource::Neutral
250 }
251
252 fn spawn_data(&self) -> i32 {
253 self.get_owner().map_or(0, |owner| owner.id())
254 }
255
256 fn restore_owner_reference(&self, owner: &SharedEntity) {
257 self.cache_owner_entity(owner);
258 }
259
260 fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
261 self.owner_uuid()
262 }
263
264 fn projectile_owner(&self) -> Option<SharedEntity> {
265 self.get_owner()
266 }
267
268 fn attackable(&self) -> bool {
269 false
270 }
271
272 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
273 Some(&self.entity_data)
274 }
275
276 fn hurt(&self, _world: &World, _source: &DamageSource, _amount: f32) -> bool {
277 false
279 }
280
281 fn save_additional(&self, nbt: &mut NbtCompound) {
282 self.save_projectile(nbt);
283 self.save_throwable_item(nbt);
284 }
285
286 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
287 self.load_projectile(nbt);
288 self.load_throwable_item(nbt);
289 }
290}
291
292impl Projectile for EnderPearlEntity {
293 fn projectile_base(&self) -> &ProjectileBase {
294 &self.projectile_base
295 }
296
297 fn on_hit_entity(&self, entity: &SharedEntity, _location: DVec3) {
298 let mut damage =
301 DamageSource::environment(&vanilla_damage_types::THROWN).with_direct_entity(self.id());
302 if let Some(owner) = self.get_owner() {
303 damage = damage.with_causing_entity(owner.id());
304 }
305 if let Some(world) = entity.level() {
306 entity.hurt(&world, &damage, 0.0);
307 }
308 }
309
310 fn on_hit(&self, hit: &ProjectileHit) {
311 self.projectile_on_hit(hit);
313
314 let Some(world) = self.level() else {
316 return;
317 };
318 if self.is_removed() {
319 return;
320 }
321
322 let teleport_pos = self.old_position();
323 if let Some(owner) = self.owner_player()
324 && let Some(player) = owner.as_player()
325 && Self::is_allowed_to_teleport_owner(&world, player)
326 {
327 self.teleport_owner(&world, &owner, player, teleport_pos);
328 }
329 self.deregister_from_owner();
330 self.set_removed(RemovalReason::Discarded);
331 }
332}
333
334impl ThrowableProjectile for EnderPearlEntity {}
335
336impl ThrowableItemProjectile for EnderPearlEntity {
337 fn get_default_item(&self) -> ItemRef {
338 &vanilla_items::ENDER_PEARL
339 }
340
341 fn set_item(&self, item: ItemStack) {
342 self.entity_data
343 .lock()
344 .throwable_item_projectile
345 .item_stack
346 .set(item);
347 }
348
349 fn get_item(&self) -> ItemStack {
350 self.entity_data
351 .lock()
352 .throwable_item_projectile
353 .item_stack
354 .get()
355 .clone()
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use std::sync::Weak;
362
363 use glam::DVec3;
364 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
365
366 use crate::entity::{Entity, Projectile, ThrowableItemProjectile};
367 use crate::world::World;
368
369 use super::EnderPearlEntity;
370
371 #[test]
372 fn shoot_aligns_velocity_with_direction() {
373 init_vanilla_registry();
374
375 let pearl = EnderPearlEntity::new(
376 &vanilla_entities::ENDER_PEARL,
377 1,
378 DVec3::ZERO,
379 Weak::<World>::new(),
380 );
381 pearl.shoot(DVec3::new(0.0, 0.0, 1.0), 1.5, 1.0);
382
383 let velocity = pearl.velocity();
384 assert!(velocity.z > 0.0);
385 assert!((velocity.length() - 1.5).abs() < 0.1);
386 assert!(velocity.x.abs() < 0.1 && velocity.y.abs() < 0.1);
387 }
388
389 #[test]
390 fn owner_round_trips() {
391 init_vanilla_registry();
392
393 let pearl = EnderPearlEntity::new(
394 &vanilla_entities::ENDER_PEARL,
395 1,
396 DVec3::ZERO,
397 Weak::<World>::new(),
398 );
399 assert!(pearl.owner_uuid().is_none());
400
401 let uuid = uuid::Uuid::from_u128(0x1234_5678_9abc_def0);
402 pearl.set_owner_uuid(Some(uuid));
403 assert_eq!(pearl.owner_uuid(), Some(uuid));
404 assert_eq!(pearl.projectile_owner_uuid(), Some(uuid));
405 }
406
407 #[test]
408 fn default_item_is_ender_pearl() {
409 init_vanilla_registry();
410
411 let pearl = EnderPearlEntity::new(
412 &vanilla_entities::ENDER_PEARL,
413 1,
414 DVec3::ZERO,
415 Weak::<World>::new(),
416 );
417 assert_eq!(pearl.get_default_item().key, vanilla_items::ENDER_PEARL.key);
418 }
419
420 #[test]
421 fn owner_death_vanish_predicate_respects_won_game() {
422 assert!(EnderPearlEntity::should_vanish_for_owner_state(
423 false, false, true
424 ));
425 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
426 false, true, true
427 ));
428 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
429 true, false, true
430 ));
431 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
432 false, false, false
433 ));
434 }
435}