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_can_survive,
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
38impl GrowingPlantHeadBlock {
39    /// Creates a new growing plant head behavior.
40    #[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    /// Configures the vanilla `updateBodyAfterConvertedFromHead` specialization.
64    #[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    /// Configures the block-specific part of vanilla `getGrowIntoState`.
74    #[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}