steel_core/behavior/blocks/vegetation/
sea_pickle_block.rs1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{
7 BlockStateProperties, BoolProperty, Direction, IntProperty,
8};
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::{REGISTRY, vanilla_blocks};
11use steel_utils::types::UpdateFlags;
12use steel_utils::{BlockPos, BlockStateId};
13
14use crate::behavior::block::{
15 BlockBehavior, default_can_be_replaced, schedule_water_tick_if_waterlogged,
16};
17use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
18use crate::behavior::context::BlockPlaceContext;
19use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext};
20use crate::entity::ai::path::PathComputationType;
21use crate::world::{LevelReader, ScheduledTickAccess, World};
22
23use super::BlockRef;
24
25#[block_behavior]
27pub struct SeaPickleBlock {
28 block: BlockRef,
29}
30
31const MAX_PICKLES: u8 = 4;
32
33const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
34const PICKLES: &IntProperty = &BlockStateProperties::PICKLES;
35
36impl SeaPickleBlock {
37 #[must_use]
39 pub const fn new(block: BlockRef) -> Self {
40 Self { block }
41 }
42
43 fn may_place_on(world: &dyn LevelReader, state: BlockStateId, pos: BlockPos) -> bool {
44 BLOCK_BEHAVIORS
45 .get_behavior(state.get_block())
46 .get_collision_boxes(state, world, pos, BlockCollisionContext::empty())
47 .iter()
48 .any(|aabb| !aabb.is_empty() && aabb.max_y() >= 1.0)
49 || world.is_face_sturdy(state, pos, Direction::Up)
50 }
51 fn is_dead(state: BlockStateId) -> bool {
52 !state.get_value(WATERLOGGED)
53 }
54}
55
56impl BlockBehavior for SeaPickleBlock {
57 fn update_shape(
58 &self,
59 state: BlockStateId,
60 world: &dyn ScheduledTickAccess,
61 pos: BlockPos,
62 _direction: Direction,
63 _neighbor_pos: BlockPos,
64 _neighbor_state: BlockStateId,
65 ) -> BlockStateId {
66 if !self.can_survive(state, world, pos) {
67 return vanilla_blocks::AIR.default_state();
68 }
69
70 schedule_water_tick_if_waterlogged(state, world, pos);
71 state
72 }
73
74 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
75 let below_pos = pos.below();
76 Self::may_place_on(world, world.get_block_state(below_pos), below_pos)
77 }
78
79 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
80 let state = context.world.get_block_state(context.place_pos());
81 if state.get_block() == self.block {
82 return Some(state.set_value(PICKLES, MAX_PICKLES.min(state.get_value(PICKLES) + 1)));
83 }
84 Some(
85 self.block
86 .default_state()
87 .set_value(WATERLOGGED, context.is_water_source()),
88 )
89 }
90 fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
91 if !context.is_secondary_use_active()
92 && context.with_item(|item| item.item() == REGISTRY.items.by_block(state.get_block()))
93 && state.get_value(PICKLES) < MAX_PICKLES
94 {
95 return true;
96 }
97 default_can_be_replaced(state, context)
98 }
99 fn is_pathfindable(
100 &self,
101 _state: BlockStateId,
102 _computation_type: PathComputationType,
103 ) -> bool {
104 false
105 }
106 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
107 Some(self)
108 }
109}
110
111impl Bonemealable for SeaPickleBlock {
112 fn is_valid_bonemeal_target(
113 &self,
114 state: BlockStateId,
115 world: &dyn LevelReader,
116 pos: BlockPos,
117 ) -> bool {
118 !Self::is_dead(state)
119 && world
120 .get_block_state(pos.below())
121 .get_block()
122 .has_tag(&BlockTag::CORAL_BLOCKS)
123 }
124 fn perform_bonemeal(
125 &self,
126 state: BlockStateId,
127 world: &Arc<World>,
128 rng: &mut dyn Rng,
129 pos: BlockPos,
130 ) {
131 let mut z_span = 1;
132 let x_start = pos.x() - 2;
133 let mut z_offset = 0;
134
135 for (count, x) in (0..5).enumerate() {
136 for z in 0..z_span {
137 let end_y = 2 + pos.y() - 1;
138
139 for start_y in (end_y - 2)..end_y {
140 let position = BlockPos::new(x_start + x, start_y, pos.z() - z_offset + z);
141
142 if position != pos
143 && rng.random_range(0..6) == 0
144 && world.get_block_state(position).get_block() == &vanilla_blocks::WATER
145 {
146 let below_state = world.get_block_state(position.below());
147
148 if below_state.get_block().has_tag(&BlockTag::CORAL_BLOCKS) {
149 let sea_pickle_state = vanilla_blocks::SEA_PICKLE
150 .default_state()
151 .set_value(PICKLES, rng.random_range(0..MAX_PICKLES) + 1);
152
153 world.set_block(position, sea_pickle_state, UpdateFlags::UPDATE_ALL);
154 }
155 }
156 }
157 }
158
159 if count < 2 {
160 z_span += 2;
161 z_offset += 1;
162 } else {
163 z_span -= 2;
164 z_offset -= 1;
165 }
166 }
167
168 let final_state = state.set_value(PICKLES, MAX_PICKLES);
169
170 world.set_block(pos, final_state, UpdateFlags::UPDATE_CLIENTS);
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use steel_registry::{init_vanilla_registry, vanilla_fluids};
177
178 use super::*;
179 use crate::behavior::init_behaviors;
180 use crate::test_support::TestLevel;
181
182 #[test]
183 fn sea_pickle_checks_survival_before_scheduling_water() {
184 init_vanilla_registry();
185 init_behaviors();
186 let behavior = SeaPickleBlock::new(&vanilla_blocks::SEA_PICKLE);
187 let state = vanilla_blocks::SEA_PICKLE
188 .default_state()
189 .set_value(WATERLOGGED, true);
190 let pos = BlockPos::new(0, 64, 0);
191 let unsupported = TestLevel::default();
192
193 assert!(
194 behavior
195 .update_shape(
196 state,
197 &unsupported,
198 pos,
199 Direction::North,
200 pos.north(),
201 vanilla_blocks::AIR.default_state(),
202 )
203 .is_air()
204 );
205 assert!(unsupported.scheduled_fluid_ticks.borrow().is_empty());
206
207 let supported =
208 TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
209 assert_eq!(
210 behavior.update_shape(
211 state,
212 &supported,
213 pos,
214 Direction::North,
215 pos.north(),
216 vanilla_blocks::AIR.default_state(),
217 ),
218 state
219 );
220 assert_eq!(
221 supported
222 .scheduled_fluid_ticks
223 .borrow()
224 .iter()
225 .map(|tick| (tick.fluid, tick.delay))
226 .collect::<Vec<_>>(),
227 vec![(&vanilla_fluids::WATER, 5)]
228 );
229 }
230}