Skip to main content

steel_core/behavior/blocks/vegetation/
leaves_block.rs

1//! Leaves block behavior implementation.
2//!
3use 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
32/// Shared behavior for vanilla leaves blocks.
33pub struct LeavesBlock {
34    block: BlockRef,
35}
36
37impl LeavesBlock {
38    /// Creates a new leaves block behavior.
39    #[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/// Used for cherry tree leaves.
132#[block_behavior]
133pub struct UntintedParticleLeavesBlock {
134    block: BlockRef,
135}
136
137impl UntintedParticleLeavesBlock {
138    /// Creates new `UntintedParticleLeavesBlock` behavior
139    #[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/// Used for oak, spruce, jungle... tree leaves.
174#[block_behavior]
175pub struct TintedParticleLeavesBlock {
176    block: BlockRef,
177}
178
179impl TintedParticleLeavesBlock {
180    /// Creates new `TintedParticleLeavesBlock` behavior
181    #[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/// Mangrove leaves behavior, including hanging propagule growth.
217#[block_behavior]
218pub struct MangroveLeavesBlock {
219    block: BlockRef,
220}
221
222impl MangroveLeavesBlock {
223    /// Creates new `MangroveLeavesBlock` behavior.
224    #[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}