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_PROPERTY: IntProperty = BlockStateProperties::AGE_1;
27
28impl BambooStalkBlock {
29 #[must_use]
31 pub const fn new(_block: BlockRef) -> Self {
32 Self
33 }
34
35 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); }
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}