Skip to main content

steel_core/behavior/blocks/vegetation/
growing_plant_head_block.rs

1use 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
24/// Shared behavior for growing plant head blocks.
25pub(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;
37/// Maximum age for vanilla growing plant heads.
38pub const MAX_AGE: u8 = 25;
39
40/// Shared behavior exposed by blocks in vanilla's `GrowingPlantHeadBlock` hierarchy.
41pub trait GrowingPlantHeadBehavior: Send + Sync {
42    /// Returns whether the block is at maximum growth age.
43    fn is_max_age(&self, state: BlockStateId) -> bool;
44
45    /// Returns the block state at maximum age.
46    fn get_max_age_state(&self, state: BlockStateId) -> BlockStateId;
47}
48
49impl GrowingPlantHeadBlock {
50    /// Creates a new growing plant head behavior.
51    #[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    /// Sets the per-block callback that computes the new body state when the
75    /// head converts into a body segment (default: leave it unchanged).
76    #[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    /// Configures the block-specific part of vanilla `getGrowIntoState`.
86    #[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}