Skip to main content

steel_core/behavior/blocks/vegetation/
snowy_block.rs

1use steel_macros::block_behavior;
2use steel_registry::blocks::BlockRef;
3use steel_registry::blocks::block_state_ext::BlockStateExt;
4use steel_registry::blocks::properties::BlockStateProperties;
5use steel_registry::vanilla_block_tags::BlockTag;
6use steel_utils::{BlockPos, BlockStateId, Direction};
7
8use crate::behavior::block::BlockBehavior;
9use crate::behavior::context::BlockPlaceContext;
10use crate::world::ScheduledTickAccess;
11
12#[must_use]
13pub(super) fn is_snowy_setting(above_state: BlockStateId) -> bool {
14    above_state.get_block().has_tag(&BlockTag::SNOW)
15}
16
17#[must_use]
18pub(super) fn snowy_placement_state(
19    block: BlockRef,
20    context: &BlockPlaceContext<'_>,
21) -> BlockStateId {
22    let above = context.world.get_block_state(context.place_pos().above());
23    block
24        .default_state()
25        .set_value(&BlockStateProperties::SNOWY, is_snowy_setting(above))
26}
27
28#[must_use]
29pub(super) fn update_snowy_shape(
30    state: BlockStateId,
31    direction: Direction,
32    neighbor_state: BlockStateId,
33) -> BlockStateId {
34    if direction == Direction::Up {
35        state.set_value(
36            &BlockStateProperties::SNOWY,
37            is_snowy_setting(neighbor_state),
38        )
39    } else {
40        state
41    }
42}
43
44/// Behavior for podzol and other basic snowy dirt blocks.
45#[block_behavior]
46pub struct SnowyBlock {
47    block: BlockRef,
48}
49
50impl SnowyBlock {
51    /// Creates a new snowy block behavior.
52    #[must_use]
53    pub const fn new(block: BlockRef) -> Self {
54        Self { block }
55    }
56}
57
58impl BlockBehavior for SnowyBlock {
59    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
60        Some(snowy_placement_state(self.block, context))
61    }
62
63    fn update_shape(
64        &self,
65        state: BlockStateId,
66        _world: &dyn ScheduledTickAccess,
67        _pos: BlockPos,
68        direction: Direction,
69        _neighbor_pos: BlockPos,
70        neighbor_state: BlockStateId,
71    ) -> BlockStateId {
72        update_snowy_shape(state, direction, neighbor_state)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
79    use steel_registry::item_stack::ItemStack;
80    use steel_registry::{init_vanilla_registry, vanilla_blocks};
81    use steel_utils::types::UpdateFlags;
82    use steel_utils::{BlockPos, ChunkPos, Direction};
83
84    use super::*;
85    use crate::behavior::init_behaviors;
86    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
87
88    const SNOWY: &BoolProperty = &BlockStateProperties::SNOWY;
89
90    #[test]
91    fn placement_resolves_snowy_from_above_block() {
92        init_vanilla_registry();
93        init_behaviors();
94
95        let world_fixture = fresh_test_world("snowy_block_placement");
96        let world = &world_fixture.world;
97        let pos = BlockPos::new(0, 64, 0);
98        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
99
100        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
101
102        for snow in [
103            &vanilla_blocks::SNOW,
104            &vanilla_blocks::SNOW_BLOCK,
105            &vanilla_blocks::POWDER_SNOW,
106        ] {
107            world.set_block(pos.above(), snow.default_state(), UpdateFlags::empty());
108            let mut stack = ItemStack::empty();
109            let context = BlockPlaceContext::directional(
110                world,
111                pos,
112                Direction::Down,
113                &mut stack,
114                Direction::Up,
115            );
116            let state = behavior
117                .get_state_for_placement(&context)
118                .expect("placement state should exist");
119            assert!(state.get_value(SNOWY));
120        }
121
122        for non_snow in [
123            &vanilla_blocks::AIR,
124            &vanilla_blocks::STONE,
125            &vanilla_blocks::ICE,
126        ] {
127            world.set_block(pos.above(), non_snow.default_state(), UpdateFlags::empty());
128            let mut stack = ItemStack::empty();
129            let context = BlockPlaceContext::directional(
130                world,
131                pos,
132                Direction::Down,
133                &mut stack,
134                Direction::Up,
135            );
136            let state = behavior
137                .get_state_for_placement(&context)
138                .expect("placement state should exist");
139            assert!(!state.get_value(SNOWY));
140        }
141    }
142
143    #[test]
144    fn upward_neighbor_update_recalculates_snowy() {
145        init_vanilla_registry();
146        init_behaviors();
147
148        let level = TestLevel::default();
149        let pos = BlockPos::new(0, 64, 0);
150        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
151
152        let base = vanilla_blocks::PODZOL
153            .default_state()
154            .set_value(SNOWY, false);
155        let snowy = vanilla_blocks::PODZOL
156            .default_state()
157            .set_value(SNOWY, true);
158
159        for snow in [
160            &vanilla_blocks::SNOW,
161            &vanilla_blocks::SNOW_BLOCK,
162            &vanilla_blocks::POWDER_SNOW,
163        ] {
164            let state = behavior.update_shape(
165                base,
166                &level,
167                pos,
168                Direction::Up,
169                pos.above(),
170                snow.default_state(),
171            );
172            assert!(state.get_value(SNOWY));
173        }
174
175        for non_snow in [
176            &vanilla_blocks::AIR,
177            &vanilla_blocks::STONE,
178            &vanilla_blocks::ICE,
179        ] {
180            let state = behavior.update_shape(
181                snowy,
182                &level,
183                pos,
184                Direction::Up,
185                pos.above(),
186                non_snow.default_state(),
187            );
188            assert!(!state.get_value(SNOWY));
189        }
190    }
191
192    #[test]
193    fn lateral_and_downward_neighbor_updates_keep_existing_state() {
194        init_vanilla_registry();
195        init_behaviors();
196
197        let level = TestLevel::default();
198        let pos = BlockPos::new(0, 64, 0);
199        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
200
201        let base = vanilla_blocks::PODZOL
202            .default_state()
203            .set_value(SNOWY, false);
204        let snowy = vanilla_blocks::PODZOL
205            .default_state()
206            .set_value(SNOWY, true);
207
208        for dir in [
209            Direction::Down,
210            Direction::North,
211            Direction::South,
212            Direction::East,
213            Direction::West,
214        ] {
215            let neighbor_pos = dir.relative(pos);
216
217            let updated_snowy = behavior.update_shape(
218                snowy,
219                &level,
220                pos,
221                dir,
222                neighbor_pos,
223                vanilla_blocks::AIR.default_state(),
224            );
225            assert_eq!(updated_snowy, snowy);
226
227            let updated_base = behavior.update_shape(
228                base,
229                &level,
230                pos,
231                dir,
232                neighbor_pos,
233                vanilla_blocks::SNOW.default_state(),
234            );
235            assert_eq!(updated_base, base);
236        }
237    }
238}