steel_core/behavior/blocks/vegetation/
leaves_block.rs1use std::sync::Arc;
4
5use rand::Rng;
6
7use crate::{
8 behavior::{BlockBehavior, BlockPlaceContext, blocks::vegetation::bonemealable::Bonemealable},
9 fluid::fluid_state_to_block,
10 world::{LevelReader, ScheduledTickAccess, World},
11};
12use steel_macros::block_behavior;
13use steel_registry::{
14 blocks::{
15 BlockRef,
16 block_state_ext::BlockStateExt as _,
17 properties::{BlockStateProperties, BoolProperty, Direction, IntProperty},
18 },
19 vanilla_block_tags::BlockTag,
20 vanilla_fluids,
21};
22use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
23
24use super::MangrovePropaguleBlock;
25
26const DISTANCE: IntProperty = BlockStateProperties::DISTANCE;
27const PERSISTENT: BoolProperty = BlockStateProperties::PERSISTENT;
28const WATERLOGGED: BoolProperty = BlockStateProperties::WATERLOGGED;
29
30pub struct LeavesBlock {
32 block: BlockRef,
33}
34
35impl LeavesBlock {
36 #[must_use]
38 pub const fn new(block: BlockRef) -> Self {
39 Self { block }
40 }
41 fn decaying(state: BlockStateId) -> bool {
42 !state.get_value(&PERSISTENT) && state.get_value(&DISTANCE) == 7
43 }
44
45 fn decayed_replacement(state: BlockStateId) -> BlockStateId {
46 fluid_state_to_block(state.get_fluid_state())
47 }
48
49 fn update_distance(
50 state: BlockStateId,
51 level: &dyn LevelReader,
52 pos: BlockPos,
53 ) -> BlockStateId {
54 let mut new_distance = 7;
55 for direction in Direction::ALL {
56 let mut neighbor_pos = pos;
57 neighbor_pos = neighbor_pos.relative(direction);
58 new_distance =
59 new_distance.min(Self::get_distance_at(level.get_block_state(neighbor_pos)) + 1);
60
61 if new_distance == 1 {
62 break;
63 }
64 }
65 state.set_value(&DISTANCE, new_distance)
66 }
67 fn get_distance_at(state: BlockStateId) -> u8 {
68 Self::get_optional_distance_at(state).unwrap_or(7)
69 }
70 fn get_optional_distance_at(state: BlockStateId) -> Option<u8> {
71 if state
72 .get_block()
73 .has_tag(&BlockTag::PREVENTS_NEARBY_LEAF_DECAY)
74 {
75 return Some(0);
76 }
77 state.try_get_value(&DISTANCE)
78 }
79}
80
81impl BlockBehavior for LeavesBlock {
82 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
83 if Self::decaying(state) {
84 world.drop_resources(state, pos);
85 world.set_block(
86 pos,
87 Self::decayed_replacement(state),
88 UpdateFlags::UPDATE_ALL,
89 );
90 }
91 }
92 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
93 world.set_block(
94 pos,
95 Self::update_distance(state, world, pos),
96 UpdateFlags::UPDATE_ALL,
97 );
98 }
99 fn update_shape(
100 &self,
101 state: BlockStateId,
102 world: &dyn ScheduledTickAccess,
103 pos: BlockPos,
104 _direction: Direction,
105 _neighbor_pos: BlockPos,
106 neighbor_state: BlockStateId,
107 ) -> BlockStateId {
108 if state.get_value(&WATERLOGGED) {
109 let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
110 world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
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}