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, 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/// Behavior for stair blocks.
24#[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    /// Creates a new stair block behavior for the given block.
35    #[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/// Weathering copper stairs share the stair shape rules and add copper aging.
150#[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    /// Creates a new weathering copper stair block behavior.
159    #[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}