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: &IntProperty = &BlockStateProperties::AGE_1;
27const STAGE: &IntProperty = &BlockStateProperties::STAGE;
28const MAX_BAMBOO_HEIGHT: i32 = 16;
29
30impl BambooStalkBlock {
31    /// Creates a new Bamboo Stalk Behavior
32    #[must_use]
33    pub const fn new(_block: BlockRef) -> Self {
34        Self
35    }
36
37    /// Checks if the Block below is in the tag `BAMBOO_PLANTABLE_ON`
38    pub fn can_survive(world: &dyn LevelReader, pos: BlockPos) -> bool {
39        let block_below = world.get_block_state(pos.below()).get_block();
40        block_below.has_tag(&BlockTag::SUPPORTS_BAMBOO)
41    }
42
43    fn stalk_segments_below(world: &dyn LevelReader, pos: BlockPos) -> i32 {
44        let mut height = 0;
45        while height < MAX_BAMBOO_HEIGHT
46            && world.get_block_state(pos.below_n(height + 1)).get_block() == &vanilla_blocks::BAMBOO
47        {
48            height += 1;
49        }
50
51        height
52    }
53
54    fn stalk_segments_above(world: &dyn LevelReader, pos: BlockPos) -> i32 {
55        let mut height = 0;
56        while height < MAX_BAMBOO_HEIGHT
57            && world.get_block_state(pos.above_n(height + 1)).get_block() == &vanilla_blocks::BAMBOO
58        {
59            height += 1;
60        }
61
62        height
63    }
64
65    fn grow(
66        world: &Arc<World>,
67        pos: BlockPos,
68        state: BlockStateId,
69        rng: &mut dyn rand::Rng,
70        height: i32,
71    ) {
72        let state_below = world.get_block_state(pos.below());
73        let state_two_below = world.get_block_state(pos.below_n(2));
74        let leaves = if height == 0 {
75            BambooLeaves::None
76        } else {
77            let leaves = Self::leaves_for_new_segment(state_below);
78            if leaves == BambooLeaves::Large
79                && state_two_below.get_block() == &vanilla_blocks::BAMBOO
80            {
81                world.set_block(
82                    pos.below(),
83                    state_below.set_value(BAMBOO_LEAVES_PROPERTY, BambooLeaves::Small),
84                    UpdateFlags::UPDATE_ALL,
85                );
86                world.set_block(
87                    pos.below_n(2),
88                    state_two_below.set_value(BAMBOO_LEAVES_PROPERTY, BambooLeaves::None),
89                    UpdateFlags::UPDATE_ALL,
90                );
91            }
92            leaves
93        };
94
95        let new_age = u8::from(
96            state.get_value(AGE) == 1 || state_two_below.get_block() == &vanilla_blocks::BAMBOO,
97        );
98
99        let new_stage = u8::from(
100            height == MAX_BAMBOO_HEIGHT - 1 || (height >= 11 && rng.random::<f32>() < 0.25),
101        );
102
103        world.set_block(
104            pos.above(),
105            vanilla_blocks::BAMBOO
106                .default_state()
107                .set_value(AGE, new_age)
108                .set_value(STAGE, new_stage)
109                .set_value(BAMBOO_LEAVES_PROPERTY, leaves),
110            UpdateFlags::UPDATE_ALL,
111        );
112    }
113
114    fn leaves_for_new_segment(state_below: BlockStateId) -> BambooLeaves {
115        if state_below.get_block() != &vanilla_blocks::BAMBOO
116            || state_below.get_value(BAMBOO_LEAVES_PROPERTY) == BambooLeaves::None
117        {
118            BambooLeaves::Small
119        } else {
120            BambooLeaves::Large
121        }
122    }
123}
124
125impl Bonemealable for BambooStalkBlock {
126    fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn rand::Rng) -> u8 {
127        1 + rng.random_range(0..2)
128    }
129
130    fn is_valid_bonemeal_target(
131        &self,
132        _state: BlockStateId,
133        world: &dyn LevelReader,
134        pos: BlockPos,
135    ) -> bool {
136        let above = Self::stalk_segments_above(world, pos);
137        let below = Self::stalk_segments_below(world, pos);
138        let growth_pos = pos.above_n(above + 1);
139        (above + below + 1 < MAX_BAMBOO_HEIGHT)
140            && world.get_block_state(pos.above_n(above)).get_value(STAGE) != 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 >= MAX_BAMBOO_HEIGHT
161                || state_above.get_value(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(vanilla_blocks::BAMBOO.default_state().set_value(AGE, 0))
193        } else if block_below == &vanilla_blocks::BAMBOO {
194            Some(
195                vanilla_blocks::BAMBOO
196                    .default_state()
197                    .set_value(AGE, state_below.get_value(AGE)),
198            )
199        } else {
200            let state_above = context.world.get_block_state(context.place_pos().above());
201            if state_above.get_block() == &vanilla_blocks::BAMBOO {
202                Some(
203                    vanilla_blocks::BAMBOO
204                        .default_state()
205                        .set_value(AGE, state_above.get_value(AGE)),
206                )
207            } else {
208                Some(vanilla_blocks::BAMBOO_SAPLING.default_state())
209            }
210        }
211    }
212
213    fn tick(&self, _state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
214        if !Self::can_survive(world, pos) {
215            world.destroy_block(pos, true);
216        }
217    }
218
219    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
220        Self::can_survive(world, pos)
221    }
222
223    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
224        if state.get_value(STAGE) != 0 {
225            return;
226        }
227        let mut rng = rand::rng();
228        if rng.random_range(0..3) == 0
229            && world.get_block_state(pos.above()).is_air()
230            && world.raw_brightness(pos.above(), 0) >= 9
231        {
232            let height = Self::stalk_segments_below(world, pos) + 1;
233            if height < 16 {
234                Self::grow(world, pos, state, &mut rng, height);
235            }
236        }
237    }
238
239    fn update_shape(
240        &self,
241        state: BlockStateId,
242        world: &dyn ScheduledTickAccess,
243        pos: BlockPos,
244        direction: steel_utils::Direction,
245        _neighbor_pos: BlockPos,
246        neighbor_state: BlockStateId,
247    ) -> BlockStateId {
248        if !Self::can_survive(world, pos) {
249            world.schedule_block_tick_default(pos, state.get_block(), 1);
250        }
251
252        let age = state.get_value(AGE);
253
254        if direction == Direction::Up
255            && neighbor_state.get_block() == &vanilla_blocks::BAMBOO
256            && neighbor_state.get_value(AGE) > age
257        {
258            return state.set_value(AGE, age.not() & 1); // 0 => 1; 1 => 0
259        }
260        state
261    }
262
263    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
264        Some(self)
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use steel_registry::{init_vanilla_registry, vanilla_blocks};
271
272    use super::*;
273
274    #[test]
275    fn bamboo_growth_does_not_read_leaves_from_non_bamboo_support() {
276        init_vanilla_registry();
277        let dirt = vanilla_blocks::DIRT.default_state();
278
279        assert_eq!(
280            BambooStalkBlock::leaves_for_new_segment(dirt),
281            BambooLeaves::Small
282        );
283    }
284}