Skip to main content

steel_core/behavior/blocks/vegetation/
mushroom_block.rs

1use 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; //
25const SPREAD_ATTEMPTS: usize = 4; //
26const MAX_SURVIVAL_BRIGHTNESS: u8 = 13;
27const BASE_HUGE_MUSHROOM_HEIGHT: i32 = 4;
28const BONEMEAL_SUCCESS_CHANCE: f32 = 0.4; //
29
30/// Vanilla `MushroomBlock` survival and growth.
31#[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    /// Creates a new mushroom block behavior.
40    #[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                    &REGISTRY,
65                    &mut worldgen_random,
66                    config,
67                    pos,
68                )
69            }
70            ConfiguredFeatureKind::HugeRedMushroom(config) => {
71                FeatureDecorationRunner::place_huge_red_mushroom_feature(
72                    world,
73                    &REGISTRY,
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}