1use crate::{
6 behavior::{
7 BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
8 block::schedule_water_tick_if_waterlogged,
9 blocks::{WeatherState, WeatheringCopper},
10 },
11 entity::{Entity, ai::path::PathComputationType},
12 player::Player,
13 world::{ScheduledTickAccess, SignalGetter as _, World, game_event::GameEventContext},
14};
15use std::sync::Arc;
16use steel_macros::block_behavior;
17use steel_registry::{
18 blocks::{
19 BlockRef,
20 block_state_ext::BlockStateExt as _,
21 properties::{BlockStateProperties, BoolProperty, Direction, EnumProperty, Half},
22 },
23 sound_event::SoundEventRef,
24 vanilla_fluids, vanilla_game_events,
25};
26use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
27
28#[block_behavior]
30pub struct TrapDoorBlock {
31 block: BlockRef,
32 #[json_arg(value, json = "type_can_open_by_hand")]
33 can_open_by_hand: bool,
34 #[json_arg(sound_events, json = "type_trapdoor_open")]
35 sound_open: SoundEventRef,
36 #[json_arg(sound_events, json = "type_trapdoor_close")]
37 sound_close: SoundEventRef,
38}
39
40const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
41const HALF: &EnumProperty<Half> = &BlockStateProperties::HALF;
42const OPEN: &BoolProperty = &BlockStateProperties::OPEN;
43const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
44const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
45
46#[block_behavior]
48pub struct WeatheringCopperTrapDoorBlock {
49 block: BlockRef,
50 #[json_arg(r#enum = "WeatherState", json = "weather_state")]
52 pub weathering: WeatheringCopper,
53 #[json_arg(value, json = "type_can_open_by_hand")]
54 can_open_by_hand: bool,
55 #[json_arg(sound_events, json = "type_trapdoor_open")]
56 sound_open: SoundEventRef,
57 #[json_arg(sound_events, json = "type_trapdoor_close")]
58 sound_close: SoundEventRef,
59}
60
61impl TrapDoorBlock {
62 #[must_use]
64 pub const fn new(
65 block: BlockRef,
66 can_open_by_hand: bool,
67 sound_open: SoundEventRef,
68 sound_close: SoundEventRef,
69 ) -> Self {
70 Self {
71 block,
72 can_open_by_hand,
73 sound_open,
74 sound_close,
75 }
76 }
77
78 fn play_sound(&self, player: Option<&Player>, world: &Arc<World>, pos: BlockPos, open: bool) {
79 let sound = if open {
80 self.sound_open
81 } else {
82 self.sound_close
83 };
84 let pitch = rand::random_range(0.9..1.0);
85 world.play_block_sound(sound, pos, 1.0, pitch, player.map(Entity::id));
86 world.game_event(
87 if open {
88 &vanilla_game_events::BLOCK_OPEN
89 } else {
90 &vanilla_game_events::BLOCK_CLOSE
91 },
92 pos,
93 &GameEventContext::new(
94 if let Some(player) = player {
95 Some(player)
96 } else {
97 None
98 },
99 None,
100 ),
101 );
102 }
103
104 fn toggle(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, player: &Player) {
105 let block_state = state.set_value(OPEN, !state.get_value(OPEN));
106 world.set_block(pos, block_state, UpdateFlags::UPDATE_CLIENTS);
107 schedule_water_tick_if_waterlogged(state, world, pos);
108
109 self.play_sound(Some(player), world, pos, block_state.get_value(OPEN));
110 }
111}
112
113impl BlockBehavior for TrapDoorBlock {
114 fn is_trapdoor(&self) -> bool {
115 true
116 }
117
118 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
119 let mut state = self.block.default_state();
120 let face = context.clicked_face();
121 if !context.replaces_clicked_block() && face.is_horizontal() {
122 state = state.set_value(FACING, face).set_value(
123 HALF,
124 if context.click_location().y - f64::from(context.place_pos().y()) > 0.5 {
125 Half::Top
126 } else {
127 Half::Bottom
128 },
129 );
130 } else {
131 state = state
132 .set_value(FACING, context.horizontal_direction().opposite())
133 .set_value(
134 HALF,
135 if face == Direction::Up {
136 Half::Bottom
137 } else {
138 Half::Top
139 },
140 );
141 }
142
143 if context.world.has_neighbor_signal(context.place_pos()) {
144 state = state.set_value(OPEN, true).set_value(POWERED, true);
145 }
146
147 Some(state.set_value(WATERLOGGED, context.is_water_source()))
148 }
149
150 fn update_shape(
151 &self,
152 state: BlockStateId,
153 world: &dyn ScheduledTickAccess,
154 pos: BlockPos,
155 _direction: Direction,
156 _neighbor_pos: BlockPos,
157 _neighbor_state: BlockStateId,
158 ) -> BlockStateId {
159 schedule_water_tick_if_waterlogged(state, world, pos);
160 state
161 }
162
163 fn use_without_item(
164 &self,
165 state: BlockStateId,
166 world: &Arc<World>,
167 pos: BlockPos,
168 player: &Player,
169 _hit_result: &BlockHitResult,
170 _inv: &mut InventoryAccess,
171 ) -> InteractionResult {
172 if self.can_open_by_hand {
173 self.toggle(state, world, pos, player);
174 InteractionResult::Success
175 } else {
176 InteractionResult::Pass
177 }
178 }
179
180 fn handle_neighbor_changed(
181 &self,
182 state: BlockStateId,
183 world: &Arc<World>,
184 pos: BlockPos,
185 _source_block: BlockRef,
186 _moved_by_piston: bool,
187 ) {
188 let signal = world.has_neighbor_signal(pos);
189 if signal == state.get_value(POWERED) {
190 return;
191 }
192
193 let mut block_state = state;
194 if signal != state.get_value(OPEN) {
195 block_state = block_state.set_value(OPEN, signal);
196 self.play_sound(None, world, pos, signal);
197 }
198 world.set_block(
199 pos,
200 block_state.set_value(POWERED, signal),
201 UpdateFlags::UPDATE_CLIENTS,
202 );
203 if block_state.get_value(WATERLOGGED) {
204 let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
205 let _ = world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
206 }
207 }
208
209 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
210 match computation_type {
211 PathComputationType::Land | PathComputationType::Air => state.get_value(OPEN),
212 PathComputationType::Water => state.get_value(WATERLOGGED),
213 }
214 }
215}
216
217impl WeatheringCopperTrapDoorBlock {
218 #[must_use]
220 pub const fn new(
221 block: BlockRef,
222 weather_state: WeatherState,
223 can_open_by_hand: bool,
224 sound_open: SoundEventRef,
225 sound_close: SoundEventRef,
226 ) -> Self {
227 Self {
228 block,
229 weathering: WeatheringCopper::new(weather_state),
230 can_open_by_hand,
231 sound_open,
232 sound_close,
233 }
234 }
235
236 const fn trapdoor(&self) -> TrapDoorBlock {
237 TrapDoorBlock::new(
238 self.block,
239 self.can_open_by_hand,
240 self.sound_open,
241 self.sound_close,
242 )
243 }
244}
245
246impl BlockBehavior for WeatheringCopperTrapDoorBlock {
247 fn is_trapdoor(&self) -> bool {
248 true
249 }
250
251 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
252 self.trapdoor().get_state_for_placement(context)
253 }
254
255 fn update_shape(
256 &self,
257 state: BlockStateId,
258 world: &dyn ScheduledTickAccess,
259 pos: BlockPos,
260 direction: Direction,
261 neighbor_pos: BlockPos,
262 neighbor_state: BlockStateId,
263 ) -> BlockStateId {
264 self.trapdoor()
265 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
266 }
267
268 fn use_without_item(
269 &self,
270 state: BlockStateId,
271 world: &Arc<World>,
272 pos: BlockPos,
273 player: &Player,
274 hit_result: &BlockHitResult,
275 inv: &mut InventoryAccess,
276 ) -> InteractionResult {
277 self.trapdoor()
278 .use_without_item(state, world, pos, player, hit_result, inv)
279 }
280
281 fn handle_neighbor_changed(
282 &self,
283 state: BlockStateId,
284 world: &Arc<World>,
285 pos: BlockPos,
286 source_block: BlockRef,
287 moved_by_piston: bool,
288 ) {
289 self.trapdoor()
290 .handle_neighbor_changed(state, world, pos, source_block, moved_by_piston);
291 }
292
293 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
294 self.trapdoor().is_pathfindable(state, computation_type)
295 }
296
297 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
298 self.weathering.change_over_time(state, world, pos);
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks};
306 use steel_utils::ChunkPos;
307
308 use crate::{
309 behavior::{BLOCK_BEHAVIORS, init_behaviors},
310 test_support::{fresh_test_world, insert_ready_full_chunk},
311 };
312
313 #[test]
314 fn closed_trapdoor_is_not_land_or_air_pathfindable() {
315 init_vanilla_registry();
316 let behavior = TrapDoorBlock::new(
317 &vanilla_blocks::OAK_TRAPDOOR,
318 true,
319 &sound_events::BLOCK_WOODEN_TRAPDOOR_OPEN,
320 &sound_events::BLOCK_WOODEN_TRAPDOOR_CLOSE,
321 );
322 let state = vanilla_blocks::OAK_TRAPDOOR
323 .default_state()
324 .set_value(OPEN, false)
325 .set_value(WATERLOGGED, false);
326
327 assert!(!behavior.is_pathfindable(state, PathComputationType::Land));
328 assert!(!behavior.is_pathfindable(state, PathComputationType::Air));
329 assert!(!behavior.is_pathfindable(state, PathComputationType::Water));
330 }
331
332 #[test]
333 fn open_waterlogged_trapdoor_matches_vanilla_pathfinding() {
334 init_vanilla_registry();
335 let behavior = TrapDoorBlock::new(
336 &vanilla_blocks::OAK_TRAPDOOR,
337 true,
338 &sound_events::BLOCK_WOODEN_TRAPDOOR_OPEN,
339 &sound_events::BLOCK_WOODEN_TRAPDOOR_CLOSE,
340 );
341 let state = vanilla_blocks::OAK_TRAPDOOR
342 .default_state()
343 .set_value(OPEN, true)
344 .set_value(WATERLOGGED, true);
345
346 assert!(behavior.is_pathfindable(state, PathComputationType::Land));
347 assert!(behavior.is_pathfindable(state, PathComputationType::Air));
348 assert!(behavior.is_pathfindable(state, PathComputationType::Water));
349 }
350
351 #[test]
352 fn redundant_redstone_notification_does_not_schedule_water_tick() {
353 init_vanilla_registry();
354 init_behaviors();
355 let world = fresh_test_world("trapdoor_redundant_redstone");
356 let pos = BlockPos::new(8, 64, 8);
357 let power_pos = pos.west();
358 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
359 let state = vanilla_blocks::OAK_TRAPDOOR
360 .default_state()
361 .set_value(WATERLOGGED, true);
362 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
363 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::OAK_TRAPDOOR);
364
365 behavior.handle_neighbor_changed(state, &world, pos, &vanilla_blocks::STONE, false);
366 assert!(!world.has_scheduled_fluid_tick(pos, &vanilla_fluids::WATER));
367
368 assert!(world.set_block(
369 power_pos,
370 vanilla_blocks::REDSTONE_BLOCK.default_state(),
371 UpdateFlags::UPDATE_NONE,
372 ));
373 behavior.handle_neighbor_changed(
374 state,
375 &world,
376 pos,
377 &vanilla_blocks::REDSTONE_BLOCK,
378 false,
379 );
380 let powered = world.get_block_state(pos);
381 assert!(powered.get_value(POWERED));
382 assert!(powered.get_value(OPEN));
383 assert!(world.has_scheduled_fluid_tick(pos, &vanilla_fluids::WATER));
384 }
385}