steel_core/behavior/blocks/vegetation/
mossy_carpet_block.rs1use 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#[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 #[must_use]
42 pub const fn new(block: BlockRef) -> Self {
43 Self { block }
44 }
45
46 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 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 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 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}