Skip to main content

steel_core/behavior/blocks/vegetation/
farmland_block.rs

1//! Farmland block implementation.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt;
8use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty, IntProperty};
9use steel_registry::{vanilla_blocks, vanilla_game_rules};
10use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
11
12use crate::behavior::block::{BlockBehavior, EntityFallDamage, EntityFallOnContext};
13use crate::behavior::context::BlockPlaceContext;
14use crate::world::World;
15
16use super::turn_to_dirt;
17
18/// Maximum moisture level for farmland.
19const MAX_MOISTURE: u8 = 7;
20const TRAMPLE_VOLUME_THRESHOLD: f64 = 0.512;
21
22/// Behavior for farmland blocks.
23///
24/// Farmland has a moisture level (0-7) that affects crop growth speed.
25/// - Moisture increases to max (7) when near water
26/// - Moisture decreases by 1 each random tick when not near water
27/// - Farmland turns back to dirt when moisture reaches 0 and no crop is planted
28#[block_behavior]
29pub struct FarmlandBlock {
30    block: BlockRef,
31}
32
33const MOISTURE: &IntProperty = &BlockStateProperties::MOISTURE;
34const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
35
36impl FarmlandBlock {
37    /// Creates a new farmland block behavior.
38    #[must_use]
39    pub const fn new(block: BlockRef) -> Self {
40        Self { block }
41    }
42
43    /// Checks if there is water within a 9x9x2 area centered on the farmland.
44    /// Vanilla checks from (-4, 0, -4) to (4, 1, 4) relative to the farmland.
45    ///
46    /// This checks both water blocks and waterlogged blocks (matching vanilla's
47    /// FluidTags.WATER check on fluid state).
48    fn is_near_water(world: &Arc<World>, pos: BlockPos) -> bool {
49        for dy in 0..=1 {
50            for dx in -4..=4 {
51                for dz in -4..=4 {
52                    let check_pos = pos.offset(dx, dy, dz);
53                    let state = world.get_block_state(check_pos);
54
55                    // Check if block is water
56                    if state.get_block() == &vanilla_blocks::WATER {
57                        return true;
58                    }
59
60                    // Check if block is waterlogged
61                    if state.try_get_value(WATERLOGGED).unwrap_or(false) {
62                        return true;
63                    }
64                }
65            }
66        }
67        false
68    }
69
70    /// Checks if the block above is a crop that should maintain the farmland.
71    /// This prevents farmland from turning to dirt when crops are planted.
72    fn should_maintain_farmland(world: &Arc<World>, pos: BlockPos) -> bool {
73        let above = world.get_block_state(pos.above());
74        let block = above.get_block();
75
76        // Check for crops that maintain farmland
77        // In vanilla this uses the MAINTAINS_FARMLAND tag
78        block == &vanilla_blocks::WHEAT
79            || block == &vanilla_blocks::CARROTS
80            || block == &vanilla_blocks::POTATOES
81            || block == &vanilla_blocks::BEETROOTS
82            || block == &vanilla_blocks::MELON_STEM
83            || block == &vanilla_blocks::PUMPKIN_STEM
84            || block == &vanilla_blocks::ATTACHED_MELON_STEM
85            || block == &vanilla_blocks::ATTACHED_PUMPKIN_STEM
86            || block == &vanilla_blocks::TORCHFLOWER_CROP
87            || block == &vanilla_blocks::PITCHER_CROP
88    }
89
90    #[must_use]
91    fn should_turn_to_dirt_on_fall(
92        context: EntityFallOnContext<'_>,
93        mob_griefing: bool,
94        random_float: f32,
95    ) -> bool {
96        f64::from(random_float) < context.fall_distance - 0.5
97            && context.entity.is_living_entity
98            && (context.entity.is_player() || mob_griefing)
99            && context.entity.bounding_box_width_squared_height() > TRAMPLE_VOLUME_THRESHOLD
100    }
101}
102
103impl BlockBehavior for FarmlandBlock {
104    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
105        // Farmland is placed with moisture 0
106        Some(self.block.default_state().set_value(MOISTURE, 0u8))
107    }
108
109    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
110        let moisture: u8 = state.get_value(MOISTURE);
111
112        let is_near_water = Self::is_near_water(world, pos);
113        let is_raining = world.is_raining_at(pos.above());
114
115        if !is_near_water && !is_raining {
116            // Not near water - decrease moisture or turn to dirt
117            if moisture > 0 {
118                // Decrease moisture by 1
119                let new_state = state.set_value(MOISTURE, moisture - 1);
120                world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
121            } else if !Self::should_maintain_farmland(world, pos) {
122                // No moisture and no crop - turn to dirt
123                turn_to_dirt(state, world, pos, None);
124            }
125        } else if moisture < MAX_MOISTURE {
126            // Near water - hydrate to max
127            let new_state = state.set_value(MOISTURE, MAX_MOISTURE);
128            world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
129        }
130    }
131
132    fn fall_on(
133        &self,
134        state: BlockStateId,
135        world: &Arc<World>,
136        pos: BlockPos,
137        context: EntityFallOnContext<'_>,
138    ) -> Option<EntityFallDamage> {
139        let mob_griefing = world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING);
140        let random_float = rand::random::<f32>();
141        if Self::should_turn_to_dirt_on_fall(context, mob_griefing, random_float) {
142            turn_to_dirt(state, world, pos, context.source_entity());
143        }
144
145        self.default_fall_on(state, world, pos, context)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    use steel_registry::{sound_events, vanilla_entities};
154
155    use crate::behavior::EntityFallOnFacts;
156
157    fn fall_context(
158        fall_distance: f64,
159        entity_type_is_player: bool,
160        is_living_entity: bool,
161        bounding_box_width: f64,
162        bounding_box_height: f64,
163    ) -> EntityFallOnContext<'static> {
164        EntityFallOnContext::new(
165            fall_distance,
166            false,
167            EntityFallOnFacts::new(
168                if entity_type_is_player {
169                    &vanilla_entities::PLAYER
170                } else {
171                    &vanilla_entities::ZOMBIE
172                },
173                is_living_entity,
174                bounding_box_width,
175                bounding_box_height,
176                (
177                    &sound_events::ENTITY_GENERIC_SMALL_FALL,
178                    &sound_events::ENTITY_GENERIC_BIG_FALL,
179                ),
180            ),
181            None,
182        )
183    }
184
185    #[test]
186    fn fall_trampling_requires_random_below_fall_distance_minus_half() {
187        assert!(FarmlandBlock::should_turn_to_dirt_on_fall(
188            fall_context(1.0, true, true, 0.6, 1.8),
189            false,
190            0.49,
191        ));
192        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
193            fall_context(1.0, true, true, 0.6, 1.8),
194            false,
195            0.5,
196        ));
197    }
198
199    #[test]
200    fn non_player_living_entities_need_mob_griefing_to_trample() {
201        let context = fall_context(1.0, false, true, 0.6, 1.8);
202
203        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
204            context, false, 0.0,
205        ));
206        assert!(FarmlandBlock::should_turn_to_dirt_on_fall(
207            context, true, 0.0,
208        ));
209    }
210
211    #[test]
212    fn small_or_non_living_entities_do_not_trample() {
213        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
214            fall_context(1.0, true, false, 0.6, 1.8),
215            false,
216            0.0,
217        ));
218        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
219            fall_context(1.0, true, true, 0.25, 0.25),
220            false,
221            0.0,
222        ));
223    }
224}