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