Skip to main content

steel_core/behavior/blocks/vegetation/
chorus_flower_block.rs

1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
7use steel_registry::level_events;
8use steel_registry::vanilla_block_tags::BlockTag;
9use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
10
11use crate::behavior::block::BlockBehavior;
12use crate::behavior::context::BlockPlaceContext;
13use crate::entity::projectile::Projectile;
14use crate::world::{ClipHitResult, LevelReader, ScheduledTickAccess, World};
15
16use super::{BlockRef, ChorusPlantBlock, default_surviving_state};
17
18const HORIZONTAL_DIRECTIONS: [Direction; 4] = [
19    Direction::North,
20    Direction::East,
21    Direction::South,
22    Direction::West,
23];
24
25/// Vanilla `ChorusFlowerBlock` survival behavior.
26#[block_behavior]
27pub struct ChorusFlowerBlock {
28    block: BlockRef,
29    #[json_arg(vanilla_blocks)]
30    plant: BlockRef,
31}
32
33const AGE: &IntProperty = &BlockStateProperties::AGE_5;
34const DEAD_AGE: u8 = 5;
35const PILLAR_SCAN_DEPTH: i32 = 4;
36const MIN_PILLAR_HEIGHT: i32 = 2;
37const GROWTH_RANDOM_RANGE: i32 = 4;
38const GROWTH_RANDOM_RANGE_ON_SUPPORT: i32 = 5;
39const BRANCH_ATTEMPT_RANDOM_RANGE: i32 = 4;
40
41impl ChorusFlowerBlock {
42    /// Creates a new chorus flower block behavior.
43    #[must_use]
44    pub const fn new(block: BlockRef, plant: BlockRef) -> Self {
45        Self { block, plant }
46    }
47
48    fn projectile_can_break(projectile: &dyn Projectile, world: &World, pos: BlockPos) -> bool {
49        projectile.projectile_may_interact(world, pos) && projectile.may_break(world)
50    }
51
52    fn all_neighbors_empty(
53        world: &dyn LevelReader,
54        pos: BlockPos,
55        ignore: Option<Direction>,
56    ) -> bool {
57        HORIZONTAL_DIRECTIONS.into_iter().all(|direction| {
58            Some(direction) == ignore || world.get_block_state(pos.relative(direction)).is_air()
59        })
60    }
61
62    fn place_grown_flower(&self, world: &Arc<World>, pos: BlockPos, age: u8) {
63        world.set_block(
64            pos,
65            self.block.default_state().set_value(AGE, age),
66            UpdateFlags::UPDATE_CLIENTS,
67        );
68        world.level_event(level_events::SOUND_CHORUS_GROW, pos, 0, None);
69    }
70
71    fn place_dead_flower(&self, world: &Arc<World>, pos: BlockPos) {
72        world.set_block(
73            pos,
74            self.block.default_state().set_value(AGE, DEAD_AGE),
75            UpdateFlags::UPDATE_CLIENTS,
76        );
77        world.level_event(level_events::SOUND_CHORUS_DEATH, pos, 0, None);
78    }
79
80    fn random_tick_with_rng(
81        &self,
82        state: BlockStateId,
83        world: &Arc<World>,
84        pos: BlockPos,
85        rng: &mut dyn Rng,
86    ) {
87        let above = pos.above();
88        if !world.get_block_state(above).is_air() || world.is_outside_build_height(above.y()) {
89            return;
90        }
91
92        let current_age = state.get_value(AGE);
93        if current_age >= DEAD_AGE {
94            return;
95        }
96
97        let mut grow_upwards = false;
98        let mut pillar_on_support_block = false;
99        let below = world.get_block_state(pos.below());
100        if below.get_block().has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER) {
101            grow_upwards = true;
102        } else if below.get_block() == self.plant {
103            let mut height = 1;
104            for _ in 0..PILLAR_SCAN_DEPTH {
105                let test_state = world.get_block_state(pos.below_n(height + 1));
106                if test_state.get_block() != self.plant {
107                    pillar_on_support_block = test_state
108                        .get_block()
109                        .has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER);
110                    break;
111                }
112                height += 1;
113            }
114
115            if height < MIN_PILLAR_HEIGHT
116                || height
117                    <= rng.random_range(
118                        0..if pillar_on_support_block {
119                            GROWTH_RANDOM_RANGE_ON_SUPPORT
120                        } else {
121                            GROWTH_RANDOM_RANGE
122                        },
123                    )
124            {
125                grow_upwards = true;
126            }
127        } else if below.is_air() {
128            grow_upwards = true;
129        }
130
131        if grow_upwards
132            && Self::all_neighbors_empty(world, above, None)
133            && world.get_block_state(pos.above_n(2)).is_air()
134        {
135            world.set_block(
136                pos,
137                ChorusPlantBlock::state_with_connections(world, pos, self.plant.default_state()),
138                UpdateFlags::UPDATE_CLIENTS,
139            );
140            self.place_grown_flower(world, above, current_age);
141        } else if current_age < DEAD_AGE - 1 {
142            let mut branch_attempts = rng.random_range(0..BRANCH_ATTEMPT_RANDOM_RANGE);
143            if pillar_on_support_block {
144                branch_attempts += 1;
145            }
146
147            let mut created_branch = false;
148            for _ in 0..branch_attempts {
149                let direction =
150                    HORIZONTAL_DIRECTIONS[rng.random_range(0..HORIZONTAL_DIRECTIONS.len())];
151                let target = pos.relative(direction);
152                if world.get_block_state(target).is_air()
153                    && world.get_block_state(target.below()).is_air()
154                    && Self::all_neighbors_empty(world, target, Some(direction.opposite()))
155                {
156                    self.place_grown_flower(world, target, current_age + 1);
157                    created_branch = true;
158                }
159            }
160
161            if created_branch {
162                world.set_block(
163                    pos,
164                    ChorusPlantBlock::state_with_connections(
165                        world,
166                        pos,
167                        self.plant.default_state(),
168                    ),
169                    UpdateFlags::UPDATE_CLIENTS,
170                );
171            } else {
172                self.place_dead_flower(world, pos);
173            }
174        } else {
175            self.place_dead_flower(world, pos);
176        }
177    }
178}
179
180impl BlockBehavior for ChorusFlowerBlock {
181    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
182        let below_state = world.get_block_state(pos.below());
183        if below_state.get_block() == self.plant
184            || below_state
185                .get_block()
186                .has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER)
187        {
188            return true;
189        }
190
191        if !below_state.is_air() {
192            return false;
193        }
194
195        let mut has_single_plant_neighbor = false;
196        for direction in HORIZONTAL_DIRECTIONS {
197            let neighbor_state = world.get_block_state(pos.relative(direction));
198            if neighbor_state.get_block() == self.plant {
199                if has_single_plant_neighbor {
200                    return false;
201                }
202                has_single_plant_neighbor = true;
203            } else if !neighbor_state.is_air() {
204                return false;
205            }
206        }
207
208        has_single_plant_neighbor
209    }
210
211    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
212        default_surviving_state(self.block, self, context)
213    }
214
215    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
216        if !self.can_survive(state, world, pos) {
217            world.destroy_block(pos, true);
218        }
219    }
220
221    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
222        self.random_tick_with_rng(state, world, pos, &mut rand::rng());
223    }
224
225    fn update_shape(
226        &self,
227        state: BlockStateId,
228        world: &dyn ScheduledTickAccess,
229        pos: BlockPos,
230        direction: Direction,
231        _neighbor_pos: BlockPos,
232        _neighbor_state: BlockStateId,
233    ) -> BlockStateId {
234        if direction != Direction::Up && !self.can_survive(state, world, pos) {
235            world.schedule_block_tick_default(pos, self.block, 1);
236        }
237        state
238    }
239
240    fn on_projectile_hit(
241        &self,
242        _state: BlockStateId,
243        world: &Arc<World>,
244        hit: &ClipHitResult,
245        projectile: &dyn Projectile,
246    ) {
247        if Self::projectile_can_break(projectile, world, hit.block_pos) {
248            world.destroy_block_by_entity(hit.block_pos, true, projectile.as_entity_event_source());
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::convert::Infallible;
256
257    use rand::TryRng;
258    use rand::{SeedableRng, rngs::StdRng};
259    use steel_registry::{init_vanilla_registry, vanilla_blocks};
260    use steel_utils::ChunkPos;
261
262    use crate::{
263        behavior::init_behaviors,
264        test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk},
265    };
266
267    use super::*;
268
269    fn behavior() -> ChorusFlowerBlock {
270        ChorusFlowerBlock::new(
271            &vanilla_blocks::CHORUS_FLOWER,
272            &vanilla_blocks::CHORUS_PLANT,
273        )
274    }
275
276    #[derive(Default)]
277    struct MaxRng;
278
279    impl TryRng for MaxRng {
280        type Error = Infallible;
281
282        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
283            Ok(u32::MAX)
284        }
285
286        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
287            Ok(u64::MAX)
288        }
289
290        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
291            dst.fill(u8::MAX);
292            Ok(())
293        }
294    }
295
296    #[test]
297    fn grows_upward_from_chorus_support() {
298        init_vanilla_registry();
299        init_behaviors();
300        let world = fresh_test_world("chorus_flower_growth");
301        let pos = BlockPos::new(8, 64, 8);
302        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
303
304        assert!(world.set_block(
305            pos.below(),
306            vanilla_blocks::END_STONE.default_state(),
307            UpdateFlags::UPDATE_NONE,
308        ));
309        let state = vanilla_blocks::CHORUS_FLOWER.default_state();
310        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
311
312        behavior().random_tick_with_rng(state, &world, pos, &mut StdRng::seed_from_u64(0));
313
314        let stem = world.get_block_state(pos);
315        assert_eq!(stem.get_block(), &vanilla_blocks::CHORUS_PLANT);
316        assert!(stem.get_value(&BlockStateProperties::UP));
317        let grown = world.get_block_state(pos.above());
318        assert_eq!(grown.get_block(), &vanilla_blocks::CHORUS_FLOWER);
319        assert_eq!(grown.get_value(AGE), 0);
320    }
321
322    #[test]
323    fn mature_flower_dies_when_it_cannot_grow() {
324        init_vanilla_registry();
325        init_behaviors();
326        let world = fresh_test_world("chorus_flower_death");
327        let pos = BlockPos::new(8, 64, 8);
328        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
329
330        assert!(world.set_block(
331            pos.below(),
332            vanilla_blocks::STONE.default_state(),
333            UpdateFlags::UPDATE_NONE,
334        ));
335        let state = vanilla_blocks::CHORUS_FLOWER
336            .default_state()
337            .set_value(AGE, DEAD_AGE - 1);
338        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
339
340        behavior().random_tick_with_rng(state, &world, pos, &mut StdRng::seed_from_u64(0));
341
342        assert_eq!(world.get_block_state(pos).get_value(AGE), DEAD_AGE);
343    }
344
345    #[test]
346    fn creates_branches_when_upward_growth_is_obstructed() {
347        init_vanilla_registry();
348        init_behaviors();
349        let world = fresh_test_world("chorus_flower_branching");
350        let pos = BlockPos::new(8, 64, 8);
351        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
352
353        assert!(world.set_block(
354            pos.below(),
355            vanilla_blocks::END_STONE.default_state(),
356            UpdateFlags::UPDATE_NONE,
357        ));
358        assert!(world.set_block(
359            pos.above().north(),
360            vanilla_blocks::STONE.default_state(),
361            UpdateFlags::UPDATE_NONE,
362        ));
363        let state = vanilla_blocks::CHORUS_FLOWER.default_state();
364        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
365
366        behavior().random_tick_with_rng(state, &world, pos, &mut MaxRng);
367
368        assert_eq!(
369            world.get_block_state(pos).get_block(),
370            &vanilla_blocks::CHORUS_PLANT
371        );
372        assert!(HORIZONTAL_DIRECTIONS.into_iter().any(|direction| {
373            let branch = world.get_block_state(pos.relative(direction));
374            branch.get_block() == &vanilla_blocks::CHORUS_FLOWER && branch.get_value(AGE) == 1
375        }));
376    }
377
378    #[test]
379    fn unsupported_flower_breaks_on_scheduled_tick() {
380        init_vanilla_registry();
381        init_behaviors();
382        let world = fresh_test_world("chorus_flower_survival");
383        let pos = BlockPos::new(8, 64, 8);
384        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
385
386        assert!(world.set_block(
387            pos.below(),
388            vanilla_blocks::STONE.default_state(),
389            UpdateFlags::UPDATE_NONE,
390        ));
391        let state = vanilla_blocks::CHORUS_FLOWER.default_state();
392        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
393
394        behavior().tick(state, &world, pos);
395
396        assert!(world.get_block_state(pos).is_air());
397    }
398
399    #[test]
400    fn schedules_survival_tick_when_non_up_neighbor_changes() {
401        init_vanilla_registry();
402        let level = TestLevel::default();
403        let pos = BlockPos::ZERO;
404        let state = vanilla_blocks::CHORUS_FLOWER.default_state();
405
406        assert_eq!(
407            behavior().update_shape(
408                state,
409                &level,
410                pos,
411                Direction::Down,
412                pos.below(),
413                vanilla_blocks::AIR.default_state(),
414            ),
415            state
416        );
417
418        assert_eq!(level.scheduled_block_ticks.borrow().len(), 1);
419        let tick = level.scheduled_block_ticks.borrow()[0];
420        assert_eq!(tick.pos, pos);
421        assert_eq!(tick.block, &vanilla_blocks::CHORUS_FLOWER);
422        assert_eq!(tick.delay, 1);
423    }
424}