steel_core/behavior/blocks/vegetation/
snowy_block.rs1use 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#[block_behavior]
46pub struct SnowyBlock {
47 block: BlockRef,
48}
49
50impl SnowyBlock {
51 #[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}