steel_core/behavior/blocks/vegetation/
mushroom_block.rs1use std::sync::{Arc, LazyLock};
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::Direction;
7use steel_registry::feature::{ConfiguredFeature, ConfiguredFeatureKind};
8use steel_registry::vanilla_block_tags::BlockTag;
9use steel_registry::{REGISTRY, vanilla_blocks};
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId};
12
13use crate::behavior::block::BlockBehavior;
14use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
15use crate::behavior::context::BlockPlaceContext;
16use crate::world::{LevelReader, ScheduledTickAccess, World};
17
18use super::BlockRef;
19
20use steel_utils::random::worldgen_random::WorldgenRandom;
21
22use crate::worldgen::feature::FeatureDecorationRunner;
23
24const SPREAD_CHANCE: u32 = 25; const SPREAD_ATTEMPTS: usize = 4; const MAX_SURVIVAL_BRIGHTNESS: u8 = 13;
27const BASE_HUGE_MUSHROOM_HEIGHT: i32 = 4;
28const BONEMEAL_SUCCESS_CHANCE: f32 = 0.4; #[block_behavior]
32pub struct MushroomBlock {
33 block: BlockRef,
34 #[json_arg(vanilla_configured_features, json = "feature")]
35 feature: &'static LazyLock<ConfiguredFeature>,
36}
37
38impl MushroomBlock {
39 #[must_use]
41 pub const fn new(block: BlockRef, feature: &'static LazyLock<ConfiguredFeature>) -> Self {
42 Self { block, feature }
43 }
44
45 fn may_place_on(state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
46 state.is_solid_render()
47 }
48
49 fn grow_mushroom(
50 &self,
51 world: &Arc<World>,
52 pos: BlockPos,
53 state: BlockStateId,
54 random: &mut dyn Rng,
55 ) -> bool {
56 let configured_feature = &**self.feature;
57 world.remove_block(pos, false);
58
59 let mut worldgen_random = WorldgenRandom::from_seed(random.random());
60 let placed = match &configured_feature.kind {
61 ConfiguredFeatureKind::HugeBrownMushroom(config) => {
62 FeatureDecorationRunner::place_huge_brown_mushroom_feature(
63 world,
64 ®ISTRY,
65 &mut worldgen_random,
66 config,
67 pos,
68 )
69 }
70 ConfiguredFeatureKind::HugeRedMushroom(config) => {
71 FeatureDecorationRunner::place_huge_red_mushroom_feature(
72 world,
73 ®ISTRY,
74 &mut worldgen_random,
75 config,
76 pos,
77 )
78 }
79 _ => false,
80 };
81
82 if placed {
83 true
84 } else {
85 world.set_block(pos, state, UpdateFlags::UPDATE_ALL);
86 false
87 }
88 }
89}
90
91impl BlockBehavior for MushroomBlock {
92 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
93 let mut random = rand::rng();
94 if random.random_range(0..SPREAD_CHANCE) == 0 {
95 let max = 5;
96
97 for block_pos in BlockPos::between_closed(pos.offset(-4, -1, -4), pos.offset(4, 1, -4))
98 {
99 if world.get_block_state(block_pos).get_block() == self.block && max - 1 <= 0 {
100 return;
101 }
102 }
103
104 let mut offset = pos.offset(
105 random.random_range(0..3) - 1,
106 random.random_range(0..2) - random.random_range(0..2),
107 random.random_range(0..3) - 1,
108 );
109 let mut pos = pos;
110 for _ in 0..SPREAD_ATTEMPTS {
111 if world.get_block_state(offset).is_air() && self.can_survive(state, world, offset)
112 {
113 pos = offset;
114 }
115
116 offset = pos.offset(
117 random.random_range(0..3) - 1,
118 random.random_range(0..2) - random.random_range(0..2),
119 random.random_range(0..3) - 1,
120 );
121 }
122
123 if world.get_block_state(offset).is_air() && self.can_survive(state, world, offset) {
124 world.set_block(offset, state, UpdateFlags::UPDATE_CLIENTS);
125 }
126 }
127 }
128
129 fn update_shape(
130 &self,
131 state: BlockStateId,
132 world: &dyn ScheduledTickAccess,
133 pos: BlockPos,
134 _direction: Direction,
135 _neighbor_pos: BlockPos,
136 _neighbor_state: BlockStateId,
137 ) -> BlockStateId {
138 if self.can_survive(state, world, pos) {
139 state
140 } else {
141 vanilla_blocks::AIR.default_state()
142 }
143 }
144
145 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
146 let below_pos = pos.below();
147 let below = world.get_block_state(below_pos);
148 if below
149 .get_block()
150 .has_tag(&BlockTag::OVERRIDES_MUSHROOM_LIGHT_REQUIREMENT)
151 {
152 return true;
153 }
154
155 world.raw_brightness(pos, 0) < MAX_SURVIVAL_BRIGHTNESS
156 && Self::may_place_on(below, world, below_pos)
157 }
158
159 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
160 Some(self.block.default_state())
161 }
162
163 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
164 Some(self)
165 }
166}
167
168impl Bonemealable for MushroomBlock {
169 fn is_valid_bonemeal_target(
170 &self,
171 _state: BlockStateId,
172 world: &dyn LevelReader,
173 pos: BlockPos,
174 ) -> bool {
175 let configured_feature = &**self.feature;
176 let (ConfiguredFeatureKind::HugeBrownMushroom(config)
177 | ConfiguredFeatureKind::HugeRedMushroom(config)) = &configured_feature.kind
178 else {
179 return false;
180 };
181 let min_height = BASE_HUGE_MUSHROOM_HEIGHT + config.foliage_radius;
182
183 !world.is_outside_build_height(pos.above_n(min_height).y())
184 }
185
186 fn is_bonemeal_success(
187 &self,
188 _state: BlockStateId,
189 _world: &Arc<World>,
190 rng: &mut dyn Rng,
191 _pos: BlockPos,
192 ) -> bool {
193 rng.random::<f32>() < BONEMEAL_SUCCESS_CHANCE
194 }
195
196 fn perform_bonemeal(
197 &self,
198 state: BlockStateId,
199 world: &Arc<World>,
200 rng: &mut dyn Rng,
201 pos: BlockPos,
202 ) {
203 self.grow_mushroom(world, pos, state, rng);
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use steel_registry::{
210 REGISTRY, init_vanilla_registry, vanilla_blocks, vanilla_configured_features,
211 };
212
213 use crate::test_support::TestLevel;
214
215 use super::*;
216
217 fn single_support_level(support: BlockStateId, raw_brightness: u8) -> TestLevel {
218 TestLevel::default()
219 .with_block(BlockPos::ZERO.below(), support)
220 .with_raw_brightness(raw_brightness)
221 }
222
223 #[test]
224 fn mushroom_survival_uses_solid_render_support() {
225 init_vanilla_registry();
226
227 let mushroom = MushroomBlock::new(
228 &vanilla_blocks::BROWN_MUSHROOM,
229 &vanilla_configured_features::HUGE_BROWN_MUSHROOM,
230 );
231 let state = REGISTRY
232 .blocks
233 .get_default_state_id(&vanilla_blocks::BROWN_MUSHROOM);
234 let pos = BlockPos::new(0, 0, 0);
235
236 let grass_block = REGISTRY
237 .blocks
238 .get_default_state_id(&vanilla_blocks::GRASS_BLOCK);
239 assert!(mushroom.can_survive(state, &single_support_level(grass_block, 12), pos));
240 assert!(!mushroom.can_survive(state, &single_support_level(grass_block, 13), pos));
241
242 let oak_leaves = REGISTRY
243 .blocks
244 .get_default_state_id(&vanilla_blocks::OAK_LEAVES);
245 assert!(!mushroom.can_survive(state, &single_support_level(oak_leaves, 0), pos));
246
247 let podzol = REGISTRY
248 .blocks
249 .get_default_state_id(&vanilla_blocks::PODZOL);
250 assert!(mushroom.can_survive(state, &single_support_level(podzol, 15), pos));
251 }
252
253 #[test]
254 fn mushroom_bonemeal_validation_checks_build_height() {
255 init_vanilla_registry();
256
257 let mushroom = MushroomBlock::new(
258 &vanilla_blocks::BROWN_MUSHROOM,
259 &vanilla_configured_features::HUGE_BROWN_MUSHROOM,
260 );
261 let state = REGISTRY
262 .blocks
263 .get_default_state_id(&vanilla_blocks::BROWN_MUSHROOM);
264 let pos = BlockPos::new(0, 0, 0);
265
266 let grass_block = REGISTRY
267 .blocks
268 .get_default_state_id(&vanilla_blocks::GRASS_BLOCK);
269 let level = single_support_level(grass_block, 0);
270 assert!(mushroom.is_valid_bonemeal_target(state, &level, pos));
271 }
272}