Skip to main content

steel_core/behavior/blocks/vegetation/
big_dripleaf_block.rs

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/// Vanilla `BigDripleafBlock` survival.
30///
31/// Survives if the block below is big dripleaf (self), big dripleaf stem, or
32/// in the `SUPPORTS_BIG_DRIPLEAF` tag.
33#[block_behavior]
34pub struct BigDripleafBlock {
35    block: BlockRef,
36}
37
38impl BigDripleafBlock {
39    /// Creates a new big dripleaf block behavior.
40    #[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    /// Determines whether big dripleaf can grow into target position
111    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    /// Places big dripleaf block on target position with properties
117    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    /// Used for bonemeal functionality on small dripleaf
134    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}