Skip to main content

steel_core/behavior/blocks/redstone/
copper_bulb_block.rs

1//! Vanilla copper-bulb edge-triggered 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;
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId};
12
13use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
14use crate::behavior::blocks::{WeatherState, WeatheringCopper};
15use crate::behavior::{BlockBehavior, BlockPlaceContext};
16use crate::world::{LevelReader, SignalGetter as _, World};
17
18/// Vanilla `CopperBulbBlock`, used directly by waxed bulb variants.
19#[block_behavior]
20pub struct CopperBulbBlock {
21    block: BlockRef,
22}
23
24const LIT: &BoolProperty = &BlockStateProperties::LIT;
25const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
26
27impl CopperBulbBlock {
28    /// Creates copper-bulb behavior.
29    #[must_use]
30    pub const fn new(block: BlockRef) -> Self {
31        Self { block }
32    }
33
34    fn check_and_flip(state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
35        let signal = world.has_neighbor_signal(pos);
36        let powered = state.get_value(POWERED);
37        if signal == powered {
38            return;
39        }
40
41        let mut new_state = state;
42        if !powered {
43            let lit = !state.get_value(LIT);
44            new_state = new_state.set_value(LIT, lit);
45            world.play_block_sound(
46                if lit {
47                    &sound_events::BLOCK_COPPER_BULB_TURN_ON
48                } else {
49                    &sound_events::BLOCK_COPPER_BULB_TURN_OFF
50                },
51                pos,
52                1.0,
53                1.0,
54                None,
55            );
56        }
57        world.set_block(
58            pos,
59            new_state.set_value(POWERED, signal),
60            UpdateFlags::UPDATE_ALL,
61        );
62    }
63
64    fn placed(state: BlockStateId, world: &Arc<World>, pos: BlockPos, old_state: BlockStateId) {
65        if old_state.get_block() != state.get_block() {
66            Self::check_and_flip(state, world, pos);
67        }
68    }
69
70    fn analog_output(world: &dyn LevelReader, pos: BlockPos) -> i32 {
71        if world.get_block_state(pos).get_value(LIT) {
72            MAX_REDSTONE_SIGNAL
73        } else {
74            MIN_REDSTONE_SIGNAL
75        }
76    }
77}
78
79impl BlockBehavior for CopperBulbBlock {
80    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
81        Some(self.block.default_state())
82    }
83
84    fn on_place(
85        &self,
86        state: BlockStateId,
87        world: &Arc<World>,
88        pos: BlockPos,
89        old_state: BlockStateId,
90        _moved_by_piston: bool,
91    ) {
92        Self::placed(state, world, pos, old_state);
93    }
94
95    fn handle_neighbor_changed(
96        &self,
97        state: BlockStateId,
98        world: &Arc<World>,
99        pos: BlockPos,
100        _source_block: BlockRef,
101        _moved_by_piston: bool,
102    ) {
103        Self::check_and_flip(state, world, pos);
104    }
105
106    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
107        true
108    }
109
110    fn get_analog_output_signal(
111        &self,
112        _state: BlockStateId,
113        world: &dyn LevelReader,
114        pos: BlockPos,
115        _direction: Direction,
116    ) -> i32 {
117        Self::analog_output(world, pos)
118    }
119}
120
121/// Unwaxed copper bulbs with vanilla oxidation behavior.
122#[block_behavior]
123pub struct WeatheringCopperBulbBlock {
124    bulb: CopperBulbBlock,
125    #[json_arg(r#enum = "WeatherState", json = "weather_state")]
126    weathering: WeatheringCopper,
127}
128
129impl WeatheringCopperBulbBlock {
130    /// Creates a weathering copper-bulb behavior.
131    #[must_use]
132    pub const fn new(block: BlockRef, weather_state: WeatherState) -> Self {
133        Self {
134            bulb: CopperBulbBlock::new(block),
135            weathering: WeatheringCopper::new(weather_state),
136        }
137    }
138}
139
140impl BlockBehavior for WeatheringCopperBulbBlock {
141    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
142        self.bulb.get_state_for_placement(context)
143    }
144
145    fn on_place(
146        &self,
147        state: BlockStateId,
148        world: &Arc<World>,
149        pos: BlockPos,
150        old_state: BlockStateId,
151        _moved_by_piston: bool,
152    ) {
153        CopperBulbBlock::placed(state, world, pos, old_state);
154    }
155
156    fn handle_neighbor_changed(
157        &self,
158        state: BlockStateId,
159        world: &Arc<World>,
160        pos: BlockPos,
161        _source_block: BlockRef,
162        _moved_by_piston: bool,
163    ) {
164        CopperBulbBlock::check_and_flip(state, world, pos);
165    }
166
167    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
168        self.weathering.change_over_time(state, world, pos);
169    }
170
171    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
172        true
173    }
174
175    fn get_analog_output_signal(
176        &self,
177        _state: BlockStateId,
178        world: &dyn LevelReader,
179        pos: BlockPos,
180        _direction: Direction,
181    ) -> i32 {
182        CopperBulbBlock::analog_output(world, pos)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use steel_registry::init_vanilla_registry;
189    use steel_registry::vanilla_blocks;
190    use steel_utils::ChunkPos;
191
192    use super::*;
193    use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
194    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
195
196    #[test]
197    fn bulb_toggles_lit_only_on_rising_edges() {
198        init_vanilla_registry();
199        init_behaviors();
200        let world = fresh_test_world("copper_bulb_edges");
201        let pos = BlockPos::new(8, 64, 8);
202        let power_pos = pos.west();
203        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
204        assert!(world.set_block(
205            power_pos,
206            vanilla_blocks::REDSTONE_BLOCK.default_state(),
207            UpdateFlags::UPDATE_NONE,
208        ));
209        assert!(world.set_block(
210            pos,
211            vanilla_blocks::WAXED_COPPER_BULB.default_state(),
212            UpdateFlags::UPDATE_ALL,
213        ));
214
215        let first_rise = world.get_block_state(pos);
216        assert!(first_rise.get_value(POWERED));
217        assert!(first_rise.get_value(LIT));
218        let behavior = BLOCK_BEHAVIORS.get_behavior(first_rise.get_block());
219        assert_eq!(
220            behavior.get_analog_output_signal(first_rise, &world, pos, Direction::North),
221            15
222        );
223
224        assert!(world.remove_block(power_pos, false));
225        let falling = world.get_block_state(pos);
226        assert!(!falling.get_value(POWERED));
227        assert!(falling.get_value(LIT));
228
229        assert!(world.set_block(
230            power_pos,
231            vanilla_blocks::REDSTONE_BLOCK.default_state(),
232            UpdateFlags::UPDATE_ALL,
233        ));
234        let second_rise = world.get_block_state(pos);
235        assert!(second_rise.get_value(POWERED));
236        assert!(!second_rise.get_value(LIT));
237    }
238}