Skip to main content

steel_core/behavior/blocks/building/
ladder_block.rs

1use crate::behavior::{BlockBehavior, BlockPlaceContext};
2use crate::world::{LevelReader, ScheduledTickAccess};
3use steel_macros::block_behavior;
4use steel_registry::blocks::BlockRef;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty, EnumProperty};
7use steel_registry::{vanilla_blocks, vanilla_fluids};
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10/// Whether the ladder is waterlogged or not.
11const WATERLOGGED: BoolProperty = BlockStateProperties::WATERLOGGED;
12
13/// The direction the ladder is facing.
14const FACING: EnumProperty<Direction> = BlockStateProperties::HORIZONTAL_FACING;
15
16/// Behavior for ladders.
17#[block_behavior]
18pub struct LadderBlock {
19    block: BlockRef,
20}
21
22impl LadderBlock {
23    /// Creates a ladder block behavior.
24    #[must_use]
25    pub const fn new(block: BlockRef) -> Self {
26        Self { block }
27    }
28}
29
30impl BlockBehavior for LadderBlock {
31    fn update_shape(
32        &self,
33        state: BlockStateId,
34        world: &dyn ScheduledTickAccess,
35        pos: BlockPos,
36        direction: Direction,
37        _neighbor_pos: BlockPos,
38        _neighbor_state: BlockStateId,
39    ) -> BlockStateId {
40        let facing: Direction = state.get_value(&FACING);
41
42        if direction == facing.opposite() && !self.can_survive(state, world, pos) {
43            return vanilla_blocks::AIR.default_state();
44        }
45
46        if state.get_value(&WATERLOGGED) {
47            let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
48            let _ = world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
49        }
50
51        state
52    }
53
54    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
55        let direction = state.get_value(&FACING);
56        can_attach_to(world, pos.relative(direction.opposite()), direction)
57    }
58
59    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
60        if !context.replaces_clicked_block() {
61            let state = context.world.get_block_state(
62                context
63                    .place_pos()
64                    .relative(context.clicked_face().opposite()),
65            );
66            if state.get_block() == self.block && state.get_value(&FACING) == context.clicked_face()
67            {
68                return None;
69            }
70        }
71
72        let mut state = self.block.default_state();
73
74        for direction in context.get_nearest_looking_directions() {
75            if !direction.is_horizontal() {
76                continue;
77            }
78
79            state = state.set_value(&FACING, direction.opposite());
80            if self.can_survive(state, context.world, context.place_pos()) {
81                return Some(state.set_value(&WATERLOGGED, context.is_water_source()));
82            }
83        }
84
85        None
86    }
87    // TODO: Implement the mirror and rotate functions
88}
89
90/// Returns whether a ladder can be placed on a particular face of a block located at a certain position.
91fn can_attach_to(world: &dyn LevelReader, pos: BlockPos, direction: Direction) -> bool {
92    let state = world.get_block_state(pos);
93    world.is_face_sturdy(state, pos, direction)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use steel_registry::fluid::FluidRef;
100    use steel_registry::init_vanilla_registry;
101
102    struct EmptyLevel;
103
104    impl LevelReader for EmptyLevel {
105        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
106            vanilla_blocks::AIR.default_state()
107        }
108
109        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
110            0
111        }
112
113        fn min_y(&self) -> i32 {
114            -64
115        }
116
117        fn height(&self) -> i32 {
118            384
119        }
120    }
121
122    impl ScheduledTickAccess for EmptyLevel {
123        fn fluid_tick_delay(&self, _fluid: FluidRef) -> i32 {
124            5
125        }
126
127        fn schedule_block_tick_default(
128            &self,
129            _pos: BlockPos,
130            _block: BlockRef,
131            _delay: i32,
132        ) -> bool {
133            true
134        }
135
136        fn schedule_fluid_tick_default(
137            &self,
138            _pos: BlockPos,
139            _fluid: FluidRef,
140            _delay: i32,
141        ) -> bool {
142            true
143        }
144    }
145
146    #[test]
147    fn support_neighbor_update_breaks_unsupported_ladder() {
148        init_vanilla_registry();
149        let behavior = LadderBlock::new(&vanilla_blocks::LADDER);
150        let state = vanilla_blocks::LADDER
151            .default_state()
152            .set_value(&FACING, Direction::East);
153
154        let updated = behavior.update_shape(
155            state,
156            &EmptyLevel,
157            BlockPos::ZERO,
158            Direction::West,
159            BlockPos::ZERO.relative(Direction::West),
160            vanilla_blocks::AIR.default_state(),
161        );
162
163        assert_eq!(updated, vanilla_blocks::AIR.default_state());
164    }
165
166    #[test]
167    fn waterlogged_ladder_contains_non_falling_source_water() {
168        init_vanilla_registry();
169        let state = vanilla_blocks::LADDER
170            .default_state()
171            .set_value(&WATERLOGGED, true);
172
173        let fluid = state.get_fluid_state();
174
175        assert_eq!(fluid.fluid_id, &vanilla_fluids::WATER);
176        assert!(fluid.is_source());
177        assert!(!fluid.falling);
178    }
179}