steel_core/entity/projectile/throwable.rs
1//! Vanilla `ThrowableProjectile` — the gravity/drag movement loop.
2
3use crate::entity::RemovalReason;
4use crate::entity::projectile::Projectile;
5
6/// Vanilla `ThrowableProjectile.getDefaultGravity`.
7const DEFAULT_GRAVITY: f64 = 0.03;
8
9/// Vanilla drag multiplier while submerged (`ThrowableProjectile.applyInertia`).
10const WATER_INERTIA: f64 = 0.8;
11
12/// Vanilla-shaped behavior shared by entities that extend `ThrowableProjectile`.
13pub trait ThrowableProjectile: Projectile {
14 /// Vanilla `ThrowableProjectile.getAirDrag`.
15 fn get_air_drag(&self) -> f32 {
16 0.99
17 }
18
19 /// Vanilla `ThrowableProjectile.getDefaultGravity` (0.03).
20 fn throwable_default_gravity(&self) -> f64 {
21 DEFAULT_GRAVITY
22 }
23
24 /// Vanilla `ThrowableProjectile.applyInertia` (water vs air drag).
25 fn apply_inertia(&self) {
26 let inertia = if self.is_in_water() {
27 // VANILLA CLIENT-LOCAL: `ThrowableProjectile.tick` creates the trailing bubbles.
28 WATER_INERTIA
29 } else {
30 f64::from(self.get_air_drag())
31 };
32 self.set_velocity(self.velocity() * inertia);
33 }
34
35 /// Vanilla `ThrowableProjectile.tick`.
36 ///
37 /// Reached from a subclass's `tick` as `super.tick()`. Applies gravity and
38 /// drag, raycasts the move vector, moves to the hit (or full move), updates
39 /// rotation, runs the `Projectile`/`Entity` base tick, then resolves the hit.
40 fn throwable_projectile_tick(&self) {
41 // Vanilla `Entity.setOldPosAndRot()` is run by the level before ticking;
42 // capture it here so `old_position()`/`old_rotation()` hold the pre-move
43 // state used by `onHit` (teleport target) and `updateRotation` (lerp base).
44 self.set_old_position_to_current();
45 self.base().set_old_rotation_to_current();
46
47 // TODO: handle_first_tick_bubble_column (bubble column shove on spawn).
48 self.apply_gravity();
49 self.apply_inertia();
50
51 let hit = self.get_hit_result_on_move_vector();
52 let new_position = match &hit {
53 Some(result) => result.location(),
54 None => self.position() + self.velocity(),
55 };
56
57 if let Err(error) = self.try_set_position(new_position) {
58 log::debug!("failed to advance projectile {}: {error}", self.id());
59 self.set_removed(RemovalReason::Discarded);
60 return;
61 }
62
63 self.update_rotation();
64 self.apply_effects_from_blocks();
65 self.projectile_base_tick();
66
67 if let Some(result) = hit
68 && self.is_alive()
69 && !self.is_world_change_pending()
70 {
71 self.hit_target_or_deflect_self(&result);
72 }
73 }
74}