steel_core/behavior/blocks/vegetation/
growing_plant_body_block.rs1use 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
23pub 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 #[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 #[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}