Skip to main content

steel_core/behavior/blocks/redstone/
lever_block.rs

1//! Vanilla lever 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_events, vanilla_game_events};
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId};
12
13use crate::behavior::blocks::face_attached_horizontal_directional_block::FaceAttachedHorizontalDirectionalBlock;
14use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
15use crate::behavior::{
16    BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
17};
18use crate::player::Player;
19use crate::world::game_event::GameEventContext;
20use crate::world::{LevelAccessor, LevelReader, ScheduledTickAccess, SignalQueryContext, World};
21
22/// Vanilla `LeverBlock` source behavior.
23#[block_behavior]
24pub struct LeverBlock {
25    face_attached: FaceAttachedHorizontalDirectionalBlock,
26}
27
28const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
29
30impl LeverBlock {
31    /// Creates lever behavior for `block`.
32    #[must_use]
33    pub const fn new(block: BlockRef) -> Self {
34        Self {
35            face_attached: FaceAttachedHorizontalDirectionalBlock::new(block),
36        }
37    }
38
39    fn update_neighbors(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
40        let support_direction =
41            FaceAttachedHorizontalDirectionalBlock::connected_direction(state).opposite();
42        world.update_neighbors_at(pos, self.face_attached.block);
43        world.update_neighbors_at(pos.relative(support_direction), self.face_attached.block);
44    }
45
46    fn pull(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
47        let powered = !state.get_value(POWERED);
48        let next_state = state.set_value(POWERED, powered);
49        world.set_block(pos, next_state, UpdateFlags::UPDATE_ALL);
50        self.update_neighbors(next_state, world, pos);
51        Self::emit_transition_effects(world, pos, powered);
52    }
53
54    fn emit_transition_effects(level: &dyn LevelAccessor, pos: BlockPos, powered: bool) {
55        level.play_block_sound(
56            &sound_events::BLOCK_LEVER_CLICK,
57            pos,
58            0.3,
59            if powered { 0.6 } else { 0.5 },
60            None,
61        );
62        level.game_event(
63            if powered {
64                &vanilla_game_events::BLOCK_ACTIVATE
65            } else {
66                &vanilla_game_events::BLOCK_DEACTIVATE
67            },
68            pos,
69            &GameEventContext::default(),
70        );
71    }
72}
73
74impl BlockBehavior for LeverBlock {
75    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
76        FaceAttachedHorizontalDirectionalBlock::can_survive(state, world, pos)
77    }
78
79    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
80        self.face_attached.state_for_placement(context)
81    }
82
83    fn update_shape(
84        &self,
85        state: BlockStateId,
86        world: &dyn ScheduledTickAccess,
87        pos: BlockPos,
88        direction: Direction,
89        _neighbor_pos: BlockPos,
90        _neighbor_state: BlockStateId,
91    ) -> BlockStateId {
92        FaceAttachedHorizontalDirectionalBlock::update_shape(state, world, pos, direction)
93    }
94
95    fn use_without_item(
96        &self,
97        state: BlockStateId,
98        world: &Arc<World>,
99        pos: BlockPos,
100        _player: &Player,
101        _hit_result: &BlockHitResult,
102        _inv: &mut InventoryAccess,
103    ) -> InteractionResult {
104        self.pull(state, world, pos);
105        InteractionResult::Success
106    }
107
108    fn affect_neighbors_after_removal(
109        &self,
110        state: BlockStateId,
111        world: &Arc<World>,
112        pos: BlockPos,
113        moved_by_piston: bool,
114    ) {
115        if !moved_by_piston && state.get_value(POWERED) {
116            self.update_neighbors(state, world, pos);
117        }
118    }
119
120    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
121        true
122    }
123
124    fn get_own_signal(
125        &self,
126        state: BlockStateId,
127        _world: &dyn LevelReader,
128        _pos: BlockPos,
129        _context: SignalQueryContext,
130    ) -> i32 {
131        if state.get_value(POWERED) {
132            MAX_REDSTONE_SIGNAL
133        } else {
134            MIN_REDSTONE_SIGNAL
135        }
136    }
137
138    fn get_direct_signal(
139        &self,
140        state: BlockStateId,
141        _world: &dyn LevelReader,
142        _pos: BlockPos,
143        direction: Direction,
144        _context: SignalQueryContext,
145    ) -> i32 {
146        if state.get_value(POWERED)
147            && FaceAttachedHorizontalDirectionalBlock::connected_direction(state) == direction
148        {
149            MAX_REDSTONE_SIGNAL
150        } else {
151            MIN_REDSTONE_SIGNAL
152        }
153    }
154
155    // Client-local interaction/ambient dust particles are omitted. Explosion
156    // toggling awaits Steel's shared block-explosion callback foundation.
157}
158
159#[cfg(test)]
160mod tests {
161    use steel_registry::blocks::properties::{AttachFace, EnumProperty};
162    use steel_registry::init_vanilla_registry;
163    use steel_registry::{sound_events, vanilla_blocks, vanilla_game_events};
164
165    use super::*;
166    use crate::test_support::TestLevel;
167
168    const ATTACH_FACE: &EnumProperty<AttachFace> = &BlockStateProperties::ATTACH_FACE;
169    const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
170
171    fn lever_state(facing: Direction, face: AttachFace, powered: bool) -> BlockStateId {
172        vanilla_blocks::LEVER
173            .default_state()
174            .set_value(HORIZONTAL_FACING, facing)
175            .set_value(ATTACH_FACE, face)
176            .set_value(POWERED, powered)
177    }
178
179    #[test]
180    fn wall_lever_survives_only_with_its_backing_face() {
181        init_vanilla_registry();
182        let behavior = LeverBlock::new(&vanilla_blocks::LEVER);
183        let pos = BlockPos::new(0, 64, 0);
184        let state = lever_state(Direction::East, AttachFace::Wall, false);
185        let supported =
186            TestLevel::default().with_block(pos.west(), vanilla_blocks::STONE.default_state());
187
188        assert!(behavior.can_survive(state, &supported, pos));
189        assert!(!behavior.can_survive(state, &TestLevel::default(), pos));
190    }
191
192    #[test]
193    fn powered_lever_strongly_powers_only_away_from_support() {
194        init_vanilla_registry();
195        let behavior = LeverBlock::new(&vanilla_blocks::LEVER);
196        let state = lever_state(Direction::East, AttachFace::Wall, true);
197        let level = TestLevel::default();
198        let pos = BlockPos::new(0, 64, 0);
199
200        assert_eq!(
201            behavior.get_own_signal(state, &level, pos, SignalQueryContext::DEFAULT),
202            15
203        );
204        assert_eq!(
205            behavior.get_direct_signal(
206                state,
207                &level,
208                pos,
209                Direction::East,
210                SignalQueryContext::DEFAULT,
211            ),
212            15
213        );
214        assert_eq!(
215            behavior.get_direct_signal(
216                state,
217                &level,
218                pos,
219                Direction::West,
220                SignalQueryContext::DEFAULT,
221            ),
222            0
223        );
224    }
225
226    #[test]
227    fn redstone_transition_side_effects_match_vanilla_for_lever() {
228        init_vanilla_registry();
229        let level = TestLevel::default();
230        let pos = BlockPos::new(3, 64, -2);
231
232        LeverBlock::emit_transition_effects(&level, pos, true);
233        LeverBlock::emit_transition_effects(&level, pos, false);
234
235        let sounds = level.block_sounds.borrow();
236        assert_eq!(sounds.len(), 2);
237        assert_eq!(sounds[0].sound, &sound_events::BLOCK_LEVER_CLICK);
238        assert_eq!(sounds[0].pitch.to_bits(), 0.6_f32.to_bits());
239        assert_eq!(sounds[0].exclude, None);
240        assert_eq!(sounds[1].pitch.to_bits(), 0.5_f32.to_bits());
241        assert_eq!(sounds[1].exclude, None);
242
243        let events = level.game_events.borrow();
244        assert_eq!(events.len(), 2);
245        assert_eq!(events[0].event, &vanilla_game_events::BLOCK_ACTIVATE);
246        assert_eq!(events[0].source_entity_id, None);
247        assert_eq!(events[1].event, &vanilla_game_events::BLOCK_DEACTIVATE);
248        assert_eq!(events[1].source_entity_id, None);
249    }
250}