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;
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
56/// Vanilla `Projectile.shoot` per-axis spread scale (`0.0172275 * uncertainty`).
57const SHOOT_INACCURACY_SCALE: f64 = 0.0172_275;
58
59const MAX_ENTITY_HIT_MARGIN: f32 = 0.3;
60
61/// Vanilla `ThrowableItemProjectile` spawn offset below the shooter's eye.
62const THROWN_ITEM_SPAWN_EYE_OFFSET: f64 = 0.1;
63
64/// Returns a triangle-distributed random value.
65#[must_use]
66pub fn triangle_random(mode: f64, deviation: f64) -> f64 {
67    mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
68}
69
70/// Result of a projectile move-vector raycast (vanilla `HitResult`).
71pub enum ProjectileHit {
72    /// The pearl's path entered a block collider.
73    Block {
74        /// Exact entry location.
75        location: DVec3,
76        /// The underlying block clip result.
77        hit: ClipHitResult,
78    },
79    /// The pearl's path intersected an entity.
80    Entity(EntityHitResult),
81}
82
83impl ProjectileHit {
84    /// Returns the world-space hit location.
85    #[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
94/// A projectile-versus-entity raycast hit (vanilla `EntityHitResult`).
95pub struct EntityHitResult {
96    /// The entity that was hit.
97    pub entity: SharedEntity,
98    /// The world-space location of the hit.
99    pub location: DVec3,
100}
101
102/// Vanilla projectile deflection behaviors (`ProjectileDeflection`).
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum ProjectileDeflection {
105    /// Continue with normal hit handling.
106    None,
107    /// Reverse the projectile with Vanilla's randomized yaw adjustment.
108    Reverse,
109    /// Redirect the projectile along the deflecting entity's look direction.
110    AimDeflect,
111    /// Redirect the projectile along the deflecting entity's movement direction.
112    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
163/// Runtime fields shared by vanilla projectiles (vanilla `Projectile` fields).
164pub struct ProjectileBase {
165    state: SyncMutex<ProjectileState>,
166}
167
168impl ProjectileBase {
169    /// Creates default projectile runtime state.
170    #[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
190/// Object-safe access to a projectile trait object from default [`Projectile`] methods.
191pub trait ProjectileEventSource {
192    /// Returns this entity as a projectile.
193    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
202/// Vanilla-shaped behavior shared by entities that extend `Projectile`.
203pub trait Projectile: Entity + ProjectileEventSource {
204    /// Returns shared projectile runtime state.
205    fn projectile_base(&self) -> &ProjectileBase;
206
207    /// Returns the horizontal hurt-knockback vector used for this projectile.
208    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    /// Applies bubble-column surface acceleration without clamping velocity.
218    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    /// Applies inside-bubble-column acceleration without clamping velocity.
232    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    /// Sets the persisted owner UUID and clears the live owner cache.
243    fn set_owner_uuid(&self, owner: Option<Uuid>) {
244        self.projectile_base().state.lock().owner = owner.map(EntityReference::from_uuid);
245    }
246
247    /// Sets the owning entity and caches its live reference.
248    fn set_owner_entity(&self, owner: Option<&SharedEntity>) {
249        self.projectile_base().state.lock().owner = owner.map(EntityReference::from_entity);
250    }
251
252    /// Caches a live owner reference when it matches the saved owner UUID.
253    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    /// Returns the owner UUID, if any.
260    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    /// Resolves the cached owner, then looks up its UUID in the current domain.
270    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    /// Returns whether this projectile may interact with the block at `pos`:
277    /// defers to the owning player's build permission, or the mobGriefing
278    /// rule for a non-player owner.
279    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    /// Returns whether this projectile type is allowed to break the blocks
290    /// it hits, per the mobGriefing-gated `projectilesCanBreakBlocks` game rule.
291    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    /// Returns whether `entity` is this projectile's owner.
299    fn owned_by(&self, entity: &dyn Entity) -> bool {
300        self.owner_uuid() == Some(entity.uuid())
301    }
302
303    /// Returns whether this projectile has been shot, as opposed to just spawned.
304    fn has_been_shot(&self) -> bool {
305        self.projectile_base().state.lock().has_been_shot
306    }
307
308    /// Marks whether this projectile has been shot.
309    fn set_has_been_shot(&self, value: bool) {
310        self.projectile_base().state.lock().has_been_shot = value;
311    }
312
313    /// Returns whether this projectile has moved outside its owner's collision range.
314    fn left_owner(&self) -> bool {
315        self.projectile_base().state.lock().left_owner
316    }
317
318    /// Updates whether this projectile has left its owner's collision range,
319    /// but only the first time it's called each tick.
320    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    /// Resets the per-tick left-owner check flag (vanilla clears it after `tick`).
333    fn reset_left_owner_checked(&self) {
334        self.projectile_base().state.lock().left_owner_checked = false;
335    }
336
337    /// Returns whether this projectile's expanded bounding box no longer
338    /// overlaps its owner or any entity riding the same vehicle tree.
339    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    /// Returns vanilla `Projectile.canHitEntity`.
368    /// Use this if you don't need to override.
369    fn can_hit_entity(&self, entity: &dyn Entity) -> bool {
370        self.base_can_hit_entity(entity)
371    }
372
373    /// Returns vanilla `Projectile.canHitEntity`.
374    /// We had to split this up, because Rust doesn't allow for calling super trait fns (without causing unbounded recursion) for overrides, so otherwise vanilla behavior would not be achievable.
375    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    /// Returns the velocity vector for shooting toward `direction` at `power`,
386    /// with random spread scaled by `uncertainty`.
387    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    /// Sets this projectile's velocity and facing rotation toward `direction`
398    /// at `power`, with random spread scaled by `uncertainty`.
399    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    /// Shoots this projectile from `source`'s rotation (offset by `y_offset`),
412    /// adding the source's own movement on top.
413    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    /// Runs vanilla `Projectile.updateRotation` (lerped toward the movement vector).
444    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    /// Applies a Vanilla projectile deflection.
454    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    /// Called after a successful Vanilla projectile deflection.
468    fn on_deflection(&self, _by_attack: bool) {}
469
470    /// Returns whether this projectile bounces from the world border.
471    fn should_bounce_on_world_border(&self) -> bool {
472        false
473    }
474
475    /// Casts the move vector and returns the nearest block/entity hit (vanilla
476    /// `ProjectileUtil.getHitResultOnMoveVector` with `this::canHitEntity`).
477    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    /// Handles a projectile hit, deflecting it off an entity or the world
516    /// border instead of resolving the hit when deflection applies.
517    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    /// Vanilla `Projectile.onHit`. Subclasses override this and call
557    /// [`Projectile::projectile_on_hit`] for the base dispatch (`super.onHit()`).
558    fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
559        self.projectile_on_hit(hit);
560    }
561
562    /// The base `Projectile.onHit` dispatch to block/entity handlers. Not meant to
563    /// be overridden — override [`Projectile::on_hit`] and delegate here instead.
564    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    /// Vanilla `Projectile.onHitEntity` (no-op by default).
606    fn on_hit_entity(self: Arc<Self>, _entity: &SharedEntity, _location: DVec3) {}
607
608    /// Default block-hit hook; concrete projectiles override this and call
609    /// [`Projectile::projectile_on_hit_block`] to preserve the base dispatch.
610    fn on_hit_block(self: Arc<Self>, hit: &ClipHitResult) {
611        self.projectile_on_hit_block(hit);
612    }
613
614    /// The base `Projectile.onHitBlock` implementation. Concrete projectiles
615    /// that override the hook call this to preserve the Java `super` dispatch.
616    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    /// Vanilla `Projectile.hurtServer`: marks hurt unless invulnerable to the
630    /// base checks, but never takes damage.
631    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    /// Vanilla `Projectile.tick` (the `super.tick()` reached from subclasses).
639    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    /// Saves vanilla `Projectile` fields (`Owner`, `LeftOwner`, `HasBeenShot`).
654    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    /// Loads vanilla `Projectile` fields (`Owner`, `LeftOwner`, `HasBeenShot`).
666    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/// Spawns a throwable-item projectile from the source's eye along its look
677/// direction and registers it in the world.
678///
679/// Mirrors vanilla `Projectile.spawnProjectileFromRotation` + `spawnProjectile`:
680/// create the projectile at the eye minus 0.1, set its owner, shoot it with
681/// `shootFromRotation`, add it to the world, then run `applyOnProjectileSpawned`
682/// (the `minecraft:projectile_spawned` enchantment effects). `create` receives
683/// the spawn position and must return the concrete projectile type. Returns
684/// `None` when the world rejects the projectile so the caller can fail the use.
685#[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/// Vanilla `ProjectileUtil.computeMargin`: ramps the entity hit margin from 0 to
731/// 0.3 over the first ticks of flight.
732#[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
737/// Outcome of casting a projectile-style ray along an entity's view vector.
738pub enum ViewVectorHitResult {
739    /// No block or matching entity was hit within range.
740    Miss,
741    /// Nearest hit was a block collider (or world border).
742    Block(ClipHitResult),
743    /// Nearest hit was a matching entity, closer than any block along the ray.
744    Entity(EntityHitResult),
745}
746
747/// Casts from the source eye along the look vector for `distance` blocks using
748/// collider shapes, then prefers a matching entity hit over the block hit.
749#[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
784/// Vanilla `ProjectileUtil.getEntityHitResult` (entity-margin overload): returns
785/// the nearest entity whose inflated box the segment `from -> to` enters.
786fn 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
815/// Clips the segment `from -> to` against `aabb`, returning the entry point.
816///
817/// Returns `from` when the segment starts inside the box and `None` when it never
818/// intersects. Mirrors vanilla `AABB.clip` using the slab method.
819fn 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
864/// Vanilla `Mth.lerp(0.2, rotO, rot)` after wrapping the old angle into range.
865fn 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}