Skip to main content

steel_core/behavior/blocks/vegetation/
nether_fungus_block.rs

1use std::sync::{Arc, LazyLock};
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::REGISTRY;
6use steel_registry::blocks::block_state_ext::BlockStateExt;
7use steel_registry::feature::{ConfiguredFeature, ConfiguredFeatureKind};
8use steel_utils::random::worldgen_random::WorldgenRandom;
9use steel_utils::{BlockPos, BlockStateId, Identifier};
10
11use crate::behavior::block::BlockBehavior;
12use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
13use crate::behavior::context::BlockPlaceContext;
14use crate::world::{LevelReader, World};
15use crate::worldgen::feature::FeatureDecorationRunner;
16
17use super::{BlockRef, default_surviving_state, survives_on_tag};
18
19const BONEMEAL_SUCCESS_CHANCE: f32 = 0.4;
20
21/// Vanilla `NetherFungusBlock` survival.
22#[block_behavior]
23pub struct NetherFungusBlock {
24    block: BlockRef,
25    #[json_arg(vanilla_blocks)]
26    required_block: BlockRef,
27    #[json_arg(vanilla_configured_features, json = "feature")]
28    feature: &'static LazyLock<ConfiguredFeature>,
29    #[json_arg(vanilla_block_tags)]
30    support_blocks: Identifier,
31}
32
33impl NetherFungusBlock {
34    /// Creates a new nether fungus block behavior.
35    #[must_use]
36    pub const fn new(
37        block: BlockRef,
38        required_block: BlockRef,
39        feature: &'static LazyLock<ConfiguredFeature>,
40        support_blocks: Identifier,
41    ) -> Self {
42        Self {
43            block,
44            required_block,
45            feature,
46            support_blocks,
47        }
48    }
49}
50
51impl BlockBehavior for NetherFungusBlock {
52    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
53        survives_on_tag(world, pos, &self.support_blocks)
54    }
55
56    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
57        default_surviving_state(self.block, self, context)
58    }
59
60    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
61        Some(self)
62    }
63}
64
65impl Bonemealable for NetherFungusBlock {
66    fn is_valid_bonemeal_target(
67        &self,
68        _state: BlockStateId,
69        world: &dyn LevelReader,
70        pos: BlockPos,
71    ) -> bool {
72        world.get_block_state(pos.below()).get_block() == self.required_block
73            && !world.is_outside_build_height(pos.above().y())
74    }
75
76    fn is_bonemeal_success(
77        &self,
78        _state: BlockStateId,
79        _world: &Arc<World>,
80        rng: &mut dyn Rng,
81        _pos: BlockPos,
82    ) -> bool {
83        rng.random::<f32>() < BONEMEAL_SUCCESS_CHANCE
84    }
85
86    fn perform_bonemeal(
87        &self,
88        _state: BlockStateId,
89        world: &Arc<World>,
90        rng: &mut dyn Rng,
91        pos: BlockPos,
92    ) {
93        let ConfiguredFeatureKind::HugeFungus(config) = &self.feature.kind else {
94            return;
95        };
96        let mut worldgen_random = WorldgenRandom::from_seed(rng.random());
97        FeatureDecorationRunner::place_planted_huge_fungus_feature(
98            world,
99            &REGISTRY,
100            &mut worldgen_random,
101            config,
102            pos,
103        );
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use std::convert::Infallible;
110
111    use rand::{SeedableRng, TryRng, rngs::StdRng};
112    use steel_registry::{
113        init_vanilla_registry, vanilla_block_tags::BlockTag, vanilla_blocks,
114        vanilla_configured_features,
115    };
116    use steel_utils::{ChunkPos, types::UpdateFlags};
117
118    use crate::{
119        behavior::init_behaviors,
120        test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk},
121    };
122
123    use super::*;
124
125    struct FixedRng(u64);
126
127    impl TryRng for FixedRng {
128        type Error = Infallible;
129
130        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
131            Ok(self.0 as u32)
132        }
133
134        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
135            Ok(self.0)
136        }
137
138        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
139            dst.fill(self.0 as u8);
140            Ok(())
141        }
142    }
143
144    fn warped_fungus() -> NetherFungusBlock {
145        NetherFungusBlock::new(
146            &vanilla_blocks::WARPED_FUNGUS,
147            &vanilla_blocks::WARPED_NYLIUM,
148            &vanilla_configured_features::WARPED_FUNGUS_PLANTED,
149            BlockTag::SUPPORTS_WARPED_FUNGUS,
150        )
151    }
152
153    fn crimson_fungus() -> NetherFungusBlock {
154        NetherFungusBlock::new(
155            &vanilla_blocks::CRIMSON_FUNGUS,
156            &vanilla_blocks::CRIMSON_NYLIUM,
157            &vanilla_configured_features::CRIMSON_FUNGUS_PLANTED,
158            BlockTag::SUPPORTS_CRIMSON_FUNGUS,
159        )
160    }
161
162    #[test]
163    fn bonemeal_requires_matching_nylium_and_build_height() {
164        init_vanilla_registry();
165        let behavior = warped_fungus();
166        let state = vanilla_blocks::WARPED_FUNGUS.default_state();
167        let pos = BlockPos::ZERO;
168        let matching = TestLevel::default()
169            .with_block(pos.below(), vanilla_blocks::WARPED_NYLIUM.default_state());
170        assert!(behavior.is_valid_bonemeal_target(state, &matching, pos));
171
172        let wrong_nylium = TestLevel::default()
173            .with_block(pos.below(), vanilla_blocks::CRIMSON_NYLIUM.default_state());
174        assert!(!behavior.is_valid_bonemeal_target(state, &wrong_nylium, pos));
175
176        let at_build_limit = TestLevel::default();
177        let top = BlockPos::new(0, at_build_limit.max_y_exclusive() - 1, 0);
178        let at_build_limit = TestLevel::default()
179            .with_block(top.below(), vanilla_blocks::WARPED_NYLIUM.default_state());
180        assert!(!behavior.is_valid_bonemeal_target(state, &at_build_limit, top));
181    }
182
183    #[test]
184    fn bonemeal_uses_vanilla_success_probability() {
185        init_vanilla_registry();
186        let behavior = warped_fungus();
187        let world = fresh_test_world("nether_fungus_probability");
188        let state = vanilla_blocks::WARPED_FUNGUS.default_state();
189        let pos = BlockPos::new(8, 64, 8);
190
191        assert!(behavior.is_bonemeal_success(state, &world, &mut FixedRng(0), pos));
192        assert!(!behavior.is_bonemeal_success(state, &world, &mut FixedRng(u64::MAX), pos));
193    }
194
195    #[test]
196    fn bonemeal_grows_both_huge_fungus_variants() {
197        init_vanilla_registry();
198        init_behaviors();
199
200        for (name, behavior, fungus, nylium, stem) in [
201            (
202                "warped_fungus_growth",
203                warped_fungus(),
204                &vanilla_blocks::WARPED_FUNGUS,
205                &vanilla_blocks::WARPED_NYLIUM,
206                &vanilla_blocks::WARPED_STEM,
207            ),
208            (
209                "crimson_fungus_growth",
210                crimson_fungus(),
211                &vanilla_blocks::CRIMSON_FUNGUS,
212                &vanilla_blocks::CRIMSON_NYLIUM,
213                &vanilla_blocks::CRIMSON_STEM,
214            ),
215        ] {
216            let world = fresh_test_world(name);
217            let pos = BlockPos::new(8, 64, 8);
218            insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
219            assert!(world.set_block(
220                pos.below(),
221                nylium.default_state(),
222                UpdateFlags::UPDATE_NONE,
223            ));
224            let state = fungus.default_state();
225            assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
226
227            behavior.perform_bonemeal(state, &world, &mut StdRng::seed_from_u64(1), pos);
228
229            assert_eq!(world.get_block_state(pos).get_block(), stem);
230            assert_eq!(world.get_block_state(pos.above_n(3)).get_block(), stem);
231        }
232    }
233}