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::{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/// Behavior for Sweet Berry Bushes
36#[block_behavior]
37pub struct SweetBerryBushBlock {
38    block: BlockRef,
39}
40
41impl SweetBerryBushBlock {
42    /// Creates a new Sweet Berry Bush Block Behavior
43    #[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}