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