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::entity::{Entity, entity_loot_ref};
24use crate::player::Player;
25use crate::world::game_event::GameEventContext;
26use crate::world::{LevelReader, ScheduledTickAccess, World};
27
28use super::BlockRef;
29
30/// Vanilla `CaveVinesBlock` (head) survival.
31#[block_behavior]
32pub struct CaveVinesBlock {
33    base: GrowingPlantHeadBlock,
34}
35
36const BERRIES: &BoolProperty = &BlockStateProperties::BERRIES;
37
38impl CaveVinesBlock {
39    /// Creates a new cave vines (head) block behavior.
40    #[must_use]
41    pub const fn new(block: BlockRef) -> Self {
42        Self {
43            base: GrowingPlantHeadBlock::new(
44                block,
45                Direction::Down,
46                false,
47                0.1,
48                &vanilla_blocks::CAVE_VINES_PLANT,
49                None,
50                Self::can_grow_into,
51            )
52            .with_update_body_after_converted_from_head(Self::update_body_after_converted_from_head)
53            .with_update_grow_into_state(Self::update_grow_into_state),
54        }
55    }
56
57    /// Cave Vines `canGrowInto()`
58    #[must_use]
59    pub fn can_grow_into(state: BlockStateId) -> bool {
60        state.is_air()
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.base.can_survive(state, world, pos)
127    }
128
129    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
130        self.base.random_tick(state, world, pos);
131    }
132
133    fn update_shape(
134        &self,
135        state: BlockStateId,
136        world: &dyn ScheduledTickAccess,
137        pos: BlockPos,
138        direction: Direction,
139        neighbor_pos: BlockPos,
140        neighbor_state: BlockStateId,
141    ) -> BlockStateId {
142        self.base
143            .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
144    }
145    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
146        self.base.tick(state, world, pos);
147    }
148
149    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
150        self.base.get_state_for_placement(context)
151    }
152
153    fn get_clone_item_stack(
154        &self,
155        _block: BlockRef,
156        _state: BlockStateId,
157        _include_data: bool,
158    ) -> Option<ItemStack> {
159        Some(ItemStack::new(&vanilla_items::GLOW_BERRIES))
160    }
161
162    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
163        Some(self)
164    }
165
166    fn as_growing_plant_head(&self) -> Option<&dyn GrowingPlantHeadBehavior> {
167        Some(&self.base)
168    }
169}
170impl Bonemealable for CaveVinesBlock {
171    fn is_valid_bonemeal_target(
172        &self,
173        state: BlockStateId,
174        _world: &dyn LevelReader,
175        _pos: BlockPos,
176    ) -> bool {
177        !state.get_value(BERRIES)
178    }
179
180    fn perform_bonemeal(
181        &self,
182        state: BlockStateId,
183        world: &Arc<World>,
184        _rng: &mut dyn Rng,
185        pos: BlockPos,
186    ) {
187        world.set_block(
188            pos,
189            state.set_value(BERRIES, true),
190            UpdateFlags::UPDATE_CLIENTS,
191        );
192    }
193
194    fn bonemeal_action_type(&self) -> BonemealAction {
195        BonemealAction::Grower
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use rand::{SeedableRng as _, rngs::StdRng};
202    use steel_registry::init_vanilla_registry;
203
204    use super::*;
205    use crate::test_support::TestLevel;
206
207    #[test]
208    fn head_conversion_preserves_berries() {
209        init_vanilla_registry();
210
211        let behavior = CaveVinesBlock::new(&vanilla_blocks::CAVE_VINES);
212        let state = vanilla_blocks::CAVE_VINES
213            .default_state()
214            .set_value(BERRIES, true);
215        let level = TestLevel::default();
216
217        let converted = behavior.update_shape(
218            state,
219            &level,
220            BlockPos::ZERO,
221            Direction::Down,
222            BlockPos::ZERO.below(),
223            vanilla_blocks::CAVE_VINES_PLANT.default_state(),
224        );
225
226        assert_eq!(converted.get_block(), &vanilla_blocks::CAVE_VINES_PLANT);
227        assert!(converted.get_value(BERRIES));
228    }
229
230    #[test]
231    fn grown_head_rolls_berries_independently() {
232        init_vanilla_registry();
233
234        let state = vanilla_blocks::CAVE_VINES.default_state();
235        let mut rng = StdRng::seed_from_u64(1);
236        let berry_states = (0..256)
237            .filter(|_| CaveVinesBlock::update_grow_into_state(state, &mut rng).get_value(BERRIES))
238            .count();
239
240        assert!(berry_states > 0);
241        assert!(berry_states < 256);
242    }
243
244    #[test]
245    fn clone_item_is_glow_berries() {
246        init_vanilla_registry();
247
248        let behavior = CaveVinesBlock::new(&vanilla_blocks::CAVE_VINES);
249        let item = behavior
250            .get_clone_item_stack(
251                &vanilla_blocks::CAVE_VINES,
252                vanilla_blocks::CAVE_VINES.default_state(),
253                false,
254            )
255            .expect("cave vines have a vanilla clone item");
256
257        assert!(item.is(&vanilla_items::GLOW_BERRIES));
258    }
259}