Skip to main content

steel_core/behavior/blocks/vegetation/
bonemealable.rs

1//! Bonemeal-related traits and helpers for block behaviors.
2
3use std::sync::Arc;
4
5use rand::{Rng, RngExt};
6use steel_registry::blocks::block_state_ext::BlockStateExt;
7use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
8
9use crate::{
10    behavior::BLOCK_BEHAVIORS,
11    behavior::blocks::vegetation::crop_block::CropLike,
12    world::{LevelReader, World},
13};
14
15/// Blocks that react to bonemeal.
16pub trait Bonemealable {
17    /// Returns the age increase from bonemeal.
18    fn get_bonemeal_age_increase(&self, _world: &Arc<World>, _rng: &mut dyn Rng) -> u8 {
19        0
20    }
21
22    /// Returns whether this block is a valid bonemeal target.
23    fn is_valid_bonemeal_target(
24        &self,
25        state: BlockStateId,
26        world: &dyn LevelReader,
27        pos: BlockPos,
28    ) -> bool;
29
30    /// Returns whether bonemeal succeeds after the target check passes.
31    fn is_bonemeal_success(
32        &self,
33        _state: BlockStateId,
34        _world: &Arc<World>,
35        _rng: &mut dyn Rng,
36        _pos: BlockPos,
37    ) -> bool {
38        true
39    }
40
41    /// Applies the bonemeal effect.
42    fn perform_bonemeal(
43        &self,
44        state: BlockStateId,
45        world: &Arc<World>,
46        rng: &mut dyn Rng,
47        pos: BlockPos,
48    );
49
50    /// Returns how this block uses bonemeal.
51    fn bonemeal_action_type(&self) -> BonemealAction {
52        BonemealAction::Grower
53    }
54}
55
56/// How bonemeal affects the block.
57pub enum BonemealAction {
58    /// Spreads growth to nearby blocks.
59    NeighborSpreader,
60    /// Grows this block directly.
61    Grower,
62}
63
64impl BonemealAction {
65    /// Returns the particle position for this bonemeal action.
66    // TODO: Rooted dirt is a GROWER in vanilla but overrides getParticlePos to pos.below().
67    // Add a per-block particle-position hook before wiring bonemeal particles.
68    #[expect(dead_code, reason = "used later for spawning the particles")]
69    const fn particle_pos(&self, pos: BlockPos) -> BlockPos {
70        match self {
71            BonemealAction::NeighborSpreader => pos.above(),
72            BonemealAction::Grower => pos,
73        }
74    }
75}
76
77/// Vanilla spreadable-neighbor target check.
78pub fn has_spreadable_neighbor_pos(
79    world: &dyn LevelReader,
80    pos: BlockPos,
81    block_to_place: BlockStateId,
82) -> bool {
83    get_spreadable_neighbor_pos(Direction::HORIZONTAL, world, pos, block_to_place).is_some()
84}
85
86/// Vanilla spreadable-neighbor target selection.
87pub fn find_spreadable_neighbor_pos(
88    world: &World,
89    pos: BlockPos,
90    block_to_place: BlockStateId,
91) -> Option<BlockPos> {
92    let mut directions = Direction::HORIZONTAL;
93    shuffle_directions(&mut directions, &mut rand::rng());
94    get_spreadable_neighbor_pos(directions, world, pos, block_to_place)
95}
96
97fn shuffle_directions(directions: &mut [Direction; 4], random: &mut impl Rng) {
98    for i in (1..directions.len()).rev() {
99        let j = random.random_range(0..=i);
100        directions.swap(i, j);
101    }
102}
103
104fn get_spreadable_neighbor_pos(
105    directions: [Direction; 4],
106    world: &dyn LevelReader,
107    pos: BlockPos,
108    block_to_place: BlockStateId,
109) -> Option<BlockPos> {
110    let behavior = BLOCK_BEHAVIORS.get_behavior_for_state(block_to_place)?;
111
112    for direction in directions {
113        let neighbor_pos = pos.relative(direction);
114        if world.get_block_state(neighbor_pos).is_air()
115            && behavior.can_survive(block_to_place, world, neighbor_pos)
116        {
117            return Some(neighbor_pos);
118        }
119    }
120
121    None
122}
123
124/// Default Bonemeal implementation for all crops
125pub trait CropBonemealExt: CropLike + Bonemealable {
126    /// Default `perform_bonemeal` implementation for all crops
127    fn default_perform_bonemeal(
128        &self,
129        state: BlockStateId,
130        world: &Arc<World>,
131        rng: &mut dyn Rng,
132        pos: BlockPos,
133    ) {
134        let new_age = self
135            .get_age(state)
136            .saturating_add(self.get_bonemeal_age_increase(world, rng))
137            .min(self.max_age());
138
139        world.set_block(
140            pos,
141            self.get_state_for_age(new_age),
142            UpdateFlags::UPDATE_ALL,
143        );
144    }
145}
146
147impl<T: CropLike + Bonemealable> CropBonemealExt for T {}