Skip to main content

steel_core/behavior/blocks/decoration/
candle_block.rs

1use 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,
16    vanilla_block_tags::BlockTag,
17    vanilla_blocks, vanilla_fluids, vanilla_game_events,
18};
19use steel_utils::{
20    BlockPos,
21    types::{self, UpdateFlags},
22};
23
24use crate::{
25    behavior::{
26        BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
27        block::{
28            default_can_be_replaced, schedule_placed_liquid_tick,
29            schedule_water_tick_if_waterlogged,
30        },
31    },
32    entity::{Entity, projectile::Projectile},
33    player::{self, Player},
34    world::{
35        ClipHitResult, LevelAccessor, LevelReader, ScheduledTickAccess, World,
36        game_event::GameEventContext,
37    },
38};
39
40const CANDLES_PROPERTY: &IntProperty = &BlockStateProperties::CANDLES;
41const LIT_PROPERTY: &BoolProperty = &BlockStateProperties::LIT;
42const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
43const MAX_CANDLES: u8 = 4;
44
45/// Behavior for all Candle type blocks
46#[block_behavior]
47pub struct CandleBlock {
48    block: BlockRef,
49}
50
51impl CandleBlock {
52    /// Creates a new candle block behavior for the given block
53    #[must_use]
54    pub const fn new(block: BlockRef) -> Self {
55        Self { block }
56    }
57
58    pub(super) fn projectile_lit_state(
59        state: steel_utils::BlockStateId,
60        projectile_is_on_fire: bool,
61    ) -> Option<steel_utils::BlockStateId> {
62        (projectile_is_on_fire
63            && state.try_get_value(WATERLOGGED) != Some(true)
64            && !state.get_value(LIT_PROPERTY))
65        .then(|| state.set_value(LIT_PROPERTY, true))
66    }
67
68    /// Checks whether the state is a lit candle or candle cake
69    #[must_use]
70    pub fn is_lit(state: steel_utils::BlockStateId) -> bool {
71        let block = state.get_block();
72        (block.has_tag(&BlockTag::CANDLES) || block.has_tag(&BlockTag::CANDLE_CAKES))
73            && state.try_get_value(LIT_PROPERTY) == Some(true)
74    }
75
76    /// Extinguishes a candle or candle cake plays the sound and emits `BLOCK_CHANGE`
77    pub fn extinguish(
78        player: Option<&Player>,
79        state: steel_utils::BlockStateId,
80        level: &(impl LevelAccessor + ?Sized),
81        pos: BlockPos,
82    ) {
83        level.set_block_state(
84            pos,
85            state.set_value(LIT_PROPERTY, false),
86            UpdateFlags::UPDATE_ALL_IMMEDIATE,
87        );
88        level.play_block_sound(&sound_events::BLOCK_CANDLE_EXTINGUISH, pos, 1.0, 1.0, None);
89        level.game_event(
90            &vanilla_game_events::BLOCK_CHANGE,
91            pos,
92            &GameEventContext::new(player.map(|player| player as &dyn Entity), None),
93        );
94    }
95}
96
97impl BlockBehavior for CandleBlock {
98    /// Checks if the candle block can survive at the given position.
99    fn can_survive(
100        &self,
101        _state: steel_utils::BlockStateId,
102        world: &dyn LevelReader,
103        pos: BlockPos,
104    ) -> bool {
105        let below_pos = pos.below();
106        world.is_face_sturdy_for(
107            world.get_block_state(below_pos),
108            below_pos,
109            Direction::Up,
110            SupportType::Center,
111        )
112    }
113
114    fn can_be_replaced(
115        &self,
116        state: steel_utils::BlockStateId,
117        context: &BlockPlaceContext<'_>,
118    ) -> bool {
119        (!context.is_secondary_use_active()
120            && context.with_item(|item| item.item() == REGISTRY.items.by_block(self.block))
121            && state.get_value(CANDLES_PROPERTY) < MAX_CANDLES)
122            || default_can_be_replaced(state, context)
123    }
124
125    fn get_state_for_placement(
126        &self,
127        context: &BlockPlaceContext<'_>,
128    ) -> Option<steel_utils::BlockStateId> {
129        let state = context.world.get_block_state(context.place_pos());
130        if state.get_block() == self.block {
131            return Some(state.set_value(
132                CANDLES_PROPERTY,
133                (state.get_value(CANDLES_PROPERTY) + 1).min(MAX_CANDLES),
134            ));
135        }
136
137        let default_state = self.block.default_state();
138        self.can_survive(default_state, context.world.as_ref(), context.place_pos())
139            .then(|| default_state.set_value(WATERLOGGED, context.is_water_source()))
140    }
141
142    fn update_shape(
143        &self,
144        state: steel_utils::BlockStateId,
145        world: &dyn ScheduledTickAccess,
146        pos: BlockPos,
147        _direction: Direction,
148        _neighbor_pos: BlockPos,
149        _neighbor_state: steel_utils::BlockStateId,
150    ) -> steel_utils::BlockStateId {
151        schedule_water_tick_if_waterlogged(state, world, pos);
152
153        if !self.can_survive(state, world, pos) {
154            return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
155        }
156        state
157    }
158
159    fn on_projectile_hit(
160        &self,
161        state: steel_utils::BlockStateId,
162        world: &Arc<World>,
163        hit: &ClipHitResult,
164        projectile: &dyn Projectile,
165    ) {
166        let Some(lit_state) = Self::projectile_lit_state(state, projectile.is_on_fire()) else {
167            return;
168        };
169        world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
170    }
171
172    fn use_item_on(
173        &self,
174        state: steel_utils::BlockStateId,
175        world: &Arc<World>,
176        pos: BlockPos,
177        player: &player::Player,
178        _hand: types::InteractionHand,
179        _hit_result: &BlockHitResult,
180        inv: &mut InventoryAccess,
181    ) -> InteractionResult {
182        let item_is_empty = inv.with_item(|item_stack| item_stack.is_empty());
183        if item_is_empty && player.abilities.lock().may_build && state.get_value(LIT_PROPERTY) {
184            Self::extinguish(Some(player), state, world, pos);
185            return InteractionResult::Success;
186        }
187
188        InteractionResult::TryEmptyHandInteraction
189    }
190
191    fn place_liquid(
192        &self,
193        level: &dyn LevelAccessor,
194        pos: BlockPos,
195        state: steel_utils::BlockStateId,
196        fluid_state: FluidState,
197    ) -> bool {
198        if state.try_get_value(WATERLOGGED) != Some(false)
199            || fluid_state.fluid_id != &vanilla_fluids::WATER
200        {
201            return false;
202        }
203
204        let waterlogged = state.set_value(WATERLOGGED, true);
205        if state.get_value(LIT_PROPERTY) {
206            Self::extinguish(None, waterlogged, level, pos);
207        } else {
208            level.set_block_state(pos, waterlogged, UpdateFlags::UPDATE_ALL);
209        }
210
211        schedule_placed_liquid_tick(level, pos, fluid_state);
212        true
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use glam::DVec3;
220    use steel_registry::{init_vanilla_registry, item_stack::ItemStack, vanilla_items};
221    use steel_utils::ChunkPos;
222
223    use crate::{
224        behavior::{PlacementOrientation, PlacementSource, init_behaviors},
225        test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk, test_world},
226    };
227
228    fn supporting_level() -> TestLevel {
229        TestLevel::default().with_block(
230            BlockPos::ZERO.below(),
231            vanilla_blocks::STONE.default_state(),
232        )
233    }
234
235    fn place_context<'a>(
236        world: &'a Arc<World>,
237        hit_pos: BlockPos,
238        direction: Direction,
239        item_in_hand: &'a mut ItemStack,
240        is_secondary_use_active: bool,
241    ) -> BlockPlaceContext<'a> {
242        let hit_result = BlockHitResult {
243            location: DVec3::ZERO,
244            direction,
245            block_pos: hit_pos,
246            miss: false,
247            inside: false,
248            world_border_hit: false,
249        };
250        let source = PlacementSource::direct(
251            None,
252            types::InteractionHand::MainHand,
253            item_in_hand,
254            PlacementOrientation::Player {
255                rotation: 0.0,
256                pitch: 0.0,
257            },
258            is_secondary_use_active,
259        );
260        BlockPlaceContext::new(world, source, &hit_result)
261    }
262
263    #[test]
264    fn waterlogged_candle_update_shape_schedules_water_tick() {
265        init_vanilla_registry();
266
267        let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
268        let state = vanilla_blocks::CANDLE
269            .default_state()
270            .set_value(WATERLOGGED, true);
271        let level = supporting_level();
272
273        assert_eq!(
274            candle.update_shape(
275                state,
276                &level,
277                BlockPos::ZERO,
278                Direction::North,
279                Direction::North.relative(BlockPos::ZERO),
280                vanilla_blocks::AIR.default_state(),
281            ),
282            state
283        );
284        assert_eq!(
285            level
286                .scheduled_fluid_ticks
287                .borrow()
288                .iter()
289                .map(|tick| tick.fluid)
290                .collect::<Vec<_>>(),
291            vec![&vanilla_fluids::WATER]
292        );
293    }
294
295    #[test]
296    fn burning_projectile_lights_only_unlit_candles() {
297        init_vanilla_registry();
298
299        let unlit = vanilla_blocks::CANDLE
300            .default_state()
301            .set_value(LIT_PROPERTY, false)
302            .set_value(WATERLOGGED, false);
303        let lit = unlit.set_value(LIT_PROPERTY, true);
304        let waterlogged = unlit.set_value(WATERLOGGED, true);
305
306        assert_eq!(CandleBlock::projectile_lit_state(unlit, true), Some(lit));
307        assert_eq!(CandleBlock::projectile_lit_state(unlit, false), None);
308        assert_eq!(CandleBlock::projectile_lit_state(lit, true), None);
309        assert_eq!(CandleBlock::projectile_lit_state(waterlogged, true), None);
310    }
311
312    #[test]
313    fn water_placement_on_lit_candle_emits_block_change_event() {
314        init_vanilla_registry();
315
316        let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
317        let state = vanilla_blocks::CANDLE
318            .default_state()
319            .set_value(WATERLOGGED, false)
320            .set_value(LIT_PROPERTY, true);
321        let level = supporting_level();
322
323        assert!(candle.place_liquid(
324            &level,
325            BlockPos::ZERO,
326            state,
327            FluidState::source(&vanilla_fluids::WATER),
328        ));
329
330        assert_eq!(
331            level
332                .block_sounds
333                .borrow()
334                .iter()
335                .map(|sound| sound.sound)
336                .collect::<Vec<_>>(),
337            vec![&sound_events::BLOCK_CANDLE_EXTINGUISH]
338        );
339        assert_eq!(
340            level
341                .game_events
342                .borrow()
343                .iter()
344                .map(|event| event.event)
345                .collect::<Vec<_>>(),
346            vec![&vanilla_game_events::BLOCK_CHANGE]
347        );
348        assert!(
349            level
350                .last_placed_state()
351                .expect("candle should be waterlogged")
352                .get_value(WATERLOGGED)
353        );
354    }
355
356    #[test]
357    fn is_lit_accepts_only_lit_candles_and_candle_cakes() {
358        init_vanilla_registry();
359
360        let candle = vanilla_blocks::CANDLE.default_state();
361        let candle_cake = vanilla_blocks::CANDLE_CAKE.default_state();
362
363        assert!(CandleBlock::is_lit(candle.set_value(LIT_PROPERTY, true)));
364        assert!(!CandleBlock::is_lit(candle.set_value(LIT_PROPERTY, false)));
365        assert!(CandleBlock::is_lit(
366            candle_cake.set_value(LIT_PROPERTY, true)
367        ));
368        assert!(!CandleBlock::is_lit(
369            candle_cake.set_value(LIT_PROPERTY, false)
370        ));
371        assert!(!CandleBlock::is_lit(
372            vanilla_blocks::CAMPFIRE
373                .default_state()
374                .set_value(LIT_PROPERTY, true)
375        ));
376    }
377
378    #[test]
379    fn extinguish_matches_vanilla_side_effects() {
380        init_vanilla_registry();
381
382        let lit = vanilla_blocks::CANDLE_CAKE
383            .default_state()
384            .set_value(LIT_PROPERTY, true);
385        let level = TestLevel::default();
386
387        CandleBlock::extinguish(None, lit, &level, BlockPos::ZERO);
388
389        let placed = level.placed_blocks.borrow();
390        assert_eq!(placed.len(), 1);
391        assert_eq!(placed[0].state, lit.set_value(LIT_PROPERTY, false));
392        assert_eq!(placed[0].flags, UpdateFlags::UPDATE_ALL_IMMEDIATE);
393
394        let sounds = level.block_sounds.borrow();
395        assert_eq!(sounds.len(), 1);
396        assert_eq!(sounds[0].sound, &sound_events::BLOCK_CANDLE_EXTINGUISH);
397        assert_eq!(sounds[0].exclude, None);
398
399        let events = level.game_events.borrow();
400        assert_eq!(events.len(), 1);
401        assert_eq!(events[0].event, &vanilla_game_events::BLOCK_CHANGE);
402        assert_eq!(events[0].affected_state, None);
403    }
404
405    #[test]
406    fn candle_can_be_replaced_matching_stack() {
407        init_vanilla_registry();
408        init_behaviors();
409        let world = test_world();
410        let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
411        let state_1 = vanilla_blocks::CANDLE.default_state();
412        let state_4 = state_1.set_value(CANDLES_PROPERTY, 4);
413
414        let mut candle_item = ItemStack::new(&vanilla_items::CANDLE);
415        let ctx = place_context(
416            world,
417            BlockPos::ZERO,
418            Direction::Up,
419            &mut candle_item,
420            false,
421        );
422        assert!(candle.can_be_replaced(state_1, &ctx));
423
424        let mut candle_item_sneak = ItemStack::new(&vanilla_items::CANDLE);
425        let ctx_sneak = place_context(
426            world,
427            BlockPos::ZERO,
428            Direction::Up,
429            &mut candle_item_sneak,
430            true,
431        );
432        assert!(!candle.can_be_replaced(state_1, &ctx_sneak));
433
434        let mut candle_item_full = ItemStack::new(&vanilla_items::CANDLE);
435        let ctx_full = place_context(
436            world,
437            BlockPos::ZERO,
438            Direction::Up,
439            &mut candle_item_full,
440            false,
441        );
442        assert!(!candle.can_be_replaced(state_4, &ctx_full));
443
444        let mut diff_item = ItemStack::new(&vanilla_items::STONE);
445        let ctx_diff = place_context(world, BlockPos::ZERO, Direction::Up, &mut diff_item, false);
446        assert!(!candle.can_be_replaced(state_1, &ctx_diff));
447
448        let mut other_candle = ItemStack::new(&vanilla_items::RED_CANDLE);
449        let ctx_other = place_context(
450            world,
451            BlockPos::ZERO,
452            Direction::Up,
453            &mut other_candle,
454            false,
455        );
456        assert!(!candle.can_be_replaced(state_1, &ctx_other));
457    }
458
459    #[test]
460    fn candle_placement_state_increments_and_preserves_properties() {
461        init_vanilla_registry();
462        init_behaviors();
463        let world_fixture = fresh_test_world("candle_placement_increments");
464        let world = &world_fixture.world;
465        let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
466
467        let pos = BlockPos::new(0, 10, 0);
468        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
469
470        let existing = vanilla_blocks::CANDLE
471            .default_state()
472            .set_value(CANDLES_PROPERTY, 2)
473            .set_value(LIT_PROPERTY, true)
474            .set_value(WATERLOGGED, false);
475        world.set_block(pos, existing, UpdateFlags::UPDATE_ALL_IMMEDIATE);
476
477        let mut item = ItemStack::new(&vanilla_items::CANDLE);
478        let ctx = place_context(world, pos, Direction::Up, &mut item, false);
479        let placed_state = candle
480            .get_state_for_placement(&ctx)
481            .expect("placement state should be present");
482        assert_eq!(placed_state.get_value(CANDLES_PROPERTY), 3);
483        assert!(placed_state.get_value(LIT_PROPERTY));
484        assert!(!placed_state.get_value(WATERLOGGED));
485    }
486
487    #[test]
488    fn candle_can_place_when_clicking_block_below() {
489        init_vanilla_registry();
490        init_behaviors();
491        let world_fixture = fresh_test_world("candle_click_block_below");
492        let world = &world_fixture.world;
493        let stone_pos = BlockPos::new(0, 10, 0);
494        let candle_pos = stone_pos.above();
495
496        insert_ready_full_chunk(world, ChunkPos::from_block_pos(stone_pos));
497
498        world.set_block(
499            stone_pos,
500            vanilla_blocks::STONE.default_state(),
501            UpdateFlags::UPDATE_ALL_IMMEDIATE,
502        );
503        world.set_block(
504            candle_pos,
505            vanilla_blocks::CANDLE.default_state(),
506            UpdateFlags::UPDATE_ALL_IMMEDIATE,
507        );
508
509        let mut candle_item = ItemStack::new(&vanilla_items::CANDLE);
510        let ctx = place_context(world, stone_pos, Direction::Up, &mut candle_item, false);
511
512        assert_eq!(ctx.hit_pos(), stone_pos);
513        assert_eq!(ctx.place_pos(), candle_pos);
514        assert!(!ctx.replaces_clicked_block());
515        assert!(ctx.can_place());
516
517        let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
518        let new_state = candle
519            .get_state_for_placement(&ctx)
520            .expect("placement state should be present");
521        assert_eq!(new_state.get_value(CANDLES_PROPERTY), 2);
522    }
523}