Skip to main content

steel_core/behavior/blocks/building/
stair_block.rs

1//! Stair block behavior implementation.
2//!
3//! Stairs recompute their `shape` property from adjacent stairs during placement
4//! and horizontal neighbor updates.
5
6use 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/// Behavior for stair blocks.
24#[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    /// Creates a new stair block behavior for the given block.
36    #[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/// Weathering copper stairs share the stair shape rules and add copper aging.
145#[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    /// Creates a new weathering copper stair block behavior.
154    #[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}