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