Skip to main content

steel_core/behavior/blocks/vegetation/
sea_pickle_block.rs

1use 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/// Vanilla `SeaPickleBlock` survival.
26#[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    /// Creates a new sea pickle block behavior.
38    #[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}