steel_core/behavior/blocks/building/
scaffolding_block.rs1use 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#[block_behavior]
36pub struct ScaffoldingBlock {
37 block: BlockRef,
38}
39
40impl ScaffoldingBlock {
41 #[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 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}