steel_core/behavior/blocks/building/
stair_block.rs1use std::sync::Arc;
7
8use steel_macros::block_behavior;
9use steel_registry::blocks::BlockRef;
10use steel_registry::blocks::block_state_ext::BlockStateExt as _;
11use steel_registry::blocks::properties::{
12 BlockStateProperties, Direction, EnumProperty, Half, StairsShape,
13};
14use steel_registry::vanilla_fluids;
15use steel_utils::{BlockPos, BlockStateId};
16
17use super::weathering_block::{WeatherState, WeatheringCopper};
18use crate::{
19 behavior::{BlockBehavior, BlockPlaceContext},
20 world::{LevelReader, ScheduledTickAccess, World},
21};
22
23#[block_behavior]
25pub struct StairBlock {
26 block: BlockRef,
27}
28
29impl StairBlock {
30 const FACING: EnumProperty<Direction> = BlockStateProperties::HORIZONTAL_FACING;
31 const HALF: EnumProperty<Half> = BlockStateProperties::HALF;
32 const SHAPE: EnumProperty<StairsShape> = BlockStateProperties::STAIRS_SHAPE;
33
34 #[must_use]
36 pub const fn new(block: BlockRef) -> Self {
37 Self { block }
38 }
39
40 fn update_stair_shape(
41 state: BlockStateId,
42 world: &dyn LevelReader,
43 pos: BlockPos,
44 ) -> BlockStateId {
45 state.set_value(&Self::SHAPE, Self::stairs_shape(state, world, pos))
46 }
47
48 fn stairs_shape(state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> StairsShape {
49 let facing = state.get_value(&Self::FACING);
50 let behind_state = world.get_block_state(facing.relative(pos));
51 if Self::is_stairs(behind_state)
52 && state.get_value(&Self::HALF) == behind_state.get_value(&Self::HALF)
53 {
54 let behind_facing = behind_state.get_value(&Self::FACING);
55 if behind_facing.get_axis() != facing.get_axis()
56 && Self::can_take_shape(state, world, pos, behind_facing.opposite())
57 {
58 if behind_facing == facing.rotate_y_counter_clockwise() {
59 return StairsShape::OuterLeft;
60 }
61 return StairsShape::OuterRight;
62 }
63 }
64
65 let front_state = world.get_block_state(facing.opposite().relative(pos));
66 if Self::is_stairs(front_state)
67 && state.get_value(&Self::HALF) == front_state.get_value(&Self::HALF)
68 {
69 let front_facing = front_state.get_value(&Self::FACING);
70 if front_facing.get_axis() != facing.get_axis()
71 && Self::can_take_shape(state, world, pos, front_facing)
72 {
73 if front_facing == facing.rotate_y_counter_clockwise() {
74 return StairsShape::InnerLeft;
75 }
76 return StairsShape::InnerRight;
77 }
78 }
79
80 StairsShape::Straight
81 }
82
83 fn can_take_shape(
84 state: BlockStateId,
85 world: &dyn LevelReader,
86 pos: BlockPos,
87 neighbor: Direction,
88 ) -> bool {
89 let neighbor_state = world.get_block_state(neighbor.relative(pos));
90 !Self::is_stairs(neighbor_state)
91 || neighbor_state.get_value(&Self::FACING) != state.get_value(&Self::FACING)
92 || neighbor_state.get_value(&Self::HALF) != state.get_value(&Self::HALF)
93 }
94
95 fn is_stairs(state: BlockStateId) -> bool {
96 state.try_get_value(&Self::SHAPE).is_some()
97 }
98}
99
100impl BlockBehavior for StairBlock {
101 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
102 let half = if context.clicked_face() != Direction::Down
103 && (context.clicked_face() == Direction::Up
104 || context.click_location().y - f64::from(context.place_pos().y()) <= 0.5)
105 {
106 Half::Bottom
107 } else {
108 Half::Top
109 };
110
111 let state = self
112 .block
113 .default_state()
114 .set_value(&Self::FACING, context.horizontal_direction())
115 .set_value(&Self::HALF, half)
116 .set_value(
117 &BlockStateProperties::WATERLOGGED,
118 context.is_water_source(),
119 );
120 Some(Self::update_stair_shape(
121 state,
122 context.world.as_ref(),
123 context.place_pos(),
124 ))
125 }
126
127 fn update_shape(
128 &self,
129 state: BlockStateId,
130 world: &dyn ScheduledTickAccess,
131 pos: BlockPos,
132 direction: Direction,
133 _neighbor_pos: BlockPos,
134 _neighbor_state: BlockStateId,
135 ) -> BlockStateId {
136 if state.get_value(&BlockStateProperties::WATERLOGGED) {
137 let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
138 let _ = world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
139 }
140
141 if direction.is_horizontal() {
142 Self::update_stair_shape(state, world, pos)
143 } else {
144 state
145 }
146 }
147}
148
149#[block_behavior]
151pub struct WeatheringCopperStairBlock {
152 stair: StairBlock,
153 #[json_arg(r#enum = "WeatherState", json = "weather_state")]
154 weathering: WeatheringCopper,
155}
156
157impl WeatheringCopperStairBlock {
158 #[must_use]
160 pub const fn new(block: BlockRef, weather_state: WeatherState) -> Self {
161 Self {
162 stair: StairBlock::new(block),
163 weathering: WeatheringCopper::new(weather_state),
164 }
165 }
166}
167
168impl BlockBehavior for WeatheringCopperStairBlock {
169 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
170 self.stair.get_state_for_placement(context)
171 }
172
173 fn update_shape(
174 &self,
175 state: BlockStateId,
176 world: &dyn ScheduledTickAccess,
177 pos: BlockPos,
178 direction: Direction,
179 neighbor_pos: BlockPos,
180 neighbor_state: BlockStateId,
181 ) -> BlockStateId {
182 self.stair
183 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
184 }
185
186 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
187 self.weathering.change_over_time(state, world, pos);
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use steel_registry::{init_vanilla_registry, vanilla_blocks};
194 use steel_utils::BlockPos;
195
196 use crate::test_support::TestLevel;
197
198 use super::*;
199
200 #[test]
201 fn stair_update_shape_recomputes_shape_from_neighbors() {
202 init_vanilla_registry();
203 let behavior = StairBlock::new(&vanilla_blocks::DARK_OAK_STAIRS);
204 let state = vanilla_blocks::DARK_OAK_STAIRS
205 .default_state()
206 .set_value(&StairBlock::FACING, Direction::West)
207 .set_value(&StairBlock::HALF, Half::Top)
208 .set_value(&StairBlock::SHAPE, StairsShape::OuterRight)
209 .set_value(&BlockStateProperties::WATERLOGGED, true);
210 let level = TestLevel::default();
211
212 let updated = behavior.update_shape(
213 state,
214 &level,
215 BlockPos::ZERO,
216 Direction::West,
217 Direction::West.relative(BlockPos::ZERO),
218 vanilla_blocks::AIR.default_state(),
219 );
220
221 assert_eq!(updated.get_value(&StairBlock::SHAPE), StairsShape::Straight);
222 assert!(updated.get_value(&BlockStateProperties::WATERLOGGED));
223 }
224}