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