Skip to main content

steel_core/behavior/blocks/building/
campfire_block.rs

1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::blocks::properties::{BlockStateProperties, Direction};
5use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt as _};
6use steel_registry::fluid::FluidState;
7use steel_registry::vanilla_damage_types;
8use steel_registry::{sound_events, vanilla_blocks, vanilla_fluids, vanilla_game_events};
9use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
10
11use crate::{
12    behavior::{BlockBehavior, BlockPlaceContext, block::schedule_placed_liquid_tick},
13    entity::{Entity, InsideBlockEffectCollector, damage::DamageSource, projectile::Projectile},
14    world::{
15        ClipHitResult, LevelAccessor, ScheduledTickAccess, World, game_event::GameEventContext,
16    },
17};
18
19/// Behavior for campfires and soul campfires.
20///
21/// TODO: Add campfire cooking, smoke particles, and dowse item ejection.
22#[block_behavior]
23pub struct CampfireBlock {
24    block: BlockRef,
25    #[json_arg(value, json = "spawn_particles")]
26    _spawn_particles: bool,
27    #[json_arg(value, json = "fire_damage")]
28    fire_damage: i32,
29}
30
31impl CampfireBlock {
32    /// Creates a campfire block behavior.
33    #[must_use]
34    pub const fn new(block: BlockRef, spawn_particles: bool, fire_damage: i32) -> Self {
35        Self {
36            block,
37            _spawn_particles: spawn_particles,
38            fire_damage,
39        }
40    }
41
42    #[must_use]
43    fn contact_damage_amount(&self, state: BlockStateId, is_living_entity: bool) -> Option<f32> {
44        if state.get_value(&BlockStateProperties::LIT) && is_living_entity {
45            Some(self.fire_damage as f32)
46        } else {
47            None
48        }
49    }
50
51    fn is_smoke_source(state: BlockStateId) -> bool {
52        state.get_block() == &vanilla_blocks::HAY_BLOCK
53    }
54
55    fn placement_state(
56        &self,
57        waterlogged: bool,
58        below_state: BlockStateId,
59        facing: Direction,
60    ) -> BlockStateId {
61        self.block
62            .default_state()
63            .set_value(&BlockStateProperties::WATERLOGGED, waterlogged)
64            .set_value(
65                &BlockStateProperties::SIGNAL_FIRE,
66                Self::is_smoke_source(below_state),
67            )
68            .set_value(&BlockStateProperties::LIT, !waterlogged)
69            .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing)
70    }
71
72    fn projectile_lit_state(
73        state: BlockStateId,
74        projectile_is_on_fire: bool,
75        may_interact: bool,
76    ) -> Option<BlockStateId> {
77        (projectile_is_on_fire
78            && may_interact
79            && !state.get_value(&BlockStateProperties::LIT)
80            && !state.get_value(&BlockStateProperties::WATERLOGGED))
81        .then(|| state.set_value(&BlockStateProperties::LIT, true))
82    }
83}
84
85impl BlockBehavior for CampfireBlock {
86    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
87        let waterlogged = context.is_water_source();
88        let below_state = context.world.get_block_state(context.place_pos().below());
89        Some(self.placement_state(waterlogged, below_state, context.horizontal_direction()))
90    }
91
92    fn update_shape(
93        &self,
94        state: BlockStateId,
95        world: &dyn ScheduledTickAccess,
96        pos: BlockPos,
97        direction: Direction,
98        _neighbor_pos: BlockPos,
99        neighbor_state: BlockStateId,
100    ) -> BlockStateId {
101        if state.get_value(&BlockStateProperties::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 direction == Direction::Down {
107            state.set_value(
108                &BlockStateProperties::SIGNAL_FIRE,
109                Self::is_smoke_source(neighbor_state),
110            )
111        } else {
112            state
113        }
114    }
115
116    fn on_projectile_hit(
117        &self,
118        state: BlockStateId,
119        world: &Arc<World>,
120        hit: &ClipHitResult,
121        projectile: &dyn Projectile,
122    ) {
123        let Some(lit_state) = Self::projectile_lit_state(
124            state,
125            projectile.is_on_fire(),
126            projectile.projectile_may_interact(world, hit.block_pos),
127        ) else {
128            return;
129        };
130        world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
131    }
132
133    fn entity_inside(
134        &self,
135        state: BlockStateId,
136        world: &Arc<World>,
137        pos: BlockPos,
138        entity: &dyn Entity,
139        effect_collector: &mut InsideBlockEffectCollector,
140        is_precise: bool,
141    ) {
142        if let Some(damage) = self.contact_damage_amount(state, entity.is_living_entity()) {
143            entity.hurt(
144                world,
145                &DamageSource::environment(&vanilla_damage_types::CAMPFIRE),
146                damage,
147            );
148        }
149
150        self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
151    }
152
153    fn place_liquid(
154        &self,
155        level: &dyn LevelAccessor,
156        pos: BlockPos,
157        state: BlockStateId,
158        fluid_state: FluidState,
159    ) -> bool {
160        if state.try_get_value(&BlockStateProperties::WATERLOGGED) != Some(false)
161            || fluid_state.fluid_id != &vanilla_fluids::WATER
162        {
163            return false;
164        }
165
166        if state.get_value(&BlockStateProperties::LIT) {
167            level.play_block_sound(
168                &sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE,
169                pos,
170                1.0,
171                1.0,
172                None,
173            );
174            level.game_event(
175                &vanilla_game_events::BLOCK_CHANGE,
176                pos,
177                &GameEventContext::new(
178                    None,
179                    Some(state.set_value(&BlockStateProperties::LIT, false)),
180                ),
181            );
182        }
183
184        level.set_block_state(
185            pos,
186            state
187                .set_value(&BlockStateProperties::WATERLOGGED, true)
188                .set_value(&BlockStateProperties::LIT, false),
189            UpdateFlags::UPDATE_ALL,
190        );
191        schedule_placed_liquid_tick(level, pos, fluid_state);
192        true
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::test_support::TestLevel;
200    use steel_registry::{
201        blocks::block_state_ext::BlockStateExt, init_vanilla_registry, vanilla_blocks,
202    };
203
204    #[test]
205    fn lit_campfire_damages_living_entities() {
206        init_vanilla_registry();
207        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
208        let state = vanilla_blocks::CAMPFIRE
209            .default_state()
210            .set_value(&BlockStateProperties::LIT, true);
211
212        assert_eq!(campfire.contact_damage_amount(state, true), Some(1.0));
213    }
214
215    #[test]
216    fn unlit_campfire_does_not_damage_entities() {
217        init_vanilla_registry();
218        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
219        let state = vanilla_blocks::CAMPFIRE
220            .default_state()
221            .set_value(&BlockStateProperties::LIT, false);
222
223        assert_eq!(campfire.contact_damage_amount(state, true), None);
224    }
225
226    #[test]
227    fn campfire_does_not_damage_non_living_entities() {
228        init_vanilla_registry();
229        let campfire = CampfireBlock::new(&vanilla_blocks::SOUL_CAMPFIRE, false, 2);
230        let state = vanilla_blocks::SOUL_CAMPFIRE
231            .default_state()
232            .set_value(&BlockStateProperties::LIT, true);
233
234        assert_eq!(campfire.contact_damage_amount(state, false), None);
235    }
236
237    #[test]
238    fn burning_projectile_lights_only_dry_unlit_campfires() {
239        init_vanilla_registry();
240
241        let unlit = vanilla_blocks::CAMPFIRE
242            .default_state()
243            .set_value(&BlockStateProperties::LIT, false)
244            .set_value(&BlockStateProperties::WATERLOGGED, false);
245        let lit = unlit.set_value(&BlockStateProperties::LIT, true);
246        let waterlogged = unlit.set_value(&BlockStateProperties::WATERLOGGED, true);
247
248        assert_eq!(
249            CampfireBlock::projectile_lit_state(unlit, true, true),
250            Some(lit)
251        );
252        assert_eq!(
253            CampfireBlock::projectile_lit_state(unlit, false, true),
254            None
255        );
256        assert_eq!(
257            CampfireBlock::projectile_lit_state(unlit, true, false),
258            None
259        );
260        assert_eq!(CampfireBlock::projectile_lit_state(lit, true, true), None);
261        assert_eq!(
262            CampfireBlock::projectile_lit_state(waterlogged, true, true),
263            None
264        );
265    }
266
267    #[test]
268    fn placement_state_sets_facing_and_signal_fire() {
269        init_vanilla_registry();
270        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
271
272        let state = campfire.placement_state(
273            false,
274            vanilla_blocks::HAY_BLOCK.default_state(),
275            Direction::East,
276        );
277
278        assert_eq!(
279            state.get_value(&BlockStateProperties::HORIZONTAL_FACING),
280            Direction::East
281        );
282        assert!(state.get_value(&BlockStateProperties::SIGNAL_FIRE));
283        assert!(state.get_value(&BlockStateProperties::LIT));
284        assert!(!state.get_value(&BlockStateProperties::WATERLOGGED));
285    }
286
287    #[test]
288    fn update_shape_recomputes_signal_fire_from_below() {
289        init_vanilla_registry();
290        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
291        let level = TestLevel::default();
292        let state = vanilla_blocks::CAMPFIRE
293            .default_state()
294            .set_value(&BlockStateProperties::SIGNAL_FIRE, false)
295            .set_value(&BlockStateProperties::WATERLOGGED, false);
296
297        let updated = campfire.update_shape(
298            state,
299            &level,
300            BlockPos::ZERO,
301            Direction::Down,
302            BlockPos::ZERO.below(),
303            vanilla_blocks::HAY_BLOCK.default_state(),
304        );
305
306        assert!(updated.get_value(&BlockStateProperties::SIGNAL_FIRE));
307    }
308
309    #[test]
310    fn water_placement_extinguishes_lit_campfire() {
311        init_vanilla_registry();
312        let level = TestLevel::default();
313        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
314        let state = vanilla_blocks::CAMPFIRE
315            .default_state()
316            .set_value(&BlockStateProperties::LIT, true)
317            .set_value(&BlockStateProperties::WATERLOGGED, false);
318        let pos = BlockPos::new(1, 2, 3);
319
320        assert!(campfire.place_liquid(
321            &level,
322            pos,
323            state,
324            FluidState::source(&vanilla_fluids::WATER),
325        ));
326
327        let placed = level
328            .last_placed_state()
329            .expect("campfire should be updated");
330        assert!(!placed.get_value(&BlockStateProperties::LIT));
331        assert!(placed.get_value(&BlockStateProperties::WATERLOGGED));
332        assert_eq!(
333            level
334                .block_sounds
335                .borrow()
336                .iter()
337                .map(|sound| sound.sound)
338                .collect::<Vec<_>>(),
339            vec![&sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE]
340        );
341        assert_eq!(
342            level
343                .scheduled_fluid_ticks
344                .borrow()
345                .iter()
346                .map(|tick| (tick.pos, tick.fluid, tick.delay))
347                .collect::<Vec<_>>(),
348            vec![(pos, &vanilla_fluids::WATER, 5)]
349        );
350        assert_eq!(
351            level
352                .game_events
353                .borrow()
354                .iter()
355                .map(|event| event.event)
356                .collect::<Vec<_>>(),
357            vec![&vanilla_game_events::BLOCK_CHANGE]
358        );
359    }
360}