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