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    /// Returns vanilla `PowderSnowBlock.canEntityWalkOnPowderSnow`.
54    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}