1use steel_macros::block_behavior;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt;
9use steel_registry::blocks::properties::{
10 BlockStateProperties, BoolProperty, Direction, EnumProperty, WallSide,
11};
12use steel_registry::blocks::shapes::{OffsetVoxelShape, offset_face_rectangles_cover};
13use steel_registry::vanilla_block_tags::BlockTag;
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
17use crate::behavior::blocks::building::FenceGateBlock;
18use crate::behavior::blocks::utils::is_excluded_for_connection;
19use crate::behavior::context::BlockPlaceContext;
20use crate::entity::ai::path::PathComputationType;
21use crate::world::{LevelReader as _, ScheduledTickAccess};
22
23#[block_behavior]
32pub struct WallBlock {
33 block: BlockRef,
34}
35
36const UP: &BoolProperty = &BlockStateProperties::UP;
38const NORTH: &EnumProperty<WallSide> = &BlockStateProperties::NORTH_WALL;
40const EAST: &EnumProperty<WallSide> = &BlockStateProperties::EAST_WALL;
42const SOUTH: &EnumProperty<WallSide> = &BlockStateProperties::SOUTH_WALL;
44const WEST: &EnumProperty<WallSide> = &BlockStateProperties::WEST_WALL;
46const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
48
49const POST_X_MIN: f64 = 7.0 / 16.0;
51const POST_X_MAX: f64 = 9.0 / 16.0;
52const POST_Z_MIN: f64 = 7.0 / 16.0;
53const POST_Z_MAX: f64 = 9.0 / 16.0;
54
55const WALL_ARM_MIN: f64 = 7.0 / 16.0;
58const WALL_ARM_MAX: f64 = 9.0 / 16.0;
59const WALL_ARM_EXTENT: f64 = 9.0 / 16.0;
60
61impl WallBlock {
62 #[must_use]
64 pub const fn new(block: BlockRef) -> Self {
65 Self { block }
66 }
67}
68
69impl BlockBehavior for WallBlock {
70 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
71 let world = context.world;
72 let pos = context.place_pos();
73
74 let north_pos = Direction::North.relative(pos);
75 let east_pos = Direction::East.relative(pos);
76 let south_pos = Direction::South.relative(pos);
77 let west_pos = Direction::West.relative(pos);
78 let top_pos = Direction::Up.relative(pos);
79
80 let north_state = world.get_block_state(north_pos);
81 let east_state = world.get_block_state(east_pos);
82 let south_state = world.get_block_state(south_pos);
83 let west_state = world.get_block_state(west_pos);
84 let top_state = world.get_block_state(top_pos);
85
86 let north = connects_to(
89 north_state,
90 world.is_face_sturdy(north_state, north_pos, Direction::South),
91 Direction::South,
92 );
93 let east = connects_to(
94 east_state,
95 world.is_face_sturdy(east_state, east_pos, Direction::West),
96 Direction::West,
97 );
98 let south = connects_to(
99 south_state,
100 world.is_face_sturdy(south_state, south_pos, Direction::North),
101 Direction::North,
102 );
103 let west = connects_to(
104 west_state,
105 world.is_face_sturdy(west_state, west_pos, Direction::East),
106 Direction::East,
107 );
108
109 let state = self
110 .block
111 .default_state()
112 .set_value(WATERLOGGED, context.is_water_source());
113
114 Some(update_wall_state(
115 state, top_pos, top_state, north, east, south, west,
116 ))
117 }
118
119 fn update_shape(
120 &self,
121 state: BlockStateId,
122 world: &dyn ScheduledTickAccess,
123 pos: BlockPos,
124 direction: Direction,
125 neighbor_pos: BlockPos,
126 neighbor_state: BlockStateId,
127 ) -> BlockStateId {
128 schedule_water_tick_if_waterlogged(state, world, pos);
129
130 match direction {
131 Direction::Down => state,
133 Direction::Up => top_update(state, neighbor_pos, neighbor_state),
134 _ => side_update(world, pos, state, neighbor_pos, neighbor_state, direction),
135 }
136 }
137
138 fn is_pathfindable(
139 &self,
140 _state: BlockStateId,
141 _computation_type: PathComputationType,
142 ) -> bool {
143 false
144 }
145}
146
147fn is_connected(state: BlockStateId, side: &EnumProperty<WallSide>) -> bool {
149 state.get_value(side) != WallSide::None
150}
151
152fn connects_to(neighbor_state: BlockStateId, face_solid: bool, direction: Direction) -> bool {
155 let block = neighbor_state.get_block();
156 let connected_fence_gate = block.has_tag(&BlockTag::FENCE_GATES)
157 && FenceGateBlock::connects_to_direction(neighbor_state, direction);
158
159 block.has_tag(&BlockTag::WALLS)
160 || (!is_excluded_for_connection(block) && face_solid)
161 || block.has_tag(&BlockTag::BARS)
162 || block.has_tag(&BlockTag::C_GLASS_PANES)
163 || connected_fence_gate
164}
165
166fn top_update(state: BlockStateId, top_pos: BlockPos, top_neighbor: BlockStateId) -> BlockStateId {
167 let north = is_connected(state, NORTH);
168 let east = is_connected(state, EAST);
169 let south = is_connected(state, SOUTH);
170 let west = is_connected(state, WEST);
171 update_wall_state(state, top_pos, top_neighbor, north, east, south, west)
172}
173
174fn side_update(
175 world: &dyn ScheduledTickAccess,
176 pos: BlockPos,
177 state: BlockStateId,
178 neighbor_pos: BlockPos,
179 neighbor: BlockStateId,
180 direction: Direction,
181) -> BlockStateId {
182 let opposite = direction.opposite();
183 let connected = connects_to(
184 neighbor,
185 world.is_face_sturdy(neighbor, neighbor_pos, opposite),
186 opposite,
187 );
188
189 let north = if direction == Direction::North {
190 connected
191 } else {
192 is_connected(state, NORTH)
193 };
194 let east = if direction == Direction::East {
195 connected
196 } else {
197 is_connected(state, EAST)
198 };
199 let south = if direction == Direction::South {
200 connected
201 } else {
202 is_connected(state, SOUTH)
203 };
204 let west = if direction == Direction::West {
205 connected
206 } else {
207 is_connected(state, WEST)
208 };
209
210 let above = Direction::Up.relative(pos);
211 let above_state = world.get_block_state(above);
212 update_wall_state(state, above, above_state, north, east, south, west)
213}
214
215#[expect(
217 clippy::fn_params_excessive_bools,
218 reason = "mirrors vanilla WallBlock north/east/south/west signature"
219)]
220fn update_wall_state(
221 state: BlockStateId,
222 top_pos: BlockPos,
223 top_neighbor: BlockStateId,
224 north: bool,
225 east: bool,
226 south: bool,
227 west: bool,
228) -> BlockStateId {
229 let above_shape = top_neighbor.get_collision_shape_at(top_pos);
230 let sides = update_sides(state, above_shape, north, east, south, west);
231 sides.set_value(UP, should_raise_post(sides, top_neighbor, above_shape))
232}
233
234#[expect(
235 clippy::fn_params_excessive_bools,
236 reason = "mirrors vanilla WallBlock north/east/south/west signature"
237)]
238fn update_sides(
239 state: BlockStateId,
240 above_shape: OffsetVoxelShape,
241 north: bool,
242 east: bool,
243 south: bool,
244 west: bool,
245) -> BlockStateId {
246 state
247 .set_value(
248 NORTH,
249 make_wall_state(
250 north,
251 above_shape,
252 WALL_ARM_MIN,
253 WALL_ARM_MAX,
254 0.0,
255 WALL_ARM_EXTENT,
256 ),
257 )
258 .set_value(
259 EAST,
260 make_wall_state(
261 east,
262 above_shape,
263 WALL_ARM_MIN,
264 1.0,
265 WALL_ARM_MIN,
266 WALL_ARM_MAX,
267 ),
268 )
269 .set_value(
270 SOUTH,
271 make_wall_state(
272 south,
273 above_shape,
274 WALL_ARM_MIN,
275 WALL_ARM_MAX,
276 WALL_ARM_MIN,
277 1.0,
278 ),
279 )
280 .set_value(
281 WEST,
282 make_wall_state(
283 west,
284 above_shape,
285 0.0,
286 WALL_ARM_EXTENT,
287 WALL_ARM_MIN,
288 WALL_ARM_MAX,
289 ),
290 )
291}
292
293fn make_wall_state(
294 connects_to_side: bool,
295 above_shape: OffsetVoxelShape,
296 x_min: f64,
297 x_max: f64,
298 z_min: f64,
299 z_max: f64,
300) -> WallSide {
301 if !connects_to_side {
302 return WallSide::None;
303 }
304 if is_covered(above_shape, x_min, x_max, z_min, z_max) {
305 WallSide::Tall
306 } else {
307 WallSide::Low
308 }
309}
310
311fn should_raise_post(
312 state: BlockStateId,
313 top_neighbor: BlockStateId,
314 above_shape: OffsetVoxelShape,
315) -> bool {
316 let top_neighbor_has_post = top_neighbor.get_block().has_tag(&BlockTag::WALLS)
317 && top_neighbor.try_get_value(UP).unwrap_or(false);
318 if top_neighbor_has_post {
319 return true;
320 }
321
322 let north_wall = state.get_value(NORTH);
323 let south_wall = state.get_value(SOUTH);
324 let east_wall = state.get_value(EAST);
325 let west_wall = state.get_value(WEST);
326
327 let north_none = north_wall == WallSide::None;
328 let south_none = south_wall == WallSide::None;
329 let east_none = east_wall == WallSide::None;
330 let west_none = west_wall == WallSide::None;
331
332 let has_corner = (north_none && south_none && west_none && east_none)
333 || (north_none != south_none)
334 || (west_none != east_none);
335 if has_corner {
336 return true;
337 }
338
339 let has_high_wall = (north_wall == WallSide::Tall && south_wall == WallSide::Tall)
340 || (east_wall == WallSide::Tall && west_wall == WallSide::Tall);
341 if has_high_wall {
342 return false;
343 }
344
345 top_neighbor
346 .get_block()
347 .has_tag(&BlockTag::WALL_POST_OVERRIDE)
348 || is_covered(above_shape, POST_X_MIN, POST_X_MAX, POST_Z_MIN, POST_Z_MAX)
349}
350
351fn is_covered(
354 above_shape: OffsetVoxelShape,
355 x_min: f64,
356 x_max: f64,
357 z_min: f64,
358 z_max: f64,
359) -> bool {
360 offset_face_rectangles_cover(above_shape, Direction::Down, x_min, x_max, z_min, z_max)
361}