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, 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/// 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        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}