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, 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/// Behavior for all Candle type blocks
41#[block_behavior]
42pub struct CandleBlock {
43    block: BlockRef,
44}
45
46impl CandleBlock {
47    /// Creates a new candle block behavior for the given block
48    #[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    /// Checks if the candle block can survive at the given position.
66    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}