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