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;
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
33impl FarmlandBlock {
34    /// Creates a new farmland block behavior.
35    #[must_use]
36    pub const fn new(block: BlockRef) -> Self {
37        Self { block }
38    }
39
40    /// Checks if there is water within a 9x9x2 area centered on the farmland.
41    /// Vanilla checks from (-4, 0, -4) to (4, 1, 4) relative to the farmland.
42    ///
43    /// This checks both water blocks and waterlogged blocks (matching vanilla's
44    /// FluidTags.WATER check on fluid state).
45    fn is_near_water(world: &Arc<World>, pos: BlockPos) -> bool {
46        for dy in 0..=1 {
47            for dx in -4..=4 {
48                for dz in -4..=4 {
49                    let check_pos = pos.offset(dx, dy, dz);
50                    let state = world.get_block_state(check_pos);
51
52                    // Check if block is water
53                    if state.get_block() == &vanilla_blocks::WATER {
54                        return true;
55                    }
56
57                    // Check if block is waterlogged
58                    if state
59                        .try_get_value(&BlockStateProperties::WATERLOGGED)
60                        .unwrap_or(false)
61                    {
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(
107            self.block
108                .default_state()
109                .set_value(&BlockStateProperties::MOISTURE, 0u8),
110        )
111    }
112
113    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
114        let moisture: u8 = state.get_value(&BlockStateProperties::MOISTURE);
115
116        let is_near_water = Self::is_near_water(world, pos);
117        let is_raining = world.is_raining_at(pos.above());
118
119        if !is_near_water && !is_raining {
120            // Not near water - decrease moisture or turn to dirt
121            if moisture > 0 {
122                // Decrease moisture by 1
123                let new_state = state.set_value(&BlockStateProperties::MOISTURE, moisture - 1);
124                world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
125            } else if !Self::should_maintain_farmland(world, pos) {
126                // No moisture and no crop - turn to dirt
127                turn_to_dirt(state, world, pos, None);
128            }
129        } else if moisture < MAX_MOISTURE {
130            // Near water - hydrate to max
131            let new_state = state.set_value(&BlockStateProperties::MOISTURE, MAX_MOISTURE);
132            world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
133        }
134    }
135
136    fn fall_on(
137        &self,
138        state: BlockStateId,
139        world: &Arc<World>,
140        pos: BlockPos,
141        context: EntityFallOnContext<'_>,
142    ) -> Option<EntityFallDamage> {
143        let mob_griefing = world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING);
144        let random_float = rand::random::<f32>();
145        if Self::should_turn_to_dirt_on_fall(context, mob_griefing, random_float) {
146            turn_to_dirt(state, world, pos, context.source_entity());
147        }
148
149        self.default_fall_on(state, world, pos, context)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    use steel_registry::{sound_events, vanilla_entities};
158
159    use crate::behavior::EntityFallOnFacts;
160
161    fn fall_context(
162        fall_distance: f64,
163        entity_type_is_player: bool,
164        is_living_entity: bool,
165        bounding_box_width: f64,
166        bounding_box_height: f64,
167    ) -> EntityFallOnContext<'static> {
168        EntityFallOnContext::new(
169            fall_distance,
170            false,
171            EntityFallOnFacts::new(
172                if entity_type_is_player {
173                    &vanilla_entities::PLAYER
174                } else {
175                    &vanilla_entities::ZOMBIE
176                },
177                is_living_entity,
178                bounding_box_width,
179                bounding_box_height,
180                (
181                    &sound_events::ENTITY_GENERIC_SMALL_FALL,
182                    &sound_events::ENTITY_GENERIC_BIG_FALL,
183                ),
184            ),
185            None,
186        )
187    }
188
189    #[test]
190    fn fall_trampling_requires_random_below_fall_distance_minus_half() {
191        assert!(FarmlandBlock::should_turn_to_dirt_on_fall(
192            fall_context(1.0, true, true, 0.6, 1.8),
193            false,
194            0.49,
195        ));
196        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
197            fall_context(1.0, true, true, 0.6, 1.8),
198            false,
199            0.5,
200        ));
201    }
202
203    #[test]
204    fn non_player_living_entities_need_mob_griefing_to_trample() {
205        let context = fall_context(1.0, false, true, 0.6, 1.8);
206
207        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
208            context, false, 0.0,
209        ));
210        assert!(FarmlandBlock::should_turn_to_dirt_on_fall(
211            context, true, 0.0,
212        ));
213    }
214
215    #[test]
216    fn small_or_non_living_entities_do_not_trample() {
217        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
218            fall_context(1.0, true, false, 0.6, 1.8),
219            false,
220            0.0,
221        ));
222        assert!(!FarmlandBlock::should_turn_to_dirt_on_fall(
223            fall_context(1.0, true, true, 0.25, 0.25),
224            false,
225            0.0,
226        ));
227    }
228}