1mod throwable;
13mod throwable_item;
14
15use std::mem;
16use std::sync::{Arc, Weak};
17
18use glam::DVec3;
19use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
20use simdnbt::owned::{NbtCompound, NbtTag};
21use steel_registry::blocks::block_state_ext::BlockStateExt as _;
22use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
23use steel_registry::vanilla_game_rules::{MOB_GRIEFING, PROJECTILES_CAN_BREAK_BLOCKS};
24use steel_registry::{REGISTRY, TaggedRegistryExt as _, vanilla_game_events};
25use steel_utils::axis::Axis;
26use steel_utils::locks::SyncMutex;
27use steel_utils::{UuidExt, WorldAabb};
28use uuid::Uuid;
29
30use crate::behavior::BLOCK_BEHAVIORS;
31use crate::entity::damage::DamageSource;
32use crate::entity::{Entity, LivingEntity, SharedEntity};
33use crate::world::game_event::GameEventContext;
34use crate::world::{ClipBlockShape, ClipFluid, ClipHitResult, World};
35
36pub use throwable::ThrowableProjectile;
37pub use throwable_item::ThrowableItemProjectile;
38
39const SHOOT_INACCURACY_SCALE: f64 = 0.0172_275;
41
42const MAX_ENTITY_HIT_MARGIN: f64 = 0.3;
44
45fn triangle_random(mode: f64, deviation: f64) -> f64 {
47 mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
48}
49
50pub enum ProjectileHit {
52 Block {
54 location: DVec3,
56 hit: ClipHitResult,
58 },
59 Entity(EntityHitResult),
61}
62
63impl ProjectileHit {
64 #[must_use]
66 pub const fn location(&self) -> DVec3 {
67 match self {
68 Self::Block { location, .. } => *location,
69 Self::Entity(hit) => hit.location,
70 }
71 }
72}
73
74pub struct EntityHitResult {
76 pub entity: SharedEntity,
78 pub location: DVec3,
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum ProjectileDeflection {
85 None,
87 Reverse,
89 AimDeflect,
91 MomentumDeflect,
93}
94
95impl ProjectileDeflection {
96 fn apply(self, projectile: &dyn Projectile, deflecting_entity: Option<&dyn Entity>) {
97 match self {
98 Self::None => {}
99 Self::Reverse => {
100 let rotation = rand::random_range(170.0..190.0);
101 projectile.set_velocity(projectile.velocity() * -0.5);
102 let (yaw, pitch) = projectile.rotation();
103 projectile.set_rotation((yaw + rotation, pitch));
104 let (old_yaw, old_pitch) = projectile.base().old_rotation();
105 projectile
106 .base()
107 .set_old_rotation((old_yaw + rotation, old_pitch));
108 projectile.mark_velocity_sync();
109 }
110 Self::AimDeflect => {
111 let Some(entity) = deflecting_entity else {
112 return;
113 };
114 projectile.set_velocity(entity.look_angle());
115 projectile.mark_velocity_sync();
116 }
117 Self::MomentumDeflect => {
118 let Some(entity) = deflecting_entity else {
119 return;
120 };
121 let movement = entity.velocity();
122 let length = movement.length();
123 let normalized = if length < 1.0e-5 {
124 DVec3::ZERO
125 } else {
126 movement / length
127 };
128 projectile.set_velocity(normalized);
129 projectile.mark_velocity_sync();
130 }
131 }
132 }
133}
134
135struct ProjectileState {
136 owner: Option<Uuid>,
137 owner_entity: Option<Weak<dyn Entity>>,
138 left_owner: bool,
139 left_owner_checked: bool,
140 has_been_shot: bool,
141 last_deflected_by: Option<Weak<dyn Entity>>,
142}
143
144pub struct ProjectileBase {
146 state: SyncMutex<ProjectileState>,
147}
148
149impl ProjectileBase {
150 #[must_use]
152 pub const fn new() -> Self {
153 Self {
154 state: SyncMutex::new(ProjectileState {
155 owner: None,
156 owner_entity: None,
157 left_owner: false,
158 left_owner_checked: false,
159 has_been_shot: false,
160 last_deflected_by: None,
161 }),
162 }
163 }
164}
165
166impl Default for ProjectileBase {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172pub trait ProjectileEventSource {
174 fn as_projectile_event_source(&self) -> &dyn Projectile;
176}
177
178impl<T: Projectile> ProjectileEventSource for T {
179 fn as_projectile_event_source(&self) -> &dyn Projectile {
180 self
181 }
182}
183
184pub trait Projectile: Entity + ProjectileEventSource {
186 fn projectile_base(&self) -> &ProjectileBase;
188
189 fn calculate_horizontal_hurt_knockback_direction(
191 &self,
192 _hurt_entity: &dyn LivingEntity,
193 _damage_source: &DamageSource,
194 ) -> (f64, f64) {
195 let movement = self.velocity();
196 (movement.x, movement.z)
197 }
198
199 fn set_owner_uuid(&self, owner: Option<Uuid>) {
203 let mut state = self.projectile_base().state.lock();
204 state.owner = owner;
205 state.owner_entity = None;
206 }
207
208 fn set_owner_entity(&self, owner: Option<&SharedEntity>) {
210 let mut state = self.projectile_base().state.lock();
211 state.owner = owner.map(|owner| owner.uuid());
212 state.owner_entity = owner.map(Arc::downgrade);
213 }
214
215 fn cache_owner_entity(&self, owner: &SharedEntity) {
217 let mut state = self.projectile_base().state.lock();
218 if state.owner == Some(owner.uuid()) {
219 state.owner_entity = Some(Arc::downgrade(owner));
220 }
221 }
222
223 fn owner_uuid(&self) -> Option<Uuid> {
225 self.projectile_base().state.lock().owner
226 }
227
228 fn get_owner(&self) -> Option<SharedEntity> {
230 let uuid = self.owner_uuid()?;
231 if let Some(owner) = self
232 .projectile_base()
233 .state
234 .lock()
235 .owner_entity
236 .as_ref()
237 .and_then(Weak::upgrade)
238 && !owner.is_removed()
239 && owner.uuid() == uuid
240 {
241 return Some(owner);
242 }
243
244 let owner = self.level()?.get_entity_by_uuid(&uuid)?;
245 self.cache_owner_entity(&owner);
246 Some(owner)
247 }
248
249 fn projectile_may_interact(&self, world: &World, pos: steel_utils::BlockPos) -> bool {
251 let Some(owner) = self.get_owner() else {
252 return true;
253 };
254 if owner.as_player().is_some() {
255 return owner.may_interact(world, pos);
256 }
257 world.get_game_rule(&MOB_GRIEFING)
258 }
259
260 fn may_break(&self, world: &World) -> bool {
262 REGISTRY
263 .entity_types
264 .is_in_tag(self.entity_type(), &EntityTypeTag::IMPACT_PROJECTILES)
265 && world.get_game_rule(&PROJECTILES_CAN_BREAK_BLOCKS)
266 }
267
268 fn owned_by(&self, entity: &dyn Entity) -> bool {
270 self.owner_uuid() == Some(entity.uuid())
271 }
272
273 fn has_been_shot(&self) -> bool {
275 self.projectile_base().state.lock().has_been_shot
276 }
277
278 fn set_has_been_shot(&self, value: bool) {
280 self.projectile_base().state.lock().has_been_shot = value;
281 }
282
283 fn left_owner(&self) -> bool {
285 self.projectile_base().state.lock().left_owner
286 }
287
288 fn check_left_owner(&self) {
290 let mut state = self.projectile_base().state.lock();
291 if state.left_owner || state.left_owner_checked {
292 return;
293 }
294 state.left_owner_checked = true;
295 drop(state);
296
297 let left = self.is_outside_owner_collision_range();
298 self.projectile_base().state.lock().left_owner = left;
299 }
300
301 fn reset_left_owner_checked(&self) {
303 self.projectile_base().state.lock().left_owner_checked = false;
304 }
305
306 fn is_outside_owner_collision_range(&self) -> bool {
308 let Some(owner) = self.get_owner() else {
309 return true;
310 };
311 let aabb = self
312 .bounding_box()
313 .expand_towards(self.velocity())
314 .inflate(1.0);
315 let root_vehicle = owner.root_vehicle().unwrap_or_else(|| owner.clone());
316 let mut to_check = vec![root_vehicle];
317 let mut visited = Vec::new();
318
319 while let Some(entity) = to_check.pop() {
320 let entity_id = entity.id();
321 if visited.contains(&entity_id) {
322 continue;
323 }
324 visited.push(entity_id);
325
326 if entity.is_pickable() && aabb.intersects(entity.bounding_box()) {
327 return false;
328 }
329 to_check.extend(entity.passengers());
330 }
331
332 true
333 }
334
335 fn can_hit_entity(&self, entity: &dyn Entity) -> bool {
337 if !entity.can_be_hit_by_projectile() {
338 return false;
339 }
340 let Some(owner) = self.get_owner() else {
341 return true;
342 };
343 self.left_owner() || !owner.is_passenger_of_same_vehicle(entity)
344 }
345
346 fn get_movement_to_shoot(&self, direction: DVec3, power: f32, uncertainty: f32) -> DVec3 {
348 let deviation = SHOOT_INACCURACY_SCALE * f64::from(uncertainty);
349 let jitter = DVec3::new(
350 triangle_random(0.0, deviation),
351 triangle_random(0.0, deviation),
352 triangle_random(0.0, deviation),
353 );
354 (direction.normalize_or_zero() + jitter) * f64::from(power)
355 }
356
357 fn shoot(&self, direction: DVec3, power: f32, uncertainty: f32) {
359 let movement = self.get_movement_to_shoot(direction, power, uncertainty);
360 self.set_velocity(movement);
361 self.mark_velocity_sync();
362
363 let horizontal = (movement.x * movement.x + movement.z * movement.z).sqrt();
364 let yaw = movement.x.atan2(movement.z).to_degrees() as f32;
365 let pitch = movement.y.atan2(horizontal).to_degrees() as f32;
366 self.set_rotation((yaw, pitch));
367 self.base().set_old_rotation_to_current();
368 }
369
370 fn shoot_from_rotation(
372 &self,
373 source: &dyn Entity,
374 x_rot: f32,
375 y_rot: f32,
376 y_offset: f32,
377 power: f32,
378 uncertainty: f32,
379 ) {
380 let yaw = y_rot.to_radians();
381 let pitch = x_rot.to_radians();
382 let pitch_offset = (x_rot + y_offset).to_radians();
383 let direction = DVec3::new(
384 f64::from(-yaw.sin() * pitch.cos()),
385 f64::from(-pitch_offset.sin()),
386 f64::from(yaw.cos() * pitch.cos()),
387 );
388 self.shoot(direction, power, uncertainty);
389
390 let source_movement = source.known_movement();
391 let added_y = if source.on_ground() {
392 0.0
393 } else {
394 source_movement.y
395 };
396 self.set_velocity(
397 self.velocity() + DVec3::new(source_movement.x, added_y, source_movement.z),
398 );
399 }
400
401 fn update_rotation(&self) {
403 let movement = self.velocity();
404 let horizontal = (movement.x * movement.x + movement.z * movement.z).sqrt();
405 let (yaw_old, pitch_old) = self.base().old_rotation();
406 let yaw = lerp_rotation(yaw_old, movement.x.atan2(movement.z).to_degrees() as f32);
407 let pitch = lerp_rotation(pitch_old, movement.y.atan2(horizontal).to_degrees() as f32);
408 self.set_rotation((yaw, pitch));
409 }
410
411 fn deflect(
413 &self,
414 deflection: ProjectileDeflection,
415 deflecting_entity: Option<&dyn Entity>,
416 new_owner_uuid: Option<Uuid>,
417 new_owner_entity: Option<&SharedEntity>,
418 by_attack: bool,
419 ) -> bool {
420 deflection.apply(self.as_projectile_event_source(), deflecting_entity);
421 let mut state = self.projectile_base().state.lock();
422 state.owner = new_owner_uuid;
423 state.owner_entity = new_owner_entity.map(Arc::downgrade);
424 drop(state);
425 self.on_deflection(by_attack);
426 true
427 }
428
429 fn on_deflection(&self, _by_attack: bool) {}
431
432 fn should_bounce_on_world_border(&self) -> bool {
434 false
435 }
436
437 fn get_hit_result_on_move_vector(&self) -> Option<ProjectileHit> {
440 let world = self.level()?;
441 let from = self.position();
442 let delta = self.velocity();
443 let to = from + delta;
444
445 let block_hit =
446 world.clip_including_border(from, to, ClipBlockShape::Collider, ClipFluid::None);
447 let entity_end = if block_hit.is_miss() {
448 to
449 } else {
450 block_hit.location
451 };
452
453 let search_box = self.bounding_box().expand_towards(delta).inflate(1.0);
454 let margin = compute_margin(self.tick_count());
455 let self_id = self.id();
456 let entity_hit = get_entity_hit_result(&world, from, entity_end, search_box, margin, |e| {
457 e.id() != self_id && self.can_hit_entity(e)
458 });
459
460 if let Some(hit) = entity_hit {
461 return Some(ProjectileHit::Entity(hit));
462 }
463 if !block_hit.is_miss() {
464 return Some(ProjectileHit::Block {
465 location: block_hit.location,
466 hit: block_hit,
467 });
468 }
469 None
470 }
471
472 fn hit_target_or_deflect_self(&self, hit: &ProjectileHit) -> ProjectileDeflection {
474 if let ProjectileHit::Entity(entity_hit) = hit {
475 let deflection = entity_hit
476 .entity
477 .deflection(self.as_projectile_event_source());
478 if deflection != ProjectileDeflection::None {
479 let already_deflected = self
480 .projectile_base()
481 .state
482 .lock()
483 .last_deflected_by
484 .as_ref()
485 .and_then(Weak::upgrade)
486 .is_some_and(|last| Arc::ptr_eq(&last, &entity_hit.entity));
487 let owner_uuid = self.owner_uuid();
488 let owner_entity = self.get_owner();
489 if !already_deflected
490 && self.deflect(
491 deflection,
492 Some(entity_hit.entity.as_ref()),
493 owner_uuid,
494 owner_entity.as_ref(),
495 false,
496 )
497 {
498 self.projectile_base().state.lock().last_deflected_by =
499 Some(Arc::downgrade(&entity_hit.entity));
500 }
501 return deflection;
502 }
503 } else if let ProjectileHit::Block { hit, .. } = hit
504 && self.should_bounce_on_world_border()
505 && hit.world_border_hit
506 {
507 let deflection = ProjectileDeflection::Reverse;
508 let owner_uuid = self.owner_uuid();
509 let owner_entity = self.get_owner();
510 if self.deflect(deflection, None, owner_uuid, owner_entity.as_ref(), false) {
511 self.set_velocity(self.velocity() * 0.2);
512 return deflection;
513 }
514 }
515
516 self.on_hit(hit);
517 ProjectileDeflection::None
518 }
519
520 fn on_hit(&self, hit: &ProjectileHit) {
523 self.projectile_on_hit(hit);
524 }
525
526 fn projectile_on_hit(&self, hit: &ProjectileHit) {
529 let world = self.level();
530 match hit {
531 ProjectileHit::Entity(entity_hit) => {
532 if REGISTRY.entity_types.is_in_tag(
533 entity_hit.entity.entity_type(),
534 &EntityTypeTag::REDIRECTABLE_PROJECTILE,
535 ) && let Some(projectile) = entity_hit.entity.as_projectile()
536 {
537 let owner_uuid = self.owner_uuid();
538 let owner_entity = self.get_owner();
539 projectile.deflect(
540 ProjectileDeflection::AimDeflect,
541 owner_entity.as_deref(),
542 owner_uuid,
543 owner_entity.as_ref(),
544 true,
545 );
546 }
547 self.on_hit_entity(&entity_hit.entity, entity_hit.location);
548 if let Some(world) = world {
549 world.game_event_at(
550 &vanilla_game_events::PROJECTILE_LAND,
551 entity_hit.location,
552 &GameEventContext::new(Some(self.as_entity_event_source()), None),
553 );
554 }
555 }
556 ProjectileHit::Block { hit, .. } => {
557 self.on_hit_block(hit);
558 if let Some(world) = world {
559 let state = world.get_block_state(hit.block_pos);
560 world.game_event(
561 &vanilla_game_events::PROJECTILE_LAND,
562 hit.block_pos,
563 &GameEventContext::new(Some(self.as_entity_event_source()), Some(state)),
564 );
565 }
566 }
567 }
568 }
569
570 fn on_hit_entity(&self, _entity: &SharedEntity, _location: DVec3) {}
572
573 fn on_hit_block(&self, hit: &ClipHitResult) {
575 self.projectile_on_hit_block(hit);
576 }
577
578 fn projectile_on_hit_block(&self, hit: &ClipHitResult) {
581 let Some(world) = self.level() else {
582 return;
583 };
584 let Some(projectile) = self.as_projectile() else {
585 return;
586 };
587 let state = world.get_block_state(hit.block_pos);
588 BLOCK_BEHAVIORS
589 .get_behavior(state.get_block())
590 .on_projectile_hit(state, &world, hit, projectile);
591 }
592
593 fn projectile_base_tick(&self) {
595 if !self.has_been_shot() {
596 if let Some(world) = self.level() {
597 let owner = self.get_owner();
598 world.game_event_at(
599 &vanilla_game_events::PROJECTILE_SHOOT,
600 self.position(),
601 &GameEventContext::new(owner.as_deref(), None),
602 );
603 }
604 self.set_has_been_shot(true);
605 }
606 self.check_left_owner();
607 self.default_tick();
608 self.reset_left_owner_checked();
609 }
610
611 fn save_projectile(&self, nbt: &mut NbtCompound) {
613 let state = self.projectile_base().state.lock();
614 if let Some(owner) = state.owner {
615 nbt.insert("Owner", NbtTag::IntArray(owner.to_int_array().to_vec()));
616 }
617 if state.left_owner {
618 nbt.insert("LeftOwner", 1i8);
619 }
620 nbt.insert("HasBeenShot", i8::from(state.has_been_shot));
621 }
622
623 fn load_projectile(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
625 let mut state = self.projectile_base().state.lock();
626 if let Some(owner_arr) = nbt.int_array("Owner")
627 && let Some(uuid) = Uuid::from_int_array(&owner_arr)
628 {
629 state.owner = Some(uuid);
630 }
631 state.left_owner = nbt.byte("LeftOwner").is_some_and(|value| value != 0);
632 state.has_been_shot = nbt.byte("HasBeenShot").is_some_and(|value| value != 0);
633 }
634}
635
636#[must_use]
639pub fn compute_margin(tick_count: i32) -> f64 {
640 (f64::from(tick_count - 2) / 20.0).clamp(0.0, MAX_ENTITY_HIT_MARGIN)
641}
642
643fn get_entity_hit_result(
646 world: &Arc<World>,
647 from: DVec3,
648 to: DVec3,
649 search_box: WorldAabb,
650 margin: f64,
651 predicate: impl Fn(&dyn Entity) -> bool,
652) -> Option<EntityHitResult> {
653 let mut nearest: Option<EntityHitResult> = None;
654 let mut nearest_dist_sq = f64::MAX;
655
656 for entity in world.get_entities_in_aabb(&search_box) {
657 if !predicate(entity.as_ref()) {
658 continue;
659 }
660 let target_box = entity.bounding_box().inflate(margin);
661 let Some(location) = clip_segment(target_box, from, to) else {
662 continue;
663 };
664 let dist_sq = from.distance_squared(location);
665 if dist_sq < nearest_dist_sq {
666 nearest_dist_sq = dist_sq;
667 nearest = Some(EntityHitResult { entity, location });
668 }
669 }
670
671 nearest
672}
673
674fn clip_segment(aabb: WorldAabb, from: DVec3, to: DVec3) -> Option<DVec3> {
679 const EPSILON: f64 = 1.0e-7;
680
681 let direction = to - from;
682 let mut t_min = 0.0_f64;
683 let mut t_max = 1.0_f64;
684
685 for axis in [Axis::X, Axis::Y, Axis::Z] {
686 let start = axis_component(from, axis);
687 let delta = axis_component(direction, axis);
688 let axis_min = aabb.min(axis);
689 let axis_max = aabb.max(axis);
690
691 if delta.abs() < EPSILON {
692 if start < axis_min || start > axis_max {
693 return None;
694 }
695 continue;
696 }
697
698 let inv_delta = 1.0 / delta;
699 let mut low = (axis_min - start) * inv_delta;
700 let mut high = (axis_max - start) * inv_delta;
701 if low > high {
702 mem::swap(&mut low, &mut high);
703 }
704
705 t_min = t_min.max(low);
706 t_max = t_max.min(high);
707 if t_min > t_max {
708 return None;
709 }
710 }
711
712 Some(from + direction * t_min)
713}
714
715const fn axis_component(vec: DVec3, axis: Axis) -> f64 {
716 match axis {
717 Axis::X => vec.x,
718 Axis::Y => vec.y,
719 Axis::Z => vec.z,
720 }
721}
722
723fn lerp_rotation(mut rot_old: f32, rot: f32) -> f32 {
725 while rot - rot_old < -180.0 {
726 rot_old -= 360.0;
727 }
728 while rot - rot_old >= 180.0 {
729 rot_old += 360.0;
730 }
731 rot_old + 0.2 * (rot - rot_old)
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use steel_registry::{
738 blocks::properties::{BlockStateProperties, Tilt},
739 entity_type::EntityTypeRef,
740 init_vanilla_registry, vanilla_blocks, vanilla_entities,
741 };
742 use steel_utils::{BlockPos, ChunkPos, Direction, types::UpdateFlags};
743
744 use crate::{
745 behavior::init_behaviors,
746 entity::{EntityBase, entities::FireworkRocketEntity},
747 test_support::{test_world, world_border_projectile_test_world},
748 };
749
750 struct OwnerCollisionProjectile {
751 base: EntityBase,
752 projectile_base: ProjectileBase,
753 }
754
755 impl OwnerCollisionProjectile {
756 fn new(id: i32, position: DVec3) -> Self {
757 Self {
758 base: EntityBase::new(
759 id,
760 position,
761 vanilla_entities::ENDER_PEARL.dimensions,
762 Weak::new(),
763 ),
764 projectile_base: ProjectileBase::new(),
765 }
766 }
767 }
768
769 crate::entity::impl_test_downcast_type!(OwnerCollisionProjectile);
770
771 impl Entity for OwnerCollisionProjectile {
772 fn base(&self) -> &EntityBase {
773 &self.base
774 }
775
776 fn entity_type(&self) -> EntityTypeRef {
777 &vanilla_entities::ENDER_PEARL
778 }
779 }
780
781 impl Projectile for OwnerCollisionProjectile {
782 fn projectile_base(&self) -> &ProjectileBase {
783 &self.projectile_base
784 }
785 }
786
787 struct OwnerCollisionTestEntity {
788 base: EntityBase,
789 pickable: bool,
790 entity_type: EntityTypeRef,
791 }
792
793 impl OwnerCollisionTestEntity {
794 fn shared(id: i32, position: DVec3, pickable: bool) -> SharedEntity {
795 Self::shared_with_type(id, position, pickable, &vanilla_entities::PIG)
796 }
797
798 fn shared_with_type(
799 id: i32,
800 position: DVec3,
801 pickable: bool,
802 entity_type: EntityTypeRef,
803 ) -> SharedEntity {
804 Arc::new(Self {
805 base: EntityBase::new(id, position, entity_type.dimensions, Weak::new()),
806 pickable,
807 entity_type,
808 })
809 }
810 }
811
812 crate::entity::impl_test_downcast_type!(OwnerCollisionTestEntity);
813
814 impl Entity for OwnerCollisionTestEntity {
815 fn base(&self) -> &EntityBase {
816 &self.base
817 }
818
819 fn entity_type(&self) -> EntityTypeRef {
820 self.entity_type
821 }
822
823 fn is_pickable(&self) -> bool {
824 self.pickable && !self.is_removed()
825 }
826 }
827
828 #[test]
829 fn compute_margin_ramps_from_zero_to_cap() {
830 assert!((compute_margin(2) - 0.0).abs() < 1.0e-9);
831 assert!((compute_margin(7) - 0.25).abs() < 1.0e-9);
832 assert!((compute_margin(100) - 0.3).abs() < 1.0e-9);
833 }
834
835 #[test]
836 fn clip_segment_hits_box_in_path() {
837 let aabb = WorldAabb::new(4.0, -0.5, -0.5, 5.0, 0.5, 0.5);
838 let hit = clip_segment(aabb, DVec3::ZERO, DVec3::new(10.0, 0.0, 0.0))
839 .expect("ray along +x should enter the box");
840 assert!((hit.x - 4.0).abs() < 1.0e-6);
841 }
842
843 #[test]
844 fn clip_segment_returns_start_when_inside() {
845 let aabb = WorldAabb::new(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0);
846 let hit = clip_segment(aabb, DVec3::ZERO, DVec3::new(0.0, 5.0, 0.0))
847 .expect("a ray starting inside the box hits at its origin");
848 assert_eq!(hit, DVec3::ZERO);
849 }
850
851 #[test]
852 fn may_break_requires_impact_projectile_tag() {
853 init_vanilla_registry();
854
855 let firework = FireworkRocketEntity::new(
856 &vanilla_entities::FIREWORK_ROCKET,
857 1,
858 DVec3::ZERO,
859 Weak::new(),
860 );
861 let ender_pearl = OwnerCollisionProjectile::new(2, DVec3::ZERO);
862
863 assert!(firework.may_break(test_world()));
864 assert!(!ender_pearl.may_break(test_world()));
865 }
866
867 #[test]
868 fn move_vector_synthesizes_world_border_block_hit() {
869 init_vanilla_registry();
870 init_behaviors();
871
872 let world = Arc::clone(world_border_projectile_test_world());
873 let firework = FireworkRocketEntity::new(
874 &vanilla_entities::FIREWORK_ROCKET,
875 3,
876 DVec3::new(4.5, 64.0, 0.0),
877 Arc::downgrade(&world),
878 );
879 firework.set_velocity(DVec3::X);
880
881 let Some(ProjectileHit::Block { location, hit }) = firework.get_hit_result_on_move_vector()
882 else {
883 panic!("firework should hit the world border");
884 };
885
886 assert!(hit.world_border_hit);
887 assert!(!hit.inside);
888 assert_eq!(hit.direction, Direction::East);
889 assert_eq!(hit.block_pos, BlockPos::new(4, 64, 0));
890 assert_eq!(location, hit.location);
891 assert!(location.x < 5.0 && location.x > 4.999);
892 }
893
894 #[test]
895 fn firework_deflects_without_exploding_on_deflecting_entity() {
896 init_vanilla_registry();
897
898 let world = Arc::clone(test_world());
899 let firework = FireworkRocketEntity::new(
900 &vanilla_entities::FIREWORK_ROCKET,
901 4,
902 DVec3::ZERO,
903 Arc::downgrade(&world),
904 );
905 firework.set_velocity(DVec3::X);
906 let deflector = OwnerCollisionTestEntity::shared_with_type(
907 5,
908 DVec3::X,
909 true,
910 &vanilla_entities::BREEZE,
911 );
912
913 let deflection =
914 firework.hit_target_or_deflect_self(&ProjectileHit::Entity(EntityHitResult {
915 entity: deflector,
916 location: DVec3::X,
917 }));
918
919 assert_eq!(deflection, ProjectileDeflection::Reverse);
920 assert_eq!(firework.velocity(), DVec3::new(-0.5, 0.0, 0.0));
921 assert!(firework.needs_velocity_sync());
922 assert!(!firework.is_removed());
923 }
924
925 #[test]
926 fn base_block_hit_dispatches_vanilla_block_callbacks() {
927 init_vanilla_registry();
928 init_behaviors();
929
930 let world = Arc::clone(test_world());
931 let chunk_map = Arc::clone(&world.chunk_map);
932 let pos = BlockPos::new(1_136, 64, 1_136);
933 let block_callback_test =
934 chunk_map.with_full_chunks_in_radius(ChunkPos::from_block_pos(pos), 0, || {
935 let unlit = vanilla_blocks::CANDLE
936 .default_state()
937 .set_value(&BlockStateProperties::LIT, false);
938 assert!(world.set_block(pos, unlit, UpdateFlags::UPDATE_ALL));
939
940 let firework = FireworkRocketEntity::new(
941 &vanilla_entities::FIREWORK_ROCKET,
942 3,
943 DVec3::new(1_136.5, 64.5, 1_136.5),
944 Arc::downgrade(&world),
945 );
946 firework.set_remaining_fire_ticks(1);
947 firework.projectile_on_hit_block(&ClipHitResult {
948 location: firework.position(),
949 direction: Direction::Up,
950 block_pos: pos,
951 miss: false,
952 inside: false,
953 world_border_hit: false,
954 });
955
956 assert!(
957 world
958 .get_block_state(pos)
959 .get_value(&BlockStateProperties::LIT)
960 );
961
962 let campfire_pos = pos.offset(1, 0, 0);
963 let unlit_campfire = vanilla_blocks::CAMPFIRE
964 .default_state()
965 .set_value(&BlockStateProperties::LIT, false)
966 .set_value(&BlockStateProperties::WATERLOGGED, false);
967 assert!(
968 world.set_block(campfire_pos, unlit_campfire, UpdateFlags::UPDATE_CLIENTS,)
969 );
970 firework.projectile_on_hit_block(&ClipHitResult {
971 location: firework.position(),
972 direction: Direction::Up,
973 block_pos: campfire_pos,
974 miss: false,
975 inside: false,
976 world_border_hit: false,
977 });
978 assert!(
979 world
980 .get_block_state(campfire_pos)
981 .get_value(&BlockStateProperties::LIT)
982 );
983
984 let dripleaf_pos = pos.offset(2, 0, 0);
985 assert!(world.set_block(
986 dripleaf_pos,
987 vanilla_blocks::BIG_DRIPLEAF.default_state(),
988 UpdateFlags::UPDATE_CLIENTS,
989 ));
990 firework.projectile_on_hit_block(&ClipHitResult {
991 location: firework.position(),
992 direction: Direction::Up,
993 block_pos: dripleaf_pos,
994 miss: false,
995 inside: false,
996 world_border_hit: false,
997 });
998 assert_eq!(
999 world
1000 .get_block_state(dripleaf_pos)
1001 .get_value(&BlockStateProperties::TILT),
1002 Tilt::Full
1003 );
1004
1005 let chorus_pos = pos.offset(3, 0, 0);
1006 assert!(world.set_block(
1007 chorus_pos,
1008 vanilla_blocks::CHORUS_FLOWER.default_state(),
1009 UpdateFlags::UPDATE_CLIENTS,
1010 ));
1011 firework.projectile_on_hit_block(&ClipHitResult {
1012 location: firework.position(),
1013 direction: Direction::Up,
1014 block_pos: chorus_pos,
1015 miss: false,
1016 inside: false,
1017 world_border_hit: false,
1018 });
1019 assert!(world.get_block_state(chorus_pos).is_air());
1020 });
1021 let result = chunk_map.chunk_runtime.block_on(block_callback_test);
1022
1023 assert_eq!(result, Some(()));
1024 }
1025
1026 #[test]
1027 fn owner_collision_range_checks_root_vehicle_passengers() {
1028 init_vanilla_registry();
1029
1030 let projectile = OwnerCollisionProjectile::new(1, DVec3::ZERO);
1031 let owner = OwnerCollisionTestEntity::shared(2, DVec3::new(10.0, 0.0, 0.0), true);
1032 let vehicle = OwnerCollisionTestEntity::shared(3, DVec3::new(10.0, 0.0, 0.0), true);
1033 let passenger = OwnerCollisionTestEntity::shared(4, DVec3::ZERO, true);
1034 EntityBase::restore_passenger_relationship(&vehicle, &owner);
1035 EntityBase::restore_passenger_relationship(&vehicle, &passenger);
1036
1037 projectile.set_owner_entity(Some(&owner));
1038
1039 assert!(!projectile.is_outside_owner_collision_range());
1040 }
1041
1042 #[test]
1043 fn owner_collision_range_ignores_non_pickable_root_vehicle_passengers() {
1044 init_vanilla_registry();
1045
1046 let projectile = OwnerCollisionProjectile::new(1, DVec3::ZERO);
1047 let owner = OwnerCollisionTestEntity::shared(2, DVec3::new(10.0, 0.0, 0.0), true);
1048 let vehicle = OwnerCollisionTestEntity::shared(3, DVec3::new(10.0, 0.0, 0.0), true);
1049 let passenger = OwnerCollisionTestEntity::shared(4, DVec3::ZERO, false);
1050 EntityBase::restore_passenger_relationship(&vehicle, &owner);
1051 EntityBase::restore_passenger_relationship(&vehicle, &passenger);
1052
1053 projectile.set_owner_entity(Some(&owner));
1054
1055 assert!(projectile.is_outside_owner_collision_range());
1056 }
1057}