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