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