Skip to main content

steel_core/behavior/blocks/vegetation/
mangrove_propagule_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::{
7    BlockStateProperties, BoolProperty, Direction, IntProperty,
8};
9use steel_registry::feature::ConfiguredFeatureKind;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::{REGISTRY, vanilla_blocks, vanilla_configured_features};
12use steel_utils::random::worldgen_random::WorldgenRandom;
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId};
15use steel_worldgen::biomes::obfuscate_biome_seed;
16
17use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
18use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
19use crate::behavior::context::BlockPlaceContext;
20use crate::world::{LevelAccessor, LevelReader, ScheduledTickAccess, World};
21use crate::worldgen::feature::FeatureDecorationRunner;
22use crate::worldgen::feature::no_nested_features;
23
24use super::BlockRef;
25
26/// Mangrove propagule block behavior.
27#[block_behavior]
28pub struct MangrovePropaguleBlock {
29    block: BlockRef,
30}
31
32const AGE: &IntProperty = &BlockStateProperties::AGE_4;
33const HANGING: &BoolProperty = &BlockStateProperties::HANGING;
34const STAGE: &IntProperty = &BlockStateProperties::STAGE;
35const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
36const MAX_AGE: u8 = 4;
37const RANDOM_GROWTH_BOUND: u32 = 7;
38const BONEMEAL_SUCCESS_CHANCE: f32 = 0.45;
39const TALL_MANGROVE_CHANCE: f32 = 0.85;
40
41impl MangrovePropaguleBlock {
42    /// Creates a new mangrove propagule block behavior.
43    #[must_use]
44    pub const fn new(block: BlockRef) -> Self {
45        Self { block }
46    }
47
48    /// Creates vanilla's initial hanging propagule state.
49    pub(crate) fn create_new_hanging_propagule() -> BlockStateId {
50        vanilla_blocks::MANGROVE_PROPAGULE
51            .default_state()
52            .set_value(HANGING, true)
53            .set_value(AGE, 0)
54    }
55
56    fn advance_hanging(state: BlockStateId, world: &dyn LevelAccessor, pos: BlockPos) -> bool {
57        let age = state.get_value(AGE);
58        if age >= MAX_AGE {
59            return false;
60        }
61
62        world.set_block_state(
63            pos,
64            state.set_value(AGE, age + 1),
65            UpdateFlags::UPDATE_CLIENTS,
66        )
67    }
68
69    fn grow_tree(
70        world: &Arc<World>,
71        pos: BlockPos,
72        state: BlockStateId,
73        rng: &mut dyn Rng,
74    ) -> bool {
75        let feature = if rng.random::<f32>() < TALL_MANGROVE_CHANCE {
76            &*vanilla_configured_features::TALL_MANGROVE
77        } else {
78            &*vanilla_configured_features::MANGROVE
79        };
80        let ConfiguredFeatureKind::Tree(config) = &feature.kind else {
81            return false;
82        };
83        let replacement = if state.get_value(WATERLOGGED) {
84            vanilla_blocks::WATER.default_state()
85        } else {
86            vanilla_blocks::AIR.default_state()
87        };
88        world.set_block(pos, replacement, UpdateFlags::UPDATE_NONE);
89
90        let mut worldgen_random = WorldgenRandom::from_seed(rng.random());
91        let mut level = Arc::clone(world);
92        let placed = FeatureDecorationRunner::place_tree_feature(
93            &mut level,
94            &REGISTRY,
95            &mut worldgen_random,
96            config,
97            pos,
98            obfuscate_biome_seed(world.seed()),
99            no_nested_features,
100        );
101
102        if placed {
103            if world.get_block_state(pos) == replacement {
104                world.send_block_updated(pos);
105            }
106        } else {
107            world.set_block(pos, state, UpdateFlags::UPDATE_NONE);
108        }
109        placed
110    }
111
112    fn advance_tree(world: &Arc<World>, pos: BlockPos, state: BlockStateId, rng: &mut dyn Rng) {
113        if state.get_value(STAGE) == 0 {
114            world.set_block(pos, state.set_value(STAGE, 1), UpdateFlags::UPDATE_NONE);
115        } else {
116            Self::grow_tree(world, pos, state, rng);
117        }
118    }
119
120    fn random_tick_with_rng(
121        state: BlockStateId,
122        world: &Arc<World>,
123        pos: BlockPos,
124        rng: &mut dyn Rng,
125    ) {
126        if state.get_value(HANGING) {
127            Self::advance_hanging(state, world, pos);
128        } else if rng.random_range(0..RANDOM_GROWTH_BOUND) == 0 {
129            Self::advance_tree(world, pos, state, rng);
130        }
131    }
132}
133
134impl BlockBehavior for MangrovePropaguleBlock {
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        schedule_water_tick_if_waterlogged(state, world, pos);
145        if direction == Direction::Up && !self.can_survive(state, world, pos) {
146            return vanilla_blocks::AIR.default_state();
147        }
148
149        state
150    }
151
152    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
153        if state.get_value(HANGING) {
154            let above = world.get_block_state(pos.above());
155            return above
156                .get_block()
157                .has_tag(&BlockTag::SUPPORTS_HANGING_MANGROVE_PROPAGULE);
158        }
159
160        let below = world.get_block_state(pos.below());
161        below
162            .get_block()
163            .has_tag(&BlockTag::SUPPORTS_MANGROVE_PROPAGULE)
164    }
165
166    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
167        let state = self
168            .block
169            .default_state()
170            .set_value(AGE, MAX_AGE)
171            .set_value(WATERLOGGED, context.is_water_source());
172        self.can_survive(state, context.world, context.place_pos())
173            .then_some(state)
174    }
175
176    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
177        Self::random_tick_with_rng(state, world, pos, &mut rand::rng());
178    }
179
180    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
181        Some(self)
182    }
183}
184
185impl Bonemealable for MangrovePropaguleBlock {
186    fn is_valid_bonemeal_target(
187        &self,
188        state: BlockStateId,
189        _world: &dyn LevelReader,
190        _pos: BlockPos,
191    ) -> bool {
192        if state.get_value(HANGING) {
193            return state.get_value(AGE) < MAX_AGE;
194        }
195
196        true
197    }
198
199    fn is_bonemeal_success(
200        &self,
201        state: BlockStateId,
202        _world: &Arc<World>,
203        rng: &mut dyn Rng,
204        _pos: BlockPos,
205    ) -> bool {
206        if state.get_value(HANGING) {
207            return state.get_value(AGE) < MAX_AGE;
208        }
209
210        rng.random::<f32>() < BONEMEAL_SUCCESS_CHANCE
211    }
212
213    fn perform_bonemeal(
214        &self,
215        state: BlockStateId,
216        world: &Arc<World>,
217        rng: &mut dyn Rng,
218        pos: BlockPos,
219    ) {
220        if state.get_value(HANGING) {
221            Self::advance_hanging(state, world, pos);
222        } else {
223            Self::advance_tree(world, pos, state, rng);
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::convert::Infallible;
231
232    use rand::{SeedableRng, TryRng, rngs::StdRng};
233    use steel_registry::item_stack::ItemStack;
234    use steel_registry::{init_vanilla_registry, vanilla_items};
235    use steel_utils::ChunkPos;
236
237    use super::*;
238    use crate::behavior::init_behaviors;
239    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
240
241    #[derive(Default)]
242    struct ZeroRng;
243
244    impl TryRng for ZeroRng {
245        type Error = Infallible;
246
247        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
248            Ok(0)
249        }
250
251        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
252            Ok(0)
253        }
254
255        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
256            dst.fill(0);
257            Ok(())
258        }
259    }
260
261    #[test]
262    fn new_hanging_propagule_starts_at_age_zero() {
263        init_vanilla_registry();
264
265        let state = MangrovePropaguleBlock::create_new_hanging_propagule();
266
267        assert_eq!(state.get_block(), &vanilla_blocks::MANGROVE_PROPAGULE);
268        assert!(state.get_value(HANGING));
269        assert_eq!(state.get_value(AGE), 0);
270    }
271
272    #[test]
273    fn placed_propagules_are_mature_and_preserve_source_water() {
274        init_vanilla_registry();
275        init_behaviors();
276        let world_fixture = fresh_test_world("mangrove_propagule_placement");
277        let world = &world_fixture.world;
278        let wet_pos = BlockPos::new(8, 64, 8);
279        let dry_pos = wet_pos.east();
280        insert_ready_full_chunk(world, ChunkPos::from_block_pos(wet_pos));
281        assert!(world.set_block(
282            wet_pos.below(),
283            vanilla_blocks::DIRT.default_state(),
284            UpdateFlags::UPDATE_NONE,
285        ));
286        assert!(world.set_block(
287            dry_pos.below(),
288            vanilla_blocks::DIRT.default_state(),
289            UpdateFlags::UPDATE_NONE,
290        ));
291        assert!(world.set_block(
292            wet_pos,
293            vanilla_blocks::WATER.default_state(),
294            UpdateFlags::UPDATE_NONE,
295        ));
296        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
297
298        let wet_state = {
299            let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
300            let context = BlockPlaceContext::directional(
301                world,
302                wet_pos,
303                Direction::Down,
304                &mut stack,
305                Direction::Up,
306            );
307            behavior
308                .get_state_for_placement(&context)
309                .expect("wet propagule should have a placement state")
310        };
311        let dry_state = {
312            let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
313            let context = BlockPlaceContext::directional(
314                world,
315                dry_pos,
316                Direction::Down,
317                &mut stack,
318                Direction::Up,
319            );
320            behavior
321                .get_state_for_placement(&context)
322                .expect("dry propagule should have a placement state")
323        };
324
325        assert_eq!(wet_state.get_value(AGE), MAX_AGE);
326        assert!(wet_state.get_value(WATERLOGGED));
327        assert_eq!(dry_state.get_value(AGE), MAX_AGE);
328        assert!(!dry_state.get_value(WATERLOGGED));
329    }
330
331    #[test]
332    fn hanging_growth_stops_at_age_four() {
333        init_vanilla_registry();
334        let level = TestLevel::default();
335        let pos = BlockPos::ZERO;
336        let age_three = MangrovePropaguleBlock::create_new_hanging_propagule().set_value(AGE, 3);
337
338        assert!(MangrovePropaguleBlock::advance_hanging(
339            age_three, &level, pos
340        ));
341        assert_eq!(
342            level
343                .last_placed_state()
344                .expect("hanging growth should place an updated state")
345                .get_value(AGE),
346            MAX_AGE
347        );
348        assert!(!MangrovePropaguleBlock::advance_hanging(
349            age_three.set_value(AGE, MAX_AGE),
350            &level,
351            pos,
352        ));
353    }
354
355    #[test]
356    fn bonemeal_targets_match_hanging_age_and_tree_height() {
357        init_vanilla_registry();
358        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
359        let hanging = MangrovePropaguleBlock::create_new_hanging_propagule();
360        let level = TestLevel::default().with_min_y(0).with_height(10);
361
362        assert!(behavior.is_valid_bonemeal_target(hanging, &level, BlockPos::ZERO));
363        assert!(!behavior.is_valid_bonemeal_target(
364            hanging.set_value(AGE, MAX_AGE),
365            &level,
366            BlockPos::ZERO,
367        ));
368        assert!(behavior.is_valid_bonemeal_target(
369            vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
370            &level,
371            BlockPos::new(0, 7, 0),
372        ));
373        assert!(behavior.is_valid_bonemeal_target(
374            vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
375            &level,
376            BlockPos::new(0, 8, 0),
377        ));
378    }
379
380    #[test]
381    fn planted_bonemeal_advances_stage_before_growing_tree() {
382        init_vanilla_registry();
383        init_behaviors();
384        let world_fixture = fresh_test_world("mangrove_propagule_stage");
385        let world = &world_fixture.world;
386        let pos = BlockPos::new(8, 64, 8);
387        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
388        let state = vanilla_blocks::MANGROVE_PROPAGULE.default_state();
389        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
390        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
391
392        behavior.perform_bonemeal(state, world, &mut ZeroRng, pos);
393
394        assert_eq!(world.get_block_state(pos).get_value(STAGE), 1);
395    }
396
397    #[test]
398    fn mature_planted_propagule_generates_mangrove_tree() {
399        init_vanilla_registry();
400        init_behaviors();
401        let world_fixture = fresh_test_world("mangrove_propagule_tree");
402        let world = &world_fixture.world;
403        let pos = BlockPos::new(8, 64, 8);
404        let center = ChunkPos::from_block_pos(pos);
405        for chunk_x in center.0.x - 1..=center.0.x + 1 {
406            for chunk_z in center.0.y - 1..=center.0.y + 1 {
407                insert_ready_full_chunk(world, ChunkPos::new(chunk_x, chunk_z));
408            }
409        }
410        for x in pos.x() - 12..=pos.x() + 12 {
411            for z in pos.z() - 12..=pos.z() + 12 {
412                assert!(world.set_block(
413                    BlockPos::new(x, pos.y() - 1, z),
414                    vanilla_blocks::DIRT.default_state(),
415                    UpdateFlags::UPDATE_NONE,
416                ));
417            }
418        }
419        let state = vanilla_blocks::MANGROVE_PROPAGULE
420            .default_state()
421            .set_value(STAGE, 1)
422            .set_value(AGE, MAX_AGE);
423        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
424        let placed = (0..64).any(|seed| {
425            let mut rng = StdRng::seed_from_u64(seed);
426            MangrovePropaguleBlock::grow_tree(world, pos, state, &mut rng)
427        });
428
429        assert!(placed);
430        assert!((pos.y()..pos.y() + 24).any(|y| {
431            world
432                .get_block_state(BlockPos::new(pos.x(), y, pos.z()))
433                .get_block()
434                == &vanilla_blocks::MANGROVE_LOG
435        }));
436    }
437
438    #[test]
439    fn unsupported_waterlogged_propagule_schedules_water_before_breaking() {
440        init_vanilla_registry();
441        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
442        let state = vanilla_blocks::MANGROVE_PROPAGULE
443            .default_state()
444            .set_value(WATERLOGGED, true);
445        let level = TestLevel::default();
446
447        assert!(
448            behavior
449                .update_shape(
450                    state,
451                    &level,
452                    BlockPos::ZERO,
453                    Direction::Up,
454                    BlockPos::ZERO.above(),
455                    vanilla_blocks::AIR.default_state(),
456                )
457                .is_air()
458        );
459        assert!(level.scheduled_water_tick());
460
461        let side_update = behavior.update_shape(
462            state,
463            &TestLevel::default(),
464            BlockPos::ZERO,
465            Direction::North,
466            BlockPos::ZERO.north(),
467            vanilla_blocks::AIR.default_state(),
468        );
469        assert_eq!(side_update, state);
470    }
471}