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