Skip to main content

steel_core/behavior/blocks/vegetation/
crop_block.rs

1//! Crop block implementation (wheat, carrots, potatoes, beetroot).
2
3use std::sync::Arc;
4
5use rand::RngExt;
6use steel_macros::block_behavior;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt;
9use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
10use steel_registry::entity_type::EntityTypeRef;
11use steel_registry::item_stack::ItemStack;
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::{vanilla_entities, vanilla_game_rules, vanilla_items};
14use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
15
16use crate::behavior::block::BlockBehavior;
17use crate::behavior::blocks::vegetation::Vegetation;
18use crate::behavior::blocks::vegetation::bonemealable::{Bonemealable, CropBonemealExt};
19use crate::behavior::blocks::vegetation::vegetation_block::{
20    survival_update_shape, vegetation_can_survive,
21};
22use crate::behavior::context::BlockPlaceContext;
23use crate::entity::{Entity, InsideBlockEffectCollector};
24use crate::world::{LevelReader, ScheduledTickAccess, World};
25
26const MOISTURE: &IntProperty = &BlockStateProperties::MOISTURE;
27
28pub(super) const MIN_CROP_LIGHT_LEVEL: u8 = 8;
29const MIN_CROP_GROWTH_LIGHT_LEVEL: u8 = 9;
30pub(super) const CROP_GROWTH_CHANCE_BASE: f32 = 25.0;
31pub(super) const ADJACENT_FARMLAND_SPEED_DIVISOR: f32 = 4.0;
32pub(super) const CROWDED_CROP_SPEED_DIVISOR: f32 = 2.0;
33
34/// Behavior for crop blocks (wheat, carrots, potatoes).
35///
36/// Crops grow through random ticks when placed on farmland with sufficient light.
37/// Growth speed is affected by nearby farmland moisture and crop arrangement.
38#[block_behavior]
39pub struct CropBlock {
40    block: BlockRef,
41}
42
43const AGE: &IntProperty = &BlockStateProperties::AGE_7;
44
45/// Calculates Vanilla crop growth speed for the supplied crop block.
46///
47/// Stems use the same farmland and neighboring-crop calculation as crops, but
48/// have different survival and mature-growth behavior.
49pub(super) fn crop_growth_speed(block: BlockRef, world: &dyn LevelReader, pos: BlockPos) -> f32 {
50    let mut speed = 1.0_f32;
51    let below = pos.below();
52
53    for dx in -1..=1 {
54        for dz in -1..=1 {
55            let state = world.get_block_state(below.offset(dx, 0, dz));
56            if !state.get_block().has_tag(&BlockTag::GROWS_CROPS) {
57                continue;
58            }
59
60            let block_speed = if state.try_get_value(MOISTURE).unwrap_or(0) > 0 {
61                3.0
62            } else {
63                1.0
64            };
65
66            speed += if dx == 0 && dz == 0 {
67                block_speed
68            } else {
69                block_speed / ADJACENT_FARMLAND_SPEED_DIVISOR
70            };
71        }
72    }
73
74    let north = pos.north();
75    let south = pos.south();
76    let west = pos.west();
77    let east = pos.east();
78    let same_block_at = |neighbor: BlockPos| block == world.get_block_state(neighbor).get_block();
79
80    let east_west = [west, east].into_iter().any(&same_block_at);
81    let north_south = [north, south].into_iter().any(&same_block_at);
82    let crowded = (east_west && north_south)
83        || [west.north(), east.north(), east.south(), west.south()]
84            .into_iter()
85            .any(same_block_at);
86
87    if crowded {
88        speed / CROWDED_CROP_SPEED_DIVISOR
89    } else {
90        speed
91    }
92}
93
94pub trait CropLike {
95    fn block(&self) -> BlockRef;
96    fn age_property(&self) -> &IntProperty;
97    fn max_age(&self) -> u8;
98    fn clone_item_stack(&self) -> ItemStack;
99
100    /// Additional checks before calling the standard `on_random_tick` code
101    fn should_random_tick(&self) -> bool {
102        true
103    }
104
105    fn get_age(&self, state: BlockStateId) -> u8 {
106        state.get_value(self.age_property())
107    }
108
109    fn get_state_for_age(&self, age: u8) -> BlockStateId {
110        self.block()
111            .default_state()
112            .set_value(self.age_property(), age)
113    }
114
115    fn is_max_age(&self, state: BlockStateId) -> bool {
116        state.get_value(self.age_property()) >= self.max_age()
117    }
118
119    fn has_sufficient_light(&self, world: &dyn LevelReader, pos: BlockPos) -> bool {
120        world.raw_brightness(pos, 0) >= MIN_CROP_LIGHT_LEVEL
121    }
122
123    fn has_sufficient_growth_light(&self, world: &dyn LevelReader, pos: BlockPos) -> bool {
124        world.raw_brightness(pos, 0) >= MIN_CROP_GROWTH_LIGHT_LEVEL
125    }
126
127    /// Calculates the growth speed based on surrounding farmland.
128    ///
129    /// Factors affecting growth speed:
130    /// - Farmland below: +1.0 (dry) or +3.0 (hydrated)
131    /// - Adjacent farmland: +0.25 (dry) or +0.75 (hydrated)
132    /// - Same crop in row: /2.0 speed penalty
133    fn get_growth_speed(&self, world: &Arc<World>, pos: BlockPos) -> f32 {
134        crop_growth_speed(self.block(), world.as_ref(), pos)
135    }
136
137    fn on_random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
138        if !self.has_sufficient_growth_light(world.as_ref(), pos) {
139            return;
140        }
141
142        let age = self.get_age(state);
143        if age < self.max_age() {
144            let growth_speed = self.get_growth_speed(world, pos);
145
146            // Random chance to grow based on growth speed
147            // Vanilla formula: random.nextInt((int)(25.0F / growthSpeed) + 1) == 0
148            let growth_chance = (CROP_GROWTH_CHANCE_BASE / growth_speed) as u32 + 1;
149
150            if rand::random::<u32>().is_multiple_of(growth_chance) {
151                let new_state = self.get_state_for_age(age + 1);
152                world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
153            }
154        }
155    }
156}
157
158pub(super) fn ravager_breaks_crop(entity_type: EntityTypeRef, mob_griefing: bool) -> bool {
159    entity_type == &vanilla_entities::RAVAGER && mob_griefing
160}
161
162pub(super) fn destroy_crop_on_ravager_contact(
163    world: &Arc<World>,
164    pos: BlockPos,
165    entity: &dyn Entity,
166) {
167    if ravager_breaks_crop(
168        entity.entity_type(),
169        world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING),
170    ) {
171        world.destroy_block_by_entity(pos, true, entity);
172    }
173}
174
175impl CropBlock {
176    /// Creates a new crop block behavior with a custom age property.
177    #[must_use]
178    pub const fn new(block: BlockRef) -> Self {
179        Self { block }
180    }
181}
182
183impl CropLike for CropBlock {
184    fn block(&self) -> BlockRef {
185        self.block
186    }
187
188    fn age_property(&self) -> &IntProperty {
189        AGE
190    }
191
192    fn max_age(&self) -> u8 {
193        7
194    }
195
196    fn clone_item_stack(&self) -> ItemStack {
197        ItemStack::new(&vanilla_items::WHEAT_SEEDS)
198    }
199}
200
201impl Bonemealable for CropBlock {
202    fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn rand::Rng) -> u8 {
203        rng.random_range(2..=5)
204    }
205
206    fn perform_bonemeal(
207        &self,
208        state: BlockStateId,
209        world: &Arc<World>,
210        rng: &mut dyn rand::Rng,
211        pos: BlockPos,
212    ) {
213        self.default_perform_bonemeal(state, world, rng, pos);
214    }
215
216    fn is_valid_bonemeal_target(
217        &self,
218        state: BlockStateId,
219        _world: &dyn LevelReader,
220        _pos: BlockPos,
221    ) -> bool {
222        !self.is_max_age(state)
223    }
224}
225
226impl<T: CropLike + Bonemealable + Send + Sync> BlockBehavior for T {
227    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
228        if self.may_place_on(
229            context.world.get_block_state(context.place_pos().below()),
230            context.world,
231            context.place_pos().below(),
232        ) {
233            Some(self.block().default_state())
234        } else {
235            None
236        }
237    }
238
239    fn can_survive(
240        &self,
241        state: BlockStateId,
242        world: &dyn LevelReader,
243        pos: steel_utils::BlockPos,
244    ) -> bool {
245        self.has_sufficient_light(world, pos) && vegetation_can_survive(self, state, world, pos)
246    }
247
248    fn update_shape(
249        &self,
250        state: BlockStateId,
251        world: &dyn ScheduledTickAccess,
252        pos: steel_utils::BlockPos,
253        _direction: steel_utils::Direction,
254        _neighbor_pos: steel_utils::BlockPos,
255        _neighbor_state: BlockStateId,
256    ) -> BlockStateId {
257        survival_update_shape(self, state, world, pos)
258    }
259
260    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
261        if self.should_random_tick() {
262            self.on_random_tick(state, world, pos);
263        }
264    }
265
266    fn entity_inside(
267        &self,
268        state: BlockStateId,
269        world: &Arc<World>,
270        pos: BlockPos,
271        entity: &dyn Entity,
272        effect_collector: &mut InsideBlockEffectCollector,
273        is_precise: bool,
274    ) {
275        destroy_crop_on_ravager_contact(world, pos, entity);
276        self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
277    }
278
279    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
280        Some(self)
281    }
282
283    fn get_clone_item_stack(
284        &self,
285        _block: BlockRef,
286        _state: BlockStateId,
287        _include_data: bool,
288    ) -> Option<ItemStack> {
289        Some(self.clone_item_stack())
290    }
291}
292
293impl<T: CropLike> Vegetation for T {
294    fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
295        state.get_block().has_tag(&BlockTag::SUPPORTS_CROPS)
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use steel_registry::{init_vanilla_registry, vanilla_blocks};
302
303    use crate::test_support::TestLevel;
304
305    use super::*;
306
307    fn crop_survival_level(support: BlockStateId, raw_brightness: u8) -> TestLevel {
308        TestLevel::default()
309            .with_block(BlockPos::ZERO.below(), support)
310            .with_raw_brightness(raw_brightness)
311    }
312
313    #[test]
314    fn crop_survival_requires_vanilla_minimum_light() {
315        init_vanilla_registry();
316
317        let crop = CropBlock::new(&vanilla_blocks::WHEAT);
318        let state = vanilla_blocks::WHEAT.default_state();
319        let farmland = vanilla_blocks::FARMLAND.default_state();
320
321        assert!(!crop.can_survive(state, &crop_survival_level(farmland, 7), BlockPos::ZERO));
322        assert!(crop.can_survive(state, &crop_survival_level(farmland, 8), BlockPos::ZERO));
323    }
324
325    #[test]
326    fn ravager_crop_breaking_requires_mob_griefing() {
327        assert!(ravager_breaks_crop(&vanilla_entities::RAVAGER, true));
328        assert!(!ravager_breaks_crop(&vanilla_entities::RAVAGER, false));
329        assert!(!ravager_breaks_crop(&vanilla_entities::ZOMBIE, true));
330    }
331}