steel_core/behavior/blocks/vegetation/
bonemealable.rs1use 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
15pub trait Bonemealable {
17 fn get_bonemeal_age_increase(&self, _world: &Arc<World>, _rng: &mut dyn Rng) -> u8 {
19 0
20 }
21
22 fn is_valid_bonemeal_target(
24 &self,
25 state: BlockStateId,
26 world: &dyn LevelReader,
27 pos: BlockPos,
28 ) -> bool;
29
30 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 fn perform_bonemeal(
43 &self,
44 state: BlockStateId,
45 world: &Arc<World>,
46 rng: &mut dyn Rng,
47 pos: BlockPos,
48 );
49
50 fn bonemeal_action_type(&self) -> BonemealAction {
52 BonemealAction::Grower
53 }
54}
55
56pub enum BonemealAction {
58 NeighborSpreader,
60 Grower,
62}
63
64impl BonemealAction {
65 #[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
77pub 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
86pub 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
124pub trait CropBonemealExt: CropLike + Bonemealable {
126 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 {}