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