steel_core/behavior/blocks/vegetation/
nether_fungus_block.rs1use 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#[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 #[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 ®ISTRY,
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}