Skip to main content

steel_core/entity/projectile/
mod.rs

1//! Vanilla `Projectile` class hierarchy, mirrored as Rust traits + base structs.
2//!
3//! Mirrors `net.minecraft.world.entity.projectile`:
4//! `Entity → Projectile → ThrowableProjectile → ThrowableItemProjectile`.
5//! Concrete projectiles embed [`ProjectileBase`] (owner / left-owner / shot state)
6//! and implement the trait stack; the per-layer tick logic chains explicitly via
7//! [`Projectile::projectile_base_tick`] and
8//! [`ThrowableProjectile::throwable_projectile_tick`] (vanilla `super.tick()`).
9//!
10//! The block + entity move-vector raycast mirrors `ProjectileUtil`.
11
12mod 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
39/// Vanilla `Projectile.shoot` per-axis spread scale (`0.0172275 * uncertainty`).
40const SHOOT_INACCURACY_SCALE: f64 = 0.0172_275;
41
42/// Vanilla `ProjectileUtil.DEFAULT_ENTITY_HIT_RESULT_MARGIN`.
43const MAX_ENTITY_HIT_MARGIN: f64 = 0.3;
44
45/// Mirrors vanilla `RandomSource.triangle(mode, deviation)`.
46fn triangle_random(mode: f64, deviation: f64) -> f64 {
47    mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
48}
49
50/// Result of a projectile move-vector raycast (vanilla `HitResult`).
51pub enum ProjectileHit {
52    /// The pearl's path entered a block collider.
53    Block {
54        /// Exact entry location.
55        location: DVec3,
56        /// The underlying block clip result.
57        hit: ClipHitResult,
58    },
59    /// The pearl's path intersected an entity.
60    Entity(EntityHitResult),
61}
62
63impl ProjectileHit {
64    /// Returns the world-space hit location.
65    #[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
74/// A projectile-versus-entity raycast hit (vanilla `EntityHitResult`).
75pub struct EntityHitResult {
76    /// The entity that was hit.
77    pub entity: SharedEntity,
78    /// The world-space location of the hit.
79    pub location: DVec3,
80}
81
82/// Vanilla projectile deflection behaviors (`ProjectileDeflection`).
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum ProjectileDeflection {
85    /// Continue with normal hit handling.
86    None,
87    /// Reverse the projectile with Vanilla's randomized yaw adjustment.
88    Reverse,
89    /// Redirect the projectile along the deflecting entity's look direction.
90    AimDeflect,
91    /// Redirect the projectile along the deflecting entity's movement direction.
92    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
144/// Runtime fields shared by vanilla projectiles (vanilla `Projectile` fields).
145pub struct ProjectileBase {
146    state: SyncMutex<ProjectileState>,
147}
148
149impl ProjectileBase {
150    /// Creates default projectile runtime state.
151    #[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
172/// Object-safe access to a projectile trait object from default [`Projectile`] methods.
173pub trait ProjectileEventSource {
174    /// Returns this entity as a projectile.
175    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
184/// Vanilla-shaped behavior shared by entities that extend `Projectile`.
185pub trait Projectile: Entity + ProjectileEventSource {
186    /// Returns shared projectile runtime state.
187    fn projectile_base(&self) -> &ProjectileBase;
188
189    /// Returns the horizontal hurt-knockback vector used for this projectile.
190    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    /// Sets the owner UUID. Vanilla stores an `EntityReference`; Steel stores the
200    /// UUID and resolves lazily.
201    // TODO: introduce an `EntityReference` type to cache the resolved owner.
202    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    /// Sets the owning entity and caches its live reference.
209    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    /// Caches a live owner reference when it matches the saved owner UUID.
216    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    /// Returns the owner UUID, if any.
224    fn owner_uuid(&self) -> Option<Uuid> {
225        self.projectile_base().state.lock().owner
226    }
227
228    /// Resolves the owning entity in the current world (vanilla `Projectile.getOwner`).
229    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    /// Returns vanilla `Projectile.mayInteract` for a block position.
250    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    /// Returns vanilla `Projectile.mayBreak`.
261    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    /// Returns vanilla `Projectile.ownedBy`.
269    fn owned_by(&self, entity: &dyn Entity) -> bool {
270        self.owner_uuid() == Some(entity.uuid())
271    }
272
273    /// Returns vanilla `Projectile.hasBeenShot`.
274    fn has_been_shot(&self) -> bool {
275        self.projectile_base().state.lock().has_been_shot
276    }
277
278    /// Sets vanilla `Projectile.hasBeenShot`.
279    fn set_has_been_shot(&self, value: bool) {
280        self.projectile_base().state.lock().has_been_shot = value;
281    }
282
283    /// Returns vanilla `Projectile.leftOwner`.
284    fn left_owner(&self) -> bool {
285        self.projectile_base().state.lock().left_owner
286    }
287
288    /// Runs vanilla `Projectile.checkLeftOwner`.
289    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    /// Resets the per-tick left-owner check flag (vanilla clears it after `tick`).
302    fn reset_left_owner_checked(&self) {
303        self.projectile_base().state.lock().left_owner_checked = false;
304    }
305
306    /// Returns vanilla `Projectile.isOutsideOwnerCollisionRange`.
307    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    /// Returns vanilla `Projectile.canHitEntity`.
336    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    /// Returns vanilla `Projectile.getMovementToShoot`.
347    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    /// Runs vanilla `Projectile.shoot`.
358    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    /// Runs vanilla `Projectile.shootFromRotation`.
371    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    /// Runs vanilla `Projectile.updateRotation` (lerped toward the movement vector).
402    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    /// Applies a Vanilla projectile deflection.
412    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    /// Called after a successful Vanilla projectile deflection.
430    fn on_deflection(&self, _by_attack: bool) {}
431
432    /// Returns whether this projectile bounces from the world border.
433    fn should_bounce_on_world_border(&self) -> bool {
434        false
435    }
436
437    /// Casts the move vector and returns the nearest block/entity hit (vanilla
438    /// `ProjectileUtil.getHitResultOnMoveVector` with `this::canHitEntity`).
439    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    /// Vanilla `Projectile.hitTargetOrDeflectSelf`.
473    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    /// Vanilla `Projectile.onHit`. Subclasses override this and call
521    /// [`Projectile::projectile_on_hit`] for the base dispatch (`super.onHit()`).
522    fn on_hit(&self, hit: &ProjectileHit) {
523        self.projectile_on_hit(hit);
524    }
525
526    /// The base `Projectile.onHit` dispatch to block/entity handlers. Not meant to
527    /// be overridden — override [`Projectile::on_hit`] and delegate here instead.
528    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    /// Vanilla `Projectile.onHitEntity` (no-op by default).
571    fn on_hit_entity(&self, _entity: &SharedEntity, _location: DVec3) {}
572
573    /// Vanilla `Projectile.onHitBlock`.
574    fn on_hit_block(&self, hit: &ClipHitResult) {
575        self.projectile_on_hit_block(hit);
576    }
577
578    /// The base `Projectile.onHitBlock` implementation. Concrete projectiles
579    /// that override the hook call this to preserve the Java `super` dispatch.
580    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    /// Vanilla `Projectile.tick` (the `super.tick()` reached from subclasses).
594    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    /// Saves vanilla `Projectile` fields (`Owner`, `LeftOwner`, `HasBeenShot`).
612    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    /// Loads vanilla `Projectile` fields.
624    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/// Vanilla `ProjectileUtil.computeMargin`: ramps the entity hit margin from 0 to
637/// 0.3 over the first ticks of flight.
638#[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
643/// Vanilla `ProjectileUtil.getEntityHitResult` (entity-margin overload): returns
644/// the nearest entity whose inflated box the segment `from -> to` enters.
645fn 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
674/// Clips the segment `from -> to` against `aabb`, returning the entry point.
675///
676/// Returns `from` when the segment starts inside the box and `None` when it never
677/// intersects. Mirrors vanilla `AABB.clip` using the slab method.
678fn 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
723/// Vanilla `Mth.lerp(0.2, rotO, rot)` after wrapping the old angle into range.
724fn 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}