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