steel_core/behavior/blocks/building/
cauldron_block.rs1use 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#[block_behavior]
28pub struct CauldronBlock {
29 block: BlockRef,
30}
31
32const LEVEL_CAULDRON: &IntProperty = &BlockStateProperties::LEVEL_CAULDRON;
33
34impl CauldronBlock {
35 #[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#[block_behavior]
105pub struct LayeredCauldronBlock {
106 block: BlockRef,
107}
108
109impl LayeredCauldronBlock {
110 #[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}