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 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    /// Vanilla `GrowingPlantHeadBlock.isMaxAge`.
43    fn is_max_age(&self, state: BlockStateId) -> bool;
44
45    /// Vanilla `GrowingPlantHeadBlock.getMaxAgeState`.
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    /// Configures the vanilla `updateBodyAfterConvertedFromHead` specialization.
75    #[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    /// Configures the block-specific part of vanilla `getGrowIntoState`.
85    #[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}