steel_core/behavior/blocks/vegetation/
cave_vines_block.rs1use 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#[block_behavior]
32pub struct CaveVinesBlock {
33 base: GrowingPlantHeadBlock,
34}
35
36const BERRIES: &BoolProperty = &BlockStateProperties::BERRIES;
37
38impl CaveVinesBlock {
39 #[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 #[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 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}