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::{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
30/// Shared behavior for vanilla leaves blocks.
31pub struct LeavesBlock {
32    block: BlockRef,
33}
34
35impl LeavesBlock {
36    /// Creates a new leaves block behavior.
37    #[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/// 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}