Skip to main content

steel_core/behavior/blocks/vegetation/
pitcher_crop.rs

1use std::sync::Arc;
2
3use rand::RngExt;
4use steel_macros::block_behavior;
5use steel_registry::{
6    blocks::{
7        BlockRef,
8        block_state_ext::BlockStateExt,
9        properties::{BlockStateProperties, DoubleBlockHalf, EnumProperty, IntProperty},
10    },
11    vanilla_block_tags::BlockTag,
12    vanilla_blocks,
13};
14use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
15
16use crate::{
17    behavior::{
18        BlockBehavior, BlockPlaceContext,
19        blocks::vegetation::{
20            Vegetation,
21            bonemealable::Bonemealable,
22            crop_block::destroy_crop_on_ravager_contact,
23            vegetation_block::{double_plant_can_survive, double_plant_update_shape},
24        },
25    },
26    entity::{Entity, InsideBlockEffectCollector},
27    world::{LevelReader, ScheduledTickAccess, World},
28};
29
30const HALF_PROPERTY: EnumProperty<DoubleBlockHalf> = BlockStateProperties::DOUBLE_BLOCK_HALF;
31const AGE_PROPERTY: IntProperty = BlockStateProperties::AGE_4;
32
33/// Behavior for Pitcher Crops
34#[block_behavior]
35pub struct PitcherCropBlock {
36    block: BlockRef,
37}
38
39impl PitcherCropBlock {
40    /// Creates a new Pitcher Crop Block Behavior
41    #[must_use]
42    pub const fn new(block: BlockRef) -> Self {
43        Self { block }
44    }
45
46    fn is_lower(state: BlockStateId) -> bool {
47        state.get_block() == &vanilla_blocks::PITCHER_CROP
48            && state.get_value(&BlockStateProperties::DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower
49    }
50
51    fn get_growth_speed(&self, world: &Arc<World>, pos: BlockPos) -> f32 {
52        let mut speed = 1.0f32;
53        let below = pos.below();
54
55        // Check 3x3 area of farmland below
56        for dx in -1..=1 {
57            for dz in -1..=1 {
58                let check_pos = below.offset(dx, 0, dz);
59                let block_state = world.get_block_state(check_pos);
60                let mut block_speed = 0.0f32;
61
62                if block_state.get_block().has_tag(&BlockTag::GROWS_CROPS) {
63                    block_speed = 1.0;
64                    // Check moisture level (defaults to 0 for non-farmland blocks)
65                    let moisture = block_state
66                        .try_get_value(&BlockStateProperties::MOISTURE)
67                        .unwrap_or(0);
68                    if moisture > 0 {
69                        block_speed = 3.0;
70                    }
71                }
72
73                // Diagonal/adjacent farmland contributes less
74                if dx != 0 || dz != 0 {
75                    block_speed /= 4.0;
76                }
77
78                speed += block_speed;
79            }
80        }
81
82        // Check for same crop in adjacent positions (reduces growth speed)
83        let north = world.get_block_state(pos.north());
84        let south = world.get_block_state(pos.south());
85        let west = world.get_block_state(pos.west());
86        let east = world.get_block_state(pos.east());
87
88        let horizontal_row = self.block == west.get_block() || self.block == east.get_block();
89        let vertical_row = self.block == north.get_block() || self.block == south.get_block();
90
91        if horizontal_row && vertical_row {
92            // Crops in both directions - penalty
93            speed /= 2.0;
94        } else {
95            // Check diagonals
96            let nw = world.get_block_state(pos.north().west());
97            let ne = world.get_block_state(pos.north().east());
98            let sw = world.get_block_state(pos.south().west());
99            let se = world.get_block_state(pos.south().east());
100
101            let has_diagonal = self.block == nw.get_block()
102                || self.block == ne.get_block()
103                || self.block == sw.get_block()
104                || self.block == se.get_block();
105
106            if has_diagonal {
107                speed /= 2.0;
108            }
109        }
110
111        speed
112    }
113
114    fn get_lower_half(
115        state: BlockStateId,
116        world: &dyn LevelReader,
117        pos: BlockPos,
118    ) -> Option<(BlockStateId, BlockPos)> {
119        if Self::is_lower(state) {
120            Some((state, pos))
121        } else {
122            let (state_below, pos_below) = (world.get_block_state(pos.below()), pos.below());
123            if Self::is_lower(state_below) {
124                Some((state_below, pos_below))
125            } else {
126                None
127            }
128        }
129    }
130
131    fn grow(world: &Arc<World>, lower_state: BlockStateId, lower_pos: BlockPos, increase: u8) {
132        let new_age = (lower_state.get_value(&AGE_PROPERTY) + increase).min(4);
133        if !Self::can_grow(world, lower_state, lower_pos, new_age) {
134            return;
135        }
136
137        let new_state = lower_state.set_value(&AGE_PROPERTY, new_age);
138        world.set_block(lower_pos, new_state, UpdateFlags::UPDATE_CLIENTS);
139
140        if new_age >= 3 {
141            world.set_block(
142                lower_pos.above(),
143                new_state.set_value(&HALF_PROPERTY, DoubleBlockHalf::Upper),
144                UpdateFlags::UPDATE_ALL,
145            );
146        }
147    }
148
149    fn can_grow(world: &dyn LevelReader, state: BlockStateId, pos: BlockPos, new_age: u8) -> bool {
150        let state_above = world.get_block_state(pos.above());
151        state.get_value(&AGE_PROPERTY) < 4
152            && world.raw_brightness(pos, 0) >= 8
153            && !world.is_outside_build_height(pos.above().y())
154            && (new_age < 3
155                || state_above.is_air()
156                || state_above.get_block() == &vanilla_blocks::PITCHER_CROP)
157    }
158}
159
160impl BlockBehavior for PitcherCropBlock {
161    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
162        if self.may_place_on(
163            context.world.get_block_state(context.place_pos().below()),
164            context.world,
165            context.place_pos().below(),
166        ) {
167            Some(self.block.default_state())
168        } else {
169            None
170        }
171    }
172
173    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
174        if Self::is_lower(state) && world.raw_brightness(pos, 0) < 8 {
175            return false;
176        }
177
178        double_plant_can_survive(self, state, world, pos)
179    }
180
181    fn update_shape(
182        &self,
183        state: BlockStateId,
184        world: &dyn ScheduledTickAccess,
185        pos: BlockPos,
186        direction: steel_utils::Direction,
187        _neighbor_pos: BlockPos,
188        neighbor_state: BlockStateId,
189    ) -> BlockStateId {
190        if state.get_value(&AGE_PROPERTY) >= 3 {
191            double_plant_update_shape(self, state, world, pos, direction, neighbor_state)
192        } else if self.can_survive(state, world, pos) {
193            state
194        } else {
195            vanilla_blocks::AIR.default_state()
196        }
197    }
198
199    fn entity_inside(
200        &self,
201        _state: BlockStateId,
202        world: &Arc<World>,
203        pos: BlockPos,
204        entity: &dyn Entity,
205        _effect_collector: &mut InsideBlockEffectCollector,
206        _is_precise: bool,
207    ) {
208        destroy_crop_on_ravager_contact(world, pos, entity);
209    }
210
211    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
212        let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) else {
213            return;
214        };
215        let growth_speed = self.get_growth_speed(world, lower_pos);
216        let should_progress_growth =
217            rand::rng().random_range(0_i32..((25.0 / growth_speed) as i32 + 1)) == 0;
218        if should_progress_growth {
219            Self::grow(world, lower_state, lower_pos, 1);
220        }
221    }
222
223    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
224        Some(self)
225    }
226}
227
228impl Vegetation for PitcherCropBlock {
229    fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
230        state.get_block().has_tag(&BlockTag::SUPPORTS_CROPS)
231    }
232}
233
234impl Bonemealable for PitcherCropBlock {
235    fn is_valid_bonemeal_target(
236        &self,
237        state: BlockStateId,
238        world: &dyn LevelReader,
239        pos: BlockPos,
240    ) -> bool {
241        let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) else {
242            return false;
243        };
244        if lower_state.get_block() != &vanilla_blocks::PITCHER_CROP
245            || lower_state.get_value(&HALF_PROPERTY) != DoubleBlockHalf::Lower
246        {
247            return false;
248        }
249
250        let new_age = lower_state.get_value(&AGE_PROPERTY) + 1;
251
252        Self::can_grow(world, lower_state, lower_pos, new_age)
253    }
254
255    fn perform_bonemeal(
256        &self,
257        state: BlockStateId,
258        world: &Arc<World>,
259        _rng: &mut dyn rand::Rng,
260        pos: BlockPos,
261    ) {
262        if let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) {
263            Self::grow(world, lower_state, lower_pos, 1);
264        }
265    }
266}