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