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        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/// 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/// Behavior for vanilla copper trapdoor blocks.
40#[block_behavior]
41pub struct WeatheringCopperTrapDoorBlock {
42    block: BlockRef,
43    /// Weathering state of the block
44    #[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    /// Creates a new trapdoor block behavior.
62    #[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    /// Creates a new copper trapdoor behavior.
223    #[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}