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 {
173 let player_world = player.get_world();
174 if Arc::ptr_eq(&player_world, world) {
175 return LivingEntity::is_alive(player) && !player.is_sleeping();
176 }
177
178 player.can_use_portal(true)
179 }
180
181 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) {
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 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 attackable(&self) -> bool {
309 false
310 }
311
312 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
313 Some(&self.entity_data)
314 }
315
316 fn save_additional(&self, nbt: &mut NbtCompound) {
317 self.save_projectile(nbt);
318 self.save_throwable_item(nbt);
319 }
320
321 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
322 self.load_projectile(nbt);
323 self.load_throwable_item(nbt);
324 }
325}
326
327impl Projectile for EnderPearlEntity {
328 fn projectile_base(&self) -> &ProjectileBase {
329 &self.projectile_base
330 }
331
332 fn on_hit_entity(&self, entity: &SharedEntity, _location: DVec3) {
333 let mut damage =
336 DamageSource::environment(&vanilla_damage_types::THROWN).with_direct_entity(self.id());
337 if let Some(owner) = self.get_owner() {
338 damage = damage.with_causing_entity(owner.id());
339 }
340 if let Some(world) = entity.level() {
341 entity.hurt(&world, &damage, 0.0);
342 }
343 }
344
345 fn on_hit(&self, hit: &ProjectileHit) {
346 self.projectile_on_hit(hit);
348
349 let Some(world) = self.level() else {
351 return;
352 };
353 if self.is_removed() {
354 return;
355 }
356
357 let teleport_pos = self.old_position();
358 if let Some(owner) = self.owner_player()
359 && let Some(player) = owner.as_player()
360 && Self::is_allowed_to_teleport_owner(&world, player)
361 {
362 self.teleport_owner(&world, &owner, player, teleport_pos);
363 }
364 self.deregister_from_owner();
365 self.set_removed(RemovalReason::Discarded);
366 }
367}
368
369impl ThrowableProjectile for EnderPearlEntity {}
370
371impl ThrowableItemProjectile for EnderPearlEntity {
372 fn get_default_item(&self) -> ItemRef {
373 &vanilla_items::ENDER_PEARL
374 }
375
376 fn set_item(&self, item: ItemStack) {
377 self.entity_data
378 .lock()
379 .throwable_item_projectile
380 .item_stack
381 .set(item);
382 }
383
384 fn get_item(&self) -> ItemStack {
385 self.entity_data
386 .lock()
387 .throwable_item_projectile
388 .item_stack
389 .get()
390 .clone()
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use std::sync::{Arc, Weak};
397
398 use glam::DVec3;
399 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
400 use steel_utils::{BlockPos, ChunkPos};
401
402 use crate::entity::{Entity, Projectile, SharedEntity, ThrowableItemProjectile};
403 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
404 use crate::world::World;
405
406 use super::EnderPearlEntity;
407
408 #[test]
409 fn shoot_aligns_velocity_with_direction() {
410 init_vanilla_registry();
411
412 let pearl = EnderPearlEntity::new(
413 &vanilla_entities::ENDER_PEARL,
414 1,
415 DVec3::ZERO,
416 Weak::<World>::new(),
417 );
418 pearl.shoot(DVec3::new(0.0, 0.0, 1.0), 1.5, 1.0);
419
420 let velocity = pearl.velocity();
421 assert!(velocity.z > 0.0);
422 assert!((velocity.length() - 1.5).abs() < 0.1);
423 assert!(velocity.x.abs() < 0.1 && velocity.y.abs() < 0.1);
424 }
425
426 #[test]
427 fn owner_round_trips() {
428 init_vanilla_registry();
429
430 let pearl = EnderPearlEntity::new(
431 &vanilla_entities::ENDER_PEARL,
432 1,
433 DVec3::ZERO,
434 Weak::<World>::new(),
435 );
436 assert!(pearl.owner_uuid().is_none());
437
438 let uuid = uuid::Uuid::from_u128(0x1234_5678_9abc_def0);
439 pearl.set_owner_uuid(Some(uuid));
440 assert_eq!(pearl.owner_uuid(), Some(uuid));
441 assert_eq!(pearl.projectile_owner_uuid(), Some(uuid));
442 }
443
444 #[test]
445 fn default_item_is_ender_pearl() {
446 init_vanilla_registry();
447
448 let pearl = EnderPearlEntity::new(
449 &vanilla_entities::ENDER_PEARL,
450 1,
451 DVec3::ZERO,
452 Weak::<World>::new(),
453 );
454 assert_eq!(pearl.get_default_item().key, vanilla_items::ENDER_PEARL.key);
455 }
456
457 #[test]
458 fn owner_death_vanish_predicate_respects_won_game() {
459 assert!(EnderPearlEntity::should_vanish_for_owner_state(
460 false, false, true
461 ));
462 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
463 false, true, true
464 ));
465 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
466 true, false, true
467 ));
468 assert!(!EnderPearlEntity::should_vanish_for_owner_state(
469 false, false, false
470 ));
471 }
472
473 #[test]
474 fn ticket_renewal_registers_with_the_resolved_owner() {
475 init_vanilla_registry();
476
477 let world = fresh_test_world("ender_pearl_ticket_owner");
478 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
479 let player = TestPlayerBuilder::new(Arc::clone(&world), "Owner", 1).build();
480 let owner: SharedEntity = player.clone();
481 let pearl = Arc::new(EnderPearlEntity::new(
482 &vanilla_entities::ENDER_PEARL,
483 2,
484 DVec3::new(0.5, 64.0, 0.5),
485 Arc::downgrade(&world),
486 ));
487 pearl.set_owner_entity(Some(&owner));
488 let shared_pearl: SharedEntity = pearl.clone();
489 if let Err(error) = world.try_add_entity(shared_pearl) {
490 panic!("test pearl should be added: {error}");
491 }
492
493 assert!(player.ender_pearls().is_empty());
494 pearl.update_ender_pearl_ticket(&world);
495
496 let registered = player.ender_pearls();
497 assert_eq!(registered.len(), 1);
498 assert_eq!(registered[0].generation(), pearl.generation());
499 }
500
501 #[test]
502 fn bubble_column_uses_clamped_entity_behavior() {
503 init_vanilla_registry();
504
505 let pearl = EnderPearlEntity::new(
506 &vanilla_entities::ENDER_PEARL,
507 1,
508 DVec3::ZERO,
509 Weak::<World>::new(),
510 );
511 let entity: &dyn Entity = &pearl;
512
513 for (above_column, drag_down, initial_y, expected_y) in [
514 (true, false, 2.0, 1.8),
515 (true, true, -2.0, -0.9),
516 (false, false, 2.0, 0.7),
517 (false, true, -2.0, -0.3),
518 ] {
519 entity.set_velocity(DVec3::new(0.25, initial_y, -0.25));
520 entity.set_fall_distance(5.0);
521
522 if above_column {
523 entity.on_above_bubble_column(drag_down, BlockPos::new(0, 0, 0));
524 } else {
525 entity.on_inside_bubble_column(drag_down);
526 }
527
528 let expected = DVec3::new(0.25, expected_y, -0.25);
529 let actual = entity.velocity();
530
531 assert!(
532 (actual - expected).abs().max_element() < 1.0e-12,
533 "above_column={above_column}, drag_down={drag_down}: \
534 expected {expected:?}, got {actual:?}"
535 );
536
537 let expected_fall_distance = if above_column { 5.0 } else { 0.0 };
538 assert!(
539 (entity.fall_distance() - expected_fall_distance).abs() < 1.0e-12,
540 "above_column={above_column}, drag_down={drag_down}: \
541 unexpected fall distance"
542 );
543 }
544 }
545}