1use std::sync::{Arc, Weak};
8
9use glam::DVec3;
10use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
11use simdnbt::owned::NbtCompound;
12use steel_macros::entity_behavior;
13use steel_protocol::packets::game::SoundSource;
14use steel_registry::blocks::block_state_ext::BlockStateExt as _;
15use steel_registry::data_components::vanilla_components::FIREWORKS;
16use steel_registry::entity_type::EntityTypeRef;
17use steel_registry::item_stack::ItemStack;
18use steel_registry::vanilla_entity_data::FireworkRocketEntityData;
19use steel_registry::{sound_events, vanilla_damage_types, vanilla_game_events, vanilla_items};
20use steel_utils::entity_events::EntityStatus;
21use steel_utils::locks::SyncMutex;
22use steel_utils::nbt::NbtValueInput as _;
23use steel_utils::{DowncastType, DowncastTypeKey};
24
25use crate::behavior::BLOCK_BEHAVIORS;
26use crate::entity::damage::DamageSource;
27use crate::entity::{
28 Entity, EntityBase, EntityBaseLoad, EntityEventSource, EntitySyncedData,
29 InsideBlockEffectCollector, LivingEntity, LivingEntityRef, Projectile, ProjectileBase,
30 ProjectileHit, RemovalReason, SharedEntity,
31};
32
33use crate::physics::MoverType;
34use crate::world::{ClipBlockShape, ClipFluid, ClipHitResult, World};
35
36const INITIAL_VERTICAL_VELOCITY: f64 = 0.05;
37const INITIAL_HORIZONTAL_DEVIATION: f64 = 0.002_297;
38const HORIZONTAL_ACCELERATION: f64 = 1.15;
39const VERTICAL_ACCELERATION: f64 = 0.04;
40const ELYTRA_TARGET_SPEED: f64 = 1.5;
41const ELYTRA_POWER_ADD: f64 = 0.1;
42const ELYTRA_VELOCITY_BLEND: f64 = 0.5;
43const EXPLOSION_RADIUS: f64 = 5.0;
44const EXPLOSION_RADIUS_SQUARED: f64 = EXPLOSION_RADIUS * EXPLOSION_RADIUS;
45
46struct FireworkRocketState {
47 life: i32,
48 lifetime: i32,
49 attached_to_entity: Option<Weak<dyn Entity>>,
50}
51
52#[entity_behavior(class = "FireworkRocketEntity")]
54pub struct FireworkRocketEntity {
55 base: EntityBase,
56 entity_type: EntityTypeRef,
57 entity_data: SyncMutex<FireworkRocketEntityData>,
58 projectile_base: ProjectileBase,
59 state: SyncMutex<FireworkRocketState>,
60}
61
62unsafe impl DowncastType for FireworkRocketEntity {
64 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/firework_rocket");
65}
66
67impl FireworkRocketEntity {
68 #[must_use]
70 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
71 Self {
72 base: EntityBase::new(id, position, entity_type.dimensions, world),
73 entity_type,
74 entity_data: SyncMutex::new(FireworkRocketEntityData::new()),
75 projectile_base: ProjectileBase::new(),
76 state: SyncMutex::new(FireworkRocketState {
77 life: 0,
78 lifetime: 0,
79 attached_to_entity: None,
80 }),
81 }
82 }
83
84 #[must_use]
86 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
87 Self {
88 base: EntityBase::from_load(load, entity_type.dimensions),
89 entity_type,
90 entity_data: SyncMutex::new(FireworkRocketEntityData::new()),
91 projectile_base: ProjectileBase::new(),
92 state: SyncMutex::new(FireworkRocketState {
93 life: 0,
94 lifetime: 0,
95 attached_to_entity: None,
96 }),
97 }
98 }
99
100 #[must_use]
102 pub fn launched(
103 entity_type: EntityTypeRef,
104 id: i32,
105 position: DVec3,
106 world: Weak<World>,
107 source_item: ItemStack,
108 ) -> Self {
109 let rocket = Self::new(entity_type, id, position, world);
110 rocket.initialize_launch(source_item);
111 rocket
112 }
113
114 #[must_use]
116 pub fn attached_to_living(
117 entity_type: EntityTypeRef,
118 id: i32,
119 world: Weak<World>,
120 source_item: ItemStack,
121 attached_to: LivingEntityRef<'_>,
122 ) -> Self {
123 let attached_to = attached_to.entity();
124 let rocket = Self::launched(entity_type, id, attached_to.position(), world, source_item);
125 rocket.set_owner_entity(Some(attached_to));
126 rocket.state.lock().attached_to_entity = Some(Arc::downgrade(attached_to));
127 if let Ok(attached_id) = u32::try_from(attached_to.id()) {
128 rocket
129 .entity_data
130 .lock()
131 .attached_to_target
132 .set(Some(attached_id));
133 }
134 rocket
135 }
136
137 fn initialize_launch(&self, source_item: ItemStack) {
138 let flight_count = source_item
139 .get(FIREWORKS)
140 .map_or(1, |fireworks| 1 + fireworks.flight_duration());
141 self.entity_data.lock().id_fireworks_item.set(source_item);
142 self.set_velocity(DVec3::new(
143 triangle_random(0.0, INITIAL_HORIZONTAL_DEVIATION),
144 INITIAL_VERTICAL_VELOCITY,
145 triangle_random(0.0, INITIAL_HORIZONTAL_DEVIATION),
146 ));
147 self.state.lock().lifetime =
148 10 * flight_count + rand::random_range(0..6) + rand::random_range(0..7);
149 }
150
151 pub fn set_shot_at_angle(&self, shot_at_angle: bool) {
153 self.entity_data.lock().shot_at_angle.set(shot_at_angle);
154 }
155
156 #[must_use]
158 pub fn is_shot_at_angle(&self) -> bool {
159 *self.entity_data.lock().shot_at_angle.get()
160 }
161
162 fn is_attached_to_entity(&self) -> bool {
163 self.entity_data.lock().attached_to_target.get().is_some()
164 }
165
166 fn attached_entity(&self, world: &Arc<World>) -> Option<SharedEntity> {
167 let attached = self.state.lock().attached_to_entity.clone();
168 if let Some(attached) = attached {
169 return attached.upgrade();
171 }
172
173 let attached_id = *self.entity_data.lock().attached_to_target.get();
174 let attached_id = i32::try_from(attached_id?).ok()?;
175 let attached = world.get_entity_by_id(attached_id)?;
176 attached.as_living_entity()?;
177 self.state.lock().attached_to_entity = Some(Arc::downgrade(&attached));
178 Some(attached)
179 }
180
181 fn tick_attached(&self, world: &Arc<World>) -> Option<ProjectileHit> {
182 if let Some(attached) = self.attached_entity(world)
183 && let Some(living) = attached.as_living_entity()
184 {
185 let hand_angle = if living.is_fall_flying() {
186 let look_angle = living.look_angle();
187 let movement = living.velocity();
188 living.set_velocity(elytra_boosted_velocity(movement, look_angle));
189 living.hand_holding_item_angle(&vanilla_items::FIREWORK_ROCKET)
190 } else {
191 DVec3::ZERO
192 };
193
194 if let Err(error) = self.try_set_position(living.position() + hand_angle) {
195 log::debug!("failed to move attached firework rocket: {error}");
196 }
197 self.set_velocity(living.velocity());
198 }
199
200 self.get_hit_result_on_move_vector()
201 }
202
203 fn tick_free_flying(self: &Arc<Self>) -> Option<ProjectileHit> {
204 if !self.is_shot_at_angle() {
205 let horizontal_acceleration = if self.horizontal_collision() {
206 1.0
207 } else {
208 HORIZONTAL_ACCELERATION
209 };
210 let movement = self.velocity();
211 self.set_velocity(DVec3::new(
212 movement.x * horizontal_acceleration,
213 movement.y + VERTICAL_ACCELERATION,
214 movement.z * horizontal_acceleration,
215 ));
216 }
217
218 let movement = self.velocity();
219 let hit = self.get_hit_result_on_move_vector();
220 Arc::clone(self).move_entity(MoverType::SelfMovement, movement);
221 self.apply_effects_from_blocks();
222 self.set_velocity(movement);
223 hit
224 }
225
226 fn explosion_count(&self) -> usize {
227 self.entity_data
228 .lock()
229 .id_fireworks_item
230 .get()
231 .get(FIREWORKS)
232 .map_or(0, |fireworks| fireworks.explosions().len())
233 }
234
235 fn has_explosion(&self) -> bool {
236 self.explosion_count() != 0
237 }
238
239 fn fireworks_damage_source(self: &Arc<Self>) -> DamageSource {
240 let mut source = DamageSource::environment(&vanilla_damage_types::FIREWORKS)
241 .with_direct_entity(Arc::<Self>::clone(self));
242 if let Some(owner) = self.get_owner() {
243 source = source.with_causing_entity(owner);
244 }
245 source
246 }
247
248 fn deal_explosion_damage(self: &Arc<Self>, world: &Arc<World>) {
249 let explosion_count = self.explosion_count();
250 if explosion_count == 0 {
251 return;
252 }
253 let damage_amount = 5.0 + explosion_count as f32 * 2.0;
254 let attached = self.attached_entity(world);
255
256 if let Some(attached) = &attached {
257 attached.hurt(world, &self.fireworks_damage_source(), damage_amount);
258 }
259
260 let rocket_position = self.position();
261 let search_box = self.bounding_box().inflate(EXPLOSION_RADIUS);
262 for target in world.get_entities_in_aabb_matching(&search_box, Entity::is_living_entity) {
263 if attached
264 .as_ref()
265 .is_some_and(|attached| Arc::ptr_eq(attached, &target))
266 {
267 continue;
268 }
269 let distance_squared = rocket_position.distance_squared(target.position());
270 if distance_squared > EXPLOSION_RADIUS_SQUARED {
271 continue;
272 }
273
274 let target_height = f64::from(target.base().dimensions().height);
275 let can_see = [0.0, 0.5].into_iter().any(|height_scale| {
276 let target_position = target.position();
277 let to = DVec3::new(
278 target_position.x,
279 target_position.y + target_height * height_scale,
280 target_position.z,
281 );
282 world
283 .clip(
284 rocket_position,
285 to,
286 ClipBlockShape::Collider,
287 ClipFluid::None,
288 )
289 .is_miss()
290 });
291 if !can_see {
292 continue;
293 }
294
295 let distance = distance_squared.sqrt();
296 let distance_scale = ((EXPLOSION_RADIUS - distance) / EXPLOSION_RADIUS).sqrt();
297 target.hurt(
298 world,
299 &self.fireworks_damage_source(),
300 damage_amount * distance_scale as f32,
301 );
302 }
303 }
304
305 fn explode(self: &Arc<Self>, world: &Arc<World>) {
306 self.broadcast_entity_event(EntityStatus::FireworksExplode);
307 let owner = self.get_owner();
308 self.game_event_with_source_entity(&vanilla_game_events::EXPLODE, owner.as_deref());
309 self.deal_explosion_damage(world);
310 self.set_removed(RemovalReason::Discarded);
311 }
312
313 fn run_hit_block_entity_inside(&self, world: &Arc<World>, hit: &ClipHitResult) {
314 let state = world.get_block_state(hit.block_pos);
315 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
316 let mut ignored_effects = InsideBlockEffectCollector::new();
317 behavior.entity_inside(
318 state,
319 world,
320 hit.block_pos,
321 self.as_entity_event_source(),
322 &mut ignored_effects,
323 true,
324 );
325 }
326
327 #[cfg(test)]
328 fn life_and_lifetime(&self) -> (i32, i32) {
329 let state = self.state.lock();
330 (state.life, state.lifetime)
331 }
332}
333
334impl Entity for FireworkRocketEntity {
335 fn base(&self) -> &EntityBase {
336 &self.base
337 }
338
339 fn entity_type(&self) -> EntityTypeRef {
340 self.entity_type
341 }
342
343 fn tick(self: Arc<Self>) {
344 self.projectile_base_tick();
345 let Some(world) = self.level() else {
346 return;
347 };
348
349 let hit = if self.is_attached_to_entity() {
350 self.tick_attached(&world)
351 } else {
352 self.tick_free_flying()
353 };
354 if !self.no_physics()
355 && self.is_alive()
356 && let Some(hit) = &hit
357 {
358 Arc::clone(&self).hit_target_or_deflect_self(hit);
359 self.mark_velocity_sync();
360 }
361
362 self.update_rotation();
363 let (play_launch_sound, expired) = {
364 let mut state = self.state.lock();
365 let play_launch_sound = state.life == 0;
366 state.life = state.life.wrapping_add(1);
367 (play_launch_sound, state.life > state.lifetime)
368 };
369 if play_launch_sound && !self.is_silent() {
370 world.play_sound_at(
371 &sound_events::ENTITY_FIREWORK_ROCKET_LAUNCH,
372 SoundSource::Ambient,
373 self.position(),
374 3.0,
375 1.0,
376 None,
377 );
378 }
379 if expired {
380 self.explode(&world);
381 }
382 }
383
384 fn spawn_data(&self) -> i32 {
385 self.get_owner().map_or(0, |owner| owner.id())
386 }
387
388 fn restore_owner_reference(&self, owner: &SharedEntity) {
389 self.cache_owner_entity(owner);
390 }
391
392 fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
393 self.owner_uuid()
394 }
395
396 fn projectile_owner(&self) -> Option<SharedEntity> {
397 self.get_owner()
398 }
399
400 fn attackable(&self) -> bool {
401 false
402 }
403
404 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
405 Some(&self.entity_data)
406 }
407
408 fn save_additional(&self, nbt: &mut NbtCompound) {
409 self.save_projectile(nbt);
410 let state = self.state.lock();
411 nbt.insert("Life", state.life);
412 nbt.insert("LifeTime", state.lifetime);
413 drop(state);
414 nbt.insert("FireworksItem", self.get_item().to_nbt_tag_ref());
415 nbt.insert("ShotAtAngle", i8::from(self.is_shot_at_angle()));
416 }
417
418 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
419 self.load_projectile(nbt);
420 {
421 let mut state = self.state.lock();
422 state.life = nbt.int_value("Life").unwrap_or(0);
423 state.lifetime = nbt.int_value("LifeTime").unwrap_or(0);
424 }
425 let item = nbt
426 .compound("FireworksItem")
427 .and_then(|item| ItemStack::from_borrowed_compound(&item))
428 .unwrap_or_else(|| ItemStack::new(&vanilla_items::FIREWORK_ROCKET));
429 self.set_item(item);
430 self.set_shot_at_angle(
431 nbt.byte_value("ShotAtAngle")
432 .is_some_and(|value| value != 0),
433 );
434 }
435}
436
437impl Projectile for FireworkRocketEntity {
438 fn projectile_base(&self) -> &ProjectileBase {
439 &self.projectile_base
440 }
441
442 fn calculate_horizontal_hurt_knockback_direction(
443 &self,
444 hurt_entity: &dyn LivingEntity,
445 _damage_source: &DamageSource,
446 ) -> (f64, f64) {
447 let delta = hurt_entity.position() - self.position();
448 (delta.x, delta.z)
449 }
450
451 fn on_hit_entity(self: Arc<Self>, _entity: &SharedEntity, _location: DVec3) {
452 if let Some(world) = self.level() {
453 self.explode(&world);
454 }
455 }
456
457 fn on_hit_block(self: Arc<Self>, hit: &ClipHitResult) {
458 if let Some(world) = self.level() {
459 self.run_hit_block_entity_inside(&world, hit);
460 if self.has_explosion() {
461 self.explode(&world);
462 }
463 }
464 self.projectile_on_hit_block(hit);
465 }
466}
467
468impl FireworkRocketEntity {
469 #[must_use]
471 pub fn get_item(&self) -> ItemStack {
472 self.entity_data.lock().id_fireworks_item.get().clone()
473 }
474
475 pub fn set_item(&self, item: ItemStack) {
477 self.entity_data.lock().id_fireworks_item.set(item);
478 }
479}
480
481fn triangle_random(mode: f64, deviation: f64) -> f64 {
482 mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
483}
484
485fn elytra_boosted_velocity(movement: DVec3, look_angle: DVec3) -> DVec3 {
486 movement
487 + look_angle * ELYTRA_POWER_ADD
488 + (look_angle * ELYTRA_TARGET_SPEED - movement) * ELYTRA_VELOCITY_BLEND
489}
490
491#[cfg(test)]
492mod tests {
493 use std::io::Cursor;
494
495 use simdnbt::borrow::read_compound as read_borrowed_compound;
496 use simdnbt::owned::NbtCompound;
497 use steel_registry::data_components::components::{FireworkExplosion, Fireworks};
498 use steel_registry::data_components::vanilla_components::FIREWORKS;
499 use steel_registry::item_stack::ItemStack;
500 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
501 use steel_utils::ChunkPos;
502
503 use crate::{
504 entity::{Entity, Projectile, entities::PigEntity},
505 test_support::{fresh_test_world, insert_ready_full_chunk, test_world},
506 };
507
508 use super::*;
509
510 #[test]
511 fn launched_rocket_uses_fireworks_flight_duration_for_lifetime() {
512 init_vanilla_registry();
513 let mut item = ItemStack::new(&vanilla_items::FIREWORK_ROCKET);
514 item.set(
515 FIREWORKS,
516 Fireworks::new(3, Vec::new()).unwrap_or_else(|error| {
517 panic!("valid firework component should construct: {error}")
518 }),
519 );
520 let rocket = FireworkRocketEntity::launched(
521 &vanilla_entities::FIREWORK_ROCKET,
522 1,
523 DVec3::ZERO,
524 Weak::new(),
525 item,
526 );
527
528 let (_, lifetime) = rocket.life_and_lifetime();
529 assert!((40..=51).contains(&lifetime));
530 assert_eq!(
531 rocket.velocity().y.to_bits(),
532 INITIAL_VERTICAL_VELOCITY.to_bits()
533 );
534 }
535
536 #[test]
537 fn firework_uses_vanilla_neutral_sound_source() {
538 init_vanilla_registry();
539 let rocket = FireworkRocketEntity::new(
540 &vanilla_entities::FIREWORK_ROCKET,
541 1,
542 DVec3::ZERO,
543 Weak::new(),
544 );
545
546 assert_eq!(rocket.sound_source(), SoundSource::Neutral);
547 }
548
549 #[test]
550 fn hurt_marks_rocket_unless_base_invulnerable_and_always_returns_false() {
551 init_vanilla_registry();
552 let rocket = FireworkRocketEntity::new(
553 &vanilla_entities::FIREWORK_ROCKET,
554 1,
555 DVec3::ZERO,
556 Weak::new(),
557 );
558 let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
559
560 assert!(!Entity::hurt(&rocket, test_world(), &source, 1.0));
561 assert!(rocket.hurt_marked());
562
563 rocket.clear_hurt_mark();
564 rocket.set_invulnerable(true);
565 assert!(!Entity::hurt(&rocket, test_world(), &source, 1.0));
566 assert!(!rocket.hurt_marked());
567 }
568
569 #[test]
570 fn firework_metadata_carries_item_attachment_and_angle() {
571 init_vanilla_registry();
572 let target: SharedEntity = Arc::new(PigEntity::new(
573 &vanilla_entities::PIG,
574 19,
575 DVec3::new(1.0, 2.0, 3.0),
576 Weak::new(),
577 ));
578 let Some(living_target) = LivingEntityRef::new(&target) else {
579 panic!("pig test entity should be living");
580 };
581 let rocket = FireworkRocketEntity::attached_to_living(
582 &vanilla_entities::FIREWORK_ROCKET,
583 2,
584 Weak::new(),
585 ItemStack::new(&vanilla_items::FIREWORK_ROCKET),
586 living_target,
587 );
588 rocket.set_shot_at_angle(true);
589
590 let data = rocket.entity_data.lock();
591 assert_eq!(*data.attached_to_target.get(), Some(19));
592 assert!(*data.shot_at_angle.get());
593 assert!(
594 data.id_fireworks_item
595 .get()
596 .is(&vanilla_items::FIREWORK_ROCKET)
597 );
598 assert_eq!(rocket.owner_uuid(), Some(target.uuid()));
599 }
600
601 #[test]
602 fn attached_rocket_keeps_its_target_when_world_ids_are_reused() {
603 let world_fixture = fresh_test_world("rocket_attachment_identity");
604 let world = &world_fixture.world;
605 let owner_world_fixture = fresh_test_world("rocket_owner_world");
606 let owner_world = &owner_world_fixture.world;
607 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
608 let target: SharedEntity = Arc::new(PigEntity::new(
609 &vanilla_entities::PIG,
610 19,
611 DVec3::ZERO,
612 Arc::downgrade(owner_world),
613 ));
614 let rocket = FireworkRocketEntity::attached_to_living(
615 &vanilla_entities::FIREWORK_ROCKET,
616 20,
617 Arc::downgrade(world),
618 ItemStack::new(&vanilla_items::FIREWORK_ROCKET),
619 LivingEntityRef::new(&target).expect("living target"),
620 );
621 assert!(Arc::ptr_eq(
622 &rocket.get_owner().expect("owner in other world"),
623 &target
624 ));
625
626 target.set_removed(RemovalReason::Discarded);
627 let original_generation = target.generation();
628 let replacement: SharedEntity = Arc::new(PigEntity::new(
629 &vanilla_entities::PIG,
630 target.id(),
631 DVec3::ZERO,
632 Arc::downgrade(world),
633 ));
634 world
635 .try_add_entity(replacement)
636 .expect("replacement registration");
637 assert!(Arc::ptr_eq(
638 &rocket.attached_entity(world).expect("original attachment"),
639 &target
640 ));
641 drop(target);
642 assert!(
643 rocket
644 .attached_entity(world)
645 .is_none_or(|entity| entity.generation() == original_generation)
646 );
647 }
648
649 #[test]
650 fn explosion_damages_replacement_with_attached_entity_id() {
651 let world_fixture = fresh_test_world("firework_explosion_identity");
652 let world = &world_fixture.world;
653 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
654 let position = DVec3::new(8.0, 64.0, 8.0);
655 let original: SharedEntity = Arc::new(PigEntity::new(
656 &vanilla_entities::PIG,
657 1,
658 position,
659 Arc::downgrade(world),
660 ));
661 world
662 .try_add_entity(Arc::clone(&original))
663 .expect("register original attachment");
664 let mut item = ItemStack::new(&vanilla_items::FIREWORK_ROCKET);
665 item.set(
666 FIREWORKS,
667 Fireworks::new(1, vec![FireworkExplosion::default()]).expect("fireworks"),
668 );
669 let rocket = Arc::new(FireworkRocketEntity::attached_to_living(
670 &vanilla_entities::FIREWORK_ROCKET,
671 2,
672 Arc::downgrade(world),
673 item,
674 LivingEntityRef::new(&original).expect("living attachment"),
675 ));
676
677 original.set_removed(RemovalReason::Discarded);
678 let replacement = Arc::new(PigEntity::new(
679 &vanilla_entities::PIG,
680 original.id(),
681 position + DVec3::X,
682 Arc::downgrade(world),
683 ));
684 world
685 .try_add_entity(Arc::<PigEntity>::clone(&replacement))
686 .expect("register replacement");
687
688 let replacement_health = replacement.get_health();
689 rocket.deal_explosion_damage(world);
690
691 assert!(
692 replacement.get_health() < replacement_health,
693 "a different allocation with the attachment ID must receive explosion damage"
694 );
695 }
696
697 #[test]
698 fn firework_state_persists_with_vanilla_keys() {
699 init_vanilla_registry();
700 let rocket = FireworkRocketEntity::launched(
701 &vanilla_entities::FIREWORK_ROCKET,
702 1,
703 DVec3::ZERO,
704 Weak::new(),
705 ItemStack::new(&vanilla_items::FIREWORK_ROCKET),
706 );
707 {
708 let mut state = rocket.state.lock();
709 state.life = 7;
710 state.lifetime = 29;
711 }
712 rocket.set_shot_at_angle(true);
713 rocket.set_owner_uuid(Some(uuid::Uuid::from_u128(42)));
714
715 let mut nbt = NbtCompound::new();
716 rocket.save_additional(&mut nbt);
717 assert_eq!(nbt.int("Life"), Some(7));
718 assert_eq!(nbt.int("LifeTime"), Some(29));
719 assert_eq!(nbt.byte("ShotAtAngle"), Some(1));
720
721 let mut bytes = Vec::new();
722 nbt.write(&mut bytes);
723 let borrowed = read_borrowed_compound(&mut Cursor::new(&bytes))
724 .unwrap_or_else(|error| panic!("test NBT should reborrow: {error}"));
725 let loaded = FireworkRocketEntity::new(
726 &vanilla_entities::FIREWORK_ROCKET,
727 2,
728 DVec3::ZERO,
729 Weak::new(),
730 );
731 loaded.load_additional((&borrowed).into());
732
733 assert_eq!(loaded.life_and_lifetime(), (7, 29));
734 assert!(loaded.is_shot_at_angle());
735 assert_eq!(loaded.owner_uuid(), Some(uuid::Uuid::from_u128(42)));
736 assert!(loaded.get_item().is(&vanilla_items::FIREWORK_ROCKET));
737 }
738
739 #[test]
740 fn firework_knockback_direction_points_from_rocket_to_target() {
741 init_vanilla_registry();
742 let rocket = FireworkRocketEntity::new(
743 &vanilla_entities::FIREWORK_ROCKET,
744 1,
745 DVec3::new(2.0, 0.0, 3.0),
746 Weak::new(),
747 );
748 let target = PigEntity::new(
749 &vanilla_entities::PIG,
750 2,
751 DVec3::new(5.0, 0.0, 1.0),
752 Weak::new(),
753 );
754 let source = DamageSource::environment(&vanilla_damage_types::FIREWORKS);
755
756 assert_eq!(
757 rocket.calculate_horizontal_hurt_knockback_direction(&target, &source),
758 (3.0, -2.0)
759 );
760 assert!(rocket.as_projectile().is_some());
761 }
762
763 #[test]
764 fn firework_damage_source_has_no_raw_position() {
765 init_vanilla_registry();
766 let rocket = Arc::new(FireworkRocketEntity::new(
767 &vanilla_entities::FIREWORK_ROCKET,
768 23,
769 DVec3::new(1.0, 2.0, 3.0),
770 Weak::new(),
771 ));
772
773 let source = rocket.fireworks_damage_source();
774
775 assert_eq!(source.direct_entity().map(|entity| entity.id()), Some(23));
776 assert!(source.source_position_raw().is_none());
777 }
778
779 #[test]
780 fn elytra_boost_matches_vanilla_vector_formula() {
781 let movement = DVec3::new(0.2, -0.1, 0.4);
782 let look_angle = DVec3::new(0.0, 0.0, 1.0);
783
784 assert_eq!(
785 elytra_boosted_velocity(movement, look_angle),
786 DVec3::new(0.1, -0.05, 1.05)
787 );
788 }
789}