steel_core/entity/projectile/
throwable.rs1use std::sync::Arc;
3use steel_registry::{blocks::block_state_ext::BlockStateExt as _, vanilla_blocks};
4use steel_utils::{BlockPos, axis::Axis};
5
6use crate::behavior::BLOCK_BEHAVIORS;
7use crate::entity::projectile::Projectile;
8use crate::entity::{InsideBlockEffectCollector, RemovalReason};
9
10const 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(ThrowableProjectile::get_air_drag(self))
34 };
35 self.set_velocity(self.velocity() * inertia);
36 }
37
38 fn throwable_projectile_tick(self: Arc<Self>) {
42 self.set_old_position_to_current();
46 self.base().set_old_rotation_to_current();
47
48 self.handle_first_tick_bubble_column();
49 self.apply_gravity();
50 self.apply_inertia();
51
52 let hit = self.get_hit_result_on_move_vector();
53 let new_position = match &hit {
54 Some(result) => result.location(),
55 None => self.position() + self.velocity(),
56 };
57
58 if let Err(error) = self.try_set_position(new_position) {
59 log::debug!("failed to advance projectile {}: {error}", self.id());
60 self.set_removed(RemovalReason::Discarded);
61 return;
62 }
63
64 self.update_rotation();
65 self.apply_effects_from_blocks();
66 self.projectile_base_tick();
67
68 if let Some(result) = hit
69 && self.is_alive()
70 && !self.is_world_change_pending()
71 {
72 self.hit_target_or_deflect_self(&result);
73 }
74 }
75
76 fn handle_first_tick_bubble_column(&self) {
78 if !self.is_first_tick() {
79 return;
80 }
81
82 let Some(world) = self.level() else {
83 return;
84 };
85
86 let bounds = self.bounding_box();
87 let min = BlockPos::containing(
88 bounds.min(Axis::X),
89 bounds.min(Axis::Y),
90 bounds.min(Axis::Z),
91 );
92 let max = BlockPos::containing(
93 bounds.max(Axis::X),
94 bounds.max(Axis::Y),
95 bounds.max(Axis::Z),
96 );
97
98 let mut ignored_effects = InsideBlockEffectCollector::new();
99
100 for pos in BlockPos::between_closed(min, max) {
101 let state = world.get_block_state(pos);
102 if state.get_block() != &vanilla_blocks::BUBBLE_COLUMN {
103 continue;
104 }
105
106 BLOCK_BEHAVIORS
107 .get_behavior(state.get_block())
108 .entity_inside(
109 state,
110 &world,
111 pos,
112 self.as_entity_event_source(),
113 &mut ignored_effects,
114 true,
115 );
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use glam::DVec3;
123 use std::sync::Arc;
124
125 use steel_registry::blocks::{
126 block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
127 };
128 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_entities};
129 use steel_utils::types::UpdateFlags;
130 use steel_utils::{BlockPos, ChunkPos};
131
132 use crate::behavior::init_behaviors;
133 use crate::entity::entities::SnowballEntity;
134 use crate::entity::{Entity, SharedEntity};
135 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
136
137 use super::ThrowableProjectile as _;
138
139 #[test]
140 fn bubble_column_affects_throwable_projectile_before_its_first_movement() {
141 init_vanilla_registry();
142 init_behaviors();
143
144 let world_fixture = fresh_test_world("throwable_first_tick_bubble_column");
145 let world = &world_fixture.world;
146 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
147
148 let bubble_pos = BlockPos::new(8, 65, 8);
149 let initial_position = DVec3::new(8.5, 65.0, 8.5);
150 let bubble_column = vanilla_blocks::BUBBLE_COLUMN
151 .default_state()
152 .set_value(&BlockStateProperties::DRAG, false);
153
154 assert!(world.set_block(bubble_pos, bubble_column, UpdateFlags::UPDATE_NONE));
155
156 let snowball = Arc::new(SnowballEntity::new(
157 &vanilla_entities::SNOWBALL,
158 1,
159 initial_position,
160 Arc::downgrade(world),
161 ));
162 world
163 .try_add_entity(Arc::clone(&snowball) as SharedEntity)
164 .expect("snowball should attach to the loaded chunk");
165
166 assert!(snowball.is_first_tick());
167
168 Arc::clone(&snowball).tick();
169
170 assert!(!snowball.is_first_tick());
171 assert!(
172 snowball.position().y > initial_position.y,
173 "the first-tick bubble-column effect should push the projectile upward before movement"
174 );
175
176 snowball
177 .try_set_position(initial_position)
178 .expect("snowball should return to its initial position");
179 snowball.set_velocity(DVec3::ZERO);
180
181 Arc::clone(&snowball).tick();
182
183 assert!(
184 snowball.position().y < initial_position.y,
185 "after the first tick, the bubble-column effect should occur after movement"
186 );
187 }
188
189 #[test]
190 fn first_tick_bubble_column_does_not_clamp_projectile_velocity() {
191 init_vanilla_registry();
192 init_behaviors();
193
194 let world_fixture = fresh_test_world("throwable_first_tick_bubble_column_velocity");
195 let world = &world_fixture.world;
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}