Skip to main content

steel_core/behavior/blocks/vegetation/
cave_vines_block.rs

1use rand::{Rng, RngExt};
2use std::sync::Arc;
3use steel_macros::block_behavior;
4use steel_protocol::packets::game::SoundSource;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
7use steel_registry::item_stack::ItemStack;
8use steel_registry::items::item::BlockHitResult;
9use steel_registry::loot_table::LootContext;
10use steel_registry::{
11    sound_events, vanilla_blocks, vanilla_game_events, vanilla_items, vanilla_loot_tables,
12};
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId, Direction};
15
16use crate::behavior::blocks::vegetation::bonemealable::BonemealAction;
17use crate::behavior::blocks::vegetation::growing_plant_head_block::{
18    GrowingPlantHeadBehavior, GrowingPlantHeadBlock,
19};
20use crate::behavior::context::BlockPlaceContext;
21use crate::behavior::{InteractionResult, InventoryAccess};
22use crate::behavior::{block::BlockBehavior, blocks::vegetation::bonemealable::Bonemealable};
23use crate::block_entity::SharedBlockEntity;
24use crate::entity::{Entity, entity_loot_ref, loot_equipment};
25use crate::player::Player;
26use crate::world::game_event::GameEventContext;
27use crate::world::{LevelReader, ScheduledTickAccess, World};
28
29use super::BlockRef;
30
31/// Vanilla `CaveVinesBlock` (head) survival.
32#[block_behavior]
33pub struct CaveVinesBlock {
34    base: GrowingPlantHeadBlock,
35}
36
37const BERRIES: &BoolProperty = &BlockStateProperties::BERRIES;
38
39impl CaveVinesBlock {
40    /// Creates a new cave vines (head) block behavior.
41    #[must_use]
42    pub const fn new(block: BlockRef) -> Self {
43        Self {
44            base: GrowingPlantHeadBlock::new(
45                block,
46                Direction::Down,
47                false,
48                0.1,
49                &vanilla_blocks::CAVE_VINES_PLANT,
50                None,
51                Self::can_grow_into,
52            )
53            .with_update_body_after_converted_from_head(Self::update_body_after_converted_from_head)
54            .with_update_grow_into_state(Self::update_grow_into_state),
55        }
56    }
57
58    /// Cave Vines `canGrowInto()`
59    #[must_use]
60    pub fn can_grow_into(state: BlockStateId) -> bool {
61        state.is_air()
62    }
63
64    fn update_body_after_converted_from_head(
65        head_state: BlockStateId,
66        body_state: BlockStateId,
67    ) -> BlockStateId {
68        body_state.set_value(BERRIES, head_state.get_value(BERRIES))
69    }
70
71    fn update_grow_into_state(state: BlockStateId, rng: &mut dyn Rng) -> BlockStateId {
72        state.set_value(BERRIES, rng.random::<f32>() < 0.11)
73    }
74
75    /// Shared behavior use block between cave vine block and plant
76    pub fn use_block(
77        source_entity: &dyn Entity,
78        state: BlockStateId,
79        world: &Arc<World>,
80        pos: BlockPos,
81    ) -> InteractionResult {
82        if !state.get_value(BERRIES) {
83            return InteractionResult::Pass;
84        }
85        let mut rng = rand::rng();
86        let equipment = loot_equipment(source_entity);
87        let mut ctx = LootContext::new(&mut rng)
88            .with_block_state(state)
89            .with_interacting_entity(entity_loot_ref(source_entity, equipment.as_ref()));
90
91        let items = vanilla_loot_tables::HARVEST_CAVE_VINE.get_random_items(&mut ctx);
92        for item in items {
93            world.pop_resource(pos, item);
94        }
95        let pitch = rng.random_range(0.8..1.2);
96        world.play_sound(
97            &sound_events::BLOCK_CAVE_VINES_PICK_BERRIES,
98            SoundSource::Blocks,
99            pos,
100            1.0,
101            pitch,
102            None,
103        );
104        let new_state = state.set_value(BERRIES, false);
105        world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
106        world.game_event(
107            &vanilla_game_events::BLOCK_CHANGE,
108            pos,
109            &GameEventContext::new(Some(source_entity), Some(new_state)),
110        );
111        InteractionResult::Success
112    }
113}
114
115impl BlockBehavior for CaveVinesBlock {
116    fn use_without_item(
117        &self,
118        state: BlockStateId,
119        world: &Arc<World>,
120        pos: BlockPos,
121        player: &Player,
122        _hit_result: &BlockHitResult,
123        _inv: &mut InventoryAccess,
124    ) -> InteractionResult {
125        CaveVinesBlock::use_block(player, state, world, pos)
126    }
127    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
128        self.base.can_survive(state, world, pos)
129    }
130
131    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
132        self.base.random_tick(state, world, pos);
133    }
134
135    fn update_shape(
136        &self,
137        state: BlockStateId,
138        world: &dyn ScheduledTickAccess,
139        pos: BlockPos,
140        direction: Direction,
141        neighbor_pos: BlockPos,
142        neighbor_state: BlockStateId,
143    ) -> BlockStateId {
144        self.base
145            .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
146    }
147    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
148        self.base.tick(state, world, pos);
149    }
150
151    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
152        self.base.get_state_for_placement(context)
153    }
154
155    fn get_clone_item_stack(
156        &self,
157        _block: BlockRef,
158        _state: BlockStateId,
159        _block_entity: Option<SharedBlockEntity>,
160        _include_data: bool,
161    ) -> Option<ItemStack> {
162        Some(ItemStack::new(&vanilla_items::GLOW_BERRIES))
163    }
164
165    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
166        Some(self)
167    }
168
169    fn as_growing_plant_head(&self) -> Option<&dyn GrowingPlantHeadBehavior> {
170        Some(&self.base)
171    }
172}
173impl Bonemealable for CaveVinesBlock {
174    fn is_valid_bonemeal_target(
175        &self,
176        state: BlockStateId,
177        _world: &dyn LevelReader,
178        _pos: BlockPos,
179    ) -> bool {
180        !state.get_value(BERRIES)
181    }
182
183    fn perform_bonemeal(
184        &self,
185        state: BlockStateId,
186        world: &Arc<World>,
187        _rng: &mut dyn Rng,
188        pos: BlockPos,
189    ) {
190        world.set_block(
191            pos,
192            state.set_value(BERRIES, true),
193            UpdateFlags::UPDATE_CLIENTS,
194        );
195    }
196
197    fn bonemeal_action_type(&self) -> BonemealAction {
198        BonemealAction::Grower
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use rand::{SeedableRng as _, rngs::StdRng};
205    use steel_registry::init_vanilla_registry;
206
207    use super::*;
208    use crate::test_support::TestLevel;
209
210    #[test]
211    fn head_conversion_preserves_berries() {
212        init_vanilla_registry();
213
214        let behavior = CaveVinesBlock::new(&vanilla_blocks::CAVE_VINES);
215        let state = vanilla_blocks::CAVE_VINES
216            .default_state()
217            .set_value(BERRIES, true);
218        let level = TestLevel::default();
219
220        let converted = behavior.update_shape(
221            state,
222            &level,
223            BlockPos::ZERO,
224            Direction::Down,
225            BlockPos::ZERO.below(),
226            vanilla_blocks::CAVE_VINES_PLANT.default_state(),
227        );
228
229        assert_eq!(converted.get_block(), &vanilla_blocks::CAVE_VINES_PLANT);
230        assert!(converted.get_value(BERRIES));
231    }
232
233    #[test]
234    fn grown_head_rolls_berries_independently() {
235        init_vanilla_registry();
236
237        let state = vanilla_blocks::CAVE_VINES.default_state();
238        let mut rng = StdRng::seed_from_u64(1);
239        let berry_states = (0..256)
240            .filter(|_| CaveVinesBlock::update_grow_into_state(state, &mut rng).get_value(BERRIES))
241            .count();
242
243        assert!(berry_states > 0);
244        assert!(berry_states < 256);
245    }
246
247    #[test]
248    fn clone_item_is_glow_berries() {
249        init_vanilla_registry();
250
251        let behavior = CaveVinesBlock::new(&vanilla_blocks::CAVE_VINES);
252        let item = behavior
253            .get_clone_item_stack(
254                &vanilla_blocks::CAVE_VINES,
255                vanilla_blocks::CAVE_VINES.default_state(),
256                None,
257                false,
258            )
259            .expect("cave vines have a vanilla clone item");
260
261        assert!(item.is(&vanilla_items::GLOW_BERRIES));
262    }
263}