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, 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#[block_behavior]
24pub struct LeverBlock {
25 face_attached: FaceAttachedHorizontalDirectionalBlock,
26}
27
28const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
29
30impl LeverBlock {
31 #[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 }
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}