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::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#[block_behavior]
30pub struct CaveVinesBlock {
31 block: BlockRef,
32}
33
34const BERRIES: BoolProperty = BlockStateProperties::BERRIES;
35
36impl CaveVinesBlock {
37 #[must_use]
39 pub const fn new(block: BlockRef) -> Self {
40 Self { block }
41 }
42
43 #[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 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}