1use std::sync::Arc;
2
3use glam::DVec3;
4use rand::RngExt;
5use steel_macros::block_behavior;
6use steel_registry::{
7 blocks::{
8 BlockRef,
9 block_state_ext::BlockStateExt,
10 properties::{BlockStateProperties, IntProperty},
11 },
12 item_stack::ItemStack,
13 items::item::BlockHitResult,
14 sound_events, vanilla_damage_types, vanilla_entities, vanilla_items,
15 vanilla_loot_tables::{self},
16};
17use steel_utils::{
18 BlockPos, BlockStateId,
19 types::{InteractionHand, UpdateFlags},
20};
21
22use crate::{
23 behavior::block::drop_from_block_interact_loot_table, block_entity::SharedBlockEntity,
24};
25use crate::{
26 behavior::{
27 BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
28 blocks::vegetation::{
29 Vegetation,
30 bonemealable::Bonemealable,
31 vegetation_block::{survival_update_shape, vegetation_can_survive},
32 },
33 },
34 entity::{Entity, InsideBlockEffectCollector, damage::DamageSource},
35 player::Player,
36 world::{LevelReader, ScheduledTickAccess, World},
37};
38
39const DAMAGE_MOVEMENT_THRESHOLD: f64 = 0.003;
40
41#[block_behavior]
43pub struct SweetBerryBushBlock {
44 block: BlockRef,
45}
46
47const AGE: &IntProperty = &BlockStateProperties::AGE_3;
48
49impl SweetBerryBushBlock {
50 #[must_use]
52 pub const fn new(block: BlockRef) -> Self {
53 Self { block }
54 }
55}
56
57impl BlockBehavior for SweetBerryBushBlock {
58 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
59 if self.may_place_on(
60 context.world.get_block_state(context.place_pos().below()),
61 context.world,
62 context.place_pos().below(),
63 ) {
64 Some(self.block.default_state().set_value(AGE, 0))
65 } else {
66 None
67 }
68 }
69
70 fn update_shape(
71 &self,
72 state: BlockStateId,
73 world: &dyn ScheduledTickAccess,
74 pos: BlockPos,
75 _direction: steel_utils::Direction,
76 _neighbor_pos: BlockPos,
77 _neighbor_state: BlockStateId,
78 ) -> BlockStateId {
79 survival_update_shape(self, state, world, pos)
80 }
81
82 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
83 vegetation_can_survive(self, state, world, pos)
84 }
85
86 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
87 let age = state.get_value(AGE);
88 if age >= 3 || rand::random_range(0..5) != 0 || world.raw_brightness(pos.above(), 0) < 9 {
89 return;
90 }
91 world.set_block(
92 pos,
93 state.set_value(AGE, age + 1),
94 UpdateFlags::UPDATE_CLIENTS,
95 );
96 }
97
98 fn entity_inside(
99 &self,
100 state: BlockStateId,
101 world: &Arc<World>,
102 _pos: BlockPos,
103 entity: &dyn Entity,
104 _effect_collector: &mut InsideBlockEffectCollector,
105 _is_precise: bool,
106 ) {
107 if !Self::applies_contact_effects(entity) {
108 return;
109 }
110
111 entity.make_stuck_in_block(state, DVec3::new(0.8, 0.75, 0.8));
112 Self::apply_contact_damage(world, state, entity);
113 }
114
115 fn use_item_on(
116 &self,
117 state: BlockStateId,
118 _world: &Arc<World>,
119 _pos: BlockPos,
120 _player: &Player,
121 _hand: InteractionHand,
122 _hit_result: &BlockHitResult,
123 inv: &mut InventoryAccess,
124 ) -> InteractionResult {
125 let is_bone_meal = inv.with_item(|item_stack| item_stack.is(&vanilla_items::BONE_MEAL));
126 let age = state.get_value(AGE);
127 if age != 3 && is_bone_meal {
128 InteractionResult::Pass
129 } else {
130 InteractionResult::TryEmptyHandInteraction
131 }
132 }
133
134 fn use_without_item(
135 &self,
136 state: BlockStateId,
137 world: &Arc<World>,
138 pos: BlockPos,
139 player: &Player,
140 _hit_result: &BlockHitResult,
141 _inv: &mut InventoryAccess,
142 ) -> InteractionResult {
143 let age = state.get_value(AGE);
144 if age <= 1 {
145 return InteractionResult::Pass;
146 }
147
148 let mut rng = rand::rng();
149
150 let items = drop_from_block_interact_loot_table(
151 &vanilla_loot_tables::HARVEST_SWEET_BERRY_BUSH,
152 state,
153 world.get_block_entity(pos),
154 None,
155 Some(player),
156 &mut rng,
157 );
158
159 for item in items {
160 world.pop_resource(pos, item);
161 }
162
163 world.play_block_sound(
164 &sound_events::BLOCK_SWEET_BERRY_BUSH_PICK_BERRIES,
165 pos,
166 1.0,
167 rng.random_range(0.8..1.2),
168 Some(player.id()),
169 );
170
171 let new_state = state.set_value(AGE, 1);
172 world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
173
174 InteractionResult::Success
175 }
176
177 fn get_clone_item_stack(
178 &self,
179 _block: BlockRef,
180 _state: BlockStateId,
181 _block_entity: Option<SharedBlockEntity>,
182 _include_data: bool,
183 ) -> Option<ItemStack> {
184 Some(ItemStack::new(&vanilla_items::SWEET_BERRIES))
185 }
186
187 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
188 Some(self)
189 }
190}
191
192impl SweetBerryBushBlock {
193 fn applies_contact_effects(entity: &dyn Entity) -> bool {
194 entity.is_living_entity()
195 && entity.entity_type() != &vanilla_entities::FOX
196 && entity.entity_type() != &vanilla_entities::BEE
197 }
198
199 fn apply_contact_damage(world: &World, state: BlockStateId, entity: &dyn Entity) {
200 if state.get_value(AGE) == 0 {
201 return;
202 }
203
204 let movement = if entity.uses_client_movement_packets() {
205 entity.known_movement()
206 } else {
207 entity.old_position() - entity.position()
208 };
209
210 if movement.x.mul_add(movement.x, movement.z * movement.z) > 0.0
211 && (movement.x.abs() >= DAMAGE_MOVEMENT_THRESHOLD
212 || movement.z.abs() >= DAMAGE_MOVEMENT_THRESHOLD)
213 {
214 entity.hurt(
215 world,
216 &DamageSource::environment(&vanilla_damage_types::SWEET_BERRY_BUSH),
217 1.0,
218 );
219 }
220 }
221}
222
223impl Bonemealable for SweetBerryBushBlock {
224 fn is_valid_bonemeal_target(
225 &self,
226 state: BlockStateId,
227 world: &dyn LevelReader,
228 pos: BlockPos,
229 ) -> bool {
230 state.get_value(AGE) < 3
231 && world.get_block_state(pos.above()).is_air()
232 && !world.is_outside_build_height(pos.above().y())
233 }
234
235 fn perform_bonemeal(
236 &self,
237 state: BlockStateId,
238 world: &Arc<World>,
239 _rng: &mut dyn rand::Rng,
240 pos: BlockPos,
241 ) {
242 let new_age = (state.get_value(AGE) + 1).min(3);
243 world.set_block(
244 pos,
245 state.set_value(AGE, new_age),
246 UpdateFlags::UPDATE_CLIENTS,
247 );
248 }
249}
250
251impl Vegetation for SweetBerryBushBlock {}
252
253#[cfg(test)]
254mod tests {
255 use std::sync::Weak;
256
257 use steel_registry::{entity_type::EntityTypeRef, init_vanilla_registry, vanilla_blocks};
258 use steel_utils::locks::SyncMutex;
259
260 use super::*;
261 use crate::entity::EntityBase;
262 use crate::test_support::test_world;
263
264 struct TestEntity {
265 base: EntityBase,
266 entity_type: EntityTypeRef,
267 is_living: bool,
268 uses_client_movement_packets: bool,
269 known_movement: DVec3,
270 damage: SyncMutex<Vec<(String, f32)>>,
271 }
272
273 impl TestEntity {
274 fn living(entity_type: EntityTypeRef) -> Self {
275 Self {
276 base: EntityBase::new(1, DVec3::ZERO, entity_type.dimensions, Weak::<World>::new()),
277 entity_type,
278 is_living: true,
279 uses_client_movement_packets: false,
280 known_movement: DVec3::ZERO,
281 damage: SyncMutex::new(Vec::new()),
282 }
283 }
284
285 fn with_position(self, position: DVec3) -> Self {
286 self.base.set_position_local(position);
287 self
288 }
289
290 fn with_old_position(self, old_position: DVec3) -> Self {
291 self.base.set_old_position(old_position);
292 self
293 }
294
295 fn with_client_movement(mut self, movement: DVec3) -> Self {
296 self.uses_client_movement_packets = true;
297 self.known_movement = movement;
298 self
299 }
300
301 fn non_living(mut self) -> Self {
302 self.is_living = false;
303 self
304 }
305
306 fn damage_events(&self) -> Vec<(String, f32)> {
307 self.damage.lock().clone()
308 }
309 }
310
311 crate::entity::impl_test_downcast_type!(TestEntity);
312
313 impl Entity for TestEntity {
314 fn base(&self) -> &EntityBase {
315 &self.base
316 }
317
318 fn entity_type(&self) -> EntityTypeRef {
319 self.entity_type
320 }
321
322 fn is_living_entity(&self) -> bool {
323 self.is_living
324 }
325
326 fn uses_client_movement_packets(&self) -> bool {
327 self.uses_client_movement_packets
328 }
329
330 fn known_movement(&self) -> DVec3 {
331 self.known_movement
332 }
333
334 fn hurt(&self, _world: &World, source: &DamageSource, amount: f32) -> bool {
335 self.damage
336 .lock()
337 .push((source.damage_type.key.path.as_ref().to_owned(), amount));
338 true
339 }
340 }
341
342 fn state_with_age(age: u8) -> BlockStateId {
343 init_vanilla_registry();
344 vanilla_blocks::SWEET_BERRY_BUSH
345 .default_state()
346 .set_value(AGE, age)
347 }
348
349 #[test]
350 fn contact_damage_uses_old_position_for_server_authored_entities() {
351 let entity = TestEntity::living(&vanilla_entities::PLAYER)
352 .with_position(DVec3::new(0.0, 0.0, 0.0))
353 .with_old_position(DVec3::new(0.004, 0.0, 0.0));
354
355 SweetBerryBushBlock::apply_contact_damage(test_world(), state_with_age(1), &entity);
356
357 assert_eq!(
358 entity.damage_events(),
359 vec![("sweet_berry_bush".to_owned(), 1.0)]
360 );
361 }
362
363 #[test]
364 fn contact_damage_uses_known_movement_for_client_authored_entities() {
365 let entity = TestEntity::living(&vanilla_entities::PLAYER)
366 .with_position(DVec3::ZERO)
367 .with_old_position(DVec3::ZERO)
368 .with_client_movement(DVec3::new(0.0, 0.0, 0.004));
369
370 SweetBerryBushBlock::apply_contact_damage(test_world(), state_with_age(1), &entity);
371
372 assert_eq!(
373 entity.damage_events(),
374 vec![("sweet_berry_bush".to_owned(), 1.0)]
375 );
376 }
377
378 #[test]
379 fn contact_damage_is_age_gated() {
380 let entity = TestEntity::living(&vanilla_entities::PLAYER)
381 .with_position(DVec3::ZERO)
382 .with_old_position(DVec3::new(0.004, 0.0, 0.0));
383
384 SweetBerryBushBlock::apply_contact_damage(test_world(), state_with_age(0), &entity);
385
386 assert_eq!(entity.damage_events().len(), 0);
387 }
388
389 #[test]
390 fn contact_damage_requires_threshold_movement() {
391 let entity = TestEntity::living(&vanilla_entities::PLAYER)
392 .with_position(DVec3::ZERO)
393 .with_old_position(DVec3::new(0.002_9, 0.0, 0.002_9));
394
395 SweetBerryBushBlock::apply_contact_damage(test_world(), state_with_age(1), &entity);
396
397 assert_eq!(entity.damage_events().len(), 0);
398 }
399
400 #[test]
401 fn foxes_bees_and_non_living_entities_are_immune_to_sweet_berry_bush_effects() {
402 let fox = TestEntity::living(&vanilla_entities::FOX);
403 let bee = TestEntity::living(&vanilla_entities::BEE);
404 let item = TestEntity::living(&vanilla_entities::ITEM).non_living();
405
406 assert!(!SweetBerryBushBlock::applies_contact_effects(&fox));
407 assert!(!SweetBerryBushBlock::applies_contact_effects(&bee));
408 assert!(!SweetBerryBushBlock::applies_contact_effects(&item));
409 }
410}