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::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#[block_behavior]
33pub struct CaveVinesBlock {
34 base: GrowingPlantHeadBlock,
35}
36
37const BERRIES: &BoolProperty = &BlockStateProperties::BERRIES;
38
39impl CaveVinesBlock {
40 #[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 #[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 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}