steel_core/behavior/blocks/vegetation/
crop_block.rs1use 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::block_entity::SharedBlockEntity;
24use crate::entity::{Entity, InsideBlockEffectCollector};
25use crate::world::{LevelReader, ScheduledTickAccess, World};
26
27const MOISTURE: &IntProperty = &BlockStateProperties::MOISTURE;
28
29pub(super) const MIN_CROP_LIGHT_LEVEL: u8 = 8;
30const MIN_CROP_GROWTH_LIGHT_LEVEL: u8 = 9;
31pub(super) const CROP_GROWTH_CHANCE_BASE: f32 = 25.0;
32pub(super) const ADJACENT_FARMLAND_SPEED_DIVISOR: f32 = 4.0;
33pub(super) const CROWDED_CROP_SPEED_DIVISOR: f32 = 2.0;
34
35#[block_behavior]
40pub struct CropBlock {
41 block: BlockRef,
42}
43
44const AGE: &IntProperty = &BlockStateProperties::AGE_7;
45
46pub(super) fn crop_growth_speed(
51 block: BlockRef,
52 world: &(impl LevelReader + ?Sized),
53 pos: BlockPos,
54) -> f32 {
55 let mut speed = 1.0_f32;
56 let below = pos.below();
57
58 for dx in -1..=1 {
59 for dz in -1..=1 {
60 let state = world.get_block_state(below.offset(dx, 0, dz));
61 if !state.get_block().has_tag(&BlockTag::GROWS_CROPS) {
62 continue;
63 }
64
65 let block_speed = if state.try_get_value(MOISTURE).unwrap_or(0) > 0 {
66 3.0
67 } else {
68 1.0
69 };
70
71 speed += if dx == 0 && dz == 0 {
72 block_speed
73 } else {
74 block_speed / ADJACENT_FARMLAND_SPEED_DIVISOR
75 };
76 }
77 }
78
79 let north = pos.north();
80 let south = pos.south();
81 let west = pos.west();
82 let east = pos.east();
83 let same_block_at = |neighbor: BlockPos| block == world.get_block_state(neighbor).get_block();
84
85 let east_west = [west, east].into_iter().any(&same_block_at);
86 let north_south = [north, south].into_iter().any(&same_block_at);
87 let crowded = (east_west && north_south)
88 || [west.north(), east.north(), east.south(), west.south()]
89 .into_iter()
90 .any(same_block_at);
91
92 if crowded {
93 speed / CROWDED_CROP_SPEED_DIVISOR
94 } else {
95 speed
96 }
97}
98
99pub(crate) trait CropLike {
100 fn block(&self) -> BlockRef;
101 fn age_property(&self) -> &IntProperty;
102 fn max_age(&self) -> u8;
103 fn clone_item_stack(&self) -> ItemStack;
104
105 fn should_random_tick(&self) -> bool {
107 true
108 }
109
110 fn get_age(&self, state: BlockStateId) -> u8 {
111 state.get_value(self.age_property())
112 }
113
114 fn get_state_for_age(&self, age: u8) -> BlockStateId {
115 self.block()
116 .default_state()
117 .set_value(self.age_property(), age)
118 }
119
120 fn is_max_age(&self, state: BlockStateId) -> bool {
121 state.get_value(self.age_property()) >= self.max_age()
122 }
123
124 fn has_sufficient_light(&self, world: &dyn LevelReader, pos: BlockPos) -> bool {
125 world.raw_brightness(pos, 0) >= MIN_CROP_LIGHT_LEVEL
126 }
127
128 fn has_sufficient_growth_light(&self, world: &dyn LevelReader, pos: BlockPos) -> bool {
129 world.raw_brightness(pos, 0) >= MIN_CROP_GROWTH_LIGHT_LEVEL
130 }
131
132 fn get_growth_speed(&self, world: &Arc<World>, pos: BlockPos) -> f32 {
139 crop_growth_speed(self.block(), world.as_ref(), pos)
140 }
141
142 fn on_random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
143 if !self.has_sufficient_growth_light(world.as_ref(), pos) {
144 return;
145 }
146
147 let age = self.get_age(state);
148 if age < self.max_age() {
149 let growth_speed = self.get_growth_speed(world, pos);
150
151 let growth_chance = (CROP_GROWTH_CHANCE_BASE / growth_speed) as u32 + 1;
154
155 if rand::random::<u32>().is_multiple_of(growth_chance) {
156 let new_state = self.get_state_for_age(age + 1);
157 world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
158 }
159 }
160 }
161}
162
163pub(super) fn ravager_breaks_crop(entity_type: EntityTypeRef, mob_griefing: bool) -> bool {
164 entity_type == &vanilla_entities::RAVAGER && mob_griefing
165}
166
167pub(super) fn destroy_crop_on_ravager_contact(
168 world: &Arc<World>,
169 pos: BlockPos,
170 entity: &dyn Entity,
171) {
172 if ravager_breaks_crop(
173 entity.entity_type(),
174 world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING),
175 ) {
176 world.destroy_block_by_entity(pos, true, entity);
177 }
178}
179
180impl CropBlock {
181 #[must_use]
183 pub const fn new(block: BlockRef) -> Self {
184 Self { block }
185 }
186}
187
188impl CropLike for CropBlock {
189 fn block(&self) -> BlockRef {
190 self.block
191 }
192
193 fn age_property(&self) -> &IntProperty {
194 AGE
195 }
196
197 fn max_age(&self) -> u8 {
198 7
199 }
200
201 fn clone_item_stack(&self) -> ItemStack {
202 ItemStack::new(&vanilla_items::WHEAT_SEEDS)
203 }
204}
205
206impl Bonemealable for CropBlock {
207 fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn rand::Rng) -> u8 {
208 rng.random_range(2..=5)
209 }
210
211 fn perform_bonemeal(
212 &self,
213 state: BlockStateId,
214 world: &Arc<World>,
215 rng: &mut dyn rand::Rng,
216 pos: BlockPos,
217 ) {
218 self.default_perform_bonemeal(state, world, rng, pos);
219 }
220
221 fn is_valid_bonemeal_target(
222 &self,
223 state: BlockStateId,
224 _world: &dyn LevelReader,
225 _pos: BlockPos,
226 ) -> bool {
227 !self.is_max_age(state)
228 }
229}
230
231impl<T: CropLike + Bonemealable + Send + Sync> BlockBehavior for T {
232 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
233 if self.may_place_on(
234 context.world.get_block_state(context.place_pos().below()),
235 context.world,
236 context.place_pos().below(),
237 ) {
238 Some(self.block().default_state())
239 } else {
240 None
241 }
242 }
243
244 fn can_survive(
245 &self,
246 state: BlockStateId,
247 world: &dyn LevelReader,
248 pos: steel_utils::BlockPos,
249 ) -> bool {
250 self.has_sufficient_light(world, pos) && vegetation_can_survive(self, state, world, pos)
251 }
252
253 fn update_shape(
254 &self,
255 state: BlockStateId,
256 world: &dyn ScheduledTickAccess,
257 pos: steel_utils::BlockPos,
258 _direction: steel_utils::Direction,
259 _neighbor_pos: steel_utils::BlockPos,
260 _neighbor_state: BlockStateId,
261 ) -> BlockStateId {
262 survival_update_shape(self, state, world, pos)
263 }
264
265 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
266 if self.should_random_tick() {
267 self.on_random_tick(state, world, pos);
268 }
269 }
270
271 fn entity_inside(
272 &self,
273 state: BlockStateId,
274 world: &Arc<World>,
275 pos: BlockPos,
276 entity: &dyn Entity,
277 effect_collector: &mut InsideBlockEffectCollector,
278 is_precise: bool,
279 ) {
280 destroy_crop_on_ravager_contact(world, pos, entity);
281 self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
282 }
283
284 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
285 Some(self)
286 }
287
288 fn get_clone_item_stack(
289 &self,
290 _block: BlockRef,
291 _state: BlockStateId,
292 _block_entity: Option<SharedBlockEntity>,
293 _include_data: bool,
294 ) -> Option<ItemStack> {
295 Some(self.clone_item_stack())
296 }
297}
298
299impl<T: CropLike> Vegetation for T {
300 fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
301 state.get_block().has_tag(&BlockTag::SUPPORTS_CROPS)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use steel_registry::{init_vanilla_registry, vanilla_blocks};
308
309 use crate::test_support::TestLevel;
310
311 use super::*;
312
313 fn crop_survival_level(support: BlockStateId, raw_brightness: u8) -> TestLevel {
314 TestLevel::default()
315 .with_block(BlockPos::ZERO.below(), support)
316 .with_raw_brightness(raw_brightness)
317 }
318
319 #[test]
320 fn crop_survival_requires_vanilla_minimum_light() {
321 init_vanilla_registry();
322
323 let crop = CropBlock::new(&vanilla_blocks::WHEAT);
324 let state = vanilla_blocks::WHEAT.default_state();
325 let farmland = vanilla_blocks::FARMLAND.default_state();
326
327 assert!(!crop.can_survive(state, &crop_survival_level(farmland, 7), BlockPos::ZERO));
328 assert!(crop.can_survive(state, &crop_survival_level(farmland, 8), BlockPos::ZERO));
329 }
330
331 #[test]
332 fn ravager_crop_breaking_requires_mob_griefing() {
333 assert!(ravager_breaks_crop(&vanilla_entities::RAVAGER, true));
334 assert!(!ravager_breaks_crop(&vanilla_entities::RAVAGER, false));
335 assert!(!ravager_breaks_crop(&vanilla_entities::ZOMBIE, true));
336 }
337}