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 = fresh_test_world("snowy_block_placement");
96        let pos = BlockPos::new(0, 64, 0);
97        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
98
99        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
100
101        for snow in [
102            &vanilla_blocks::SNOW,
103            &vanilla_blocks::SNOW_BLOCK,
104            &vanilla_blocks::POWDER_SNOW,
105        ] {
106            world.set_block(pos.above(), snow.default_state(), UpdateFlags::empty());
107            let mut stack = ItemStack::empty();
108            let context = BlockPlaceContext::directional(
109                &world,
110                pos,
111                Direction::Down,
112                &mut stack,
113                Direction::Up,
114            );
115            let state = behavior
116                .get_state_for_placement(&context)
117                .expect("placement state should exist");
118            assert!(state.get_value(SNOWY));
119        }
120
121        for non_snow in [
122            &vanilla_blocks::AIR,
123            &vanilla_blocks::STONE,
124            &vanilla_blocks::ICE,
125        ] {
126            world.set_block(pos.above(), non_snow.default_state(), UpdateFlags::empty());
127            let mut stack = ItemStack::empty();
128            let context = BlockPlaceContext::directional(
129                &world,
130                pos,
131                Direction::Down,
132                &mut stack,
133                Direction::Up,
134            );
135            let state = behavior
136                .get_state_for_placement(&context)
137                .expect("placement state should exist");
138            assert!(!state.get_value(SNOWY));
139        }
140    }
141
142    #[test]
143    fn upward_neighbor_update_recalculates_snowy() {
144        init_vanilla_registry();
145        init_behaviors();
146
147        let level = TestLevel::default();
148        let pos = BlockPos::new(0, 64, 0);
149        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
150
151        let base = vanilla_blocks::PODZOL
152            .default_state()
153            .set_value(SNOWY, false);
154        let snowy = vanilla_blocks::PODZOL
155            .default_state()
156            .set_value(SNOWY, true);
157
158        for snow in [
159            &vanilla_blocks::SNOW,
160            &vanilla_blocks::SNOW_BLOCK,
161            &vanilla_blocks::POWDER_SNOW,
162        ] {
163            let state = behavior.update_shape(
164                base,
165                &level,
166                pos,
167                Direction::Up,
168                pos.above(),
169                snow.default_state(),
170            );
171            assert!(state.get_value(SNOWY));
172        }
173
174        for non_snow in [
175            &vanilla_blocks::AIR,
176            &vanilla_blocks::STONE,
177            &vanilla_blocks::ICE,
178        ] {
179            let state = behavior.update_shape(
180                snowy,
181                &level,
182                pos,
183                Direction::Up,
184                pos.above(),
185                non_snow.default_state(),
186            );
187            assert!(!state.get_value(SNOWY));
188        }
189    }
190
191    #[test]
192    fn lateral_and_downward_neighbor_updates_keep_existing_state() {
193        init_vanilla_registry();
194        init_behaviors();
195
196        let level = TestLevel::default();
197        let pos = BlockPos::new(0, 64, 0);
198        let behavior = SnowyBlock::new(&vanilla_blocks::PODZOL);
199
200        let base = vanilla_blocks::PODZOL
201            .default_state()
202            .set_value(SNOWY, false);
203        let snowy = vanilla_blocks::PODZOL
204            .default_state()
205            .set_value(SNOWY, true);
206
207        for dir in [
208            Direction::Down,
209            Direction::North,
210            Direction::South,
211            Direction::East,
212            Direction::West,
213        ] {
214            let neighbor_pos = dir.relative(pos);
215
216            let updated_snowy = behavior.update_shape(
217                snowy,
218                &level,
219                pos,
220                dir,
221                neighbor_pos,
222                vanilla_blocks::AIR.default_state(),
223            );
224            assert_eq!(updated_snowy, snowy);
225
226            let updated_base = behavior.update_shape(
227                base,
228                &level,
229                pos,
230                dir,
231                neighbor_pos,
232                vanilla_blocks::SNOW.default_state(),
233            );
234            assert_eq!(updated_base, base);
235        }
236    }
237}