steel_core/behavior/blocks/decoration/
candle_block.rs1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::{
5 REGISTRY,
6 blocks::{
7 BlockRef,
8 block_state_ext::BlockStateExt,
9 properties::{BlockStateProperties, BoolProperty, IntProperty},
10 shapes::SupportType,
11 },
12 entity_data::Direction,
13 fluid::FluidState,
14 items::item::BlockHitResult,
15 sound_events, vanilla_blocks, vanilla_fluids, vanilla_game_events,
16};
17use steel_utils::{
18 BlockPos,
19 types::{self, UpdateFlags},
20};
21
22use crate::{
23 behavior::{
24 BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
25 block::{schedule_placed_liquid_tick, schedule_water_tick_if_waterlogged},
26 },
27 entity::projectile::Projectile,
28 player,
29 world::{
30 ClipHitResult, LevelAccessor, LevelReader, ScheduledTickAccess, World,
31 game_event::GameEventContext,
32 },
33};
34
35const CANDLES_PROPERTY: &IntProperty = &BlockStateProperties::CANDLES;
36const LIT_PROPERTY: &BoolProperty = &BlockStateProperties::LIT;
37const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
38const MAX_CANDLES: u8 = 4;
39
40#[block_behavior]
42pub struct CandleBlock {
43 block: BlockRef,
44}
45
46impl CandleBlock {
47 #[must_use]
49 pub const fn new(block: BlockRef) -> Self {
50 Self { block }
51 }
52
53 pub(super) fn projectile_lit_state(
54 state: steel_utils::BlockStateId,
55 projectile_is_on_fire: bool,
56 ) -> Option<steel_utils::BlockStateId> {
57 (projectile_is_on_fire
58 && state.try_get_value(WATERLOGGED) != Some(true)
59 && !state.get_value(LIT_PROPERTY))
60 .then(|| state.set_value(LIT_PROPERTY, true))
61 }
62}
63
64impl BlockBehavior for CandleBlock {
65 fn can_survive(
67 &self,
68 _state: steel_utils::BlockStateId,
69 world: &dyn LevelReader,
70 pos: BlockPos,
71 ) -> bool {
72 let below_pos = pos.below();
73 world.is_face_sturdy_for(
74 world.get_block_state(below_pos),
75 below_pos,
76 Direction::Up,
77 SupportType::Center,
78 )
79 }
80
81 fn get_state_for_placement(
82 &self,
83 context: &BlockPlaceContext<'_>,
84 ) -> Option<steel_utils::BlockStateId> {
85 let default_state = self.block.default_state();
86 if self.can_survive(default_state, context.world, context.place_pos()) {
87 return Some(default_state.set_value(WATERLOGGED, context.is_water_source()));
88 }
89 None
90 }
91
92 fn update_shape(
93 &self,
94 state: steel_utils::BlockStateId,
95 world: &dyn ScheduledTickAccess,
96 pos: BlockPos,
97 _direction: Direction,
98 _neighbor_pos: BlockPos,
99 _neighbor_state: steel_utils::BlockStateId,
100 ) -> steel_utils::BlockStateId {
101 schedule_water_tick_if_waterlogged(state, world, pos);
102
103 if !self.can_survive(state, world, pos) {
104 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
105 }
106 state
107 }
108
109 fn on_projectile_hit(
110 &self,
111 state: steel_utils::BlockStateId,
112 world: &Arc<World>,
113 hit: &ClipHitResult,
114 projectile: &dyn Projectile,
115 ) {
116 let Some(lit_state) = Self::projectile_lit_state(state, projectile.is_on_fire()) else {
117 return;
118 };
119 world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
120 }
121
122 fn use_item_on(
123 &self,
124 state: steel_utils::BlockStateId,
125 world: &Arc<World>,
126 pos: BlockPos,
127 _player: &player::Player,
128 _hand: types::InteractionHand,
129 _hit_result: &BlockHitResult,
130 inv: &mut InventoryAccess,
131 ) -> InteractionResult {
132 let item_is_empty = inv.with_item(|item_stack| item_stack.is_empty());
133 if item_is_empty {
134 if !state.get_value(LIT_PROPERTY) {
135 return InteractionResult::Pass;
136 }
137 let new_state = state.set_value(LIT_PROPERTY, false);
138 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
139 return InteractionResult::Success;
140 }
141
142 if self
143 .get_clone_item_stack(self.block, state, false)
144 .is_some_and(|it| inv.with_item(|item_stack| it.is(item_stack.item)))
145 {
146 let candles_amount = state.get_value(CANDLES_PROPERTY);
147 if candles_amount < MAX_CANDLES {
148 let new_state = state.set_value(CANDLES_PROPERTY, candles_amount + 1);
149 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
150 return InteractionResult::Success;
151 }
152 }
153
154 InteractionResult::TryEmptyHandInteraction
155 }
156
157 fn place_liquid(
158 &self,
159 level: &dyn LevelAccessor,
160 pos: BlockPos,
161 state: steel_utils::BlockStateId,
162 fluid_state: FluidState,
163 ) -> bool {
164 if state.try_get_value(WATERLOGGED) != Some(false)
165 || fluid_state.fluid_id != &vanilla_fluids::WATER
166 {
167 return false;
168 }
169
170 let waterlogged = state.set_value(WATERLOGGED, true);
171 if state.get_value(LIT_PROPERTY) {
172 let extinguished = waterlogged.set_value(LIT_PROPERTY, false);
173 level.set_block_state(pos, extinguished, UpdateFlags::UPDATE_ALL_IMMEDIATE);
174 level.play_block_sound(&sound_events::BLOCK_CANDLE_EXTINGUISH, pos, 1.0, 1.0, None);
175 level.game_event(
176 &vanilla_game_events::BLOCK_CHANGE,
177 pos,
178 &GameEventContext::new(None, Some(extinguished)),
179 );
180 } else {
181 level.set_block_state(pos, waterlogged, UpdateFlags::UPDATE_ALL);
182 }
183
184 schedule_placed_liquid_tick(level, pos, fluid_state);
185 true
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::test_support::TestLevel;
193 use steel_registry::init_vanilla_registry;
194
195 fn supporting_level() -> TestLevel {
196 TestLevel::default().with_block(
197 BlockPos::ZERO.below(),
198 vanilla_blocks::STONE.default_state(),
199 )
200 }
201
202 #[test]
203 fn waterlogged_candle_update_shape_schedules_water_tick() {
204 init_vanilla_registry();
205
206 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
207 let state = vanilla_blocks::CANDLE
208 .default_state()
209 .set_value(WATERLOGGED, true);
210 let level = supporting_level();
211
212 assert_eq!(
213 candle.update_shape(
214 state,
215 &level,
216 BlockPos::ZERO,
217 Direction::North,
218 Direction::North.relative(BlockPos::ZERO),
219 vanilla_blocks::AIR.default_state(),
220 ),
221 state
222 );
223 assert_eq!(
224 level
225 .scheduled_fluid_ticks
226 .borrow()
227 .iter()
228 .map(|tick| tick.fluid)
229 .collect::<Vec<_>>(),
230 vec![&vanilla_fluids::WATER]
231 );
232 }
233
234 #[test]
235 fn burning_projectile_lights_only_unlit_candles() {
236 init_vanilla_registry();
237
238 let unlit = vanilla_blocks::CANDLE
239 .default_state()
240 .set_value(LIT_PROPERTY, false)
241 .set_value(WATERLOGGED, false);
242 let lit = unlit.set_value(LIT_PROPERTY, true);
243 let waterlogged = unlit.set_value(WATERLOGGED, true);
244
245 assert_eq!(CandleBlock::projectile_lit_state(unlit, true), Some(lit));
246 assert_eq!(CandleBlock::projectile_lit_state(unlit, false), None);
247 assert_eq!(CandleBlock::projectile_lit_state(lit, true), None);
248 assert_eq!(CandleBlock::projectile_lit_state(waterlogged, true), None);
249 }
250
251 #[test]
252 fn water_placement_on_lit_candle_emits_block_change_event() {
253 init_vanilla_registry();
254
255 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
256 let state = vanilla_blocks::CANDLE
257 .default_state()
258 .set_value(WATERLOGGED, false)
259 .set_value(LIT_PROPERTY, true);
260 let level = supporting_level();
261
262 assert!(candle.place_liquid(
263 &level,
264 BlockPos::ZERO,
265 state,
266 FluidState::source(&vanilla_fluids::WATER),
267 ));
268
269 assert_eq!(
270 level
271 .block_sounds
272 .borrow()
273 .iter()
274 .map(|sound| sound.sound)
275 .collect::<Vec<_>>(),
276 vec![&sound_events::BLOCK_CANDLE_EXTINGUISH]
277 );
278 assert_eq!(
279 level
280 .game_events
281 .borrow()
282 .iter()
283 .map(|event| event.event)
284 .collect::<Vec<_>>(),
285 vec![&vanilla_game_events::BLOCK_CHANGE]
286 );
287 assert!(
288 level
289 .last_placed_state()
290 .expect("candle should be waterlogged")
291 .get_value(WATERLOGGED)
292 );
293 }
294}