steel_core/behavior/blocks/vegetation/
bamboo.rs1use 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#[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 #[must_use]
33 pub const fn new(_block: BlockRef) -> Self {
34 Self
35 }
36
37 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); }
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}