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