steel_core/behavior/blocks/vegetation/
kelp_block.rs1use std::sync::Arc;
2
3use crate::behavior::block::BlockBehavior;
4use crate::behavior::blocks::vegetation::bonemealable::{BonemealAction, Bonemealable};
5use crate::behavior::blocks::vegetation::growing_plant_head_block::GrowingPlantHeadBlock;
6use crate::behavior::context::BlockPlaceContext;
7use crate::world::{LevelReader, ScheduledTickAccess, World};
8
9use rand::Rng;
10use steel_macros::block_behavior;
11use steel_registry::blocks::block_state_ext::BlockStateExt;
12use steel_registry::blocks::properties::Direction;
13use steel_registry::fluid::{FluidRef, FluidStateExt};
14use steel_registry::item_stack::ItemStack;
15use steel_registry::{vanilla_blocks, vanilla_items};
16use steel_utils::{BlockPos, BlockStateId};
17
18use super::{BlockRef, kelp_can_survive};
19
20#[block_behavior]
22pub struct KelpBlock {
23 block: BlockRef,
24}
25
26const GROW_PER_TICK_PROBABILITY: f64 = 0.14;
27
28impl KelpBlock {
29 #[must_use]
31 pub const fn new(block: BlockRef) -> Self {
32 Self { block }
33 }
34
35 fn can_grow_into(state: BlockStateId) -> bool {
36 state.get_block() == &vanilla_blocks::WATER
37 }
38
39 fn get_blocks_to_grow_when_bonemealed(_rng: &mut dyn Rng) -> i32 {
40 1
41 }
42
43 const fn growing_plant_head_block(&self) -> GrowingPlantHeadBlock {
44 GrowingPlantHeadBlock::new(
45 self.block,
46 Direction::Up,
47 true,
48 GROW_PER_TICK_PROBABILITY,
49 &vanilla_blocks::KELP_PLANT,
50 Some(Self::get_blocks_to_grow_when_bonemealed),
51 Self::can_grow_into,
52 )
53 }
54}
55
56impl BlockBehavior for KelpBlock {
57 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
58 kelp_can_survive(world, pos)
59 }
60
61 fn get_clone_item_stack(
62 &self,
63 _block: BlockRef,
64 _state: BlockStateId,
65 _include_data: bool,
66 ) -> Option<ItemStack> {
67 Some(ItemStack::new(&vanilla_items::KELP))
68 }
69
70 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
71 self.growing_plant_head_block()
72 .random_tick(state, world, pos);
73 }
74
75 fn update_shape(
76 &self,
77 state: BlockStateId,
78 world: &dyn ScheduledTickAccess,
79 pos: BlockPos,
80 direction: Direction,
81 neighbor_pos: BlockPos,
82 neighbor_state: BlockStateId,
83 ) -> BlockStateId {
84 self.growing_plant_head_block().update_shape(
85 state,
86 world,
87 pos,
88 direction,
89 neighbor_pos,
90 neighbor_state,
91 )
92 }
93
94 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
95 let fluid_state = context
96 .world
97 .get_block_state(context.place_pos())
98 .get_fluid_state();
99 if fluid_state.is_water() && fluid_state.is_full() {
100 return self
101 .growing_plant_head_block()
102 .get_state_for_placement(context);
103 }
104 None
105 }
106
107 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
108 self.growing_plant_head_block().tick(state, world, pos);
109 }
110
111 fn is_liquid_container(&self, _state: BlockStateId) -> bool {
112 true
113 }
114
115 fn can_place_liquid(&self, _state: BlockStateId, _fluid: FluidRef) -> bool {
116 false
117 }
118
119 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
120 Some(self)
121 }
122}
123
124impl Bonemealable for KelpBlock {
125 fn is_valid_bonemeal_target(
126 &self,
127 state: BlockStateId,
128 world: &dyn LevelReader,
129 pos: BlockPos,
130 ) -> bool {
131 self.growing_plant_head_block()
132 .is_valid_bonemeal_target(state, world, pos)
133 }
134
135 fn perform_bonemeal(
136 &self,
137 state: BlockStateId,
138 world: &Arc<World>,
139 rng: &mut dyn Rng,
140 pos: BlockPos,
141 ) {
142 self.growing_plant_head_block()
143 .perform_bonemeal(state, world, rng, pos);
144 }
145
146 fn bonemeal_action_type(&self) -> BonemealAction {
147 BonemealAction::Grower
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::test_support::TestLevel;
155 use steel_registry::init_vanilla_registry;
156
157 #[test]
158 fn kelp_update_shape_schedules_water_tick() {
159 init_vanilla_registry();
160
161 let kelp = KelpBlock::new(&vanilla_blocks::KELP);
162 let level =
163 TestLevel::default().with_default_block_state(vanilla_blocks::WATER.default_state());
164 let state = vanilla_blocks::KELP.default_state();
165
166 assert_eq!(
167 kelp.update_shape(
168 state,
169 &level,
170 BlockPos::ZERO,
171 Direction::North,
172 Direction::North.relative(BlockPos::ZERO),
173 vanilla_blocks::WATER.default_state(),
174 ),
175 state
176 );
177 assert!(level.scheduled_water_tick());
178 }
179
180 #[test]
181 fn kelp_head_update_shape_schedules_break_tick_when_unsupported() {
182 init_vanilla_registry();
183
184 let kelp = KelpBlock::new(&vanilla_blocks::KELP);
185 let level =
186 TestLevel::default().with_default_block_state(vanilla_blocks::WATER.default_state());
187 let state = vanilla_blocks::KELP.default_state();
188
189 let updated = kelp.update_shape(
190 state,
191 &level,
192 BlockPos::ZERO,
193 Direction::Down,
194 BlockPos::ZERO.below(),
195 vanilla_blocks::WATER.default_state(),
196 );
197
198 assert_eq!(updated, state);
199 assert!(
200 level
201 .scheduled_block_ticks
202 .borrow()
203 .iter()
204 .any(|tick| tick.block == &vanilla_blocks::KELP && tick.delay == 1)
205 );
206 }
207
208 #[test]
209 fn kelp_head_converts_to_body_when_connected_above() {
210 init_vanilla_registry();
211
212 let kelp = KelpBlock::new(&vanilla_blocks::KELP);
213 let level =
214 TestLevel::default().with_default_block_state(vanilla_blocks::WATER.default_state());
215 let state = vanilla_blocks::KELP.default_state();
216
217 let updated = kelp.update_shape(
218 state,
219 &level,
220 BlockPos::ZERO,
221 Direction::Up,
222 BlockPos::ZERO.above(),
223 vanilla_blocks::KELP_PLANT.default_state(),
224 );
225
226 assert_eq!(updated.get_block(), &vanilla_blocks::KELP_PLANT);
227 assert!(level.scheduled_fluid_ticks.borrow().is_empty());
228 }
229}