1use rand::{Rng, RngExt};
2use std::sync::Arc;
3use steel_macros::block_behavior;
4use steel_registry::blocks::block_state_ext::BlockStateExt;
5use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty, EnumProperty, Tilt};
6use steel_registry::fluid::{FluidState, FluidStateExt};
7use steel_registry::sound_event::SoundEventRef;
8use steel_registry::sound_events::{BLOCK_BIG_DRIPLEAF_TILT_DOWN, BLOCK_BIG_DRIPLEAF_TILT_UP};
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::vanilla_fluids::{self};
11use steel_registry::{vanilla_blocks, vanilla_game_events};
12use steel_utils::types::UpdateFlags;
13use steel_utils::{BlockPos, BlockStateId, Direction};
14
15use super::BlockRef;
16use crate::behavior::block::BlockBehavior;
17use crate::behavior::blocks::BigDripleafStemBlock;
18use crate::behavior::blocks::vegetation::bonemealable::{BonemealAction, Bonemealable};
19use crate::behavior::context::BlockPlaceContext;
20use crate::entity::{Entity, InsideBlockEffectCollector, projectile::Projectile};
21use crate::world::game_event::GameEventContext;
22use crate::world::tick_scheduler::TickPriority;
23use crate::world::{ClipHitResult, LevelReader, ScheduledTickAccess, SignalGetter as _, World};
24
25const TILT: EnumProperty<Tilt> = BlockStateProperties::TILT;
26const WATERLOGGED: BoolProperty = BlockStateProperties::WATERLOGGED;
27const FACING: EnumProperty<Direction> = BlockStateProperties::FACING;
28
29#[block_behavior]
34pub struct BigDripleafBlock {
35 block: BlockRef,
36}
37
38impl BigDripleafBlock {
39 #[must_use]
41 pub const fn new(block: BlockRef) -> Self {
42 Self { block }
43 }
44
45 fn can_entity_tilt(pos: &BlockPos, entity: &dyn Entity) -> bool {
46 entity.on_ground() && entity.position().y > f64::from(pos.y()) + 0.6875_f64
47 }
48
49 fn set_tilt_and_schedule_tick(
50 &self,
51 state_id: BlockStateId,
52 world: &Arc<World>,
53 pos: &BlockPos,
54 tilt: Tilt,
55 sound_wrapper: Option<SoundEventRef>,
56 ) {
57 Self::set_tilt(state_id, world, pos, tilt.clone());
58 if let Some(tilt_sound) = sound_wrapper {
59 Self::play_tilt_sound(world, pos, tilt_sound);
60 }
61 let tick_delay = match tilt {
62 Tilt::None => None,
63 Tilt::Unstable | Tilt::Partial => Some(10),
64 Tilt::Full => Some(100),
65 };
66 if let Some(tick_delay) = tick_delay {
67 world.schedule_block_tick(*pos, self.block, tick_delay, TickPriority::Normal);
68 }
69 }
70
71 const fn tilt_causes_vibration(tilt: &Tilt) -> bool {
72 matches!(tilt, Tilt::None | Tilt::Partial | Tilt::Full)
73 }
74
75 fn set_tilt(state_id: BlockStateId, world: &Arc<World>, pos: &BlockPos, new_tilt: Tilt) {
76 let previous_tilt = state_id.get_value(&TILT);
77 let new_state = state_id.set_value(&TILT, new_tilt.clone());
78
79 world.set_block(*pos, new_state, UpdateFlags::UPDATE_CLIENTS);
80
81 if Self::tilt_causes_vibration(&new_tilt) && new_tilt != previous_tilt {
82 world.game_event(
83 &vanilla_game_events::BLOCK_CHANGE,
84 *pos,
85 &GameEventContext::default(),
86 );
87 }
88 }
89
90 fn play_tilt_sound(world: &Arc<World>, pos: &BlockPos, tilt_sound: SoundEventRef) {
91 let pitch = rand::rng().random_range(0.8f32..1.2f32);
92 world.play_block_sound(tilt_sound, *pos, 1f32, pitch, None);
93 }
94
95 fn reset_tilt(state_id: BlockStateId, world: &Arc<World>, pos: &BlockPos) {
96 Self::set_tilt(state_id, world, pos, Tilt::None);
97 let tilt = state_id.get_value(&TILT);
98
99 if tilt != Tilt::None {
100 Self::play_tilt_sound(world, pos, &BLOCK_BIG_DRIPLEAF_TILT_UP);
101 }
102 }
103
104 fn can_replace(old_state: BlockStateId) -> bool {
105 old_state.is_air()
106 || old_state.get_block() == &vanilla_blocks::WATER
107 || old_state.get_block() == &vanilla_blocks::SMALL_DRIPLEAF
108 }
109
110 pub fn can_grow_into(world: &dyn LevelReader, pos: BlockPos) -> bool {
112 let state = world.get_block_state(pos);
113 !world.is_outside_build_height(pos.y()) && Self::can_replace(state)
114 }
115
116 pub fn place(
118 world: &Arc<World>,
119 pos: BlockPos,
120 fluid_state: FluidState,
121 facing: Direction,
122 ) -> bool {
123 let new_state = vanilla_blocks::BIG_DRIPLEAF
124 .default_state()
125 .set_value(
126 &WATERLOGGED,
127 fluid_state.is_source() && fluid_state.is_water(),
128 )
129 .set_value(&FACING, facing);
130 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL)
131 }
132
133 pub fn place_with_random_height(
135 world: &Arc<World>,
136 rng: &mut dyn Rng,
137 stem_bottom_pos: BlockPos,
138 facing: Direction,
139 ) {
140 let desired_height = rng.random_range(2..5);
141 let mut pos = stem_bottom_pos;
142 let mut height = 0;
143
144 while height < desired_height && Self::can_grow_into(world, pos) {
145 height += 1;
146 pos = pos.relative(Direction::Up);
147 }
148
149 let leaf_y = stem_bottom_pos.y() + height - 1;
150 pos = pos.at_y(stem_bottom_pos.y());
151
152 while pos.y() < leaf_y {
153 BigDripleafStemBlock::place(
154 world,
155 pos,
156 world.get_block_state(pos).get_fluid_state(),
157 facing,
158 );
159 pos = pos.relative(Direction::Up);
160 }
161 Self::place(
162 world,
163 pos,
164 world.get_block_state(pos).get_fluid_state(),
165 facing,
166 );
167 }
168}
169
170impl BlockBehavior for BigDripleafBlock {
171 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
172 let below = world.get_block_state(pos.below());
173 let below_block = below.get_block();
174 below_block == self.block
175 || below_block == &vanilla_blocks::BIG_DRIPLEAF_STEM
176 || below_block.has_tag(&BlockTag::SUPPORTS_BIG_DRIPLEAF)
177 }
178
179 fn update_shape(
180 &self,
181 state: BlockStateId,
182 world: &dyn ScheduledTickAccess,
183 pos: BlockPos,
184 direction: Direction,
185 _neighbor_pos: BlockPos,
186 neighbor_state: BlockStateId,
187 ) -> BlockStateId {
188 if direction == Direction::Down && !self.can_survive(state, world, pos) {
189 return vanilla_blocks::AIR.default_state();
190 }
191 if state.get_value(&WATERLOGGED) {
192 world.schedule_fluid_tick_default(
193 pos,
194 &vanilla_fluids::WATER,
195 vanilla_fluids::WATER.tick_delay as i32,
196 );
197 }
198
199 if direction == Direction::Up && neighbor_state.get_block() == self.block {
200 vanilla_blocks::BIG_DRIPLEAF_STEM
201 .default_state()
202 .with_properties_of(state)
203 } else {
204 state
205 }
206 }
207
208 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
209 let below_state = context.world.get_block_state(context.place_pos().below());
210 let below_is_dripleaf_part = below_state.get_block() == &vanilla_blocks::BIG_DRIPLEAF
211 || below_state.get_block() == &vanilla_blocks::BIG_DRIPLEAF_STEM;
212 let facing = {
213 if below_is_dripleaf_part {
214 below_state.get_value(&FACING)
215 } else {
216 context.horizontal_direction().opposite()
217 }
218 };
219 Some(
220 self.block
221 .default_state()
222 .set_value(&WATERLOGGED, context.is_water_source())
223 .set_value(&FACING, facing),
224 )
225 }
226
227 fn entity_inside(
228 &self,
229 state: BlockStateId,
230 world: &Arc<World>,
231 pos: BlockPos,
232 entity: &dyn Entity,
233 _effect_collector: &mut InsideBlockEffectCollector,
234 _is_precise: bool,
235 ) {
236 let tilt = state.get_value(&TILT);
237 if tilt == Tilt::None
238 && BigDripleafBlock::can_entity_tilt(&pos, entity)
239 && !world.has_neighbor_signal(pos)
240 {
241 Self::set_tilt_and_schedule_tick(self, state, world, &pos, Tilt::Unstable, None);
242 }
243 }
244
245 fn on_projectile_hit(
246 &self,
247 state: BlockStateId,
248 world: &Arc<World>,
249 hit: &ClipHitResult,
250 _projectile: &dyn Projectile,
251 ) {
252 self.set_tilt_and_schedule_tick(
253 state,
254 world,
255 &hit.block_pos,
256 Tilt::Full,
257 Some(&BLOCK_BIG_DRIPLEAF_TILT_DOWN),
258 );
259 }
260
261 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
262 if world.has_neighbor_signal(pos) {
263 Self::reset_tilt(state, world, &pos);
264 return;
265 }
266
267 let tilt = state.get_value(&TILT);
268
269 if tilt == Tilt::Unstable {
270 Self::set_tilt_and_schedule_tick(
271 self,
272 state,
273 world,
274 &pos,
275 Tilt::Partial,
276 Some(&BLOCK_BIG_DRIPLEAF_TILT_DOWN),
277 );
278 } else if tilt == Tilt::Partial {
279 Self::set_tilt_and_schedule_tick(
280 self,
281 state,
282 world,
283 &pos,
284 Tilt::Full,
285 Some(&BLOCK_BIG_DRIPLEAF_TILT_DOWN),
286 );
287 } else if tilt == Tilt::Full {
288 Self::reset_tilt(state, world, &pos);
289 }
290 }
291
292 fn handle_neighbor_changed(
293 &self,
294 state: BlockStateId,
295 world: &Arc<World>,
296 pos: BlockPos,
297 _source_block: BlockRef,
298 _moved_by_piston: bool,
299 ) {
300 if world.has_neighbor_signal(pos) {
301 Self::reset_tilt(state, world, &pos);
302 }
303 }
304
305 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
306 Some(self)
307 }
308}
309
310impl Bonemealable for BigDripleafBlock {
311 fn is_valid_bonemeal_target(
312 &self,
313 _state: BlockStateId,
314 world: &dyn LevelReader,
315 pos: BlockPos,
316 ) -> bool {
317 let grow_pos = pos.above();
318 Self::can_grow_into(world, grow_pos)
319 }
320
321 fn perform_bonemeal(
322 &self,
323 state: BlockStateId,
324 world: &Arc<World>,
325 _rng: &mut dyn Rng,
326 pos: BlockPos,
327 ) {
328 let above_pos = pos.above();
329 if Self::can_grow_into(world, above_pos) {
330 let facing = state.get_value(&FACING);
331 BigDripleafStemBlock::place(
332 world,
333 pos,
334 world.get_block_state(pos).get_fluid_state(),
335 facing,
336 );
337 Self::place(
338 world,
339 above_pos,
340 world.get_block_state(above_pos).get_fluid_state(),
341 facing,
342 );
343 }
344 }
345
346 fn bonemeal_action_type(&self) -> BonemealAction {
347 BonemealAction::Grower
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use std::sync::Arc;
354
355 use glam::DVec3;
356 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_entities};
357 use steel_utils::{ChunkPos, types::UpdateFlags};
358
359 use super::*;
360 use crate::{
361 behavior::{BLOCK_BEHAVIORS, init_behaviors},
362 entity::{InsideBlockEffectCollector, entities::RawEntity},
363 test_support::{fresh_test_world, insert_ready_full_chunk},
364 };
365
366 #[test]
367 fn redstone_holds_big_dripleaf_upright() {
368 init_vanilla_registry();
369 init_behaviors();
370 let world = fresh_test_world("big_dripleaf_redstone");
371 let pos = BlockPos::new(8, 64, 8);
372 let power_pos = pos.west();
373 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
374 assert!(world.set_block(
375 power_pos,
376 vanilla_blocks::REDSTONE_BLOCK.default_state(),
377 UpdateFlags::UPDATE_NONE,
378 ));
379 let tilted = vanilla_blocks::BIG_DRIPLEAF
380 .default_state()
381 .set_value(&BlockStateProperties::TILT, Tilt::Partial);
382 assert!(world.set_block(pos, tilted, UpdateFlags::UPDATE_NONE));
383 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::BIG_DRIPLEAF);
384
385 behavior.handle_neighbor_changed(
386 tilted,
387 &world,
388 pos,
389 &vanilla_blocks::REDSTONE_BLOCK,
390 false,
391 );
392 assert_eq!(
393 world
394 .get_block_state(pos)
395 .get_value(&BlockStateProperties::TILT),
396 Tilt::None,
397 );
398
399 let entity = Arc::new(RawEntity::new(
400 7_003,
401 DVec3::new(8.5, 65.0, 8.5),
402 Arc::downgrade(&world),
403 &vanilla_entities::PIG,
404 ));
405 entity.set_on_ground(true);
406 let mut effects = InsideBlockEffectCollector::new();
407 behavior.entity_inside(
408 world.get_block_state(pos),
409 &world,
410 pos,
411 entity.as_ref(),
412 &mut effects,
413 true,
414 );
415 assert_eq!(
416 world
417 .get_block_state(pos)
418 .get_value(&BlockStateProperties::TILT),
419 Tilt::None,
420 );
421 assert!(!world.has_scheduled_block_tick(pos, &vanilla_blocks::BIG_DRIPLEAF));
422 }
423}