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