Skip to main content

steel_core/behavior/blocks/building/
cauldron_block.rs

1//! State-driven cauldron behavior needed by comparators.
2//!
3//! Cauldron item interactions and drip filling remain separate block mechanics;
4//! this module implements the complete vanilla analog-output contract shared by
5//! empty, water, and powder-snow cauldrons.
6
7use std::sync::Arc;
8
9use steel_macros::block_behavior;
10use steel_registry::{
11    blocks::{
12        BlockRef,
13        block_state_ext::BlockStateExt as _,
14        properties::{BlockStateProperties, Direction},
15    },
16    level_events, vanilla_blocks, vanilla_fluids, vanilla_game_events,
17};
18use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
19
20use crate::{
21    behavior::{BlockBehavior, BlockPlaceContext, blocks::vegetation},
22    entity::ai::path::PathComputationType,
23    world::{LevelReader, World, game_event::GameEventContext},
24};
25
26/// Vanilla empty cauldron behavior.
27#[block_behavior]
28pub struct CauldronBlock {
29    block: BlockRef,
30}
31
32impl CauldronBlock {
33    /// Creates empty cauldron behavior.
34    #[must_use]
35    pub const fn new(block: BlockRef) -> Self {
36        Self { block }
37    }
38}
39
40impl BlockBehavior for CauldronBlock {
41    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
42        Some(self.block.default_state())
43    }
44
45    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
46        true
47    }
48
49    fn get_analog_output_signal(
50        &self,
51        _state: BlockStateId,
52        _world: &dyn LevelReader,
53        _pos: BlockPos,
54        _direction: Direction,
55    ) -> i32 {
56        0
57    }
58
59    fn is_pathfindable(
60        &self,
61        _state: BlockStateId,
62        _computation_type: PathComputationType,
63    ) -> bool {
64        false
65    }
66
67    fn tick(&self, _state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
68        let Some(stalactite_pos) =
69            vegetation::find_stalactite_tip_above_cauldron(world.as_ref(), pos)
70        else {
71            return;
72        };
73        let Some(fluid) = vegetation::get_cauldron_fill_fluid_type(world, stalactite_pos) else {
74            return;
75        };
76
77        if fluid == &vanilla_fluids::WATER {
78            let new_state = vanilla_blocks::WATER_CAULDRON
79                .default_state()
80                .set_value(&BlockStateProperties::LEVEL_CAULDRON, 1);
81            world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
82            world.game_event(
83                &vanilla_game_events::BLOCK_CHANGE,
84                pos,
85                &GameEventContext::new(None, Some(new_state)),
86            );
87            world.level_event(level_events::SOUND_DRIP_WATER_INTO_CAULDRON, pos, 0, None);
88        } else if fluid == &vanilla_fluids::LAVA {
89            let new_state = vanilla_blocks::LAVA_CAULDRON.default_state();
90            world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
91            world.game_event(
92                &vanilla_game_events::BLOCK_CHANGE,
93                pos,
94                &GameEventContext::new(None, Some(new_state)),
95            );
96            world.level_event(level_events::SOUND_DRIP_LAVA_INTO_CAULDRON, pos, 0, None);
97        }
98    }
99}
100
101/// Vanilla layered water and powder-snow cauldron behavior.
102#[block_behavior]
103pub struct LayeredCauldronBlock {
104    block: BlockRef,
105}
106
107impl LayeredCauldronBlock {
108    /// Creates layered cauldron behavior.
109    #[must_use]
110    pub const fn new(block: BlockRef) -> Self {
111        Self { block }
112    }
113}
114
115impl BlockBehavior for LayeredCauldronBlock {
116    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
117        Some(self.block.default_state())
118    }
119
120    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
121        true
122    }
123
124    fn get_analog_output_signal(
125        &self,
126        state: BlockStateId,
127        _world: &dyn LevelReader,
128        _pos: BlockPos,
129        _direction: Direction,
130    ) -> i32 {
131        i32::from(state.get_value(&BlockStateProperties::LEVEL_CAULDRON))
132    }
133
134    fn is_pathfindable(
135        &self,
136        _state: BlockStateId,
137        _computation_type: PathComputationType,
138    ) -> bool {
139        false
140    }
141
142    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
143        let Some(stalactite_pos) =
144            vegetation::find_stalactite_tip_above_cauldron(world.as_ref(), pos)
145        else {
146            return;
147        };
148        let Some(fluid) = vegetation::get_cauldron_fill_fluid_type(world, stalactite_pos) else {
149            return;
150        };
151
152        if fluid == &vanilla_fluids::WATER {
153            let level = state.get_value(&BlockStateProperties::LEVEL_CAULDRON);
154            if level < BlockStateProperties::LEVEL_CAULDRON.max {
155                let new_state = state.set_value(&BlockStateProperties::LEVEL_CAULDRON, level + 1);
156                world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
157                world.game_event(
158                    &vanilla_game_events::BLOCK_CHANGE,
159                    pos,
160                    &GameEventContext::new(None, Some(new_state)),
161                );
162                world.level_event(level_events::SOUND_DRIP_WATER_INTO_CAULDRON, pos, 0, None);
163            }
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use steel_registry::{init_vanilla_registry, vanilla_blocks};
171
172    use super::*;
173    use crate::{
174        behavior::{BLOCK_BEHAVIORS, init_behaviors},
175        test_support::TestLevel,
176    };
177
178    #[test]
179    fn registered_cauldron_behaviors_expose_vanilla_fill_levels() {
180        init_vanilla_registry();
181        init_behaviors();
182        let level = TestLevel::default();
183        let pos = BlockPos::ZERO;
184
185        let empty = vanilla_blocks::CAULDRON.default_state();
186        let empty_behavior = BLOCK_BEHAVIORS.get_behavior(empty.get_block());
187        assert!(empty_behavior.has_analog_output_signal(empty));
188        assert_eq!(
189            empty_behavior.get_analog_output_signal(empty, &level, pos, Direction::North),
190            0,
191        );
192
193        for level_value in 1..=3 {
194            let state = vanilla_blocks::WATER_CAULDRON
195                .default_state()
196                .set_value(&BlockStateProperties::LEVEL_CAULDRON, level_value);
197            let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
198            assert_eq!(
199                behavior.get_analog_output_signal(state, &level, pos, Direction::North),
200                i32::from(level_value),
201            );
202        }
203    }
204}