steel_core/behavior/blocks/vegetation/
farmland_block.rs1use 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
18const MAX_MOISTURE: u8 = 7;
20const TRAMPLE_VOLUME_THRESHOLD: f64 = 0.512;
21
22#[block_behavior]
29pub struct FarmlandBlock {
30 block: BlockRef,
31}
32
33const MOISTURE: &IntProperty = &BlockStateProperties::MOISTURE;
34const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
35
36impl FarmlandBlock {
37 #[must_use]
39 pub const fn new(block: BlockRef) -> Self {
40 Self { block }
41 }
42
43 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 if state.get_block() == &vanilla_blocks::WATER {
57 return true;
58 }
59
60 if state.try_get_value(WATERLOGGED).unwrap_or(false) {
62 return true;
63 }
64 }
65 }
66 }
67 false
68 }
69
70 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 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 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 if moisture > 0 {
118 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 turn_to_dirt(state, world, pos, None);
124 }
125 } else if moisture < MAX_MOISTURE {
126 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}