Skip to main content

steel_core/behavior/blocks/building/
potent_sulfur_block.rs

1//! `PotentSulfurBlock` behavior
2
3use std::sync::{Arc, Weak};
4
5use steel_macros::block_behavior;
6use steel_registry::block_entity_type::BlockEntityTypeRef;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt as _;
9use steel_registry::blocks::properties::{BlockStateProperties, EnumProperty, PotentSulfurState};
10use steel_registry::sound_events;
11use steel_registry::vanilla_block_entity_types;
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::vanilla_game_events;
14use steel_utils::{BlockPos, BlockStateId, Direction, Downcast as _};
15
16use crate::behavior::{BlockBehavior, BlockEntityCreation, BlockPlaceContext};
17use crate::block_entity::entities::PotentSulfurBlockEntity;
18use crate::block_entity::{BLOCK_ENTITIES, BlockEntityTicker};
19use crate::fluid::FluidStateExt as _;
20use crate::world::{LevelReader, ScheduledTickAccess, World, game_event::GameEventContext};
21
22/// Vanilla `PotentSulfurBlock` behavior
23#[block_behavior]
24pub struct PotentSulfurBlock {
25    block: BlockRef,
26}
27
28const POTENT_SULFUR_STATE: &EnumProperty<PotentSulfurState> =
29    &BlockStateProperties::POTENT_SULFUR_STATE;
30
31impl PotentSulfurBlock {
32    /// New potent sulfur block behavior
33    #[must_use]
34    pub const fn new(block: BlockRef) -> Self {
35        Self { block }
36    }
37
38    fn valid_state(state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> BlockStateId {
39        let above_fluid = world.get_block_state(pos.above()).get_fluid_state();
40        if !above_fluid.is_source() || !above_fluid.is_water() {
41            return state.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Dry);
42        }
43
44        let below = world.get_block_state(pos.below());
45        let below_fluid = below.get_fluid_state();
46        let fluid_ok = below_fluid.is_empty() || below_fluid.is_source();
47
48        if below
49            .get_block()
50            .has_tag(&BlockTag::CAUSES_CONTINUOUS_GEYSER_ERUPTIONS)
51            && fluid_ok
52        {
53            return state.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Continuous);
54        }
55
56        if below
57            .get_block()
58            .has_tag(&BlockTag::CAUSES_PERIODIC_GEYSER_ERUPTIONS)
59            && fluid_ok
60        {
61            let is_geyser = matches!(
62                state.get_value(POTENT_SULFUR_STATE),
63                PotentSulfurState::Dormant | PotentSulfurState::Erupting
64            );
65            if !is_geyser
66                && let Some(block_entity) = world.get_block_entity(pos)
67                && let Some(potent_sulfur) = block_entity.downcast_ref::<PotentSulfurBlockEntity>()
68            {
69                potent_sulfur.reset_countdown();
70            }
71
72            if state.get_value(POTENT_SULFUR_STATE) == PotentSulfurState::Erupting {
73                return state;
74            }
75            return state.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Dormant);
76        }
77
78        state.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Wet)
79    }
80}
81
82impl BlockBehavior for PotentSulfurBlock {
83    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
84        Some(Self::valid_state(
85            self.block.default_state(),
86            context.world,
87            context.place_pos(),
88        ))
89    }
90
91    fn update_shape(
92        &self,
93        state: BlockStateId,
94        world: &dyn ScheduledTickAccess,
95        pos: BlockPos,
96        _direction: Direction,
97        _neighbor_pos: BlockPos,
98        _neighbor_state: BlockStateId,
99    ) -> BlockStateId {
100        Self::valid_state(state, world, pos)
101    }
102
103    fn on_place(
104        &self,
105        state: BlockStateId,
106        world: &Arc<World>,
107        pos: BlockPos,
108        _old_state: BlockStateId,
109        _moved_by_piston: bool,
110    ) {
111        let current = state.get_value(POTENT_SULFUR_STATE);
112        if !matches!(
113            current,
114            PotentSulfurState::Erupting | PotentSulfurState::Continuous
115        ) {
116            return;
117        }
118
119        world.block_event(pos, self.block, 0, 0);
120        let sound = if current == PotentSulfurState::Continuous {
121            &sound_events::BLOCK_POTENT_SULFUR_GEYSER_CONTINUOUS_ERUPTION
122        } else {
123            &sound_events::BLOCK_POTENT_SULFUR_GEYSER_ERUPTION
124        };
125        world.play_block_sound(sound, pos, 1.0, 1.0, None);
126        world.game_event(
127            &vanilla_game_events::BLOCK_ACTIVATE,
128            pos,
129            &GameEventContext::new(None, Some(state)),
130        );
131    }
132
133    // `animateTick` only creates client-local dust particles; the server has no
134    // corresponding work to perform.
135
136    fn trigger_event(
137        &self,
138        _state: BlockStateId,
139        world: &Arc<World>,
140        pos: BlockPos,
141        _param_a: i32,
142        _param_b: i32,
143    ) -> bool {
144        if let Some(block_entity) = world.get_block_entity(pos)
145            && let Some(sulfur) = block_entity.downcast_ref::<PotentSulfurBlockEntity>()
146        {
147            sulfur.set_eruption_tick(world.game_time());
148        }
149        true
150    }
151
152    fn new_block_entity(
153        &self,
154        level: Weak<World>,
155        pos: BlockPos,
156        state: BlockStateId,
157    ) -> BlockEntityCreation {
158        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
159            &vanilla_block_entity_types::POTENT_SULFUR,
160            level,
161            pos,
162            state,
163        ))
164    }
165
166    fn get_block_entity_ticker(
167        &self,
168        _world: &Arc<World>,
169        state: BlockStateId,
170        block_entity_type: BlockEntityTypeRef,
171    ) -> Option<BlockEntityTicker> {
172        if state.get_value(POTENT_SULFUR_STATE) == PotentSulfurState::Dry {
173            return None;
174        }
175        BlockEntityTicker::for_matching_entity_tick(
176            block_entity_type,
177            &vanilla_block_entity_types::POTENT_SULFUR,
178        )
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use steel_registry::init_vanilla_registry;
185    use steel_registry::vanilla_blocks;
186
187    use super::*;
188    use crate::test_support::fresh_test_world;
189
190    #[test]
191    fn potent_sulfur_ticker_selection_matches_live_geyser_state() {
192        init_vanilla_registry();
193        let world = fresh_test_world("potent_sulfur_ticker");
194        let behavior = PotentSulfurBlock::new(&vanilla_blocks::POTENT_SULFUR);
195        let base = vanilla_blocks::POTENT_SULFUR.default_state();
196
197        let dry = base.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Dry);
198        assert!(
199            behavior
200                .get_block_entity_ticker(&world, dry, &vanilla_block_entity_types::POTENT_SULFUR,)
201                .is_none()
202        );
203
204        let wet = base.set_value(POTENT_SULFUR_STATE, PotentSulfurState::Wet);
205
206        for sulfur_state in [
207            PotentSulfurState::Wet,
208            PotentSulfurState::Dormant,
209            PotentSulfurState::Erupting,
210            PotentSulfurState::Continuous,
211        ] {
212            let state = base.set_value(POTENT_SULFUR_STATE, sulfur_state);
213            assert!(
214                behavior
215                    .get_block_entity_ticker(
216                        &world,
217                        state,
218                        &vanilla_block_entity_types::POTENT_SULFUR,
219                    )
220                    .is_some()
221            );
222        }
223
224        assert!(
225            behavior
226                .get_block_entity_ticker(&world, wet, &vanilla_block_entity_types::CHEST)
227                .is_none()
228        );
229    }
230}