Skip to main content

steel_core/behavior/blocks/building/
powder_snow_block.rs

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