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