Skip to main content

steel_core/behavior/blocks/building/
trapdoor_block.rs

1//! Trapdoor block behavior implementation.
2//!
3//! Trapdoors react to neighboring redstone signal sources and conductors.
4
5use 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/// Behavior for vanilla trapdoor blocks.
29#[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/// Behavior for vanilla copper trapdoor blocks.
47#[block_behavior]
48pub struct WeatheringCopperTrapDoorBlock {
49    block: BlockRef,
50    /// Weathering state of the block
51    #[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    /// Creates a new trapdoor block behavior.
63    #[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    /// Creates a new copper trapdoor behavior.
219    #[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}