steel_core/behavior/blocks/building/
ladder_block.rs1use 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
10const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
12
13const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
15
16#[block_behavior]
18pub struct LadderBlock {
19 block: BlockRef,
20}
21
22impl LadderBlock {
23 #[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
89fn 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}