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