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: &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/// Weathering copper stairs share the stair shape rules and add copper aging.
141#[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    /// Creates a new weathering copper stair block behavior.
150    #[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}