steel_core/behavior/blocks/redstone/
lever_block.rs1use 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#[block_behavior]
23pub struct LeverBlock {
24 face_attached: FaceAttachedHorizontalDirectionalBlock,
25}
26
27impl LeverBlock {
28 #[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 }
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}