steel_core/behavior/blocks/vegetation/
pitcher_crop.rs1use std::sync::Arc;
2
3use rand::RngExt;
4use steel_macros::block_behavior;
5use steel_registry::{
6 blocks::{
7 BlockRef,
8 block_state_ext::BlockStateExt,
9 properties::{BlockStateProperties, DoubleBlockHalf, EnumProperty, IntProperty},
10 },
11 vanilla_block_tags::BlockTag,
12 vanilla_blocks,
13};
14use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
15
16use crate::{
17 behavior::{
18 BlockBehavior, BlockPlaceContext,
19 blocks::{
20 DoublePlantBlock,
21 vegetation::{
22 Vegetation, bonemealable::Bonemealable, crop_block::destroy_crop_on_ravager_contact,
23 },
24 },
25 },
26 entity::{Entity, InsideBlockEffectCollector},
27 world::{LevelReader, ScheduledTickAccess, World},
28};
29
30use super::crop_block::{
31 ADJACENT_FARMLAND_SPEED_DIVISOR, CROP_GROWTH_CHANCE_BASE, CROWDED_CROP_SPEED_DIVISOR,
32 MIN_CROP_LIGHT_LEVEL,
33};
34
35const HALF_PROPERTY: &EnumProperty<DoubleBlockHalf> = &BlockStateProperties::DOUBLE_BLOCK_HALF;
36const AGE: &IntProperty = &BlockStateProperties::AGE_4;
37
38#[block_behavior]
40pub struct PitcherCropBlock {
41 base: DoublePlantBlock,
42}
43
44const MOISTURE: &IntProperty = &BlockStateProperties::MOISTURE;
45
46impl PitcherCropBlock {
47 #[must_use]
49 pub const fn new(block: BlockRef) -> Self {
50 Self {
51 base: DoublePlantBlock::new(block),
52 }
53 }
54
55 fn is_lower(state: BlockStateId) -> bool {
56 state.get_block() == &vanilla_blocks::PITCHER_CROP
57 && state.get_value(HALF_PROPERTY) == DoubleBlockHalf::Lower
58 }
59
60 fn get_growth_speed(&self, world: &Arc<World>, pos: BlockPos) -> f32 {
61 let mut speed = 1.0f32;
62 let below = pos.below();
63
64 for dx in -1..=1 {
66 for dz in -1..=1 {
67 let check_pos = below.offset(dx, 0, dz);
68 let block_state = world.get_block_state(check_pos);
69 let mut block_speed = 0.0f32;
70
71 if block_state.get_block().has_tag(&BlockTag::GROWS_CROPS) {
72 block_speed = 1.0;
73 let moisture = block_state.try_get_value(MOISTURE).unwrap_or(0);
75 if moisture > 0 {
76 block_speed = 3.0;
77 }
78 }
79
80 if dx != 0 || dz != 0 {
82 block_speed /= ADJACENT_FARMLAND_SPEED_DIVISOR;
83 }
84
85 speed += block_speed;
86 }
87 }
88
89 let north = world.get_block_state(pos.north());
91 let south = world.get_block_state(pos.south());
92 let west = world.get_block_state(pos.west());
93 let east = world.get_block_state(pos.east());
94
95 let horizontal_row =
96 self.base.block == west.get_block() || self.base.block == east.get_block();
97 let vertical_row =
98 self.base.block == north.get_block() || self.base.block == south.get_block();
99
100 if horizontal_row && vertical_row {
101 speed /= CROWDED_CROP_SPEED_DIVISOR;
103 } else {
104 let nw = world.get_block_state(pos.north().west());
106 let ne = world.get_block_state(pos.north().east());
107 let sw = world.get_block_state(pos.south().west());
108 let se = world.get_block_state(pos.south().east());
109
110 let has_diagonal = self.base.block == nw.get_block()
111 || self.base.block == ne.get_block()
112 || self.base.block == sw.get_block()
113 || self.base.block == se.get_block();
114
115 if has_diagonal {
116 speed /= CROWDED_CROP_SPEED_DIVISOR;
117 }
118 }
119
120 speed
121 }
122
123 fn get_lower_half(
124 state: BlockStateId,
125 world: &dyn LevelReader,
126 pos: BlockPos,
127 ) -> Option<(BlockStateId, BlockPos)> {
128 if Self::is_lower(state) {
129 Some((state, pos))
130 } else {
131 let (state_below, pos_below) = (world.get_block_state(pos.below()), pos.below());
132 if Self::is_lower(state_below) {
133 Some((state_below, pos_below))
134 } else {
135 None
136 }
137 }
138 }
139
140 fn grow(world: &Arc<World>, lower_state: BlockStateId, lower_pos: BlockPos, increase: u8) {
141 let new_age = (lower_state.get_value(AGE) + increase).min(AGE.max);
142 if !Self::can_grow(world, lower_state, lower_pos, new_age) {
143 return;
144 }
145
146 let new_state = lower_state.set_value(AGE, new_age);
147 world.set_block(lower_pos, new_state, UpdateFlags::UPDATE_CLIENTS);
148
149 if new_age >= 3 {
150 world.set_block(
151 lower_pos.above(),
152 new_state.set_value(HALF_PROPERTY, DoubleBlockHalf::Upper),
153 UpdateFlags::UPDATE_ALL,
154 );
155 }
156 }
157
158 fn can_grow(world: &dyn LevelReader, state: BlockStateId, pos: BlockPos, new_age: u8) -> bool {
159 let state_above = world.get_block_state(pos.above());
160 state.get_value(AGE) < AGE.max
161 && world.raw_brightness(pos, 0) >= MIN_CROP_LIGHT_LEVEL
162 && !world.is_outside_build_height(pos.above().y())
163 && (new_age < 3
164 || state_above.is_air()
165 || state_above.get_block() == &vanilla_blocks::PITCHER_CROP)
166 }
167}
168
169impl BlockBehavior for PitcherCropBlock {
170 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
171 if self.may_place_on(
172 context.world.get_block_state(context.place_pos().below()),
173 context.world,
174 context.place_pos().below(),
175 ) {
176 Some(self.base.block.default_state())
177 } else {
178 None
179 }
180 }
181
182 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
183 if Self::is_lower(state) && world.raw_brightness(pos, 0) < MIN_CROP_LIGHT_LEVEL {
184 return false;
185 }
186
187 self.base.can_survive(state, world, pos)
188 }
189
190 fn update_shape(
191 &self,
192 state: BlockStateId,
193 world: &dyn ScheduledTickAccess,
194 pos: BlockPos,
195 direction: steel_utils::Direction,
196 neighbor_pos: BlockPos,
197 neighbor_state: BlockStateId,
198 ) -> BlockStateId {
199 if state.get_value(AGE) >= 3 {
200 self.base
201 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
202 } else if self.can_survive(state, world, pos) {
203 state
204 } else {
205 vanilla_blocks::AIR.default_state()
206 }
207 }
208
209 fn entity_inside(
210 &self,
211 _state: BlockStateId,
212 world: &Arc<World>,
213 pos: BlockPos,
214 entity: &dyn Entity,
215 _effect_collector: &mut InsideBlockEffectCollector,
216 _is_precise: bool,
217 ) {
218 destroy_crop_on_ravager_contact(world, pos, entity);
219 }
220
221 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
222 let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) else {
223 return;
224 };
225 let growth_speed = self.get_growth_speed(world, lower_pos);
226 let should_progress_growth = rand::rng()
227 .random_range(0_i32..((CROP_GROWTH_CHANCE_BASE / growth_speed) as i32 + 1))
228 == 0;
229 if should_progress_growth {
230 Self::grow(world, lower_state, lower_pos, 1);
231 }
232 }
233
234 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
235 Some(self)
236 }
237}
238
239impl Vegetation for PitcherCropBlock {
240 fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
241 state.get_block().has_tag(&BlockTag::SUPPORTS_CROPS)
242 }
243}
244
245impl Bonemealable for PitcherCropBlock {
246 fn is_valid_bonemeal_target(
247 &self,
248 state: BlockStateId,
249 world: &dyn LevelReader,
250 pos: BlockPos,
251 ) -> bool {
252 let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) else {
253 return false;
254 };
255 if lower_state.get_block() != &vanilla_blocks::PITCHER_CROP
256 || lower_state.get_value(HALF_PROPERTY) != DoubleBlockHalf::Lower
257 {
258 return false;
259 }
260
261 let new_age = lower_state.get_value(AGE) + 1;
262
263 Self::can_grow(world, lower_state, lower_pos, new_age)
264 }
265
266 fn perform_bonemeal(
267 &self,
268 state: BlockStateId,
269 world: &Arc<World>,
270 _rng: &mut dyn rand::Rng,
271 pos: BlockPos,
272 ) {
273 if let Some((lower_state, lower_pos)) = Self::get_lower_half(state, world, pos) {
274 Self::grow(world, lower_state, lower_pos, 1);
275 }
276 }
277}