Skip to main content

steel_core/behavior/blocks/vegetation/
mossy_carpet_block.rs

1use std::sync::Arc;
2
3use crate::behavior::PlacementSource;
4use crate::behavior::block::BlockBehavior;
5use crate::behavior::blocks::MultifaceBlock;
6use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
7use crate::behavior::context::BlockPlaceContext;
8use crate::world::{LevelReader, ScheduledTickAccess, World};
9use rand::Rng;
10use steel_macros::block_behavior;
11use steel_registry::blocks::BlockRef;
12use steel_registry::blocks::block_state_ext::BlockStateExt;
13use steel_registry::blocks::properties::{
14    BlockStateProperties, BoolProperty, Direction, EnumProperty, WallSide,
15};
16use steel_registry::vanilla_blocks;
17use steel_utils::types::UpdateFlags;
18use steel_utils::{BlockPos, BlockStateId};
19
20/// Vanilla `MossyCarpetBlock` survival and side state updates.
21#[block_behavior]
22pub struct MossyCarpetBlock {
23    block: BlockRef,
24}
25
26const BASE: &BoolProperty = &BlockStateProperties::BOTTOM;
27const EAST_WALL: &EnumProperty<WallSide> = &BlockStateProperties::EAST_WALL;
28const NORTH_WALL: &EnumProperty<WallSide> = &BlockStateProperties::NORTH_WALL;
29const SOUTH_WALL: &EnumProperty<WallSide> = &BlockStateProperties::SOUTH_WALL;
30const WEST_WALL: &EnumProperty<WallSide> = &BlockStateProperties::WEST_WALL;
31
32impl MossyCarpetBlock {
33    pub(crate) const HORIZONTAL_DIRECTIONS: [Direction; 4] = [
34        Direction::North,
35        Direction::East,
36        Direction::South,
37        Direction::West,
38    ];
39
40    /// Creates a new mossy-carpet block behavior.
41    #[must_use]
42    pub const fn new(block: BlockRef) -> Self {
43        Self { block }
44    }
45
46    /// Vanilla `MossyCarpetBlock.getPropertyForFace`.
47    pub(crate) const fn wall_property(direction: Direction) -> &'static EnumProperty<WallSide> {
48        match direction {
49            Direction::North => NORTH_WALL,
50            Direction::East => EAST_WALL,
51            Direction::South => SOUTH_WALL,
52            Direction::West => WEST_WALL,
53            Direction::Down | Direction::Up => {
54                panic!("mossy carpet has no wall property for vertical direction")
55            }
56        }
57    }
58
59    /// Vanilla `MossyCarpetBlock.hasFaces`.
60    pub(crate) fn has_faces(state: BlockStateId) -> bool {
61        if state.get_value(BASE) {
62            return true;
63        }
64
65        for direction in Self::HORIZONTAL_DIRECTIONS {
66            let property = Self::wall_property(direction);
67            if state.get_value(property) != WallSide::None {
68                return true;
69            }
70        }
71
72        false
73    }
74
75    /// Vanilla `MossyCarpetBlock.canSupportAtFace`.
76    pub(crate) fn can_support_at_face(
77        world: &dyn LevelReader,
78        pos: BlockPos,
79        direction: Direction,
80    ) -> bool {
81        direction != Direction::Up && MultifaceBlock::can_attach_to(world, pos, direction)
82    }
83
84    /// Vanilla `MossyCarpetBlock.getUpdatedState`.
85    pub(crate) fn updated_state(
86        mut state: BlockStateId,
87        world: &dyn LevelReader,
88        pos: BlockPos,
89        create_sides: bool,
90    ) -> BlockStateId {
91        let create_sides = create_sides || state.get_value(BASE);
92        let mut above_state = None;
93        let mut below_state = None;
94
95        for direction in Self::HORIZONTAL_DIRECTIONS {
96            let property = Self::wall_property(direction);
97            let mut side = if Self::can_support_at_face(world, pos, direction) {
98                if create_sides {
99                    WallSide::Low
100                } else {
101                    state.get_value(property)
102                }
103            } else {
104                WallSide::None
105            };
106
107            if side == WallSide::Low {
108                let above = *above_state.get_or_insert_with(|| world.get_block_state(pos.above()));
109                if above.get_block() == &vanilla_blocks::PALE_MOSS_CARPET
110                    && above.get_value(property) != WallSide::None
111                    && !above.get_value(BASE)
112                {
113                    side = WallSide::Tall;
114                }
115
116                if !state.get_value(BASE) {
117                    let below =
118                        *below_state.get_or_insert_with(|| world.get_block_state(pos.below()));
119                    if below.get_block() == &vanilla_blocks::PALE_MOSS_CARPET
120                        && below.get_value(property) == WallSide::None
121                    {
122                        side = WallSide::None;
123                    }
124                }
125            }
126
127            state = state.set_value(property, side);
128        }
129
130        state
131    }
132
133    fn create_topper_with_side_chance(
134        world: &dyn LevelReader,
135        pos: BlockPos,
136        side_survival_test: bool,
137    ) -> BlockStateId {
138        let above = pos.above();
139        let above_previous_state = world.get_block_state(above);
140        let is_mossy_carpet_above =
141            above_previous_state.get_block() == &vanilla_blocks::PALE_MOSS_CARPET;
142        if (!is_mossy_carpet_above || !above_previous_state.get_value(BASE))
143            && (is_mossy_carpet_above || above_previous_state.is_replaceable())
144        {
145            let no_carpet_base_state = &vanilla_blocks::PALE_MOSS_CARPET
146                .default_state()
147                .set_value(BASE, false);
148            let mut above_state =
149                Self::updated_state(*no_carpet_base_state, world, pos.above(), true);
150
151            for dir in Direction::HORIZONTAL {
152                let property = Self::wall_property(dir);
153                if above_state.get_value(property) != WallSide::None && !side_survival_test {
154                    above_state = above_state.set_value(property, WallSide::None);
155                }
156            }
157
158            if Self::has_faces(above_state) && above_state != above_previous_state {
159                return above_state;
160            }
161            return vanilla_blocks::AIR.default_state();
162        }
163        vanilla_blocks::AIR.default_state()
164    }
165}
166
167impl BlockBehavior for MossyCarpetBlock {
168    fn update_shape(
169        &self,
170        state: BlockStateId,
171        world: &dyn ScheduledTickAccess,
172        pos: BlockPos,
173        _direction: Direction,
174        _neighbor_pos: BlockPos,
175        _neighbor_state: BlockStateId,
176    ) -> BlockStateId {
177        if !self.can_survive(state, world, pos) {
178            return vanilla_blocks::AIR.default_state();
179        }
180
181        let updated = Self::updated_state(state, world, pos, false);
182        if Self::has_faces(updated) {
183            updated
184        } else {
185            vanilla_blocks::AIR.default_state()
186        }
187    }
188
189    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
190        if state.get_value(BASE) {
191            !world.get_block_state(pos.below()).is_air()
192        } else {
193            let below = world.get_block_state(pos.below());
194            below.get_block() == self.block && below.get_value(BASE)
195        }
196    }
197
198    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
199        let state = Self::updated_state(
200            self.block.default_state(),
201            context.world,
202            context.place_pos(),
203            true,
204        );
205        (self.can_survive(state, context.world, context.place_pos()) && Self::has_faces(state))
206            .then_some(state)
207    }
208
209    fn set_placed_by(
210        &self,
211        _state: BlockStateId,
212        world: &Arc<World>,
213        pos: BlockPos,
214        _source: &PlacementSource<'_>,
215    ) {
216        let random = rand::random::<bool>();
217        let topper = Self::create_topper_with_side_chance(world, pos, random);
218        if !topper.is_air() {
219            world.set_block(pos.above(), topper, UpdateFlags::UPDATE_ALL);
220        }
221    }
222
223    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
224        Some(self)
225    }
226}
227
228impl Bonemealable for MossyCarpetBlock {
229    fn is_valid_bonemeal_target(
230        &self,
231        state: BlockStateId,
232        world: &dyn LevelReader,
233        pos: BlockPos,
234    ) -> bool {
235        state.get_value(BASE) && !Self::create_topper_with_side_chance(world, pos, true).is_air()
236    }
237
238    fn perform_bonemeal(
239        &self,
240        _state: BlockStateId,
241        world: &Arc<World>,
242        _rng: &mut dyn Rng,
243        pos: BlockPos,
244    ) {
245        let topper = Self::create_topper_with_side_chance(world, pos, true);
246        if !topper.is_air() {
247            world.set_block(pos.above(), topper, UpdateFlags::UPDATE_ALL);
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use steel_registry::init_vanilla_registry;
255
256    use super::*;
257    use crate::behavior::init_behaviors;
258    use crate::test_support::TestLevel;
259
260    #[test]
261    fn updated_state_walls_up_against_the_supporting_block() {
262        init_vanilla_registry();
263        init_behaviors();
264
265        let pos = BlockPos::new(0, 64, 0);
266        let level =
267            TestLevel::default().with_block(pos.north(), vanilla_blocks::STONE.default_state());
268
269        let updated = MossyCarpetBlock::updated_state(
270            vanilla_blocks::PALE_MOSS_CARPET.default_state(),
271            &level,
272            pos,
273            true,
274        );
275
276        assert_eq!(updated.get_value(NORTH_WALL), WallSide::Low);
277        assert_eq!(updated.get_value(SOUTH_WALL), WallSide::None);
278    }
279}