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