steel_core/behavior/blocks/vegetation/
growing_plant_head_block.rs1use rand::{Rng, RngExt, rng};
2use std::sync::Arc;
3use steel_registry::{
4 blocks::{
5 BlockRef,
6 block_state_ext::BlockStateExt,
7 properties::{BlockStateProperties, IntProperty, Property},
8 },
9 vanilla_fluids,
10};
11use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
12
13use crate::{
14 behavior::{
15 BlockBehavior, BlockPlaceContext,
16 blocks::vegetation::{
17 bonemealable::{BonemealAction, Bonemealable},
18 growing_plant_block,
19 },
20 },
21 world::{LevelAccessor, LevelReader, ScheduledTickAccess, World},
22};
23
24pub(crate) struct GrowingPlantHeadBlock {
26 block: BlockRef,
27 growth_direction: Direction,
28 schedule_fluid_ticks: bool,
29 grow_per_tick_probability: f64,
30 body_block: BlockRef,
31 update_body_after_converted_from_head: fn(BlockStateId, BlockStateId) -> BlockStateId,
32 update_grow_into_state: fn(BlockStateId, &mut dyn Rng) -> BlockStateId,
33 get_blocks_to_grow_when_bonemealed: Option<fn(&mut dyn Rng) -> i32>,
34 can_grow_into: fn(BlockStateId) -> bool,
35}
36const AGE: &IntProperty = &BlockStateProperties::AGE_25;
37pub const MAX_AGE: u8 = 25;
39
40pub trait GrowingPlantHeadBehavior: Send + Sync {
42 fn is_max_age(&self, state: BlockStateId) -> bool;
44
45 fn get_max_age_state(&self, state: BlockStateId) -> BlockStateId;
47}
48
49impl GrowingPlantHeadBlock {
50 #[must_use]
52 pub const fn new(
53 block: BlockRef,
54 growth_direction: Direction,
55 schedule_fluid_ticks: bool,
56 grow_per_tick_probability: f64,
57 body_block: BlockRef,
58 get_blocks_to_grow_when_bonemealed: Option<fn(&mut dyn Rng) -> i32>,
59 can_grow_into: fn(BlockStateId) -> bool,
60 ) -> Self {
61 Self {
62 block,
63 growth_direction,
64 schedule_fluid_ticks,
65 grow_per_tick_probability,
66 body_block,
67 update_body_after_converted_from_head: Self::unchanged_converted_state,
68 update_grow_into_state: Self::unchanged_grown_state,
69 get_blocks_to_grow_when_bonemealed,
70 can_grow_into,
71 }
72 }
73
74 #[must_use]
77 pub const fn with_update_body_after_converted_from_head(
78 mut self,
79 update: fn(BlockStateId, BlockStateId) -> BlockStateId,
80 ) -> Self {
81 self.update_body_after_converted_from_head = update;
82 self
83 }
84
85 #[must_use]
87 pub const fn with_update_grow_into_state(
88 mut self,
89 update: fn(BlockStateId, &mut dyn Rng) -> BlockStateId,
90 ) -> Self {
91 self.update_grow_into_state = update;
92 self
93 }
94
95 fn cycle_age(grow_from_state: BlockStateId) -> BlockStateId {
96 let values = AGE.get_possible_values();
97 let current = grow_from_state.get_value(AGE);
98
99 let Some(next_age) = values
100 .iter()
101 .position(|v| *v == current)
102 .map(|i| values[(i + 1) % values.len()])
103 else {
104 return grow_from_state;
105 };
106 grow_from_state.set_value(AGE, next_age)
107 }
108 const fn unchanged_converted_state(
109 _head_state: BlockStateId,
110 body_state: BlockStateId,
111 ) -> BlockStateId {
112 body_state
113 }
114
115 fn unchanged_grown_state(state: BlockStateId, _rng: &mut dyn Rng) -> BlockStateId {
116 state
117 }
118
119 pub fn get_head_state(block: BlockRef, rng: &mut dyn Rng) -> BlockStateId {
120 block
121 .default_state()
122 .set_value(AGE, rng.random_range(0..MAX_AGE))
123 }
124
125 fn state_for_placement(
126 &self,
127 world: &dyn LevelReader,
128 pos: BlockPos,
129 rng: &mut dyn Rng,
130 ) -> BlockStateId {
131 let growth_direction_state = world.get_block_state(pos.relative(self.growth_direction));
132 let growth_direction_block = growth_direction_state.get_block();
133 if growth_direction_block == self.block || growth_direction_block == self.body_block {
134 return self.body_block.default_state();
135 }
136
137 Self::get_head_state(self.block, rng)
138 }
139}
140
141impl BlockBehavior for GrowingPlantHeadBlock {
142 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
143 growing_plant_block::can_survive(
144 world,
145 pos,
146 self.growth_direction,
147 state.get_block(),
148 self.body_block,
149 )
150 }
151 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
152 let mut rng = rng();
153 if state.get_value(AGE) < MAX_AGE && rng.random::<f64>() < self.grow_per_tick_probability {
154 let growth_pos = pos.relative(self.growth_direction);
155 if (self.can_grow_into)(world.get_block_state(growth_pos)) {
156 let grown_state = (self.update_grow_into_state)(Self::cycle_age(state), &mut rng);
157 world.set_block_state(growth_pos, grown_state, UpdateFlags::UPDATE_ALL);
158 }
159 }
160 }
161 fn update_shape(
162 &self,
163 state: BlockStateId,
164 world: &dyn ScheduledTickAccess,
165 pos: BlockPos,
166 direction: Direction,
167 _neighbor_pos: BlockPos,
168 neighbor_state: BlockStateId,
169 ) -> BlockStateId {
170 if direction == self.growth_direction.opposite() {
171 if self.can_survive(state, world, pos) {
172 let neighbor_in_growth_direction =
173 world.get_block_state(pos.relative(self.growth_direction));
174 if neighbor_in_growth_direction.get_block() == self.block
175 || neighbor_in_growth_direction.get_block() == self.body_block
176 {
177 return (self.update_body_after_converted_from_head)(
178 state,
179 self.body_block.default_state(),
180 );
181 }
182 } else {
183 world.schedule_block_tick_default(pos, self.block, 1);
184 }
185 }
186 if direction != self.growth_direction
187 || neighbor_state.get_block() != self.block
188 && neighbor_state.get_block() != self.body_block
189 {
190 if self.schedule_fluid_ticks {
191 world.schedule_fluid_tick_default(
192 pos,
193 &vanilla_fluids::WATER,
194 vanilla_fluids::WATER.tick_delay as i32,
195 );
196 }
197 return state;
198 }
199 (self.update_body_after_converted_from_head)(state, self.body_block.default_state())
200 }
201 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
202 if !self.can_survive(state, world, pos) {
203 world.destroy_block(pos, true);
204 }
205 }
206 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
207 Some(self.state_for_placement(context.world, context.place_pos(), &mut rng()))
208 }
209 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
210 Some(self)
211 }
212
213 fn as_growing_plant_head(&self) -> Option<&dyn GrowingPlantHeadBehavior> {
214 Some(self)
215 }
216}
217
218impl GrowingPlantHeadBehavior for GrowingPlantHeadBlock {
219 fn is_max_age(&self, state: BlockStateId) -> bool {
220 state.get_value(AGE) == MAX_AGE
221 }
222
223 fn get_max_age_state(&self, state: BlockStateId) -> BlockStateId {
224 state.set_value(AGE, MAX_AGE)
225 }
226}
227
228impl Bonemealable for GrowingPlantHeadBlock {
229 fn is_valid_bonemeal_target(
230 &self,
231 _state: BlockStateId,
232 world: &dyn LevelReader,
233 pos: BlockPos,
234 ) -> bool {
235 let growth_pos = pos.relative(self.growth_direction);
236 let state = world.get_block_state(growth_pos);
237 (self.can_grow_into)(state) && !world.is_outside_build_height(growth_pos.y())
238 }
239
240 fn is_bonemeal_success(
241 &self,
242 _state: BlockStateId,
243 _world: &Arc<World>,
244 _rng: &mut dyn Rng,
245 _pos: BlockPos,
246 ) -> bool {
247 true
248 }
249
250 fn perform_bonemeal(
251 &self,
252 state: BlockStateId,
253 world: &Arc<World>,
254 rng: &mut dyn Rng,
255 pos: BlockPos,
256 ) {
257 let mut forward_pos = pos.relative(self.growth_direction);
258 let mut next_age = (state.get_value(AGE) + 1).min(MAX_AGE);
259 let Some(get_blocks_to_grow) = self.get_blocks_to_grow_when_bonemealed else {
260 return;
261 };
262 let blocks_to_grow = get_blocks_to_grow(rng);
263
264 for _ in 0..blocks_to_grow {
265 if !(self.can_grow_into)(world.get_block_state(forward_pos))
266 || world.is_outside_build_height(forward_pos.y())
267 {
268 break;
269 }
270
271 world.set_block(
272 forward_pos,
273 state.set_value(AGE, next_age),
274 UpdateFlags::UPDATE_ALL,
275 );
276 forward_pos = forward_pos.relative(self.growth_direction);
277 next_age = MAX_AGE.min(next_age + 1);
278 }
279 }
280
281 fn bonemeal_action_type(&self) -> BonemealAction {
282 BonemealAction::Grower
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use rand::{SeedableRng as _, rngs::StdRng};
289 use steel_registry::{init_vanilla_registry, vanilla_blocks};
290
291 use super::*;
292 use crate::{behavior::blocks::CaveVinesBlock, test_support::TestLevel};
293
294 #[test]
295 fn connected_placement_uses_body_state() {
296 init_vanilla_registry();
297
298 let behavior = GrowingPlantHeadBlock::new(
299 &vanilla_blocks::CAVE_VINES,
300 Direction::Down,
301 false,
302 0.1,
303 &vanilla_blocks::CAVE_VINES_PLANT,
304 None,
305 CaveVinesBlock::can_grow_into,
306 );
307 let level = TestLevel::default().with_block(
308 BlockPos::ZERO.below(),
309 vanilla_blocks::CAVE_VINES.default_state(),
310 );
311 let mut rng = StdRng::seed_from_u64(1);
312
313 let state = behavior.state_for_placement(&level, BlockPos::ZERO, &mut rng);
314
315 assert_eq!(state, vanilla_blocks::CAVE_VINES_PLANT.default_state());
316 }
317}