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_math::{DEGREE_180, DEGREE_360};
22use steel_registry::blocks::block_state_ext::BlockStateExt as _;
23use steel_registry::item_stack::ItemStack;
24use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
25use steel_registry::vanilla_game_rules::{MOB_GRIEFING, PROJECTILES_CAN_BREAK_BLOCKS};
26use steel_registry::{REGISTRY, TaggedRegistryExt as _, vanilla_game_events};
27use steel_utils::axis::Axis;
28use steel_utils::locks::SyncMutex;
29use steel_utils::{BlockPos, UuidExt, WorldAabb};
30use uuid::Uuid;
31
32use crate::behavior::BLOCK_BEHAVIORS;
33use crate::enchantment_helper;
34use crate::entity::damage::DamageSource;
35use crate::entity::{Entity, LivingEntity, SharedEntity};
36use crate::player::Player;
37use crate::world::game_event::GameEventContext;
38use crate::world::{ClipBlockShape, ClipFluid, ClipHitResult, World};
39
40use super::{
41 BUBBLE_COLUMN_ABOVE_UP_ACCELERATION, BUBBLE_COLUMN_DOWN_ACCELERATION,
42 BUBBLE_COLUMN_INSIDE_UP_ACCELERATION,
43};
44
45pub use throwable::ThrowableProjectile;
46pub use throwable_item::ThrowableItemProjectile;
47
48const SHOOT_INACCURACY_SCALE: f64 = 0.0172_275;
50
51const MAX_ENTITY_HIT_MARGIN: f64 = 0.3;
53
54const THROWN_ITEM_SPAWN_EYE_OFFSET: f64 = 0.1;
56
57#[must_use]
59pub fn triangle_random(mode: f64, deviation: f64) -> f64 {
60 mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
61}
62
63pub enum ProjectileHit {
65 Block {
67 location: DVec3,
69 hit: ClipHitResult,
71 },
72 Entity(EntityHitResult),
74}
75
76impl ProjectileHit {
77 #[must_use]
79 pub const fn location(&self) -> DVec3 {
80 match self {
81 Self::Block { location, .. } => *location,
82 Self::Entity(hit) => hit.location,
83 }
84 }
85}
86
87pub struct EntityHitResult {
89 pub entity: SharedEntity,
91 pub location: DVec3,
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum ProjectileDeflection {
98 None,
100 Reverse,
102 AimDeflect,
104 MomentumDeflect,
106}
107
108impl ProjectileDeflection {
109 fn apply(self, projectile: &dyn Projectile, deflecting_entity: Option<&dyn Entity>) {
110 match self {
111 Self::None => {}
112 Self::Reverse => {
113 let rotation = rand::random_range(170.0..190.0);
114 projectile.set_velocity(projectile.velocity() * -0.5);
115 let (yaw, pitch) = projectile.rotation();
116 projectile.set_rotation((yaw + rotation, pitch));
117 let (old_yaw, old_pitch) = projectile.base().old_rotation();
118 projectile
119 .base()
120 .set_old_rotation((old_yaw + rotation, old_pitch));
121 projectile.mark_velocity_sync();
122 }
123 Self::AimDeflect => {
124 let Some(entity) = deflecting_entity else {
125 return;
126 };
127 projectile.set_velocity(entity.look_angle());
128 projectile.mark_velocity_sync();
129 }
130 Self::MomentumDeflect => {
131 let Some(entity) = deflecting_entity else {
132 return;
133 };
134 let movement = entity.velocity();
135 let length = movement.length();
136 let normalized = if length < 1.0e-5 {
137 DVec3::ZERO
138 } else {
139 movement / length
140 };
141 projectile.set_velocity(normalized);
142 projectile.mark_velocity_sync();
143 }
144 }
145 }
146}
147
148struct ProjectileState {
149 owner: Option<Uuid>,
150 owner_entity: Option<Weak<dyn Entity>>,
151 left_owner: bool,
152 left_owner_checked: bool,
153 has_been_shot: bool,
154 last_deflected_by: Option<Weak<dyn Entity>>,
155}
156
157pub struct ProjectileBase {
159 state: SyncMutex<ProjectileState>,
160}
161
162impl ProjectileBase {
163 #[must_use]
165 pub const fn new() -> Self {
166 Self {
167 state: SyncMutex::new(ProjectileState {
168 owner: None,
169 owner_entity: None,
170 left_owner: false,
171 left_owner_checked: false,
172 has_been_shot: false,
173 last_deflected_by: None,
174 }),
175 }
176 }
177}
178
179impl Default for ProjectileBase {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185pub trait ProjectileEventSource {
187 fn as_projectile_event_source(&self) -> &dyn Projectile;
189}
190
191impl<T: Projectile> ProjectileEventSource for T {
192 fn as_projectile_event_source(&self) -> &dyn Projectile {
193 self
194 }
195}
196
197pub trait Projectile: Entity + ProjectileEventSource {
199 fn projectile_base(&self) -> &ProjectileBase;
201
202 fn calculate_horizontal_hurt_knockback_direction(
204 &self,
205 _hurt_entity: &dyn LivingEntity,
206 _damage_source: &DamageSource,
207 ) -> (f64, f64) {
208 let movement = self.velocity();
209 (movement.x, movement.z)
210 }
211
212 fn on_above_bubble_column_projectile(&self, drag_down: bool, pos: BlockPos) {
214 let acceleration = if drag_down {
215 -BUBBLE_COLUMN_DOWN_ACCELERATION
216 } else {
217 BUBBLE_COLUMN_ABOVE_UP_ACCELERATION
218 };
219 self.set_velocity(self.velocity() + DVec3::new(0.0, acceleration, 0.0));
220
221 if let Some(world) = self.level() {
222 world.send_bubble_column_particles(pos);
223 }
224 }
225
226 fn on_inside_bubble_column_projectile(&self, drag_down: bool) {
228 let acceleration = if drag_down {
229 -BUBBLE_COLUMN_DOWN_ACCELERATION
230 } else {
231 BUBBLE_COLUMN_INSIDE_UP_ACCELERATION
232 };
233 self.set_velocity(self.velocity() + DVec3::new(0.0, acceleration, 0.0));
234 self.reset_fall_distance();
235 }
236
237 fn set_owner_uuid(&self, owner: Option<Uuid>) {
241 let mut state = self.projectile_base().state.lock();
242 state.owner = owner;
243 state.owner_entity = None;
244 }
245
246 fn set_owner_entity(&self, owner: Option<&SharedEntity>) {
248 let mut state = self.projectile_base().state.lock();
249 state.owner = owner.map(|owner| owner.uuid());
250 state.owner_entity = owner.map(Arc::downgrade);
251 }
252
253 fn cache_owner_entity(&self, owner: &SharedEntity) {
255 let mut state = self.projectile_base().state.lock();
256 if state.owner == Some(owner.uuid()) {
257 state.owner_entity = Some(Arc::downgrade(owner));
258 }
259 }
260
261 fn owner_uuid(&self) -> Option<Uuid> {
263 self.projectile_base().state.lock().owner
264 }
265
266 fn get_owner(&self) -> Option<SharedEntity> {
268 let uuid = self.owner_uuid()?;
269 if let Some(owner) = self
270 .projectile_base()
271 .state
272 .lock()
273 .owner_entity
274 .as_ref()
275 .and_then(Weak::upgrade)
276 && !owner.is_removed()
277 && owner.uuid() == uuid
278 {
279 return Some(owner);
280 }
281
282 let owner = self.level()?.get_entity_by_uuid(&uuid)?;
283 self.cache_owner_entity(&owner);
284 Some(owner)
285 }
286
287 fn projectile_may_interact(&self, world: &World, pos: steel_utils::BlockPos) -> bool {
289 let Some(owner) = self.get_owner() else {
290 return true;
291 };
292 if owner.as_player().is_some() {
293 return owner.may_interact(world, pos);
294 }
295 world.get_game_rule(&MOB_GRIEFING)
296 }
297
298 fn may_break(&self, world: &World) -> bool {
300 REGISTRY
301 .entity_types
302 .is_in_tag(self.entity_type(), &EntityTypeTag::IMPACT_PROJECTILES)
303 && world.get_game_rule(&PROJECTILES_CAN_BREAK_BLOCKS)
304 }
305
306 fn owned_by(&self, entity: &dyn Entity) -> bool {
308 self.owner_uuid() == Some(entity.uuid())
309 }
310
311 fn has_been_shot(&self) -> bool {
313 self.projectile_base().state.lock().has_been_shot
314 }
315
316 fn set_has_been_shot(&self, value: bool) {
318 self.projectile_base().state.lock().has_been_shot = value;
319 }
320
321 fn left_owner(&self) -> bool {
323 self.projectile_base().state.lock().left_owner
324 }
325
326 fn check_left_owner(&self) {
328 let mut state = self.projectile_base().state.lock();
329 if state.left_owner || state.left_owner_checked {
330 return;
331 }
332 state.left_owner_checked = true;
333 drop(state);
334
335 let left = self.is_outside_owner_collision_range();
336 self.projectile_base().state.lock().left_owner = left;
337 }
338
339 fn reset_left_owner_checked(&self) {
341 self.projectile_base().state.lock().left_owner_checked = false;
342 }
343
344 fn is_outside_owner_collision_range(&self) -> bool {
346 let Some(owner) = self.get_owner() else {
347 return true;
348 };
349 let aabb = self
350 .bounding_box()
351 .expand_towards(self.velocity())
352 .inflate(1.0);
353 let root_vehicle = owner.root_vehicle().unwrap_or_else(|| owner.clone());
354 let mut to_check = vec![root_vehicle];
355 let mut visited = Vec::new();
356
357 while let Some(entity) = to_check.pop() {
358 let entity_id = entity.id();
359 if visited.contains(&entity_id) {
360 continue;
361 }
362 visited.push(entity_id);
363
364 if entity.is_pickable() && aabb.intersects(entity.bounding_box()) {
365 return false;
366 }
367 to_check.extend(entity.passengers());
368 }
369
370 true
371 }
372
373 fn can_hit_entity(&self, entity: &dyn Entity) -> bool {
376 self.base_can_hit_entity(entity)
377 }
378
379 fn base_can_hit_entity(&self, entity: &dyn Entity) -> bool {
382 if !entity.can_be_hit_by_projectile() {
383 return false;
384 }
385 let Some(owner) = self.get_owner() else {
386 return true;
387 };
388 self.left_owner() || !owner.is_passenger_of_same_vehicle(entity)
389 }
390
391 fn get_movement_to_shoot(&self, direction: DVec3, power: f32, uncertainty: f32) -> DVec3 {
393 let deviation = SHOOT_INACCURACY_SCALE * f64::from(uncertainty);
394 let jitter = DVec3::new(
395 triangle_random(0.0, deviation),
396 triangle_random(0.0, deviation),
397 triangle_random(0.0, deviation),
398 );
399 (direction.normalize_or_zero() + jitter) * f64::from(power)
400 }
401
402 fn shoot(&self, direction: DVec3, power: f32, uncertainty: f32) {
404 let movement = self.get_movement_to_shoot(direction, power, uncertainty);
405 self.set_velocity(movement);
406 self.mark_velocity_sync();
407
408 let horizontal = (movement.x * movement.x + movement.z * movement.z).sqrt();
409 let yaw = movement.x.atan2(movement.z).to_degrees() as f32;
410 let pitch = movement.y.atan2(horizontal).to_degrees() as f32;
411 self.set_rotation((yaw, pitch));
412 self.base().set_old_rotation_to_current();
413 }
414
415 fn shoot_from_rotation(
417 &self,
418 source: &dyn Entity,
419 x_rot: f32,
420 y_rot: f32,
421 y_offset: f32,
422 power: f32,
423 uncertainty: f32,
424 ) {
425 let yaw = y_rot.to_radians();
426 let pitch = x_rot.to_radians();
427 let pitch_offset = (x_rot + y_offset).to_radians();
428 let direction = DVec3::new(
429 f64::from(-yaw.sin() * pitch.cos()),
430 f64::from(-pitch_offset.sin()),
431 f64::from(yaw.cos() * pitch.cos()),
432 );
433 self.shoot(direction, power, uncertainty);
434
435 let source_movement = source.known_movement();
436 let added_y = if source.on_ground() {
437 0.0
438 } else {
439 source_movement.y
440 };
441 self.set_velocity(
442 self.velocity() + DVec3::new(source_movement.x, added_y, source_movement.z),
443 );
444 }
445
446 fn update_rotation(&self) {
448 let movement = self.velocity();
449 let horizontal = (movement.x * movement.x + movement.z * movement.z).sqrt();
450 let (yaw_old, pitch_old) = self.base().old_rotation();
451 let yaw = lerp_rotation(yaw_old, movement.x.atan2(movement.z).to_degrees() as f32);
452 let pitch = lerp_rotation(pitch_old, movement.y.atan2(horizontal).to_degrees() as f32);
453 self.set_rotation((yaw, pitch));
454 }
455
456 fn deflect(
458 &self,
459 deflection: ProjectileDeflection,
460 deflecting_entity: Option<&dyn Entity>,
461 new_owner_uuid: Option<Uuid>,
462 new_owner_entity: Option<&SharedEntity>,
463 by_attack: bool,
464 ) -> bool {
465 deflection.apply(self.as_projectile_event_source(), deflecting_entity);
466 let mut state = self.projectile_base().state.lock();
467 state.owner = new_owner_uuid;
468 state.owner_entity = new_owner_entity.map(Arc::downgrade);
469 drop(state);
470 self.on_deflection(by_attack);
471 true
472 }
473
474 fn on_deflection(&self, _by_attack: bool) {}
476
477 fn should_bounce_on_world_border(&self) -> bool {
479 false
480 }
481
482 fn get_hit_result_on_move_vector(&self) -> Option<ProjectileHit> {
485 const EPSILON: f64 = 1.0e-12;
486 let world = self.level()?;
487 let from = self.position();
488 let delta = self.velocity();
489 if delta.length_squared() < EPSILON {
490 return None;
491 }
492 let to = from + delta;
493
494 let block_hit =
495 world.clip_including_border(from, to, ClipBlockShape::Collider, ClipFluid::None);
496 let entity_end = if block_hit.is_miss() {
497 to
498 } else {
499 block_hit.location
500 };
501
502 let search_box = self.bounding_box().expand_towards(delta).inflate(1.0);
503 let margin = compute_margin(self.tick_count());
504 let self_id = self.id();
505 let entity_hit =
506 get_entity_hit_result(world.as_ref(), from, entity_end, search_box, margin, |e| {
507 e.id() != self_id && self.can_hit_entity(e)
508 });
509
510 if let Some(hit) = entity_hit {
511 return Some(ProjectileHit::Entity(hit));
512 }
513 if !block_hit.is_miss() {
514 return Some(ProjectileHit::Block {
515 location: block_hit.location,
516 hit: block_hit,
517 });
518 }
519 None
520 }
521
522 fn hit_target_or_deflect_self(&self, hit: &ProjectileHit) -> ProjectileDeflection {
524 if let ProjectileHit::Entity(entity_hit) = hit {
525 let deflection = entity_hit
526 .entity
527 .deflection(self.as_projectile_event_source());
528 if deflection != ProjectileDeflection::None {
529 let already_deflected = self
530 .projectile_base()
531 .state
532 .lock()
533 .last_deflected_by
534 .as_ref()
535 .and_then(Weak::upgrade)
536 .is_some_and(|last| Arc::ptr_eq(&last, &entity_hit.entity));
537 let owner_uuid = self.owner_uuid();
538 let owner_entity = self.get_owner();
539 if !already_deflected
540 && self.deflect(
541 deflection,
542 Some(entity_hit.entity.as_ref()),
543 owner_uuid,
544 owner_entity.as_ref(),
545 false,
546 )
547 {
548 self.projectile_base().state.lock().last_deflected_by =
549 Some(Arc::downgrade(&entity_hit.entity));
550 }
551 return deflection;
552 }
553 } else if let ProjectileHit::Block { hit, .. } = hit
554 && self.should_bounce_on_world_border()
555 && hit.world_border_hit
556 {
557 let deflection = ProjectileDeflection::Reverse;
558 let owner_uuid = self.owner_uuid();
559 let owner_entity = self.get_owner();
560 if self.deflect(deflection, None, owner_uuid, owner_entity.as_ref(), false) {
561 self.set_velocity(self.velocity() * 0.2);
562 return deflection;
563 }
564 }
565
566 self.on_hit(hit);
567 ProjectileDeflection::None
568 }
569
570 fn on_hit(&self, hit: &ProjectileHit) {
573 self.projectile_on_hit(hit);
574 }
575
576 fn projectile_on_hit(&self, hit: &ProjectileHit) {
579 let world = self.level();
580 match hit {
581 ProjectileHit::Entity(entity_hit) => {
582 if REGISTRY.entity_types.is_in_tag(
583 entity_hit.entity.entity_type(),
584 &EntityTypeTag::REDIRECTABLE_PROJECTILE,
585 ) && let Some(projectile) = entity_hit.entity.as_projectile()
586 {
587 let owner_uuid = self.owner_uuid();
588 let owner_entity = self.get_owner();
589 projectile.deflect(
590 ProjectileDeflection::AimDeflect,
591 owner_entity.as_deref(),
592 owner_uuid,
593 owner_entity.as_ref(),
594 true,
595 );
596 }
597 self.on_hit_entity(&entity_hit.entity, entity_hit.location);
598 if let Some(world) = world {
599 world.game_event_at(
600 &vanilla_game_events::PROJECTILE_LAND,
601 entity_hit.location,
602 &GameEventContext::new(Some(self.as_entity_event_source()), None),
603 );
604 }
605 }
606 ProjectileHit::Block { hit, .. } => {
607 self.on_hit_block(hit);
608 if let Some(world) = world {
609 let state = world.get_block_state(hit.block_pos);
610 world.game_event(
611 &vanilla_game_events::PROJECTILE_LAND,
612 hit.block_pos,
613 &GameEventContext::new(Some(self.as_entity_event_source()), Some(state)),
614 );
615 }
616 }
617 }
618 }
619
620 fn on_hit_entity(&self, _entity: &SharedEntity, _location: DVec3) {}
622
623 fn on_hit_block(&self, hit: &ClipHitResult) {
625 self.projectile_on_hit_block(hit);
626 }
627
628 fn projectile_on_hit_block(&self, hit: &ClipHitResult) {
631 let Some(world) = self.level() else {
632 return;
633 };
634 let Some(projectile) = self.as_projectile() else {
635 return;
636 };
637 let state = world.get_block_state(hit.block_pos);
638 BLOCK_BEHAVIORS
639 .get_behavior(state.get_block())
640 .on_projectile_hit(state, &world, hit, projectile);
641 }
642
643 fn hurt(&self, _world: &World, source: &DamageSource, _amount: f32) -> bool {
646 if !self.is_invulnerable_to_base(source) {
647 self.mark_hurt();
648 }
649 false
650 }
651
652 fn projectile_base_tick(&self) {
654 if !self.has_been_shot() {
655 let owner = self.get_owner();
656 self.game_event_with_source_entity(
657 &vanilla_game_events::PROJECTILE_SHOOT,
658 owner.as_deref(),
659 );
660 self.set_has_been_shot(true);
661 }
662 self.check_left_owner();
663 self.default_tick();
664 self.reset_left_owner_checked();
665 }
666
667 fn save_projectile(&self, nbt: &mut NbtCompound) {
669 let state = self.projectile_base().state.lock();
670 if let Some(owner) = state.owner {
671 nbt.insert("Owner", NbtTag::IntArray(owner.to_int_array().to_vec()));
672 }
673 if state.left_owner {
674 nbt.insert("LeftOwner", 1i8);
675 }
676 nbt.insert("HasBeenShot", i8::from(state.has_been_shot));
677 }
678
679 fn load_projectile(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
681 let mut state = self.projectile_base().state.lock();
682 if let Some(owner_arr) = nbt.int_array("Owner")
683 && let Some(uuid) = Uuid::from_int_array(&owner_arr)
684 {
685 state.owner = Some(uuid);
686 }
687 state.left_owner = nbt.byte("LeftOwner").is_some_and(|value| value != 0);
688 state.has_been_shot = nbt.byte("HasBeenShot").is_some_and(|value| value != 0);
689 }
690}
691
692#[must_use]
702pub fn spawn_throwable_item_projectile<E>(
703 world: &Arc<World>,
704 player: &Player,
705 item_stack: &mut ItemStack,
706 power: f32,
707 uncertainty: f32,
708 create: impl FnOnce(DVec3) -> E,
709) -> Option<SharedEntity>
710where
711 E: Projectile + ThrowableItemProjectile + Entity + 'static,
712{
713 let player_pos = player.position();
714 let spawn_pos = DVec3::new(
715 player_pos.x,
716 player.get_eye_y() - THROWN_ITEM_SPAWN_EYE_OFFSET,
717 player_pos.z,
718 );
719
720 let entity = create(spawn_pos);
721 if let Some(owner) = world.players.get_by_uuid(&player.gameprofile.id) {
722 let owner: SharedEntity = owner;
723 entity.set_owner_entity(Some(&owner));
724 } else {
725 entity.set_owner_uuid(Some(player.gameprofile.id));
726 }
727 entity.set_item_clamped(item_stack.clone());
728
729 let (yaw, player_pitch) = player.rotation();
730 entity.shoot_from_rotation(player, player_pitch, yaw, 0.0, power, uncertainty);
731
732 let entity: SharedEntity = Arc::new(entity);
733 if let Err(error) = world.try_add_entity(Arc::clone(&entity)) {
734 log::debug!("failed to spawn throwable item projectile: {error}");
735 return None;
736 }
737 enchantment_helper::on_projectile_spawned(world, item_stack, entity.as_ref(), Some(player));
738
739 Some(entity)
740}
741
742#[must_use]
745pub fn compute_margin(tick_count: i32) -> f64 {
746 (f64::from(tick_count - 2) / 20.0).clamp(0.0, MAX_ENTITY_HIT_MARGIN)
747}
748
749pub enum ViewVectorHitResult {
751 Miss,
753 Block(ClipHitResult),
755 Entity(EntityHitResult),
757}
758
759#[must_use]
764pub fn get_hit_result_on_view_vector(
765 world: &World,
766 source: &dyn Entity,
767 distance: f64,
768 matching: impl Fn(&dyn Entity) -> bool,
769) -> ViewVectorHitResult {
770 let position = source.position();
771 let from = DVec3::new(position.x, source.get_eye_y(), position.z);
772 let delta = source.look_angle() * distance;
773 let to = from + delta;
774
775 let block_hit =
776 world.clip_including_border(from, to, ClipBlockShape::Collider, ClipFluid::None);
777 let entity_end = if block_hit.is_miss() {
778 to
779 } else {
780 block_hit.location
781 };
782
783 let search_box = source.bounding_box().expand_towards(delta).inflate(1.0);
784 let source_id = source.id();
785 let entity_hit = get_entity_hit_result(world, from, entity_end, search_box, 0.0, |entity| {
786 entity.id() != source_id && matching(entity)
787 });
788
789 if let Some(hit) = entity_hit {
790 return ViewVectorHitResult::Entity(hit);
791 }
792 if !block_hit.is_miss() {
793 return ViewVectorHitResult::Block(block_hit);
794 }
795 ViewVectorHitResult::Miss
796}
797
798fn get_entity_hit_result(
801 world: &World,
802 from: DVec3,
803 to: DVec3,
804 search_box: WorldAabb,
805 margin: f64,
806 predicate: impl Fn(&dyn Entity) -> bool,
807) -> Option<EntityHitResult> {
808 let mut nearest: Option<EntityHitResult> = None;
809 let mut nearest_dist_sq = f64::MAX;
810
811 for entity in world.get_entities_in_aabb(&search_box) {
812 if !predicate(entity.as_ref()) {
813 continue;
814 }
815 let target_box = entity.bounding_box().inflate(margin);
816 let Some(location) = clip_segment(target_box, from, to) else {
817 continue;
818 };
819 let dist_sq = from.distance_squared(location);
820 if dist_sq < nearest_dist_sq {
821 nearest_dist_sq = dist_sq;
822 nearest = Some(EntityHitResult { entity, location });
823 }
824 }
825
826 nearest
827}
828
829fn clip_segment(aabb: WorldAabb, from: DVec3, to: DVec3) -> Option<DVec3> {
834 const EPSILON: f64 = 1.0e-7;
835
836 let direction = to - from;
837 let mut t_min = 0.0_f64;
838 let mut t_max = 1.0_f64;
839
840 for axis in [Axis::X, Axis::Y, Axis::Z] {
841 let start = axis_component(from, axis);
842 let delta = axis_component(direction, axis);
843 let axis_min = aabb.min(axis);
844 let axis_max = aabb.max(axis);
845
846 if delta.abs() < EPSILON {
847 if start < axis_min || start > axis_max {
848 return None;
849 }
850 continue;
851 }
852
853 let inv_delta = 1.0 / delta;
854 let mut low = (axis_min - start) * inv_delta;
855 let mut high = (axis_max - start) * inv_delta;
856 if low > high {
857 mem::swap(&mut low, &mut high);
858 }
859
860 t_min = t_min.max(low);
861 t_max = t_max.min(high);
862 if t_min > t_max {
863 return None;
864 }
865 }
866
867 Some(from + direction * t_min)
868}
869
870const fn axis_component(vec: DVec3, axis: Axis) -> f64 {
871 match axis {
872 Axis::X => vec.x,
873 Axis::Y => vec.y,
874 Axis::Z => vec.z,
875 }
876}
877
878fn lerp_rotation(mut rot_old: f32, rot: f32) -> f32 {
880 while rot - rot_old < -DEGREE_180 {
881 rot_old -= DEGREE_360;
882 }
883 while rot - rot_old >= DEGREE_180 {
884 rot_old += DEGREE_360;
885 }
886 rot_old + 0.2 * (rot - rot_old)
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use steel_registry::{
893 blocks::properties::{BlockStateProperties, Tilt},
894 entity_type::EntityTypeRef,
895 init_vanilla_registry, vanilla_blocks, vanilla_entities,
896 };
897 use steel_utils::{BlockPos, ChunkPos, Direction, types::UpdateFlags};
898
899 use crate::{
900 behavior::init_behaviors,
901 entity::{EntityBase, entities::FireworkRocketEntity},
902 test_support::{test_world, world_border_projectile_test_world},
903 };
904
905 struct OwnerCollisionProjectile {
906 base: EntityBase,
907 projectile_base: ProjectileBase,
908 }
909
910 impl OwnerCollisionProjectile {
911 fn new(id: i32, position: DVec3) -> Self {
912 Self {
913 base: EntityBase::new(
914 id,
915 position,
916 vanilla_entities::ENDER_PEARL.dimensions,
917 Weak::new(),
918 ),
919 projectile_base: ProjectileBase::new(),
920 }
921 }
922 }
923
924 crate::entity::impl_test_downcast_type!(OwnerCollisionProjectile);
925
926 impl Entity for OwnerCollisionProjectile {
927 fn base(&self) -> &EntityBase {
928 &self.base
929 }
930
931 fn entity_type(&self) -> EntityTypeRef {
932 &vanilla_entities::ENDER_PEARL
933 }
934 }
935
936 impl Projectile for OwnerCollisionProjectile {
937 fn projectile_base(&self) -> &ProjectileBase {
938 &self.projectile_base
939 }
940 }
941
942 struct OwnerCollisionTestEntity {
943 base: EntityBase,
944 pickable: bool,
945 entity_type: EntityTypeRef,
946 }
947
948 impl OwnerCollisionTestEntity {
949 fn shared(id: i32, position: DVec3, pickable: bool) -> SharedEntity {
950 Self::shared_with_type(id, position, pickable, &vanilla_entities::PIG)
951 }
952
953 fn shared_with_type(
954 id: i32,
955 position: DVec3,
956 pickable: bool,
957 entity_type: EntityTypeRef,
958 ) -> SharedEntity {
959 Arc::new(Self {
960 base: EntityBase::new(id, position, entity_type.dimensions, Weak::new()),
961 pickable,
962 entity_type,
963 })
964 }
965 }
966
967 crate::entity::impl_test_downcast_type!(OwnerCollisionTestEntity);
968
969 impl Entity for OwnerCollisionTestEntity {
970 fn base(&self) -> &EntityBase {
971 &self.base
972 }
973
974 fn entity_type(&self) -> EntityTypeRef {
975 self.entity_type
976 }
977
978 fn is_pickable(&self) -> bool {
979 self.pickable && !self.is_removed()
980 }
981 }
982
983 #[test]
984 fn compute_margin_ramps_from_zero_to_cap() {
985 assert!((compute_margin(2) - 0.0).abs() < 1.0e-9);
986 assert!((compute_margin(7) - 0.25).abs() < 1.0e-9);
987 assert!((compute_margin(100) - 0.3).abs() < 1.0e-9);
988 }
989
990 #[test]
991 fn clip_segment_hits_box_in_path() {
992 let aabb = WorldAabb::new(4.0, -0.5, -0.5, 5.0, 0.5, 0.5);
993 let hit = clip_segment(aabb, DVec3::ZERO, DVec3::new(10.0, 0.0, 0.0))
994 .expect("ray along +x should enter the box");
995 assert!((hit.x - 4.0).abs() < 1.0e-6);
996 }
997
998 #[test]
999 fn clip_segment_returns_start_when_inside() {
1000 let aabb = WorldAabb::new(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0);
1001 let hit = clip_segment(aabb, DVec3::ZERO, DVec3::new(0.0, 5.0, 0.0))
1002 .expect("a ray starting inside the box hits at its origin");
1003 assert_eq!(hit, DVec3::ZERO);
1004 }
1005
1006 #[test]
1007 fn may_break_requires_impact_projectile_tag() {
1008 init_vanilla_registry();
1009
1010 let firework = FireworkRocketEntity::new(
1011 &vanilla_entities::FIREWORK_ROCKET,
1012 1,
1013 DVec3::ZERO,
1014 Weak::new(),
1015 );
1016 let ender_pearl = OwnerCollisionProjectile::new(2, DVec3::ZERO);
1017
1018 assert!(firework.may_break(test_world()));
1019 assert!(!ender_pearl.may_break(test_world()));
1020 }
1021
1022 #[test]
1023 fn move_vector_synthesizes_world_border_block_hit() {
1024 init_vanilla_registry();
1025 init_behaviors();
1026
1027 let world = Arc::clone(world_border_projectile_test_world());
1028 let firework = FireworkRocketEntity::new(
1029 &vanilla_entities::FIREWORK_ROCKET,
1030 3,
1031 DVec3::new(4.5, 64.0, 0.0),
1032 Arc::downgrade(&world),
1033 );
1034 firework.set_velocity(DVec3::X);
1035
1036 let Some(ProjectileHit::Block { location, hit }) = firework.get_hit_result_on_move_vector()
1037 else {
1038 panic!("firework should hit the world border");
1039 };
1040
1041 assert!(hit.world_border_hit);
1042 assert!(!hit.inside);
1043 assert_eq!(hit.direction, Direction::East);
1044 assert_eq!(hit.block_pos, BlockPos::new(4, 64, 0));
1045 assert_eq!(location, hit.location);
1046 assert!(location.x < 5.0 && location.x > 4.999);
1047 }
1048
1049 #[test]
1050 fn firework_deflects_without_exploding_on_deflecting_entity() {
1051 init_vanilla_registry();
1052
1053 let world = Arc::clone(test_world());
1054 let firework = FireworkRocketEntity::new(
1055 &vanilla_entities::FIREWORK_ROCKET,
1056 4,
1057 DVec3::ZERO,
1058 Arc::downgrade(&world),
1059 );
1060 firework.set_velocity(DVec3::X);
1061 let deflector = OwnerCollisionTestEntity::shared_with_type(
1062 5,
1063 DVec3::X,
1064 true,
1065 &vanilla_entities::BREEZE,
1066 );
1067
1068 let deflection =
1069 firework.hit_target_or_deflect_self(&ProjectileHit::Entity(EntityHitResult {
1070 entity: deflector,
1071 location: DVec3::X,
1072 }));
1073
1074 assert_eq!(deflection, ProjectileDeflection::Reverse);
1075 assert_eq!(firework.velocity(), DVec3::new(-0.5, 0.0, 0.0));
1076 assert!(firework.needs_velocity_sync());
1077 assert!(!firework.is_removed());
1078 }
1079
1080 #[test]
1081 fn base_block_hit_dispatches_vanilla_block_callbacks() {
1082 init_vanilla_registry();
1083 init_behaviors();
1084
1085 let world = Arc::clone(test_world());
1086 let chunk_map = Arc::clone(&world.chunk_map);
1087 let pos = BlockPos::new(1_136, 64, 1_136);
1088 let block_callback_test =
1089 chunk_map.with_full_chunks_in_radius(ChunkPos::from_block_pos(pos), 0, || {
1090 let unlit = vanilla_blocks::CANDLE
1091 .default_state()
1092 .set_value(&BlockStateProperties::LIT, false);
1093 assert!(world.set_block(pos, unlit, UpdateFlags::UPDATE_ALL));
1094
1095 let firework = FireworkRocketEntity::new(
1096 &vanilla_entities::FIREWORK_ROCKET,
1097 3,
1098 DVec3::new(1_136.5, 64.5, 1_136.5),
1099 Arc::downgrade(&world),
1100 );
1101 firework.set_remaining_fire_ticks(1);
1102 firework.projectile_on_hit_block(&ClipHitResult {
1103 location: firework.position(),
1104 direction: Direction::Up,
1105 block_pos: pos,
1106 miss: false,
1107 inside: false,
1108 world_border_hit: false,
1109 });
1110
1111 assert!(
1112 world
1113 .get_block_state(pos)
1114 .get_value(&BlockStateProperties::LIT)
1115 );
1116
1117 let campfire_pos = pos.offset(1, 0, 0);
1118 let unlit_campfire = vanilla_blocks::CAMPFIRE
1119 .default_state()
1120 .set_value(&BlockStateProperties::LIT, false)
1121 .set_value(&BlockStateProperties::WATERLOGGED, false);
1122 assert!(
1123 world.set_block(campfire_pos, unlit_campfire, UpdateFlags::UPDATE_CLIENTS,)
1124 );
1125 firework.projectile_on_hit_block(&ClipHitResult {
1126 location: firework.position(),
1127 direction: Direction::Up,
1128 block_pos: campfire_pos,
1129 miss: false,
1130 inside: false,
1131 world_border_hit: false,
1132 });
1133 assert!(
1134 world
1135 .get_block_state(campfire_pos)
1136 .get_value(&BlockStateProperties::LIT)
1137 );
1138
1139 let dripleaf_pos = pos.offset(2, 0, 0);
1140 assert!(world.set_block(
1141 dripleaf_pos,
1142 vanilla_blocks::BIG_DRIPLEAF.default_state(),
1143 UpdateFlags::UPDATE_CLIENTS,
1144 ));
1145 firework.projectile_on_hit_block(&ClipHitResult {
1146 location: firework.position(),
1147 direction: Direction::Up,
1148 block_pos: dripleaf_pos,
1149 miss: false,
1150 inside: false,
1151 world_border_hit: false,
1152 });
1153 assert_eq!(
1154 world
1155 .get_block_state(dripleaf_pos)
1156 .get_value(&BlockStateProperties::TILT),
1157 Tilt::Full
1158 );
1159
1160 let chorus_pos = pos.offset(3, 0, 0);
1161 assert!(world.set_block(
1162 chorus_pos,
1163 vanilla_blocks::CHORUS_FLOWER.default_state(),
1164 UpdateFlags::UPDATE_CLIENTS,
1165 ));
1166 firework.projectile_on_hit_block(&ClipHitResult {
1167 location: firework.position(),
1168 direction: Direction::Up,
1169 block_pos: chorus_pos,
1170 miss: false,
1171 inside: false,
1172 world_border_hit: false,
1173 });
1174 assert!(world.get_block_state(chorus_pos).is_air());
1175 });
1176 let result = chunk_map.chunk_runtime.block_on(block_callback_test);
1177
1178 assert_eq!(result, Some(()));
1179 }
1180
1181 #[test]
1182 fn owner_collision_range_checks_root_vehicle_passengers() {
1183 init_vanilla_registry();
1184
1185 let projectile = OwnerCollisionProjectile::new(1, DVec3::ZERO);
1186 let owner = OwnerCollisionTestEntity::shared(2, DVec3::new(10.0, 0.0, 0.0), true);
1187 let vehicle = OwnerCollisionTestEntity::shared(3, DVec3::new(10.0, 0.0, 0.0), true);
1188 let passenger = OwnerCollisionTestEntity::shared(4, DVec3::ZERO, true);
1189 EntityBase::restore_passenger_relationship(&vehicle, &owner);
1190 EntityBase::restore_passenger_relationship(&vehicle, &passenger);
1191
1192 projectile.set_owner_entity(Some(&owner));
1193
1194 assert!(!projectile.is_outside_owner_collision_range());
1195 }
1196
1197 #[test]
1198 fn owner_collision_range_ignores_non_pickable_root_vehicle_passengers() {
1199 init_vanilla_registry();
1200
1201 let projectile = OwnerCollisionProjectile::new(1, DVec3::ZERO);
1202 let owner = OwnerCollisionTestEntity::shared(2, DVec3::new(10.0, 0.0, 0.0), true);
1203 let vehicle = OwnerCollisionTestEntity::shared(3, DVec3::new(10.0, 0.0, 0.0), true);
1204 let passenger = OwnerCollisionTestEntity::shared(4, DVec3::ZERO, false);
1205 EntityBase::restore_passenger_relationship(&vehicle, &owner);
1206 EntityBase::restore_passenger_relationship(&vehicle, &passenger);
1207
1208 projectile.set_owner_entity(Some(&owner));
1209
1210 assert!(projectile.is_outside_owner_collision_range());
1211 }
1212}