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,
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 if state.get_value(&WATERLOGGED) {
102 let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
103 let _ = world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
104 }
105
106 if !self.can_survive(state, world, pos) {
107 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
108 }
109 state
110 }
111
112 fn on_projectile_hit(
113 &self,
114 state: steel_utils::BlockStateId,
115 world: &Arc<World>,
116 hit: &ClipHitResult,
117 projectile: &dyn Projectile,
118 ) {
119 let Some(lit_state) = Self::projectile_lit_state(state, projectile.is_on_fire()) else {
120 return;
121 };
122 world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
123 }
124
125 fn use_item_on(
126 &self,
127 state: steel_utils::BlockStateId,
128 world: &Arc<World>,
129 pos: BlockPos,
130 _player: &player::Player,
131 _hand: types::InteractionHand,
132 _hit_result: &BlockHitResult,
133 inv: &mut InventoryAccess,
134 ) -> InteractionResult {
135 let item_is_empty = inv.with_item(|item_stack| item_stack.is_empty());
136 if item_is_empty {
137 if !state.get_value(&LIT_PROPERTY) {
138 return InteractionResult::Pass;
139 }
140 let new_state = state.set_value(&LIT_PROPERTY, false);
141 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
142 return InteractionResult::Success;
143 }
144
145 if self
146 .get_clone_item_stack(self.block, state, false)
147 .is_some_and(|it| inv.with_item(|item_stack| it.is(item_stack.item)))
148 {
149 let candles_amount = state.get_value(&CANDLES_PROPERTY);
150 if candles_amount < MAX_CANDLES {
151 let new_state = state.set_value(&CANDLES_PROPERTY, candles_amount + 1);
152 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
153 return InteractionResult::Success;
154 }
155 }
156
157 InteractionResult::TryEmptyHandInteraction
158 }
159
160 fn place_liquid(
161 &self,
162 level: &dyn LevelAccessor,
163 pos: BlockPos,
164 state: steel_utils::BlockStateId,
165 fluid_state: FluidState,
166 ) -> bool {
167 if state.try_get_value(&WATERLOGGED) != Some(false)
168 || fluid_state.fluid_id != &vanilla_fluids::WATER
169 {
170 return false;
171 }
172
173 let waterlogged = state.set_value(&WATERLOGGED, true);
174 if state.get_value(&LIT_PROPERTY) {
175 let extinguished = waterlogged.set_value(&LIT_PROPERTY, false);
176 level.set_block_state(pos, extinguished, UpdateFlags::UPDATE_ALL_IMMEDIATE);
177 level.play_block_sound(&sound_events::BLOCK_CANDLE_EXTINGUISH, pos, 1.0, 1.0, None);
178 level.game_event(
179 &vanilla_game_events::BLOCK_CHANGE,
180 pos,
181 &GameEventContext::new(None, Some(extinguished)),
182 );
183 } else {
184 level.set_block_state(pos, waterlogged, UpdateFlags::UPDATE_ALL);
185 }
186
187 schedule_placed_liquid_tick(level, pos, fluid_state);
188 true
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::test_support::TestLevel;
196 use steel_registry::init_vanilla_registry;
197
198 fn supporting_level() -> TestLevel {
199 TestLevel::default().with_block(
200 BlockPos::ZERO.below(),
201 vanilla_blocks::STONE.default_state(),
202 )
203 }
204
205 #[test]
206 fn waterlogged_candle_update_shape_schedules_water_tick() {
207 init_vanilla_registry();
208
209 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
210 let state = vanilla_blocks::CANDLE
211 .default_state()
212 .set_value(&WATERLOGGED, true);
213 let level = supporting_level();
214
215 assert_eq!(
216 candle.update_shape(
217 state,
218 &level,
219 BlockPos::ZERO,
220 Direction::North,
221 Direction::North.relative(BlockPos::ZERO),
222 vanilla_blocks::AIR.default_state(),
223 ),
224 state
225 );
226 assert_eq!(
227 level
228 .scheduled_fluid_ticks
229 .borrow()
230 .iter()
231 .map(|tick| tick.fluid)
232 .collect::<Vec<_>>(),
233 vec![&vanilla_fluids::WATER]
234 );
235 }
236
237 #[test]
238 fn burning_projectile_lights_only_unlit_candles() {
239 init_vanilla_registry();
240
241 let unlit = vanilla_blocks::CANDLE
242 .default_state()
243 .set_value(&LIT_PROPERTY, false)
244 .set_value(&WATERLOGGED, false);
245 let lit = unlit.set_value(&LIT_PROPERTY, true);
246 let waterlogged = unlit.set_value(&WATERLOGGED, true);
247
248 assert_eq!(CandleBlock::projectile_lit_state(unlit, true), Some(lit));
249 assert_eq!(CandleBlock::projectile_lit_state(unlit, false), None);
250 assert_eq!(CandleBlock::projectile_lit_state(lit, true), None);
251 assert_eq!(CandleBlock::projectile_lit_state(waterlogged, true), None);
252 }
253
254 #[test]
255 fn water_placement_on_lit_candle_emits_block_change_event() {
256 init_vanilla_registry();
257
258 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
259 let state = vanilla_blocks::CANDLE
260 .default_state()
261 .set_value(&WATERLOGGED, false)
262 .set_value(&LIT_PROPERTY, true);
263 let level = supporting_level();
264
265 assert!(candle.place_liquid(
266 &level,
267 BlockPos::ZERO,
268 state,
269 FluidState::source(&vanilla_fluids::WATER),
270 ));
271
272 assert_eq!(
273 level
274 .block_sounds
275 .borrow()
276 .iter()
277 .map(|sound| sound.sound)
278 .collect::<Vec<_>>(),
279 vec![&sound_events::BLOCK_CANDLE_EXTINGUISH]
280 );
281 assert_eq!(
282 level
283 .game_events
284 .borrow()
285 .iter()
286 .map(|event| event.event)
287 .collect::<Vec<_>>(),
288 vec![&vanilla_game_events::BLOCK_CHANGE]
289 );
290 assert!(
291 level
292 .last_placed_state()
293 .expect("candle should be waterlogged")
294 .get_value(&WATERLOGGED)
295 );
296 }
297}