Skip to main content

steel_core/behavior/blocks/vegetation/
growing_plant_body_block.rs

1use rand::{Rng, rng};
2use std::sync::Arc;
3use steel_registry::{
4    REGISTRY,
5    blocks::{BlockRef, block_state_ext::BlockStateExt},
6    vanilla_fluids,
7};
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::{
11    behavior::{
12        BLOCK_BEHAVIORS, BlockBehavior, BlockPlaceContext,
13        block::default_can_be_replaced,
14        blocks::vegetation::{
15            bonemealable::{BonemealAction, Bonemealable},
16            get_top_connected_block, growing_plant_can_survive,
17            growing_plant_head_block::GrowingPlantHeadBlock,
18        },
19    },
20    world::{LevelReader, ScheduledTickAccess, World},
21};
22
23/// Shared behavior for growing plant blocks.
24pub struct GrowingPlantBodyBlock {
25    block: BlockRef,
26    growth_direction: Direction,
27    schedule_fluid_ticks: bool,
28    head_block: BlockRef,
29    update_head_after_converted_from_body: fn(BlockStateId, BlockStateId) -> BlockStateId,
30    can_grow_into: fn(BlockStateId) -> bool,
31}
32
33impl GrowingPlantBodyBlock {
34    /// Creates a new growing plant body behavior.
35    #[must_use]
36    pub const fn new(
37        block: BlockRef,
38        growth_direction: Direction,
39        schedule_fluid_ticks: bool,
40        head_block: BlockRef,
41        can_grow_into: fn(BlockStateId) -> bool,
42    ) -> Self {
43        Self {
44            block,
45            growth_direction,
46            schedule_fluid_ticks,
47            head_block,
48            update_head_after_converted_from_body: Self::unchanged_converted_state,
49            can_grow_into,
50        }
51    }
52
53    /// Configures the vanilla `updateHeadAfterConvertedFromBody` specialization.
54    #[must_use]
55    pub const fn with_update_head_after_converted_from_body(
56        mut self,
57        update: fn(BlockStateId, BlockStateId) -> BlockStateId,
58    ) -> Self {
59        self.update_head_after_converted_from_body = update;
60        self
61    }
62
63    const fn unchanged_converted_state(
64        _body_state: BlockStateId,
65        head_state: BlockStateId,
66    ) -> BlockStateId {
67        head_state
68    }
69    fn get_head_pos(
70        &self,
71        world: &dyn LevelReader,
72        pos: BlockPos,
73        body_block: BlockRef,
74    ) -> Option<BlockPos> {
75        get_top_connected_block(
76            world,
77            pos,
78            body_block,
79            self.growth_direction,
80            self.head_block,
81        )
82    }
83}
84
85impl BlockBehavior for GrowingPlantBodyBlock {
86    fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
87        let default_result = default_can_be_replaced(state, context);
88        if !default_result {
89            return false;
90        }
91
92        context.with_item(|item| item.item() != REGISTRY.items.by_block(self.head_block))
93    }
94
95    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
96        growing_plant_can_survive(
97            world,
98            pos,
99            self.growth_direction,
100            self.head_block,
101            state.get_block(),
102        )
103    }
104
105    fn update_shape(
106        &self,
107        state: BlockStateId,
108        world: &dyn ScheduledTickAccess,
109        pos: BlockPos,
110        direction: Direction,
111        _neighbor_pos: BlockPos,
112        neighbor_state: BlockStateId,
113    ) -> BlockStateId {
114        if direction == self.growth_direction.opposite() && !self.can_survive(state, world, pos) {
115            world.schedule_block_tick_default(pos, self.block, 1);
116        }
117        let head_block = self.head_block;
118        if direction == self.growth_direction
119            && neighbor_state.get_block() != self.block
120            && neighbor_state.get_block() != head_block
121        {
122            let mut rng = rng();
123            return (self.update_head_after_converted_from_body)(
124                state,
125                GrowingPlantHeadBlock::get_head_state(self.head_block, &mut rng),
126            );
127        }
128        if self.schedule_fluid_ticks {
129            world.schedule_fluid_tick_default(
130                pos,
131                &vanilla_fluids::WATER,
132                vanilla_fluids::WATER.tick_delay as i32,
133            );
134        }
135        state
136    }
137    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
138        if !self.can_survive(state, world, pos) {
139            world.destroy_block(pos, true);
140        }
141    }
142    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
143        Some(self.block.default_state())
144    }
145    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
146        Some(self)
147    }
148}
149impl Bonemealable for GrowingPlantBodyBlock {
150    fn is_valid_bonemeal_target(
151        &self,
152        state: BlockStateId,
153        world: &dyn LevelReader,
154        pos: BlockPos,
155    ) -> bool {
156        let Some(head_pos) = self.get_head_pos(world, pos, state.get_block()) else {
157            return false;
158        };
159        let growth_pos = head_pos.relative(self.growth_direction);
160        (self.can_grow_into)(world.get_block_state(growth_pos))
161            && !world.is_outside_build_height(growth_pos.y())
162    }
163
164    fn is_bonemeal_success(
165        &self,
166        _state: BlockStateId,
167        _world: &Arc<World>,
168        _rng: &mut dyn Rng,
169        _pos: BlockPos,
170    ) -> bool {
171        true
172    }
173
174    fn perform_bonemeal(
175        &self,
176        state: BlockStateId,
177        world: &Arc<World>,
178        rng: &mut dyn Rng,
179        pos: BlockPos,
180    ) {
181        let Some(head_pos) = self.get_head_pos(world, pos, state.get_block()) else {
182            return;
183        };
184        let forward_state = world.get_block_state(head_pos);
185        let Some(behavior) = BLOCK_BEHAVIORS
186            .get_behavior(forward_state.get_block())
187            .as_bonemealable()
188        else {
189            return;
190        };
191        behavior.perform_bonemeal(forward_state, world, rng, head_pos);
192    }
193
194    fn bonemeal_action_type(&self) -> BonemealAction {
195        BonemealAction::Grower
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use glam::DVec3;
202    use steel_registry::{
203        init_vanilla_registry, item_stack::ItemStack, vanilla_blocks, vanilla_items,
204    };
205    use steel_utils::{BlockPos, types::InteractionHand};
206
207    use super::*;
208    use crate::{
209        behavior::{
210            BlockHitResult, PlacementOrientation, PlacementSource, blocks::CaveVinesBlock,
211            init_behaviors,
212        },
213        test_support::test_world,
214    };
215
216    fn place_context(item_in_hand: &mut ItemStack) -> BlockPlaceContext<'_> {
217        let hit_result = BlockHitResult {
218            location: DVec3::ZERO,
219            direction: Direction::Down,
220            block_pos: BlockPos::ZERO,
221            miss: false,
222            inside: false,
223            world_border_hit: false,
224        };
225        let source = PlacementSource::direct(
226            None,
227            InteractionHand::MainHand,
228            item_in_hand,
229            PlacementOrientation::Player {
230                rotation: 0.0,
231                pitch: 0.0,
232            },
233            false,
234        );
235        BlockPlaceContext::new(test_world(), source, &hit_result)
236    }
237
238    #[test]
239    fn replacement_rejects_head_item_and_preserves_default_result() {
240        init_vanilla_registry();
241        init_behaviors();
242
243        let behavior = GrowingPlantBodyBlock::new(
244            &vanilla_blocks::CAVE_VINES_PLANT,
245            Direction::Down,
246            false,
247            &vanilla_blocks::CAVE_VINES,
248            CaveVinesBlock::can_grow_into,
249        );
250        let mut glow_berries = ItemStack::new(&vanilla_items::GLOW_BERRIES);
251        let context = place_context(&mut glow_berries);
252        let replaceable_state = vanilla_blocks::SHORT_GRASS.default_state();
253        assert!(default_can_be_replaced(replaceable_state, &context));
254        assert!(!behavior.can_be_replaced(replaceable_state, &context));
255
256        let mut stone = ItemStack::new(&vanilla_items::STONE);
257        let context = place_context(&mut stone);
258        let body_state = vanilla_blocks::CAVE_VINES_PLANT.default_state();
259        assert_eq!(
260            behavior.can_be_replaced(body_state, &context),
261            default_can_be_replaced(body_state, &context)
262        );
263    }
264}