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;
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
33impl FarmlandBlock {
34 #[must_use]
36 pub const fn new(block: BlockRef) -> Self {
37 Self { block }
38 }
39
40 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 if state.get_block() == &vanilla_blocks::WATER {
54 return true;
55 }
56
57 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 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(
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 if moisture > 0 {
122 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 turn_to_dirt(state, world, pos, None);
128 }
129 } else if moisture < MAX_MOISTURE {
130 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}