Skip to main content

steel_core/behavior/blocks/building/
scaffolding_block.rs

1use steel_macros::block_behavior;
2use steel_registry::blocks::{
3    BlockRef,
4    block_state_ext::BlockStateExt,
5    properties::{BlockStateProperties, Direction},
6    shapes::VoxelShape,
7};
8use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId};
9
10use crate::behavior::{
11    BlockBehavior, BlockCollisionContext, BlockPlaceContext,
12    block::schedule_water_tick_if_waterlogged,
13};
14use crate::world::{LevelReader, ScheduledTickAccess};
15
16const SHAPE_STABLE_BOXES: &[BlockLocalAabb] = &[
17    BlockLocalAabb::new(0.0, 0.875, 0.0, 1.0, 1.0, 1.0),
18    BlockLocalAabb::new(0.0, 0.0, 0.0, 0.125, 1.0, 0.125),
19    BlockLocalAabb::new(0.875, 0.0, 0.0, 1.0, 1.0, 0.125),
20    BlockLocalAabb::new(0.0, 0.0, 0.875, 0.125, 1.0, 1.0),
21    BlockLocalAabb::new(0.875, 0.0, 0.875, 1.0, 1.0, 1.0),
22];
23const SHAPE_UNSTABLE_BOTTOM_BOXES: &[BlockLocalAabb] =
24    &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.125, 1.0)];
25const SHAPE_BELOW_BLOCK_BOXES: &[BlockLocalAabb] =
26    &[BlockLocalAabb::new(0.0, -1.0, 0.0, 1.0, 0.0, 1.0)];
27
28const SHAPE_STABLE: VoxelShape = VoxelShape::from_boxes(SHAPE_STABLE_BOXES);
29const SHAPE_UNSTABLE_BOTTOM: VoxelShape = VoxelShape::from_boxes(SHAPE_UNSTABLE_BOTTOM_BOXES);
30const SHAPE_BELOW_BLOCK: VoxelShape = VoxelShape::from_boxes(SHAPE_BELOW_BLOCK_BOXES);
31
32/// Vanilla scaffolding collision-shape behavior.
33///
34/// TODO: Add vanilla placement, stability distance updates, falling conversion, and waterlogging.
35#[block_behavior]
36pub struct ScaffoldingBlock {
37    block: BlockRef,
38}
39
40impl ScaffoldingBlock {
41    /// Creates a scaffolding block behavior.
42    #[must_use]
43    pub const fn new(block: BlockRef) -> Self {
44        Self { block }
45    }
46}
47
48impl BlockBehavior for ScaffoldingBlock {
49    fn update_shape(
50        &self,
51        state: BlockStateId,
52        world: &dyn ScheduledTickAccess,
53        pos: BlockPos,
54        _direction: Direction,
55        _neighbor_pos: BlockPos,
56        _neighbor_state: BlockStateId,
57    ) -> BlockStateId {
58        schedule_water_tick_if_waterlogged(state, world, pos);
59        world.schedule_block_tick_default(pos, self.block, 1);
60        state
61    }
62
63    // TODO: Mirror vanilla scaffolding placement here, including WATERLOGGED,
64    // STABILITY_DISTANCE, BOTTOM, and on_place.
65    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
66        Some(self.block.default_state())
67    }
68
69    fn get_collision_shape(
70        &self,
71        state: BlockStateId,
72        _world: &dyn LevelReader,
73        pos: BlockPos,
74        context: BlockCollisionContext,
75    ) -> VoxelShape {
76        if context.is_placement() {
77            return VoxelShape::EMPTY;
78        }
79
80        if context.is_above(VoxelShape::FULL_BLOCK, pos, true) && !context.is_descending() {
81            return SHAPE_STABLE;
82        }
83
84        let distance = state.get_value(&BlockStateProperties::STABILITY_DISTANCE);
85        let bottom = state.get_value(&BlockStateProperties::BOTTOM);
86        if distance != 0 && bottom && context.is_above(SHAPE_BELOW_BLOCK, pos, true) {
87            SHAPE_UNSTABLE_BOTTOM
88        } else {
89            VoxelShape::EMPTY
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
98
99    use crate::test_support::TestLevel;
100
101    fn scaffolding_state(distance: u8, bottom: bool) -> BlockStateId {
102        vanilla_blocks::SCAFFOLDING
103            .default_state()
104            .set_value(&BlockStateProperties::STABILITY_DISTANCE, distance)
105            .set_value(&BlockStateProperties::BOTTOM, bottom)
106    }
107
108    fn collision_shape(state: BlockStateId, context: BlockCollisionContext) -> VoxelShape {
109        let behavior = ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING);
110        let level = TestLevel::default().with_min_y(0);
111        behavior.get_collision_shape(state, &level, BlockPos::new(0, 64, 0), context)
112    }
113
114    #[test]
115    fn placement_context_has_no_scaffolding_collision() {
116        init_vanilla_registry();
117
118        let shape = collision_shape(
119            scaffolding_state(0, false),
120            BlockCollisionContext::pre_move(65.0, false),
121        );
122
123        assert_eq!(shape, VoxelShape::EMPTY);
124    }
125
126    #[test]
127    fn entity_above_scaffolding_collides_with_stable_shape() {
128        init_vanilla_registry();
129
130        let shape = collision_shape(
131            scaffolding_state(0, false),
132            BlockCollisionContext::entity(65.0, false),
133        );
134
135        assert_eq!(shape, SHAPE_STABLE);
136    }
137
138    #[test]
139    fn descending_entity_only_collides_with_unstable_bottom_shape() {
140        init_vanilla_registry();
141
142        let shape = collision_shape(
143            scaffolding_state(1, true),
144            BlockCollisionContext::entity(64.5, true),
145        );
146
147        assert_eq!(shape, SHAPE_UNSTABLE_BOTTOM);
148    }
149
150    #[test]
151    fn non_bottom_descending_scaffolding_has_empty_collision() {
152        init_vanilla_registry();
153
154        let shape = collision_shape(
155            scaffolding_state(1, false),
156            BlockCollisionContext::entity(64.5, true),
157        );
158
159        assert_eq!(shape, VoxelShape::EMPTY);
160    }
161
162    #[test]
163    fn shape_update_schedules_stability_and_water_ticks() {
164        init_vanilla_registry();
165        let behavior = ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING);
166        let state = vanilla_blocks::SCAFFOLDING
167            .default_state()
168            .set_value(&BlockStateProperties::WATERLOGGED, true);
169        let pos = BlockPos::new(0, 64, 0);
170        let level = TestLevel::default();
171
172        assert_eq!(
173            behavior.update_shape(
174                state,
175                &level,
176                pos,
177                Direction::North,
178                pos.north(),
179                vanilla_blocks::AIR.default_state(),
180            ),
181            state
182        );
183        assert_eq!(
184            level
185                .scheduled_block_ticks
186                .borrow()
187                .iter()
188                .map(|tick| (tick.pos, tick.block, tick.delay))
189                .collect::<Vec<_>>(),
190            vec![(pos, &vanilla_blocks::SCAFFOLDING, 1)]
191        );
192        assert_eq!(
193            level
194                .scheduled_fluid_ticks
195                .borrow()
196                .iter()
197                .map(|tick| (tick.pos, tick.fluid, tick.delay))
198                .collect::<Vec<_>>(),
199            vec![(pos, &vanilla_fluids::WATER, 5)]
200        );
201    }
202}