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