steel_core/behavior/blocks/vegetation/
leaves_block.rs1use std::sync::Arc;
4
5use rand::Rng;
6
7use crate::{
8 behavior::{
9 BlockBehavior, BlockPlaceContext, block::schedule_water_tick_if_waterlogged,
10 blocks::vegetation::bonemealable::Bonemealable,
11 },
12 fluid::fluid_state_to_block,
13 world::{LevelReader, ScheduledTickAccess, World},
14};
15use steel_macros::block_behavior;
16use steel_registry::{
17 blocks::{
18 BlockRef,
19 block_state_ext::BlockStateExt as _,
20 properties::{BlockStateProperties, BoolProperty, Direction, IntProperty},
21 },
22 vanilla_block_tags::BlockTag,
23};
24use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
25
26use super::MangrovePropaguleBlock;
27
28const DISTANCE: &IntProperty = &BlockStateProperties::DISTANCE;
29const PERSISTENT: &BoolProperty = &BlockStateProperties::PERSISTENT;
30const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
31
32pub struct LeavesBlock {
34 block: BlockRef,
35}
36
37impl LeavesBlock {
38 #[must_use]
40 pub const fn new(block: BlockRef) -> Self {
41 Self { block }
42 }
43 fn decaying(state: BlockStateId) -> bool {
44 !state.get_value(PERSISTENT) && state.get_value(DISTANCE) == DISTANCE.max
45 }
46
47 fn decayed_replacement(state: BlockStateId) -> BlockStateId {
48 fluid_state_to_block(state.get_fluid_state())
49 }
50
51 fn update_distance(
52 state: BlockStateId,
53 level: &dyn LevelReader,
54 pos: BlockPos,
55 ) -> BlockStateId {
56 let mut new_distance = DISTANCE.max;
57 for direction in Direction::ALL {
58 let mut neighbor_pos = pos;
59 neighbor_pos = neighbor_pos.relative(direction);
60 new_distance =
61 new_distance.min(Self::get_distance_at(level.get_block_state(neighbor_pos)) + 1);
62
63 if new_distance == 1 {
64 break;
65 }
66 }
67 state.set_value(DISTANCE, new_distance)
68 }
69 fn get_distance_at(state: BlockStateId) -> u8 {
70 Self::get_optional_distance_at(state).unwrap_or(DISTANCE.max)
71 }
72 fn get_optional_distance_at(state: BlockStateId) -> Option<u8> {
73 if state
74 .get_block()
75 .has_tag(&BlockTag::PREVENTS_NEARBY_LEAF_DECAY)
76 {
77 return Some(0);
78 }
79 state.try_get_value(DISTANCE)
80 }
81}
82
83impl BlockBehavior for LeavesBlock {
84 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
85 if Self::decaying(state) {
86 world.drop_resources(state, pos);
87 world.set_block(
88 pos,
89 Self::decayed_replacement(state),
90 UpdateFlags::UPDATE_ALL,
91 );
92 }
93 }
94 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
95 world.set_block(
96 pos,
97 Self::update_distance(state, world, pos),
98 UpdateFlags::UPDATE_ALL,
99 );
100 }
101 fn update_shape(
102 &self,
103 state: BlockStateId,
104 world: &dyn ScheduledTickAccess,
105 pos: BlockPos,
106 _direction: Direction,
107 _neighbor_pos: BlockPos,
108 neighbor_state: BlockStateId,
109 ) -> BlockStateId {
110 schedule_water_tick_if_waterlogged(state, world, pos);
111
112 let distance_from_neighbor = Self::get_distance_at(neighbor_state) + 1;
113 if distance_from_neighbor != 1 || state.get_value(DISTANCE) != distance_from_neighbor {
114 world.schedule_block_tick_default(pos, self.block, 1);
115 }
116 state
117 }
118 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
119 let state = self
120 .block
121 .default_state()
122 .set_value(PERSISTENT, true)
123 .set_value(WATERLOGGED, context.is_water_source());
124 Some(Self::update_distance(
125 state,
126 context.world,
127 context.place_pos(),
128 ))
129 }
130}
131#[block_behavior]
133pub struct UntintedParticleLeavesBlock {
134 block: BlockRef,
135}
136
137impl UntintedParticleLeavesBlock {
138 #[must_use]
140 pub const fn new(block: BlockRef) -> Self {
141 Self { block }
142 }
143
144 const fn leaves(&self) -> LeavesBlock {
145 LeavesBlock::new(self.block)
146 }
147}
148
149impl BlockBehavior for UntintedParticleLeavesBlock {
150 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
151 self.leaves().random_tick(state, world, pos);
152 }
153 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
154 self.leaves().tick(state, world, pos);
155 }
156 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
157 self.leaves().get_state_for_placement(context)
158 }
159
160 fn update_shape(
161 &self,
162 state: BlockStateId,
163 world: &dyn ScheduledTickAccess,
164 pos: BlockPos,
165 direction: Direction,
166 neighbor_pos: BlockPos,
167 neighbor_state: BlockStateId,
168 ) -> BlockStateId {
169 self.leaves()
170 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
171 }
172}
173#[block_behavior]
175pub struct TintedParticleLeavesBlock {
176 block: BlockRef,
177}
178
179impl TintedParticleLeavesBlock {
180 #[must_use]
182 pub const fn new(block: BlockRef) -> Self {
183 Self { block }
184 }
185
186 const fn leaves(&self) -> LeavesBlock {
187 LeavesBlock::new(self.block)
188 }
189}
190
191impl BlockBehavior for TintedParticleLeavesBlock {
192 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
193 self.leaves().random_tick(state, world, pos);
194 }
195 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
196 self.leaves().tick(state, world, pos);
197 }
198 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
199 self.leaves().get_state_for_placement(context)
200 }
201
202 fn update_shape(
203 &self,
204 state: BlockStateId,
205 world: &dyn ScheduledTickAccess,
206 pos: BlockPos,
207 direction: Direction,
208 neighbor_pos: BlockPos,
209 neighbor_state: BlockStateId,
210 ) -> BlockStateId {
211 self.leaves()
212 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
213 }
214}
215
216#[block_behavior]
218pub struct MangroveLeavesBlock {
219 block: BlockRef,
220}
221
222impl MangroveLeavesBlock {
223 #[must_use]
225 pub const fn new(block: BlockRef) -> Self {
226 Self { block }
227 }
228
229 const fn leaves(&self) -> LeavesBlock {
230 LeavesBlock::new(self.block)
231 }
232}
233
234impl BlockBehavior for MangroveLeavesBlock {
235 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
236 self.leaves().random_tick(state, world, pos);
237 }
238
239 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
240 self.leaves().tick(state, world, pos);
241 }
242
243 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
244 self.leaves().get_state_for_placement(context)
245 }
246
247 fn update_shape(
248 &self,
249 state: BlockStateId,
250 world: &dyn ScheduledTickAccess,
251 pos: BlockPos,
252 direction: Direction,
253 neighbor_pos: BlockPos,
254 neighbor_state: BlockStateId,
255 ) -> BlockStateId {
256 self.leaves()
257 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
258 }
259
260 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
261 Some(self)
262 }
263}
264
265impl Bonemealable for MangroveLeavesBlock {
266 fn is_valid_bonemeal_target(
267 &self,
268 _state: BlockStateId,
269 world: &dyn LevelReader,
270 pos: BlockPos,
271 ) -> bool {
272 world.get_block_state(pos.below()).is_air()
273 }
274
275 fn perform_bonemeal(
276 &self,
277 _state: BlockStateId,
278 world: &Arc<World>,
279 _rng: &mut dyn Rng,
280 pos: BlockPos,
281 ) {
282 world.set_block(
283 pos.below(),
284 MangrovePropaguleBlock::create_new_hanging_propagule(),
285 UpdateFlags::UPDATE_CLIENTS,
286 );
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use steel_registry::{init_vanilla_registry, vanilla_blocks};
293
294 use crate::{
295 behavior::{BLOCK_BEHAVIORS, init_behaviors},
296 test_support::TestLevel,
297 };
298
299 use super::*;
300
301 #[test]
302 fn waterlogged_leaves_decay_into_water() {
303 init_vanilla_registry();
304 init_behaviors();
305 let state = vanilla_blocks::OAK_LEAVES
306 .default_state()
307 .set_value(WATERLOGGED, true);
308
309 let replacement = LeavesBlock::decayed_replacement(state);
310
311 assert_eq!(replacement.get_block(), &vanilla_blocks::WATER);
312 }
313
314 #[test]
315 fn distance_updates_from_decay_preventing_blocks() {
316 init_vanilla_registry();
317 let level = TestLevel::default().with_block(
318 BlockPos::ZERO.relative(Direction::East),
319 vanilla_blocks::OAK_LOG.default_state(),
320 );
321
322 let updated = LeavesBlock::update_distance(
323 vanilla_blocks::OAK_LEAVES.default_state(),
324 &level,
325 BlockPos::ZERO,
326 );
327
328 assert_eq!(updated.get_value(DISTANCE), 1);
329 }
330
331 #[test]
332 fn mangrove_leaves_register_bonemeal_behavior() {
333 init_vanilla_registry();
334 init_behaviors();
335 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::MANGROVE_LEAVES);
336
337 assert!(behavior.as_bonemealable().is_some());
338 }
339
340 #[test]
341 fn mangrove_leaves_require_air_below_for_bonemeal() {
342 init_vanilla_registry();
343 let behavior = MangroveLeavesBlock::new(&vanilla_blocks::MANGROVE_LEAVES);
344 let state = vanilla_blocks::MANGROVE_LEAVES.default_state();
345 let empty_level = TestLevel::default();
346 assert!(behavior.is_valid_bonemeal_target(state, &empty_level, BlockPos::ZERO));
347
348 let blocked_level = TestLevel::default().with_block(
349 BlockPos::ZERO.below(),
350 vanilla_blocks::STONE.default_state(),
351 );
352 assert!(!behavior.is_valid_bonemeal_target(state, &blocked_level, BlockPos::ZERO));
353 }
354}