Skip to main content

steel_core/behavior/blocks/vegetation/
vine_block.rs

1use std::sync::Arc;
2use steel_macros::block_behavior;
3use steel_registry::blocks::block_state_ext::BlockStateExt;
4use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
5use steel_registry::{vanilla_blocks, vanilla_game_rules};
6use steel_utils::axis::Axis;
7use steel_utils::types::UpdateFlags;
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::behavior::block::{BlockBehavior, default_can_be_replaced};
11use crate::behavior::blocks::MultifaceBlock;
12use crate::behavior::context::BlockPlaceContext;
13use crate::world::{LevelReader, ScheduledTickAccess, World};
14
15use super::BlockRef;
16
17/// Vanilla `VineBlock` survival and neighbor shape updates.
18#[block_behavior]
19pub struct VineBlock {
20    block: BlockRef,
21}
22
23const EAST: &BoolProperty = &BlockStateProperties::EAST;
24const NORTH: &BoolProperty = &BlockStateProperties::NORTH;
25const SOUTH: &BoolProperty = &BlockStateProperties::SOUTH;
26const UP: &BoolProperty = &BlockStateProperties::UP;
27const WEST: &BoolProperty = &BlockStateProperties::WEST;
28
29impl VineBlock {
30    /// Creates a new vine block behavior.
31    #[must_use]
32    pub const fn new(block: BlockRef) -> Self {
33        Self { block }
34    }
35
36    fn has_faces(state: BlockStateId) -> bool {
37        Self::count_faces(state) > 0
38    }
39
40    fn count_faces(state: BlockStateId) -> usize {
41        VINE_FACE_DIRECTIONS
42            .into_iter()
43            .filter(|direction| state.get_value(get_property_for_face(*direction)))
44            .count()
45    }
46
47    fn can_support_at_face(
48        &self,
49        world: &dyn LevelReader,
50        pos: BlockPos,
51        direction: Direction,
52    ) -> bool {
53        if direction == Direction::Down {
54            return false;
55        }
56
57        if Self::is_acceptable_neighbour(world, pos.relative(direction), direction) {
58            return true;
59        }
60
61        if direction.get_axis() == Axis::Y {
62            return false;
63        }
64
65        let property = get_property_for_face(direction);
66        let above = world.get_block_state(pos.above());
67        above.get_block() == self.block && above.get_value(property)
68    }
69    fn is_acceptable_neighbour(
70        level: &dyn LevelReader,
71        neighbour_pos: BlockPos,
72        direction_to_neighbour: Direction,
73    ) -> bool {
74        MultifaceBlock::can_attach_to_state(
75            level,
76            direction_to_neighbour,
77            neighbour_pos,
78            level.get_block_state(neighbour_pos),
79        )
80    }
81    fn can_spread(&self, world: &Arc<World>, pos: BlockPos) -> bool {
82        let mut max = 5;
83
84        for x in (pos.x() - 4)..=(pos.x() + 4) {
85            for y in (pos.y() - 1)..=(pos.y() + 1) {
86                for z in (pos.z() - 4)..=(pos.z() + 4) {
87                    let block_pos = BlockPos::new(x, y, z);
88
89                    if world.get_block_state(block_pos).get_block() == self.block {
90                        max -= 1;
91
92                        if max <= 0 {
93                            return false;
94                        }
95                    }
96                }
97            }
98        }
99
100        true
101    }
102
103    fn copy_random_faces(from: BlockStateId, to: BlockStateId) -> BlockStateId {
104        let mut result = to;
105        for direction in Direction::HORIZONTAL {
106            if rand::random_bool(0.5) {
107                let property_for_face = get_property_for_face(direction);
108                if from.get_value(property_for_face) {
109                    result = result.set_value(property_for_face, true);
110                }
111            }
112        }
113
114        result
115    }
116    fn has_horizontal_connection(state: BlockStateId) -> bool {
117        for dir in Direction::HORIZONTAL {
118            let property = get_property_for_face(dir);
119            if state.get_value(property) {
120                return true;
121            }
122        }
123        false
124    }
125    fn updated_state(
126        &self,
127        mut state: BlockStateId,
128        world: &dyn LevelReader,
129        pos: BlockPos,
130    ) -> BlockStateId {
131        let above_pos = pos.above();
132        if state.get_value(UP) {
133            state = state.set_value(
134                UP,
135                Self::is_acceptable_neighbour(world, above_pos, Direction::Down),
136            );
137        }
138
139        let mut above_state: Option<BlockStateId> = None;
140        for direction in Direction::HORIZONTAL {
141            let property = get_property_for_face(direction);
142            if !state.get_value(property) {
143                continue;
144            }
145
146            let mut can_support = self.can_support_at_face(world, pos, direction);
147            if !can_support {
148                let above = *above_state.get_or_insert_with(|| world.get_block_state(above_pos));
149                can_support = above.get_block() == self.block && above.get_value(property);
150            }
151
152            state = state.set_value(property, can_support);
153        }
154
155        state
156    }
157    fn try_spread_horizontal(
158        &self,
159        state: BlockStateId,
160        world: &Arc<World>,
161        pos: BlockPos,
162        test_direction: Direction,
163    ) {
164        if !self.can_spread(world, pos) {
165            return;
166        }
167
168        let test_pos = pos.relative(test_direction);
169        let edge_state = world.get_block_state(test_pos);
170
171        if edge_state.is_air() {
172            let cw = test_direction.rotate_y_clockwise();
173            let cocw = test_direction.rotate_y_counter_clockwise();
174            let cw_property = get_property_for_face(cw);
175            let cocw_property = get_property_for_face(cocw);
176            let cw_has_connecting_face = state.get_value(cw_property);
177            let cocw_has_connecting_face = state.get_value(cocw_property);
178            let cw_test_pos = test_pos.relative(cw);
179            let cocw_test_pos = test_pos.relative(cocw);
180
181            if cw_has_connecting_face && Self::is_acceptable_neighbour(world, cw_test_pos, cw) {
182                world.set_block(
183                    test_pos,
184                    self.block.default_state().set_value(cw_property, true),
185                    UpdateFlags::UPDATE_CLIENTS,
186                );
187            } else if cocw_has_connecting_face
188                && Self::is_acceptable_neighbour(world, cocw_test_pos, cocw)
189            {
190                world.set_block(
191                    test_pos,
192                    self.block.default_state().set_value(cocw_property, true),
193                    UpdateFlags::UPDATE_CLIENTS,
194                );
195            } else {
196                let opposite = test_direction.opposite();
197                if cw_has_connecting_face
198                    && world.get_block_state(cw_test_pos).is_air()
199                    && Self::is_acceptable_neighbour(world, pos.relative(cw), opposite)
200                {
201                    world.set_block(
202                        cw_test_pos,
203                        self.block
204                            .default_state()
205                            .set_value(get_property_for_face(opposite), true),
206                        UpdateFlags::UPDATE_CLIENTS,
207                    );
208                } else if cocw_has_connecting_face
209                    && world.get_block_state(cocw_test_pos).is_air()
210                    && Self::is_acceptable_neighbour(world, pos.relative(cocw), opposite)
211                {
212                    world.set_block(
213                        cocw_test_pos,
214                        self.block
215                            .default_state()
216                            .set_value(get_property_for_face(opposite), true),
217                        UpdateFlags::UPDATE_CLIENTS,
218                    );
219                } else if rand::random_range(0.0..1.0) < 0.05
220                    && Self::is_acceptable_neighbour(world, test_pos.above(), Direction::Up)
221                {
222                    world.set_block(
223                        test_pos,
224                        self.block
225                            .default_state()
226                            .set_value(get_property_for_face(Direction::Up), true),
227                        UpdateFlags::UPDATE_CLIENTS,
228                    );
229                }
230            }
231        } else if Self::is_acceptable_neighbour(world, test_pos, test_direction) {
232            world.set_block(
233                pos,
234                state.set_value(get_property_for_face(test_direction), true),
235                UpdateFlags::UPDATE_CLIENTS,
236            );
237        }
238    }
239
240    fn try_spread_vertical(
241        &self,
242        state: BlockStateId,
243        world: &Arc<World>,
244        pos: BlockPos,
245        test_direction: Direction,
246    ) {
247        let above_pos = pos.above();
248
249        if test_direction == Direction::Up && pos.y() < world.get_max_y() {
250            if self.can_support_at_face(world, pos, test_direction) {
251                world.set_block(
252                    pos,
253                    state.set_value(get_property_for_face(Direction::Up), true),
254                    UpdateFlags::UPDATE_CLIENTS,
255                );
256                return;
257            }
258            if world.get_block_state(above_pos).is_air() {
259                if !self.can_spread(world, pos) {
260                    return;
261                }
262                let mut above_state = state;
263                for direction in Direction::HORIZONTAL {
264                    if rand::random_bool(0.5)
265                        || !Self::is_acceptable_neighbour(
266                            world,
267                            above_pos.relative(direction),
268                            direction,
269                        )
270                    {
271                        above_state =
272                            above_state.set_value(get_property_for_face(direction), false);
273                    }
274                }
275                if Self::has_horizontal_connection(above_state) {
276                    world.set_block(above_pos, above_state, UpdateFlags::UPDATE_CLIENTS);
277                }
278                return;
279            }
280        }
281
282        if pos.y() > world.min_y() {
283            let below_pos = pos.below();
284            let below_state = world.get_block_state(below_pos);
285            if below_state.is_air() || below_state.get_block() == self.block {
286                let before = if below_state.is_air() {
287                    self.block.default_state()
288                } else {
289                    below_state
290                };
291                let after = Self::copy_random_faces(state, before);
292                if before != after && Self::has_horizontal_connection(after) {
293                    world.set_block(below_pos, after, UpdateFlags::UPDATE_CLIENTS);
294                }
295            }
296        }
297    }
298}
299
300const VINE_FACE_DIRECTIONS: [Direction; 5] = [
301    Direction::Up,
302    Direction::North,
303    Direction::East,
304    Direction::South,
305    Direction::West,
306];
307
308fn get_property_for_face(direction: Direction) -> &'static BoolProperty {
309    match direction {
310        Direction::Up => UP,
311        Direction::North => NORTH,
312        Direction::East => EAST,
313        Direction::South => SOUTH,
314        Direction::West => WEST,
315        Direction::Down => unreachable!("vine has no DOWN face property"),
316    }
317}
318
319impl BlockBehavior for VineBlock {
320    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
321        Self::has_faces(self.updated_state(state, world, pos))
322    }
323
324    fn update_shape(
325        &self,
326        state: BlockStateId,
327        world: &dyn ScheduledTickAccess,
328        pos: BlockPos,
329        direction: Direction,
330        _neighbor_pos: BlockPos,
331        _neighbor_state: BlockStateId,
332    ) -> BlockStateId {
333        if direction == Direction::Down {
334            return state;
335        }
336
337        let updated = self.updated_state(state, world, pos);
338        if Self::has_faces(updated) {
339            updated
340        } else {
341            vanilla_blocks::AIR.default_state()
342        }
343    }
344
345    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
346        if !world.get_game_rule(&vanilla_game_rules::SPREAD_VINES) {
347            return;
348        }
349        if rand::random_range(0..4) != 0 {
350            return;
351        }
352
353        let test_direction = Direction::random();
354
355        if test_direction.axis().is_horizontal()
356            && !state.get_value(get_property_for_face(test_direction))
357        {
358            self.try_spread_horizontal(state, world, pos, test_direction);
359        } else {
360            self.try_spread_vertical(state, world, pos, test_direction);
361        }
362    }
363
364    fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
365        let clicked_state = context.world.get_block_state(context.place_pos());
366        if clicked_state.get_block() == self.block {
367            Self::count_faces(clicked_state) < VINE_FACE_DIRECTIONS.len()
368        } else {
369            default_can_be_replaced(state, context)
370        }
371    }
372
373    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
374        let clicked_pos = context.place_pos();
375        let clicked_state = context.world.get_block_state(clicked_pos);
376        let clicked_vine = clicked_state.get_block() == self.block;
377        let result = if clicked_vine {
378            clicked_state
379        } else {
380            self.block.default_state()
381        };
382
383        for direction in context.get_nearest_looking_directions() {
384            if direction != Direction::Down {
385                let face = get_property_for_face(direction);
386                let face_occupied = clicked_vine && clicked_state.get_value(face);
387                if !face_occupied && self.can_support_at_face(context.world, clicked_pos, direction)
388                {
389                    return Some(result.set_value(face, true));
390                }
391            }
392        }
393        if clicked_vine {
394            return Some(result);
395        }
396        None
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::test_support::TestLevel;
404    use steel_registry::init_vanilla_registry;
405
406    #[test]
407    fn face_count_matches_vanilla_replacement_limit() {
408        init_vanilla_registry();
409
410        let mut state = vanilla_blocks::VINE.default_state();
411        assert_eq!(VineBlock::count_faces(state), 0);
412
413        for (expected, direction) in VINE_FACE_DIRECTIONS.into_iter().enumerate() {
414            state = state.set_value(get_property_for_face(direction), true);
415            assert_eq!(VineBlock::count_faces(state), expected + 1);
416        }
417    }
418
419    #[test]
420    fn shape_update_removes_faceless_vine() {
421        init_vanilla_registry();
422
423        let vine = VineBlock::new(&vanilla_blocks::VINE);
424        let state = vanilla_blocks::VINE.default_state();
425        let level = TestLevel::default();
426
427        assert_eq!(
428            vine.update_shape(
429                state,
430                &level,
431                BlockPos::ZERO,
432                Direction::North,
433                BlockPos::ZERO.north(),
434                vanilla_blocks::AIR.default_state(),
435            ),
436            vanilla_blocks::AIR.default_state()
437        );
438    }
439}