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_can_survive,
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]
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}