steel_core/entity/projectile/
throwable.rs1use steel_registry::{blocks::block_state_ext::BlockStateExt as _, vanilla_blocks};
3use steel_utils::{BlockPos, axis::Axis};
4
5use crate::behavior::BLOCK_BEHAVIORS;
6use crate::entity::projectile::Projectile;
7use crate::entity::{InsideBlockEffectCollector, RemovalReason};
8
9const DEFAULT_GRAVITY: f64 = 0.03;
11
12const WATER_INERTIA: f64 = 0.8;
14
15pub trait ThrowableProjectile: Projectile {
17 fn get_air_drag(&self) -> f32 {
19 0.99
20 }
21
22 fn throwable_default_gravity(&self) -> f64 {
24 DEFAULT_GRAVITY
25 }
26
27 fn apply_inertia(&self) {
29 let inertia = if self.is_in_water() {
30 WATER_INERTIA
32 } else {
33 f64::from(self.get_air_drag())
34 };
35 self.set_velocity(self.velocity() * inertia);
36 }
37
38 fn throwable_projectile_tick(&self) {
44 self.set_old_position_to_current();
48 self.base().set_old_rotation_to_current();
49
50 self.handle_first_tick_bubble_column();
51 self.apply_gravity();
52 self.apply_inertia();
53
54 let hit = self.get_hit_result_on_move_vector();
55 let new_position = match &hit {
56 Some(result) => result.location(),
57 None => self.position() + self.velocity(),
58 };
59
60 if let Err(error) = self.try_set_position(new_position) {
61 log::debug!("failed to advance projectile {}: {error}", self.id());
62 self.set_removed(RemovalReason::Discarded);
63 return;
64 }
65
66 self.update_rotation();
67 self.apply_effects_from_blocks();
68 self.projectile_base_tick();
69
70 if let Some(result) = hit
71 && self.is_alive()
72 && !self.is_world_change_pending()
73 {
74 self.hit_target_or_deflect_self(&result);
75 }
76 }
77
78 fn handle_first_tick_bubble_column(&self) {
80 if !self.is_first_tick() {
81 return;
82 }
83
84 let Some(world) = self.level() else {
85 return;
86 };
87
88 let bounds = self.bounding_box();
89 let min = BlockPos::containing(
90 bounds.min(Axis::X),
91 bounds.min(Axis::Y),
92 bounds.min(Axis::Z),
93 );
94 let max = BlockPos::containing(
95 bounds.max(Axis::X),
96 bounds.max(Axis::Y),
97 bounds.max(Axis::Z),
98 );
99
100 let mut ignored_effects = InsideBlockEffectCollector::new();
101
102 for pos in BlockPos::between_closed(min, max) {
103 let state = world.get_block_state(pos);
104 if state.get_block() != &vanilla_blocks::BUBBLE_COLUMN {
105 continue;
106 }
107
108 BLOCK_BEHAVIORS
109 .get_behavior(state.get_block())
110 .entity_inside(
111 state,
112 &world,
113 pos,
114 self.as_entity_event_source(),
115 &mut ignored_effects,
116 true,
117 );
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use glam::DVec3;
125 use std::sync::Arc;
126
127 use steel_registry::blocks::{
128 block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
129 };
130 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_entities};
131 use steel_utils::types::UpdateFlags;
132 use steel_utils::{BlockPos, ChunkPos};
133
134 use crate::behavior::init_behaviors;
135 use crate::entity::entities::SnowballEntity;
136 use crate::entity::{Entity, SharedEntity};
137 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
138
139 use super::ThrowableProjectile as _;
140
141 #[test]
142 fn bubble_column_affects_throwable_projectile_before_its_first_movement() {
143 init_vanilla_registry();
144 init_behaviors();
145
146 let world = fresh_test_world("throwable_first_tick_bubble_column");
147 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
148
149 let bubble_pos = BlockPos::new(8, 65, 8);
150 let initial_position = DVec3::new(8.5, 65.0, 8.5);
151 let bubble_column = vanilla_blocks::BUBBLE_COLUMN
152 .default_state()
153 .set_value(&BlockStateProperties::DRAG, false);
154
155 assert!(world.set_block(bubble_pos, bubble_column, UpdateFlags::UPDATE_NONE));
156
157 let snowball = Arc::new(SnowballEntity::new(
158 &vanilla_entities::SNOWBALL,
159 1,
160 initial_position,
161 Arc::downgrade(&world),
162 ));
163 world
164 .try_add_entity(Arc::clone(&snowball) as SharedEntity)
165 .expect("snowball should attach to the loaded chunk");
166
167 assert!(snowball.is_first_tick());
168
169 snowball.tick();
170
171 assert!(!snowball.is_first_tick());
172 assert!(
173 snowball.position().y > initial_position.y,
174 "the first-tick bubble-column effect should push the projectile upward before movement"
175 );
176
177 snowball
178 .try_set_position(initial_position)
179 .expect("snowball should return to its initial position");
180 snowball.set_velocity(DVec3::ZERO);
181
182 snowball.tick();
183
184 assert!(
185 snowball.position().y < initial_position.y,
186 "after the first tick, the bubble-column effect should occur after movement"
187 );
188 }
189
190 #[test]
191 fn first_tick_bubble_column_does_not_clamp_projectile_velocity() {
192 init_vanilla_registry();
193 init_behaviors();
194
195 let world = fresh_test_world("throwable_first_tick_bubble_column_velocity");
196 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
197
198 let bubble_pos = BlockPos::new(8, 65, 8);
199 let initial_position = DVec3::new(8.5, 65.0, 8.5);
200
201 for (open_above, drag_down, initial_y, expected_y) in [
202 (true, false, 2.0, 2.1),
203 (true, true, -2.0, -2.03),
204 (false, false, 2.0, 2.06),
205 (false, true, -2.0, -2.03),
206 ] {
207 let bubble_column = vanilla_blocks::BUBBLE_COLUMN
208 .default_state()
209 .set_value(&BlockStateProperties::DRAG, drag_down);
210
211 assert!(world.set_block(bubble_pos, bubble_column, UpdateFlags::UPDATE_NONE,));
212
213 let above_state = if open_above {
214 vanilla_blocks::AIR.default_state()
215 } else {
216 vanilla_blocks::WATER.default_state()
217 };
218
219 world.set_block(bubble_pos.above(), above_state, UpdateFlags::UPDATE_NONE);
221
222 let snowball = SnowballEntity::new(
223 &vanilla_entities::SNOWBALL,
224 1,
225 initial_position,
226 Arc::downgrade(&world),
227 );
228 snowball.set_velocity(DVec3::new(0.25, initial_y, -0.25));
229
230 assert!(snowball.is_first_tick());
231 snowball.handle_first_tick_bubble_column();
232
233 let expected = DVec3::new(0.25, expected_y, -0.25);
234 let actual = snowball.velocity();
235
236 assert!(
237 (actual - expected).abs().max_element() < 1.0e-12,
238 "open_above={open_above}, drag_down={drag_down}: \
239 expected {expected:?}, got {actual:?}"
240 );
241 }
242 }
243}