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