1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_macros::block_behavior;
5use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt as _, shapes::VoxelShape};
6use steel_registry::item_stack::ItemStack;
7use steel_registry::sound_event::SoundEventRef;
8use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
9use steel_registry::{
10 REGISTRY, TaggedRegistryExt, sound_events, vanilla_blocks, vanilla_entities,
11 vanilla_game_rules, vanilla_items,
12};
13use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId, types::UpdateFlags};
14
15use crate::{
16 behavior::block::PickupResult,
17 behavior::{
18 BlockBehavior, BlockCollisionContext, BlockPlaceContext, EntityFallDamage,
19 EntityFallOnContext,
20 },
21 entity::ai::path::PathComputationType,
22 entity::{Entity, InsideBlockEffectCollector, InsideBlockEffectType},
23 inventory::equipment::EquipmentSlot,
24 player::Player,
25 world::{ConditionalBlockSetResult, LevelReader, World},
26};
27
28const IN_BLOCK_SPEED_MULTIPLIER: DVec3 = DVec3::new(0.9, 1.5, 0.9);
29const NUM_BLOCKS_TO_FALL_INTO_BLOCK: f64 = 2.5;
30const MIN_FALL_DISTANCE_FOR_SOUND: f64 = 4.0;
31const MIN_FALL_DISTANCE_FOR_BIG_SOUND: f64 = 7.0;
32const FALLING_COLLISION_BOXES: &[BlockLocalAabb] =
33 &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.9, 1.0)];
34const FALLING_COLLISION_SHAPE: VoxelShape = VoxelShape::from_boxes(FALLING_COLLISION_BOXES);
35
36#[block_behavior]
42pub struct PowderSnowBlock {
43 block: BlockRef,
44}
45
46impl PowderSnowBlock {
47 #[must_use]
49 pub const fn new(block: BlockRef) -> Self {
50 Self { block }
51 }
52
53 pub(crate) fn can_entity_walk_on_powder_snow<E: Entity + ?Sized>(entity: &E) -> bool {
55 if REGISTRY.entity_types.is_in_tag(
56 entity.entity_type(),
57 &EntityTypeTag::POWDER_SNOW_WALKABLE_MOBS,
58 ) {
59 return true;
60 }
61
62 let Some(living) = entity.as_living_entity() else {
63 return false;
64 };
65 let mut has_leather_boots = false;
66 living.with_equipment_slot(EquipmentSlot::Feet, &mut |item_stack| {
67 has_leather_boots = item_stack.is(&vanilla_items::LEATHER_BOOTS);
68 });
69 has_leather_boots
70 }
71
72 #[must_use]
73 fn fall_sound(context: EntityFallOnContext<'_>) -> Option<SoundEventRef> {
74 if context.fall_distance < MIN_FALL_DISTANCE_FOR_SOUND || !context.entity.is_living_entity {
75 return None;
76 }
77
78 let (small, big) = context.entity.fall_sounds;
79 Some(if context.fall_distance < MIN_FALL_DISTANCE_FOR_BIG_SOUND {
80 small
81 } else {
82 big
83 })
84 }
85}
86
87impl BlockBehavior for PowderSnowBlock {
88 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
89 Some(self.block.default_state())
90 }
91
92 fn pickup_block(
93 &self,
94 world: &Arc<World>,
95 pos: BlockPos,
96 state: BlockStateId,
97 _player: Option<&Player>,
98 ) -> Option<PickupResult> {
99 if world.set_block_if_unchanged(
100 pos,
101 state,
102 vanilla_blocks::AIR.default_state(),
103 UpdateFlags::UPDATE_ALL_IMMEDIATE,
104 ) != ConditionalBlockSetResult::Changed
105 {
106 return None;
107 }
108
109 world.destroy_block_effect(pos, u32::from(state.0), None);
110
111 Some(PickupResult {
112 filled_bucket: ItemStack::new(&vanilla_items::POWDER_SNOW_BUCKET),
113 sound: Some(&sound_events::ITEM_BUCKET_FILL_POWDER_SNOW),
114 })
115 }
116
117 fn is_pathfindable(
118 &self,
119 _state: BlockStateId,
120 _computation_type: PathComputationType,
121 ) -> bool {
122 true
123 }
124
125 fn fall_on(
126 &self,
127 _state: BlockStateId,
128 _world: &Arc<World>,
129 _pos: BlockPos,
130 context: EntityFallOnContext<'_>,
131 ) -> Option<EntityFallDamage> {
132 if let Some(sound) = Self::fall_sound(context)
133 && let Some(entity) = context.source_entity()
134 {
135 entity.play_sound(sound, 1.0, 1.0);
136 }
137
138 None
139 }
140
141 fn get_entity_inside_collision_shape(
142 &self,
143 state: BlockStateId,
144 world: &dyn LevelReader,
145 pos: BlockPos,
146 entity: &dyn Entity,
147 ) -> VoxelShape {
148 let collision_shape = self.get_collision_shape(
149 state,
150 world,
151 pos,
152 BlockCollisionContext::entity(entity.position().y, entity.is_descending())
153 .with_fall_distance(entity.fall_distance())
154 .with_can_walk_on_powder_snow(Self::can_entity_walk_on_powder_snow(entity))
155 .with_falling_block(entity.entity_type() == &vanilla_entities::FALLING_BLOCK),
156 );
157 if collision_shape.is_empty() {
158 self.default_get_entity_inside_collision_shape(state, world, pos, entity)
159 } else {
160 collision_shape
161 }
162 }
163
164 fn get_collision_shape(
165 &self,
166 state: BlockStateId,
167 world: &dyn LevelReader,
168 pos: BlockPos,
169 context: BlockCollisionContext,
170 ) -> VoxelShape {
171 if context.is_placement() {
172 return VoxelShape::EMPTY;
173 }
174 if context.fall_distance() > NUM_BLOCKS_TO_FALL_INTO_BLOCK {
175 return FALLING_COLLISION_SHAPE;
176 }
177 if context.is_falling_block()
178 || (context.can_walk_on_powder_snow()
179 && context.is_above(VoxelShape::FULL_BLOCK, pos, false)
180 && !context.is_descending())
181 {
182 return self.default_get_collision_shape(state, world, pos, context);
183 }
184
185 VoxelShape::EMPTY
186 }
187
188 fn entity_inside(
189 &self,
190 state: BlockStateId,
191 world: &Arc<World>,
192 pos: BlockPos,
193 entity: &dyn Entity,
194 effect_collector: &mut InsideBlockEffectCollector,
195 _is_precise: bool,
196 ) {
197 if !entity.is_living_entity() || entity.in_block_state(world).get_block() == self.block {
198 entity.make_stuck_in_block(state, IN_BLOCK_SPEED_MULTIPLIER);
199 }
200
201 let world = Arc::clone(world);
202 effect_collector.run_before(
203 InsideBlockEffectType::Extinguish,
204 Box::new(move |entity| {
205 if !entity.is_on_fire() {
206 return;
207 }
208
209 let mob_griefing = world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING);
210 if (mob_griefing || entity.entity_type() == &vanilla_entities::PLAYER)
211 && entity.may_interact(world.as_ref(), pos)
212 {
213 world.destroy_block(pos, false);
214 }
215 }),
216 );
217 effect_collector.apply(InsideBlockEffectType::Freeze);
218 effect_collector.apply(InsideBlockEffectType::Extinguish);
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks, vanilla_entities};
227
228 use crate::behavior::EntityFallOnFacts;
229 use crate::test_support::TestLevel;
230
231 fn empty_level() -> TestLevel {
232 TestLevel::default().with_min_y(0)
233 }
234
235 fn powder_snow() -> PowderSnowBlock {
236 PowderSnowBlock::new(&vanilla_blocks::POWDER_SNOW)
237 }
238
239 fn powder_snow_state() -> BlockStateId {
240 vanilla_blocks::POWDER_SNOW.default_state()
241 }
242
243 fn fall_context(fall_distance: f64, is_living_entity: bool) -> EntityFallOnContext<'static> {
244 EntityFallOnContext::new(
245 fall_distance,
246 false,
247 EntityFallOnFacts::new(
248 &vanilla_entities::PLAYER,
249 is_living_entity,
250 0.6,
251 1.8,
252 (
253 &sound_events::ENTITY_PLAYER_SMALL_FALL,
254 &sound_events::ENTITY_PLAYER_BIG_FALL,
255 ),
256 ),
257 None,
258 )
259 }
260
261 #[test]
262 fn powder_snow_fall_sound_uses_vanilla_living_thresholds() {
263 init_vanilla_registry();
264 assert!(PowderSnowBlock::fall_sound(fall_context(3.99, true)).is_none());
265 assert_eq!(
266 PowderSnowBlock::fall_sound(fall_context(4.0, true)),
267 Some(&sound_events::ENTITY_PLAYER_SMALL_FALL)
268 );
269 assert_eq!(
270 PowderSnowBlock::fall_sound(fall_context(7.0, true)),
271 Some(&sound_events::ENTITY_PLAYER_BIG_FALL)
272 );
273 assert!(PowderSnowBlock::fall_sound(fall_context(7.0, false)).is_none());
274 }
275
276 #[test]
277 fn falling_entities_collide_with_lower_powder_snow_shape() {
278 init_vanilla_registry();
279 let behavior = powder_snow();
280 let state = powder_snow_state();
281 let pos = BlockPos::new(0, 64, 0);
282
283 let shape = behavior.get_collision_shape(
284 state,
285 &empty_level(),
286 pos,
287 BlockCollisionContext::entity(64.0, false)
288 .with_fall_distance(NUM_BLOCKS_TO_FALL_INTO_BLOCK + 0.01),
289 );
290
291 assert_eq!(shape, FALLING_COLLISION_SHAPE);
292 }
293
294 #[test]
295 fn walkable_entities_use_default_powder_snow_collision_shape_when_above() {
296 init_vanilla_registry();
297 let behavior = powder_snow();
298 let state = powder_snow_state();
299 let pos = BlockPos::new(0, 64, 0);
300 let context = BlockCollisionContext::entity(65.0, false).with_can_walk_on_powder_snow(true);
301
302 let shape = behavior.get_collision_shape(state, &empty_level(), pos, context);
303
304 assert_eq!(
305 shape,
306 behavior.default_get_collision_shape(state, &empty_level(), pos, context)
307 );
308 }
309
310 #[test]
311 fn non_walkable_or_descending_entities_have_no_powder_snow_collision() {
312 init_vanilla_registry();
313 let behavior = powder_snow();
314 let state = powder_snow_state();
315 let pos = BlockPos::new(0, 64, 0);
316
317 let non_walkable_shape = behavior.get_collision_shape(
318 state,
319 &empty_level(),
320 pos,
321 BlockCollisionContext::entity(65.0, false),
322 );
323 let descending_shape = behavior.get_collision_shape(
324 state,
325 &empty_level(),
326 pos,
327 BlockCollisionContext::entity(65.0, true).with_can_walk_on_powder_snow(true),
328 );
329
330 assert_eq!(non_walkable_shape, VoxelShape::EMPTY);
331 assert_eq!(descending_shape, VoxelShape::EMPTY);
332 }
333
334 #[test]
335 fn falling_blocks_use_default_powder_snow_collision_shape() {
336 init_vanilla_registry();
337 let behavior = powder_snow();
338 let state = powder_snow_state();
339 let pos = BlockPos::new(0, 64, 0);
340 let context = BlockCollisionContext::entity(64.0, true).with_falling_block(true);
341
342 let shape = behavior.get_collision_shape(state, &empty_level(), pos, context);
343
344 assert_eq!(
345 shape,
346 behavior.default_get_collision_shape(state, &empty_level(), pos, context)
347 );
348 }
349
350 #[test]
351 fn placement_context_has_no_powder_snow_collision() {
352 init_vanilla_registry();
353 let behavior = powder_snow();
354 let state = powder_snow_state();
355 let pos = BlockPos::new(0, 64, 0);
356
357 let shape = behavior.get_collision_shape(
358 state,
359 &empty_level(),
360 pos,
361 BlockCollisionContext::with_position(65.0, false)
362 .with_can_walk_on_powder_snow(true)
363 .with_fall_distance(NUM_BLOCKS_TO_FALL_INTO_BLOCK + 0.01),
364 );
365
366 assert_eq!(shape, VoxelShape::EMPTY);
367 }
368}