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