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