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}
88
89/// Returns whether a ladder can be placed on a particular face of a block located at a certain position.
90fn can_attach_to(world: &dyn LevelReader, pos: BlockPos, direction: Direction) -> bool {
91    let state = world.get_block_state(pos);
92    world.is_face_sturdy(state, pos, direction)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use steel_registry::fluid::FluidRef;
99    use steel_registry::init_vanilla_registry;
100    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
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            OverworldNoiseSettings::MIN_Y
115        }
116
117        fn height(&self) -> i32 {
118            OverworldNoiseSettings::HEIGHT
119        }
120
121        fn sea_level(&self) -> i32 {
122            OverworldNoiseSettings::SEA_LEVEL
123        }
124    }
125
126    impl ScheduledTickAccess for EmptyLevel {
127        fn fluid_tick_delay(&self, _fluid: FluidRef) -> i32 {
128            5
129        }
130
131        fn schedule_block_tick_default(
132            &self,
133            _pos: BlockPos,
134            _block: BlockRef,
135            _delay: i32,
136        ) -> bool {
137            true
138        }
139
140        fn schedule_fluid_tick_default(
141            &self,
142            _pos: BlockPos,
143            _fluid: FluidRef,
144            _delay: i32,
145        ) -> bool {
146            true
147        }
148    }
149
150    #[test]
151    fn support_neighbor_update_breaks_unsupported_ladder() {
152        init_vanilla_registry();
153        let behavior = LadderBlock::new(&vanilla_blocks::LADDER);
154        let state = vanilla_blocks::LADDER
155            .default_state()
156            .set_value(FACING, Direction::East);
157
158        let updated = behavior.update_shape(
159            state,
160            &EmptyLevel,
161            BlockPos::ZERO,
162            Direction::West,
163            BlockPos::ZERO.relative(Direction::West),
164            vanilla_blocks::AIR.default_state(),
165        );
166
167        assert_eq!(updated, vanilla_blocks::AIR.default_state());
168    }
169
170    #[test]
171    fn waterlogged_ladder_contains_non_falling_source_water() {
172        init_vanilla_registry();
173        let state = vanilla_blocks::LADDER
174            .default_state()
175            .set_value(WATERLOGGED, true);
176
177        let fluid = state.get_fluid_state();
178
179        assert_eq!(fluid.fluid_id, &vanilla_fluids::WATER);
180        assert!(fluid.is_source());
181        assert!(!fluid.falling);
182    }
183}