Skip to main content

steel_core/behavior/blocks/redstone/pressure_plate/
standard.rs

1//! Vanilla binary pressure-plate behavior.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty, Direction};
9use steel_registry::sound_event::SoundEventRef;
10use steel_utils::{BlockPos, BlockStateId};
11
12use super::base::BasePressurePlateBlock;
13use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
14use crate::behavior::{BlockBehavior, BlockPlaceContext};
15use crate::entity::{Entity, InsideBlockEffectCollector};
16use crate::world::{LevelReader, ScheduledTickAccess, SignalQueryContext, World};
17
18const PRESSED_TIME: i32 = 20;
19
20/// Vanilla `BlockSetType.PressurePlateSensitivity` values.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum PressurePlateSensitivity {
23    /// Any non-spectating entity that responds to block triggers.
24    Everything,
25    /// Only entities implementing vanilla living-entity behavior.
26    Mobs,
27}
28
29/// Vanilla on/off pressure plates, including wood and stone variants.
30#[block_behavior]
31pub struct PressurePlateBlock {
32    base: BasePressurePlateBlock,
33    #[json_arg(
34        r#enum = "PressurePlateSensitivity",
35        json = "type_pressure_plate_sensitivity"
36    )]
37    sensitivity: PressurePlateSensitivity,
38    #[json_arg(sound_events, json = "type_pressure_plate_click_on")]
39    sound_click_on: SoundEventRef,
40    #[json_arg(sound_events, json = "type_pressure_plate_click_off")]
41    sound_click_off: SoundEventRef,
42}
43
44const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
45
46impl PressurePlateBlock {
47    /// Creates a binary pressure-plate behavior from extracted block-set data.
48    #[must_use]
49    pub const fn new(
50        block: BlockRef,
51        sensitivity: PressurePlateSensitivity,
52        sound_click_on: SoundEventRef,
53        sound_click_off: SoundEventRef,
54    ) -> Self {
55        Self {
56            base: BasePressurePlateBlock::new(block),
57            sensitivity,
58            sound_click_on,
59            sound_click_off,
60        }
61    }
62
63    fn signal_for_state(state: BlockStateId) -> i32 {
64        if state.get_value(POWERED) {
65            MAX_REDSTONE_SIGNAL
66        } else {
67            MIN_REDSTONE_SIGNAL
68        }
69    }
70
71    fn state_for_signal(state: BlockStateId, signal: i32) -> BlockStateId {
72        state.set_value(POWERED, signal > 0)
73    }
74
75    fn signal_strength(&self, world: &World, pos: BlockPos) -> i32 {
76        let count =
77            BasePressurePlateBlock::entity_count(world, pos, |entity| match self.sensitivity {
78                PressurePlateSensitivity::Everything => true,
79                // Class-hierarchy checks stay capability-based: raw fallback
80                // entities become eligible when they gain `LivingEntity` behavior.
81                PressurePlateSensitivity::Mobs => entity.is_living_entity(),
82            });
83        if count > 0 {
84            MAX_REDSTONE_SIGNAL
85        } else {
86            MIN_REDSTONE_SIGNAL
87        }
88    }
89
90    fn check_pressed(
91        &self,
92        source_entity: Option<&dyn Entity>,
93        world: &Arc<World>,
94        pos: BlockPos,
95        state: BlockStateId,
96        old_signal: i32,
97    ) {
98        let signal = self.signal_strength(world.as_ref(), pos);
99        self.base.check_pressed(
100            source_entity,
101            world,
102            pos,
103            old_signal,
104            signal,
105            Self::state_for_signal(state, signal),
106            PRESSED_TIME,
107            self.sound_click_on,
108            self.sound_click_off,
109        );
110    }
111}
112
113impl BlockBehavior for PressurePlateBlock {
114    fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
115        true
116    }
117
118    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
119        BasePressurePlateBlock::can_survive(world, pos)
120    }
121
122    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
123        self.base.state_for_placement(context)
124    }
125
126    fn update_shape(
127        &self,
128        state: BlockStateId,
129        world: &dyn ScheduledTickAccess,
130        pos: BlockPos,
131        direction: Direction,
132        _neighbor_pos: BlockPos,
133        _neighbor_state: BlockStateId,
134    ) -> BlockStateId {
135        BasePressurePlateBlock::update_shape(state, world, pos, direction)
136    }
137
138    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
139        let signal = Self::signal_for_state(state);
140        if signal > 0 {
141            self.check_pressed(None, world, pos, state, signal);
142        }
143    }
144
145    fn entity_inside(
146        &self,
147        state: BlockStateId,
148        world: &Arc<World>,
149        pos: BlockPos,
150        entity: &dyn Entity,
151        _effect_collector: &mut InsideBlockEffectCollector,
152        _is_precise: bool,
153    ) {
154        let signal = Self::signal_for_state(state);
155        if signal == 0 {
156            self.check_pressed(Some(entity), world, pos, state, signal);
157        }
158    }
159
160    fn affect_neighbors_after_removal(
161        &self,
162        state: BlockStateId,
163        world: &Arc<World>,
164        pos: BlockPos,
165        moved_by_piston: bool,
166    ) {
167        self.base.affect_neighbors_after_removal(
168            world,
169            pos,
170            moved_by_piston,
171            Self::signal_for_state(state),
172        );
173    }
174
175    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
176        true
177    }
178
179    fn get_own_signal(
180        &self,
181        state: BlockStateId,
182        _world: &dyn LevelReader,
183        _pos: BlockPos,
184        _context: SignalQueryContext,
185    ) -> i32 {
186        Self::signal_for_state(state)
187    }
188
189    fn get_direct_signal(
190        &self,
191        state: BlockStateId,
192        _world: &dyn LevelReader,
193        _pos: BlockPos,
194        direction: Direction,
195        _context: SignalQueryContext,
196    ) -> i32 {
197        if direction == Direction::Up {
198            Self::signal_for_state(state)
199        } else {
200            0
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks};
208
209    use super::*;
210    use crate::test_support::TestLevel;
211
212    fn stone_pressure_plate() -> PressurePlateBlock {
213        PressurePlateBlock::new(
214            &vanilla_blocks::STONE_PRESSURE_PLATE,
215            PressurePlateSensitivity::Mobs,
216            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_ON,
217            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_OFF,
218        )
219    }
220
221    #[test]
222    fn pressure_plate_survives_on_rigid_or_center_support() {
223        init_vanilla_registry();
224        let behavior = stone_pressure_plate();
225        let pos = BlockPos::new(0, 64, 0);
226        let state = vanilla_blocks::STONE_PRESSURE_PLATE.default_state();
227        let rigid =
228            TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
229        let center =
230            TestLevel::default().with_block(pos.below(), vanilla_blocks::OAK_FENCE.default_state());
231
232        assert!(behavior.can_survive(state, &rigid, pos));
233        assert!(behavior.can_survive(state, &center, pos));
234        assert!(!behavior.can_survive(state, &TestLevel::default(), pos));
235    }
236
237    #[test]
238    fn powered_pressure_plate_strongly_powers_only_upward() {
239        init_vanilla_registry();
240        let behavior = stone_pressure_plate();
241        let state = vanilla_blocks::STONE_PRESSURE_PLATE
242            .default_state()
243            .set_value(POWERED, true);
244        let level = TestLevel::default();
245
246        assert_eq!(
247            behavior.get_own_signal(state, &level, BlockPos::ZERO, SignalQueryContext::DEFAULT,),
248            15
249        );
250        assert_eq!(
251            behavior.get_direct_signal(
252                state,
253                &level,
254                BlockPos::ZERO,
255                Direction::Up,
256                SignalQueryContext::DEFAULT,
257            ),
258            15
259        );
260        assert_eq!(
261            behavior.get_direct_signal(
262                state,
263                &level,
264                BlockPos::ZERO,
265                Direction::North,
266                SignalQueryContext::DEFAULT,
267            ),
268            0
269        );
270    }
271}