Skip to main content

steel_core/behavior/blocks/vegetation/
sweet_berry_bush.rs

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/// Behavior for Sweet Berry Bushes
42#[block_behavior]
43pub struct SweetBerryBushBlock {
44    block: BlockRef,
45}
46
47const AGE: &IntProperty = &BlockStateProperties::AGE_3;
48
49impl SweetBerryBushBlock {
50    /// Creates a new Sweet Berry Bush Block Behavior
51    #[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}