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, 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
28impl PotentSulfurBlock {
29    /// New potent sulfur block behavior
30    #[must_use]
31    pub const fn new(block: BlockRef) -> Self {
32        Self { block }
33    }
34
35    fn valid_state(state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> BlockStateId {
36        let above_fluid = world.get_block_state(pos.above()).get_fluid_state();
37        if !above_fluid.is_source() || !above_fluid.is_water() {
38            return state.set_value(
39                &BlockStateProperties::POTENT_SULFUR_STATE,
40                PotentSulfurState::Dry,
41            );
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(
54                &BlockStateProperties::POTENT_SULFUR_STATE,
55                PotentSulfurState::Continuous,
56            );
57        }
58
59        if below
60            .get_block()
61            .has_tag(&BlockTag::CAUSES_PERIODIC_GEYSER_ERUPTIONS)
62            && fluid_ok
63        {
64            let is_geyser = matches!(
65                state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE),
66                PotentSulfurState::Dormant | PotentSulfurState::Erupting
67            );
68            if !is_geyser
69                && let Some(block_entity) = world.get_block_entity(pos)
70                && let Some(potent_sulfur) = block_entity.downcast_ref::<PotentSulfurBlockEntity>()
71            {
72                potent_sulfur.reset_countdown();
73            }
74
75            if state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE)
76                == PotentSulfurState::Erupting
77            {
78                return state;
79            }
80            return state.set_value(
81                &BlockStateProperties::POTENT_SULFUR_STATE,
82                PotentSulfurState::Dormant,
83            );
84        }
85
86        state.set_value(
87            &BlockStateProperties::POTENT_SULFUR_STATE,
88            PotentSulfurState::Wet,
89        )
90    }
91}
92
93impl BlockBehavior for PotentSulfurBlock {
94    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
95        Some(Self::valid_state(
96            self.block.default_state(),
97            context.world,
98            context.place_pos(),
99        ))
100    }
101
102    fn update_shape(
103        &self,
104        state: BlockStateId,
105        world: &dyn ScheduledTickAccess,
106        pos: BlockPos,
107        _direction: Direction,
108        _neighbor_pos: BlockPos,
109        _neighbor_state: BlockStateId,
110    ) -> BlockStateId {
111        Self::valid_state(state, world, pos)
112    }
113
114    fn on_place(
115        &self,
116        state: BlockStateId,
117        world: &Arc<World>,
118        pos: BlockPos,
119        _old_state: BlockStateId,
120        _moved_by_piston: bool,
121    ) {
122        let current = state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE);
123        if !matches!(
124            current,
125            PotentSulfurState::Erupting | PotentSulfurState::Continuous
126        ) {
127            return;
128        }
129
130        world.block_event(pos, self.block, 0, 0);
131        let sound = if current == PotentSulfurState::Continuous {
132            &sound_events::BLOCK_POTENT_SULFUR_GEYSER_CONTINUOUS_ERUPTION
133        } else {
134            &sound_events::BLOCK_POTENT_SULFUR_GEYSER_ERUPTION
135        };
136        world.play_block_sound(sound, pos, 1.0, 1.0, None);
137        world.game_event(
138            &vanilla_game_events::BLOCK_ACTIVATE,
139            pos,
140            &GameEventContext::new(None, Some(state)),
141        );
142    }
143
144    // TODO: Implement vanilla animateTick once Steel has client-side ambient tick/particle support:
145    // sulfur bubbles above non-dry states and occasional noxious gas ambient sound.
146
147    fn trigger_event(
148        &self,
149        _state: BlockStateId,
150        world: &Arc<World>,
151        pos: BlockPos,
152        _param_a: i32,
153        _param_b: i32,
154    ) -> bool {
155        if let Some(block_entity) = world.get_block_entity(pos)
156            && let Some(sulfur) = block_entity.downcast_ref::<PotentSulfurBlockEntity>()
157        {
158            sulfur.set_eruption_tick(world.game_time());
159        }
160        true
161    }
162
163    fn new_block_entity(
164        &self,
165        level: Weak<World>,
166        pos: BlockPos,
167        state: BlockStateId,
168    ) -> BlockEntityCreation {
169        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
170            &vanilla_block_entity_types::POTENT_SULFUR,
171            level,
172            pos,
173            state,
174        ))
175    }
176
177    fn get_block_entity_ticker(
178        &self,
179        _world: &Arc<World>,
180        state: BlockStateId,
181        block_entity_type: BlockEntityTypeRef,
182    ) -> Option<BlockEntityTicker> {
183        if state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE) == PotentSulfurState::Dry {
184            return None;
185        }
186        BlockEntityTicker::for_matching_entity_tick(
187            block_entity_type,
188            &vanilla_block_entity_types::POTENT_SULFUR,
189        )
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use steel_registry::init_vanilla_registry;
196    use steel_registry::vanilla_blocks;
197
198    use super::*;
199    use crate::test_support::fresh_test_world;
200
201    #[test]
202    fn potent_sulfur_ticker_selection_matches_live_geyser_state() {
203        init_vanilla_registry();
204        let world = fresh_test_world("potent_sulfur_ticker");
205        let behavior = PotentSulfurBlock::new(&vanilla_blocks::POTENT_SULFUR);
206        let base = vanilla_blocks::POTENT_SULFUR.default_state();
207
208        let dry = base.set_value(
209            &BlockStateProperties::POTENT_SULFUR_STATE,
210            PotentSulfurState::Dry,
211        );
212        assert!(
213            behavior
214                .get_block_entity_ticker(&world, dry, &vanilla_block_entity_types::POTENT_SULFUR,)
215                .is_none()
216        );
217
218        let wet = base.set_value(
219            &BlockStateProperties::POTENT_SULFUR_STATE,
220            PotentSulfurState::Wet,
221        );
222
223        for sulfur_state in [
224            PotentSulfurState::Wet,
225            PotentSulfurState::Dormant,
226            PotentSulfurState::Erupting,
227            PotentSulfurState::Continuous,
228        ] {
229            let state = base.set_value(&BlockStateProperties::POTENT_SULFUR_STATE, sulfur_state);
230            assert!(
231                behavior
232                    .get_block_entity_ticker(
233                        &world,
234                        state,
235                        &vanilla_block_entity_types::POTENT_SULFUR,
236                    )
237                    .is_some()
238            );
239        }
240
241        assert!(
242            behavior
243                .get_block_entity_ticker(&world, wet, &vanilla_block_entity_types::CHEST)
244                .is_none()
245        );
246    }
247}