Skip to main content

steel_core/behavior/blocks/vegetation/
bamboo.rs

1use std::{ops::Not, sync::Arc};
2
3use rand::RngExt;
4use steel_macros::block_behavior;
5use steel_registry::{
6    blocks::{
7        BlockRef,
8        block_state_ext::BlockStateExt,
9        properties::{BambooLeaves, BlockStateProperties, EnumProperty, IntProperty},
10    },
11    vanilla_block_tags::BlockTag,
12    vanilla_blocks,
13};
14use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
15
16use crate::{
17    behavior::{BlockBehavior, BlockPlaceContext, blocks::vegetation::bonemealable::Bonemealable},
18    world::{LevelReader, ScheduledTickAccess, World},
19};
20
21/// Behavior for the Bamboo Stalk Block
22#[block_behavior]
23pub struct BambooStalkBlock;
24
25const BAMBOO_LEAVES_PROPERTY: EnumProperty<BambooLeaves> = BlockStateProperties::BAMBOO_LEAVES;
26const AGE_PROPERTY: IntProperty = BlockStateProperties::AGE_1;
27
28impl BambooStalkBlock {
29    /// Creates a new Bamboo Stalk Behavior
30    #[must_use]
31    pub const fn new(_block: BlockRef) -> Self {
32        Self
33    }
34
35    /// Checks if the Block below is in the tag `BAMBOO_PLANTABLE_ON`
36    pub fn can_survive(world: &dyn LevelReader, pos: BlockPos) -> bool {
37        let block_below = world.get_block_state(pos.below()).get_block();
38        block_below.has_tag(&BlockTag::SUPPORTS_BAMBOO)
39    }
40
41    fn stalk_segments_below(world: &dyn LevelReader, pos: BlockPos) -> i32 {
42        let mut height = 0;
43        while height < 16
44            && world.get_block_state(pos.below_n(height + 1)).get_block() == &vanilla_blocks::BAMBOO
45        {
46            height += 1;
47        }
48
49        height
50    }
51
52    fn stalk_segments_above(world: &dyn LevelReader, pos: BlockPos) -> i32 {
53        let mut height = 0;
54        while height < 16
55            && world.get_block_state(pos.above_n(height + 1)).get_block() == &vanilla_blocks::BAMBOO
56        {
57            height += 1;
58        }
59
60        height
61    }
62
63    fn grow(
64        world: &Arc<World>,
65        pos: BlockPos,
66        state: BlockStateId,
67        rng: &mut dyn rand::Rng,
68        height: i32,
69    ) {
70        let state_below = world.get_block_state(pos.below());
71        let state_two_below = world.get_block_state(pos.below_n(2));
72        let leaves = if height == 0 {
73            BambooLeaves::None
74        } else {
75            let leaves = Self::leaves_for_new_segment(state_below);
76            if leaves == BambooLeaves::Large
77                && state_two_below.get_block() == &vanilla_blocks::BAMBOO
78            {
79                world.set_block(
80                    pos.below(),
81                    state_below.set_value(&BAMBOO_LEAVES_PROPERTY, BambooLeaves::Small),
82                    UpdateFlags::UPDATE_ALL,
83                );
84                world.set_block(
85                    pos.below_n(2),
86                    state_two_below.set_value(&BAMBOO_LEAVES_PROPERTY, BambooLeaves::None),
87                    UpdateFlags::UPDATE_ALL,
88                );
89            }
90            leaves
91        };
92
93        let new_age = u8::from(
94            state.get_value(&AGE_PROPERTY) == 1
95                || state_two_below.get_block() == &vanilla_blocks::BAMBOO,
96        );
97
98        let new_stage = u8::from(height == 15 || (height >= 11 && rng.random::<f32>() < 0.25));
99
100        world.set_block(
101            pos.above(),
102            vanilla_blocks::BAMBOO
103                .default_state()
104                .set_value(&AGE_PROPERTY, new_age)
105                .set_value(&BlockStateProperties::STAGE, new_stage)
106                .set_value(&BlockStateProperties::BAMBOO_LEAVES, leaves),
107            UpdateFlags::UPDATE_ALL,
108        );
109    }
110
111    fn leaves_for_new_segment(state_below: BlockStateId) -> BambooLeaves {
112        if state_below.get_block() != &vanilla_blocks::BAMBOO
113            || state_below.get_value(&BAMBOO_LEAVES_PROPERTY) == BambooLeaves::None
114        {
115            BambooLeaves::Small
116        } else {
117            BambooLeaves::Large
118        }
119    }
120}
121
122impl Bonemealable for BambooStalkBlock {
123    fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn rand::Rng) -> u8 {
124        1 + rng.random_range(0..2)
125    }
126
127    fn is_valid_bonemeal_target(
128        &self,
129        _state: BlockStateId,
130        world: &dyn LevelReader,
131        pos: BlockPos,
132    ) -> bool {
133        let above = Self::stalk_segments_above(world, pos);
134        let below = Self::stalk_segments_below(world, pos);
135        let growth_pos = pos.above_n(above + 1);
136        (above + below + 1 < 16)
137            && world
138                .get_block_state(pos.above_n(above))
139                .get_value(&BlockStateProperties::STAGE)
140                != 1
141            && !world.is_outside_build_height(growth_pos.y())
142            && world.get_block_state(growth_pos).is_air()
143    }
144
145    fn perform_bonemeal(
146        &self,
147        _state: BlockStateId,
148        world: &Arc<World>,
149        rng: &mut dyn rand::Rng,
150        pos: BlockPos,
151    ) {
152        let above = Self::stalk_segments_above(world, pos);
153        let below = Self::stalk_segments_below(world, pos);
154        let total_height = above + below + 1;
155
156        for i in 0..i32::from(self.get_bonemeal_age_increase(world, rng)) {
157            let pos_above = pos.above_n(above + i);
158            let state_above = world.get_block_state(pos_above);
159            let growth_pos = pos_above.above();
160            if total_height + i >= 16
161                || state_above.get_value(&BlockStateProperties::STAGE) == 1
162                || !world.is_in_valid_bounds(growth_pos)
163                || !world.get_block_state(growth_pos).is_air()
164            {
165                return;
166            }
167
168            Self::grow(world, pos_above, state_above, rng, total_height + i);
169        }
170    }
171}
172
173impl BlockBehavior for BambooStalkBlock {
174    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
175        if !context
176            .world
177            .get_block_state(context.place_pos())
178            .get_fluid_state()
179            .is_empty()
180        {
181            return None;
182        }
183
184        let state_below = context.world.get_block_state(context.place_pos().below());
185        let block_below = state_below.get_block();
186
187        if !block_below.has_tag(&BlockTag::SUPPORTS_BAMBOO) {
188            return None;
189        }
190
191        if block_below == &vanilla_blocks::BAMBOO_SAPLING {
192            Some(
193                vanilla_blocks::BAMBOO
194                    .default_state()
195                    .set_value(&AGE_PROPERTY, 0),
196            )
197        } else if block_below == &vanilla_blocks::BAMBOO {
198            Some(vanilla_blocks::BAMBOO.default_state().set_value(
199                &AGE_PROPERTY,
200                state_below.get_value(&BlockStateProperties::AGE_1),
201            ))
202        } else {
203            let state_above = context.world.get_block_state(context.place_pos().above());
204            if state_above.get_block() == &vanilla_blocks::BAMBOO {
205                Some(
206                    vanilla_blocks::BAMBOO
207                        .default_state()
208                        .set_value(&AGE_PROPERTY, state_above.get_value(&AGE_PROPERTY)),
209                )
210            } else {
211                Some(vanilla_blocks::BAMBOO_SAPLING.default_state())
212            }
213        }
214    }
215
216    fn tick(&self, _state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
217        if !Self::can_survive(world, pos) {
218            world.destroy_block(pos, true);
219        }
220    }
221
222    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
223        Self::can_survive(world, pos)
224    }
225
226    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
227        if state.get_value(&BlockStateProperties::STAGE) != 0 {
228            return;
229        }
230        let mut rng = rand::rng();
231        if rng.random_range(0..3) == 0
232            && world.get_block_state(pos.above()).is_air()
233            && world.raw_brightness(pos.above(), 0) >= 9
234        {
235            let height = Self::stalk_segments_below(world, pos) + 1;
236            if height < 16 {
237                Self::grow(world, pos, state, &mut rng, height);
238            }
239        }
240    }
241
242    fn update_shape(
243        &self,
244        state: BlockStateId,
245        world: &dyn ScheduledTickAccess,
246        pos: BlockPos,
247        direction: steel_utils::Direction,
248        _neighbor_pos: BlockPos,
249        neighbor_state: BlockStateId,
250    ) -> BlockStateId {
251        if !Self::can_survive(world, pos) {
252            world.schedule_block_tick_default(pos, state.get_block(), 1);
253        }
254
255        let age = state.get_value(&AGE_PROPERTY);
256
257        if direction == Direction::Up
258            && neighbor_state.get_block() == &vanilla_blocks::BAMBOO
259            && neighbor_state.get_value(&AGE_PROPERTY) > age
260        {
261            return state.set_value(&AGE_PROPERTY, age.not() & 1); // 0 => 1; 1 => 0
262        }
263        state
264    }
265
266    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
267        Some(self)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use steel_registry::{init_vanilla_registry, vanilla_blocks};
274
275    use super::*;
276
277    #[test]
278    fn bamboo_growth_does_not_read_leaves_from_non_bamboo_support() {
279        init_vanilla_registry();
280        let dirt = vanilla_blocks::DIRT.default_state();
281
282        assert_eq!(
283            BambooStalkBlock::leaves_for_new_segment(dirt),
284            BambooLeaves::Small
285        );
286    }
287}