1use 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, SPAWN_MOBS};
25use steel_registry::{sound_events, vanilla_damage_types, vanilla_entities, vanilla_items};
26use steel_utils::ChunkPos;
27use steel_utils::locks::SyncMutex;
28use steel_utils::{BlockPos, Downcast as _, DowncastType, DowncastTypeKey};
29
30use crate::chunk::chunk_map::ENDER_PEARL_TICKET_TIMEOUT;
31use crate::entity::damage::DamageSource;
32use crate::entity::entities::EndermiteEntity;
33use crate::entity::{
34 ENTITIES, Entity, EntityBase, EntityBaseLoad, EntitySyncedData, LivingEntity, Projectile,
35 ProjectileBase, ProjectileHit, RemovalReason, SharedEntity, ThrowableItemProjectile,
36 ThrowableProjectile, change_entity_world, next_entity_id,
37};
38use crate::player::Player;
39use crate::portal::{TeleportPostTransition, TeleportTransition};
40use crate::world::World;
41
42const TELEPORT_DAMAGE: f32 = 5.0;
44
45const ENDERMITE_SPAWN_CHANCE: f32 = 0.05;
47
48#[entity_behavior(class = "ThrownEnderpearl")]
50pub struct EnderPearlEntity {
51 base: EntityBase,
53 entity_type: EntityTypeRef,
55 entity_data: SyncMutex<EnderPearlEntityData>,
57 projectile_base: ProjectileBase,
59 ticket_timer: SyncMutex<i64>,
61}
62
63unsafe impl DowncastType for EnderPearlEntity {
65 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/ender_pearl");
66}
67
68impl EnderPearlEntity {
69 #[must_use]
71 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
72 Self {
73 base: EntityBase::new(id, position, entity_type.dimensions, world),
74 entity_type,
75 entity_data: SyncMutex::new(EnderPearlEntityData::new()),
76 projectile_base: ProjectileBase::new(),
77 ticket_timer: SyncMutex::new(0),
78 }
79 }
80
81 #[must_use]
83 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
84 Self {
85 base: EntityBase::from_load(load, entity_type.dimensions),
86 entity_type,
87 entity_data: SyncMutex::new(EnderPearlEntityData::new()),
88 projectile_base: ProjectileBase::new(),
89 ticket_timer: SyncMutex::new(0),
90 }
91 }
92
93 fn owner_player(&self) -> Option<SharedEntity> {
95 let owner = self.get_owner()?;
96 owner.as_player()?;
97 Some(owner)
98 }
99
100 fn deregister_from_owner(&self) {
103 if let Some(owner) = self.owner_player()
104 && let Some(player) = owner.as_player()
105 {
106 player.deregister_ender_pearl(self.uuid());
107 }
108 }
109
110 fn update_ender_pearl_ticket(&self, world: &Arc<World>) {
117 let current_chunk = ChunkPos::from_entity_pos(self.position());
118 let crossed_border = ChunkPos::from_entity_pos(self.old_position()) != current_chunk;
119
120 let mut timer = self.ticket_timer.lock();
121 *timer -= 1;
122 if *timer > 0 && !crossed_border {
123 return;
124 }
125
126 let Some(owner) = self.owner_player() else {
127 return;
128 };
129 let Some(player) = owner.as_player() else {
130 return;
131 };
132 let Some(pearl) = world.get_entity_by_uuid(&self.uuid()) else {
133 return;
134 };
135 if pearl.generation() != self.generation() {
136 return;
137 }
138
139 player.register_ender_pearl(&pearl);
140 world.chunk_map.place_ender_pearl_ticket(current_chunk);
141 *timer = ENDER_PEARL_TICKET_TIMEOUT - 1;
143 }
144
145 fn should_vanish_on_owner_death(&self, world: &Arc<World>) -> bool {
148 let Some(owner) = self.owner_player() else {
149 return false;
150 };
151 let Some(player) = owner.as_player() else {
152 return false;
153 };
154 Self::should_vanish_for_owner_state(
157 LivingEntity::is_alive(player),
158 player.has_won_game(),
159 world.get_game_rule(&ENDER_PEARLS_VANISH_ON_DEATH),
160 )
161 }
162
163 const fn should_vanish_for_owner_state(
164 owner_alive: bool,
165 owner_won_game: bool,
166 vanish_on_death_rule: bool,
167 ) -> bool {
168 !owner_alive && !owner_won_game && vanish_on_death_rule
169 }
170
171 fn is_allowed_to_teleport_owner(world: &Arc<World>, player: &Player) -> bool {
175 let player_world = player.get_world();
176 if Arc::ptr_eq(&player_world, world) {
177 return LivingEntity::is_alive(player) && !player.is_sleeping();
178 }
179
180 player.can_use_portal(true)
181 }
182
183 fn teleport_owner(
185 &self,
186 world: &Arc<World>,
187 owner: &SharedEntity,
188 player: &Player,
189 teleport_pos: DVec3,
190 ) {
191 if world.get_game_rule(&SPAWN_MOBS) && rand::random::<f32>() < ENDERMITE_SPAWN_CHANCE {
192 let endermite = ENTITIES.create(
193 &vanilla_entities::ENDERMITE,
194 next_entity_id(),
195 teleport_pos,
196 Arc::downgrade(world),
197 );
198 if let Some(endermite) = endermite {
199 if let Some(endermite_entity) = endermite.as_ref().downcast_ref::<EndermiteEntity>()
200 {
201 endermite_entity.set_player_spawned(true);
202 }
203 let _ = world.try_add_entity(endermite);
204 }
205 }
206 if self.is_on_portal_cooldown() {
207 player.reset_portal_cooldown();
208 }
209
210 let transition = TeleportTransition {
211 target_world: Arc::clone(world),
212 position: teleport_pos,
213 rotation: (0.0, 0.0),
214 velocity: DVec3::ZERO,
215 relatives: RelativeMovement::ROTATION.union(RelativeMovement::DELTA),
216 portal_cooldown: player.portal_cooldown(),
217 as_passenger: false,
218 post_transition: TeleportPostTransition::do_nothing(),
219 };
220 let Some(new_owner) = change_entity_world(Arc::clone(owner), &transition) else {
221 log::debug!("failed to teleport ender pearl owner {}", self.id());
222 return;
223 };
224 let Some(new_player) = new_owner.as_player() else {
225 return;
226 };
227
228 new_player.reset_fall_distance();
229 new_player.reset_current_impulse_context();
230
231 let damage = DamageSource::environment(&vanilla_damage_types::ENDER_PEARL);
232 new_player.hurt(world, &damage, TELEPORT_DAMAGE);
233
234 world.play_sound_at(
235 &sound_events::ENTITY_PLAYER_TELEPORT,
236 SoundSource::Players,
237 teleport_pos,
238 1.0,
239 1.0,
240 None,
241 );
242 }
243}
244
245impl Entity for EnderPearlEntity {
246 fn base(&self) -> &EntityBase {
247 &self.base
248 }
249
250 fn entity_type(&self) -> EntityTypeRef {
251 self.entity_type
252 }
253
254 fn tick(self: Arc<Self>) {
255 let Some(world) = self.level() else {
259 self.throwable_projectile_tick();
260 return;
261 };
262
263 if self.should_vanish_on_owner_death(&world) {
264 self.deregister_from_owner();
265 self.set_removed(RemovalReason::Discarded);
266 return;
267 }
268
269 Arc::clone(&self).throwable_projectile_tick();
270
271 if self.is_alive() {
272 self.update_ender_pearl_ticket(&world);
273 }
274 }
275
276 fn get_default_gravity(&self) -> f64 {
277 self.throwable_default_gravity()
278 }
279
280 fn on_above_bubble_column(&self, drag_down: bool, pos: BlockPos) {
281 self.default_on_above_bubble_column(drag_down, pos);
282 }
283
284 fn on_inside_bubble_column(&self, drag_down: bool) {
285 self.default_on_inside_bubble_column(drag_down);
286 }
287
288 fn sound_source(&self) -> SoundSource {
289 SoundSource::Neutral
290 }
291
292 fn spawn_data(&self) -> i32 {
293 self.get_owner().map_or(0, |owner| owner.id())
294 }
295
296 fn restore_owner_reference(&self, owner: &SharedEntity) {
297 self.cache_owner_entity(owner);
298 }
299
300 fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
301 self.owner_uuid()
302 }
303
304 fn projectile_owner(&self) -> Option<SharedEntity> {
305 self.get_owner()
306 }
307
308 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
309 Some(&self.entity_data)
310 }
311
312 fn save_additional(&self, nbt: &mut NbtCompound) {
313 self.save_projectile(nbt);
314 self.save_throwable_item(nbt);
315 }
316
317 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
318 self.load_projectile(nbt);
319 self.load_throwable_item(nbt);
320 }
321}
322
323impl Projectile for EnderPearlEntity {
324 fn projectile_base(&self) -> &ProjectileBase {
325 &self.projectile_base
326 }
327
328 fn on_hit_entity(self: Arc<Self>, entity: &SharedEntity, _location: DVec3) {
329 let mut damage = DamageSource::environment(&vanilla_damage_types::THROWN)
332 .with_direct_entity(Arc::<Self>::clone(&self));
333 if let Some(owner) = self.get_owner() {
334 damage = damage.with_causing_entity(owner);
335 }
336 if let Some(world) = entity.level() {
337 entity.hurt(&world, &damage, 0.0);
338 }
339 }
340
341 fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
342 Arc::clone(&self).projectile_on_hit(hit);
344
345 let Some(world) = self.level() else {
347 return;
348 };
349 if self.is_removed() {
350 return;
351 }
352
353 let teleport_pos = self.old_position();
354 if let Some(owner) = self.owner_player()
355 && let Some(player) = owner.as_player()
356 && Self::is_allowed_to_teleport_owner(&world, player)
357 {
358 self.teleport_owner(&world, &owner, player, teleport_pos);
359 }
360 self.deregister_from_owner();
361 self.set_removed(RemovalReason::Discarded);
362 }
363}
364
365impl ThrowableProjectile for EnderPearlEntity {}
366
367impl ThrowableItemProjectile for EnderPearlEntity {
368 fn get_default_item(&self) -> ItemRef {
369 &vanilla_items::ENDER_PEARL
370 }
371
372 fn set_item(&self, item: ItemStack) {
373 self.entity_data
374 .lock()
375 .throwable_item_projectile
376 .item_stack
377 .set(item);
378 }
379
380 fn get_item(&self) -> ItemStack {
381 self.entity_data
382 .lock()
383 .throwable_item_projectile
384 .item_stack
385 .get()
386 .clone()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::sync::{Arc, Weak};
393
394 use glam::DVec3;
395 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
396 use steel_utils::{BlockPos, ChunkPos};
397
398 use crate::entity::{Entity, Projectile, SharedEntity, ThrowableItemProjectile};
399 use crate::player::Player;
400 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
401 use crate::world::World;
402
403 use super::EnderPearlEntity;
404
405 #[test]
406 fn shoot_aligns_velocity_with_direction() {
407 init_vanilla_registry();
408
409 let pearl = EnderPearlEntity::new(
410 &vanilla_entities::ENDER_PEARL,
411 1,
412 DVec3::ZERO,
413 Weak::<World>::new(),
414 );
415 pearl.shoot(DVec3::new(0.0, 0.0, 1.0), 1.5, 1.0);
416
417 let velocity = pearl.velocity();
418 assert!(velocity.z > 0.0);
419 assert!((velocity.length() - 1.5).abs() < 0.1);
420 assert!(velocity.x.abs() < 0.1 && velocity.y.abs() < 0.1);
421 }
422
423 #[test]
424 fn owner_round_trips() {
425 init_vanilla_registry();
426
427 let pearl = EnderPearlEntity::new(
428 &vanilla_entities::ENDER_PEARL,
429 1,
430 DVec3::ZERO,
431 Weak::<World>::new(),
432 );
433 assert!(pearl.owner_uuid().is_none());
434
435 let uuid = uuid::Uuid::from_u128(0x1234_5678_9abc_def0);
436 pearl.set_owner_uuid(Some(uuid));
437 assert_eq!(pearl.owner_uuid(), Some(uuid));
438 assert_eq!(pearl.projectile_owner_uuid(), Some(uuid));
439 }
440
441 #[test]
442 fn default_item_is_ender_pearl() {
443 init_vanilla_registry();
444
445 let pearl = EnderPearlEntity::new(
446 &vanilla_entities::ENDER_PEARL,
447 1,
448 DVec3::ZERO,
449 Weak::<World>::new(),
450 );
451 assert_eq!(pearl.get_default_item().key, vanilla_items::ENDER_PEARL.key);
452 }
453
454 #[test]
455 fn owner_death_vanish_predicate_respects_won_game() {
456 assert!(EnderPearlEntity::should_vanish_for_owner_state(
457 false, false, true
458 ));
459 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
460 false, true, true
461 ));
462 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
463 true, false, true
464 ));
465 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
466 false, false, false
467 ));
468 }
469
470 #[test]
471 fn ticket_renewal_registers_with_the_resolved_owner() {
472 init_vanilla_registry();
473
474 let world_fixture = fresh_test_world("ender_pearl_ticket_owner");
475 let world = &world_fixture.world;
476 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
477 let player = TestPlayerBuilder::new(Arc::clone(world), "Owner", 1).build();
478 let owner: SharedEntity = Arc::<Player>::clone(&player);
479 let pearl = Arc::new(EnderPearlEntity::new(
480 &vanilla_entities::ENDER_PEARL,
481 2,
482 DVec3::new(0.5, 64.0, 0.5),
483 Arc::downgrade(world),
484 ));
485 pearl.set_owner_entity(Some(&owner));
486 let shared_pearl: SharedEntity = Arc::<EnderPearlEntity>::clone(&pearl);
487 if let Err(error) = world.try_add_entity(shared_pearl) {
488 panic!("test pearl should be added: {error}");
489 }
490
491 assert!(player.ender_pearls().is_empty());
492 pearl.update_ender_pearl_ticket(world);
493
494 let registered = player.ender_pearls();
495 assert_eq!(registered.len(), 1);
496 assert_eq!(registered[0].generation(), pearl.generation());
497 }
498
499 #[test]
500 fn bubble_column_uses_clamped_entity_behavior() {
501 init_vanilla_registry();
502
503 let pearl = EnderPearlEntity::new(
504 &vanilla_entities::ENDER_PEARL,
505 1,
506 DVec3::ZERO,
507 Weak::<World>::new(),
508 );
509 let entity: &dyn Entity = &pearl;
510
511 for (above_column, drag_down, initial_y, expected_y) in [
512 (true, false, 2.0, 1.8),
513 (true, true, -2.0, -0.9),
514 (false, false, 2.0, 0.7),
515 (false, true, -2.0, -0.3),
516 ] {
517 entity.set_velocity(DVec3::new(0.25, initial_y, -0.25));
518 entity.set_fall_distance(5.0);
519
520 if above_column {
521 entity.on_above_bubble_column(drag_down, BlockPos::new(0, 0, 0));
522 } else {
523 entity.on_inside_bubble_column(drag_down);
524 }
525
526 let expected = DVec3::new(0.25, expected_y, -0.25);
527 let actual = entity.velocity();
528
529 assert!(
530 (actual - expected).abs().max_element() < 1.0e-12,
531 "above_column={above_column}, drag_down={drag_down}: \
532 expected {expected:?}, got {actual:?}"
533 );
534
535 let expected_fall_distance = if above_column { 5.0 } else { 0.0 };
536 assert!(
537 (entity.fall_distance() - expected_fall_distance).abs() < 1.0e-12,
538 "above_column={above_column}, drag_down={drag_down}: \
539 unexpected fall distance"
540 );
541 }
542 }
543}