Skip to main content

steel_core/behavior/blocks/building/
campfire_block.rs

1use std::sync::{Arc, Weak};
2
3use steel_macros::block_behavior;
4use steel_registry::block_entity_type::BlockEntityTypeRef;
5use steel_registry::blocks::properties::{
6    BlockStateProperties, BoolProperty, Direction, EnumProperty,
7};
8use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt as _};
9use steel_registry::fluid::FluidState;
10use steel_registry::recipe::{SingleItemRecipeInput, vanilla_recipe_types};
11use steel_registry::vanilla_damage_types;
12use steel_registry::{
13    REGISTRY, sound_events, vanilla_block_entity_types, vanilla_block_tags::BlockTag,
14    vanilla_blocks, vanilla_custom_stats, vanilla_fluids, vanilla_game_events,
15};
16use steel_utils::{
17    BlockPos, BlockStateId, Downcast as _,
18    types::{InteractionHand, UpdateFlags},
19};
20
21use crate::behavior::block::schedule_water_tick_if_waterlogged;
22use crate::player::Player;
23use crate::{
24    behavior::{
25        BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
26        block::{BlockEntityCreation, schedule_placed_liquid_tick},
27        context::BlockHitResult,
28    },
29    block_entity::{BLOCK_ENTITIES, BlockEntityTicker, entities::CampfireBlockEntity},
30    entity::{Entity, InsideBlockEffectCollector, damage::DamageSource, projectile::Projectile},
31    world::{
32        ClipHitResult, LevelAccessor, ScheduledTickAccess, World, game_event::GameEventContext,
33    },
34};
35
36/// Behavior for campfires and soul campfires.
37///
38/// Client-local smoke particles remain client work; cooking is server-owned here.
39#[block_behavior]
40pub struct CampfireBlock {
41    block: BlockRef,
42    #[json_arg(value, json = "spawn_particles")]
43    _spawn_particles: bool,
44    #[json_arg(value, json = "fire_damage")]
45    fire_damage: i32,
46}
47
48const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
49const LIT: &BoolProperty = &BlockStateProperties::LIT;
50const SIGNAL_FIRE: &BoolProperty = &BlockStateProperties::SIGNAL_FIRE;
51const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
52
53impl CampfireBlock {
54    /// Creates a campfire block behavior.
55    #[must_use]
56    pub const fn new(block: BlockRef, spawn_particles: bool, fire_damage: i32) -> Self {
57        Self {
58            block,
59            _spawn_particles: spawn_particles,
60            fire_damage,
61        }
62    }
63
64    #[must_use]
65    fn contact_damage_amount(&self, state: BlockStateId, is_living_entity: bool) -> Option<f32> {
66        if state.get_value(LIT) && is_living_entity {
67            Some(self.fire_damage as f32)
68        } else {
69            None
70        }
71    }
72
73    fn is_smoke_source(state: BlockStateId) -> bool {
74        state.get_block() == &vanilla_blocks::HAY_BLOCK
75    }
76
77    fn placement_state(
78        &self,
79        waterlogged: bool,
80        below_state: BlockStateId,
81        facing: Direction,
82    ) -> BlockStateId {
83        self.block
84            .default_state()
85            .set_value(WATERLOGGED, waterlogged)
86            .set_value(SIGNAL_FIRE, Self::is_smoke_source(below_state))
87            .set_value(LIT, !waterlogged)
88            .set_value(HORIZONTAL_FACING, facing)
89    }
90
91    fn projectile_lit_state(
92        state: BlockStateId,
93        projectile_is_on_fire: bool,
94        may_interact: bool,
95    ) -> Option<BlockStateId> {
96        (projectile_is_on_fire
97            && may_interact
98            && !state.get_value(LIT)
99            && !state.get_value(WATERLOGGED))
100        .then(|| state.set_value(LIT, true))
101    }
102
103    /// Whether `state` is a lit campfire or soul campfire.
104    #[must_use]
105    pub fn is_lit_campfire(state: BlockStateId) -> bool {
106        state.get_block().has_tag(&BlockTag::CAMPFIRES) && state.try_get_value(LIT) == Some(true)
107    }
108
109    /// The shared part of putting out a campfire (shovels, water potions,
110    /// water flowing in).
111    pub fn douse(source: Option<&dyn Entity>, level: &dyn LevelAccessor, pos: BlockPos) {
112        level.game_event(
113            &vanilla_game_events::BLOCK_CHANGE,
114            pos,
115            &GameEventContext::new(source, None),
116        );
117    }
118}
119
120impl BlockBehavior for CampfireBlock {
121    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
122        let waterlogged = context.is_water_source();
123        let below_state = context.world.get_block_state(context.place_pos().below());
124        Some(self.placement_state(waterlogged, below_state, context.horizontal_direction()))
125    }
126
127    fn use_item_on(
128        &self,
129        _state: BlockStateId,
130        world: &Arc<World>,
131        pos: BlockPos,
132        player: &Player,
133        _hand: InteractionHand,
134        _hit_result: &BlockHitResult,
135        inv: &mut InventoryAccess,
136    ) -> InteractionResult {
137        let accepted = inv.with_item(|stack| {
138            REGISTRY
139                .recipes
140                .find_match(
141                    &vanilla_recipe_types::CAMPFIRE_COOKING,
142                    &SingleItemRecipeInput::new(stack.clone()),
143                )
144                .is_some()
145        });
146        if !accepted {
147            return InteractionResult::TryEmptyHandInteraction;
148        }
149
150        let Some(block_entity) = world.get_block_entity(pos) else {
151            return InteractionResult::TryEmptyHandInteraction;
152        };
153        let Some(campfire) = block_entity.downcast_ref::<CampfireBlockEntity>() else {
154            return InteractionResult::TryEmptyHandInteraction;
155        };
156        let item = inv.with_item(|stack| stack.copy_with_count(1));
157        if !campfire.place_food(player, item) {
158            return InteractionResult::Consume;
159        }
160        if !player.has_infinite_materials() {
161            inv.with_item(|stack| stack.shrink(1));
162        }
163        player.award_custom_stat(&vanilla_custom_stats::INTERACT_WITH_CAMPFIRE);
164        InteractionResult::SuccessServer
165    }
166
167    fn update_shape(
168        &self,
169        state: BlockStateId,
170        world: &dyn ScheduledTickAccess,
171        pos: BlockPos,
172        direction: Direction,
173        _neighbor_pos: BlockPos,
174        neighbor_state: BlockStateId,
175    ) -> BlockStateId {
176        schedule_water_tick_if_waterlogged(state, world, pos);
177
178        if direction == Direction::Down {
179            state.set_value(SIGNAL_FIRE, Self::is_smoke_source(neighbor_state))
180        } else {
181            state
182        }
183    }
184
185    fn on_projectile_hit(
186        &self,
187        state: BlockStateId,
188        world: &Arc<World>,
189        hit: &ClipHitResult,
190        projectile: &dyn Projectile,
191    ) {
192        let Some(lit_state) = Self::projectile_lit_state(
193            state,
194            projectile.is_on_fire(),
195            projectile.projectile_may_interact(world, hit.block_pos),
196        ) else {
197            return;
198        };
199        world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
200    }
201
202    fn entity_inside(
203        &self,
204        state: BlockStateId,
205        world: &Arc<World>,
206        pos: BlockPos,
207        entity: &dyn Entity,
208        effect_collector: &mut InsideBlockEffectCollector,
209        is_precise: bool,
210    ) {
211        if let Some(damage) = self.contact_damage_amount(state, entity.is_living_entity()) {
212            entity.hurt(
213                world,
214                &DamageSource::environment(&vanilla_damage_types::CAMPFIRE),
215                damage,
216            );
217        }
218
219        self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
220    }
221
222    fn place_liquid(
223        &self,
224        level: &dyn LevelAccessor,
225        pos: BlockPos,
226        state: BlockStateId,
227        fluid_state: FluidState,
228    ) -> bool {
229        if state.try_get_value(WATERLOGGED) != Some(false)
230            || fluid_state.fluid_id != &vanilla_fluids::WATER
231        {
232            return false;
233        }
234
235        if state.get_value(LIT) {
236            level.play_block_sound(
237                &sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE,
238                pos,
239                1.0,
240                1.0,
241                None,
242            );
243            Self::douse(None, level, pos);
244        }
245
246        level.set_block_state(
247            pos,
248            state.set_value(WATERLOGGED, true).set_value(LIT, false),
249            UpdateFlags::UPDATE_ALL,
250        );
251        schedule_placed_liquid_tick(level, pos, fluid_state);
252        true
253    }
254
255    fn new_block_entity(
256        &self,
257        level: Weak<World>,
258        pos: BlockPos,
259        state: BlockStateId,
260    ) -> BlockEntityCreation {
261        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
262            &vanilla_block_entity_types::CAMPFIRE,
263            level,
264            pos,
265            state,
266        ))
267    }
268
269    fn get_block_entity_ticker(
270        &self,
271        _world: &Arc<World>,
272        _state: BlockStateId,
273        block_entity_type: BlockEntityTypeRef,
274    ) -> Option<BlockEntityTicker> {
275        BlockEntityTicker::for_matching_entity_tick(
276            block_entity_type,
277            &vanilla_block_entity_types::CAMPFIRE,
278        )
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::test_support::TestLevel;
286    use steel_registry::{
287        blocks::block_state_ext::BlockStateExt, init_vanilla_registry, vanilla_blocks,
288    };
289
290    #[test]
291    fn lit_campfire_damages_living_entities() {
292        init_vanilla_registry();
293        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
294        let state = vanilla_blocks::CAMPFIRE
295            .default_state()
296            .set_value(LIT, true);
297
298        assert_eq!(campfire.contact_damage_amount(state, true), Some(1.0));
299    }
300
301    #[test]
302    fn unlit_campfire_does_not_damage_entities() {
303        init_vanilla_registry();
304        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
305        let state = vanilla_blocks::CAMPFIRE
306            .default_state()
307            .set_value(LIT, false);
308
309        assert_eq!(campfire.contact_damage_amount(state, true), None);
310    }
311
312    #[test]
313    fn campfire_does_not_damage_non_living_entities() {
314        init_vanilla_registry();
315        let campfire = CampfireBlock::new(&vanilla_blocks::SOUL_CAMPFIRE, false, 2);
316        let state = vanilla_blocks::SOUL_CAMPFIRE
317            .default_state()
318            .set_value(LIT, true);
319
320        assert_eq!(campfire.contact_damage_amount(state, false), None);
321    }
322
323    #[test]
324    fn burning_projectile_lights_only_dry_unlit_campfires() {
325        init_vanilla_registry();
326
327        let unlit = vanilla_blocks::CAMPFIRE
328            .default_state()
329            .set_value(LIT, false)
330            .set_value(WATERLOGGED, false);
331        let lit = unlit.set_value(LIT, true);
332        let waterlogged = unlit.set_value(WATERLOGGED, true);
333
334        assert_eq!(
335            CampfireBlock::projectile_lit_state(unlit, true, true),
336            Some(lit)
337        );
338        assert_eq!(
339            CampfireBlock::projectile_lit_state(unlit, false, true),
340            None
341        );
342        assert_eq!(
343            CampfireBlock::projectile_lit_state(unlit, true, false),
344            None
345        );
346        assert_eq!(CampfireBlock::projectile_lit_state(lit, true, true), None);
347        assert_eq!(
348            CampfireBlock::projectile_lit_state(waterlogged, true, true),
349            None
350        );
351    }
352
353    #[test]
354    fn placement_state_sets_facing_and_signal_fire() {
355        init_vanilla_registry();
356        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
357
358        let state = campfire.placement_state(
359            false,
360            vanilla_blocks::HAY_BLOCK.default_state(),
361            Direction::East,
362        );
363
364        assert_eq!(state.get_value(HORIZONTAL_FACING), Direction::East);
365        assert!(state.get_value(SIGNAL_FIRE));
366        assert!(state.get_value(LIT));
367        assert!(!state.get_value(WATERLOGGED));
368    }
369
370    #[test]
371    fn update_shape_recomputes_signal_fire_from_below() {
372        init_vanilla_registry();
373        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
374        let level = TestLevel::default();
375        let state = vanilla_blocks::CAMPFIRE
376            .default_state()
377            .set_value(SIGNAL_FIRE, false)
378            .set_value(WATERLOGGED, false);
379
380        let updated = campfire.update_shape(
381            state,
382            &level,
383            BlockPos::ZERO,
384            Direction::Down,
385            BlockPos::ZERO.below(),
386            vanilla_blocks::HAY_BLOCK.default_state(),
387        );
388
389        assert!(updated.get_value(SIGNAL_FIRE));
390    }
391
392    #[test]
393    fn water_placement_extinguishes_lit_campfire() {
394        init_vanilla_registry();
395        let level = TestLevel::default();
396        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
397        let state = vanilla_blocks::CAMPFIRE
398            .default_state()
399            .set_value(LIT, true)
400            .set_value(WATERLOGGED, false);
401        let pos = BlockPos::new(1, 2, 3);
402
403        assert!(campfire.place_liquid(
404            &level,
405            pos,
406            state,
407            FluidState::source(&vanilla_fluids::WATER),
408        ));
409
410        let placed = level
411            .last_placed_state()
412            .expect("campfire should be updated");
413        assert!(!placed.get_value(LIT));
414        assert!(placed.get_value(WATERLOGGED));
415        assert_eq!(
416            level
417                .block_sounds
418                .borrow()
419                .iter()
420                .map(|sound| sound.sound)
421                .collect::<Vec<_>>(),
422            vec![&sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE]
423        );
424        assert_eq!(
425            level
426                .scheduled_fluid_ticks
427                .borrow()
428                .iter()
429                .map(|tick| (tick.pos, tick.fluid, tick.delay))
430                .collect::<Vec<_>>(),
431            vec![(pos, &vanilla_fluids::WATER, 5)]
432        );
433        assert_eq!(
434            level
435                .game_events
436                .borrow()
437                .iter()
438                .map(|event| event.event)
439                .collect::<Vec<_>>(),
440            vec![&vanilla_game_events::BLOCK_CHANGE]
441        );
442        assert_eq!(level.game_events.borrow()[0].affected_state, None);
443    }
444
445    #[test]
446    fn is_lit_campfire_accepts_only_lit_campfires() {
447        init_vanilla_registry();
448
449        let campfire = vanilla_blocks::CAMPFIRE.default_state();
450        let soul_campfire = vanilla_blocks::SOUL_CAMPFIRE.default_state();
451
452        assert!(CampfireBlock::is_lit_campfire(
453            campfire.set_value(LIT, true)
454        ));
455        assert!(!CampfireBlock::is_lit_campfire(
456            campfire.set_value(LIT, false)
457        ));
458        assert!(CampfireBlock::is_lit_campfire(
459            soul_campfire.set_value(LIT, true)
460        ));
461        assert!(!CampfireBlock::is_lit_campfire(
462            vanilla_blocks::CANDLE.default_state().set_value(LIT, true)
463        ));
464    }
465
466    #[test]
467    fn douse_emits_block_change_without_touching_the_block() {
468        init_vanilla_registry();
469        let level = TestLevel::default();
470        let pos = BlockPos::new(1, 2, 3);
471
472        CampfireBlock::douse(None, &level, pos);
473
474        assert!(level.last_placed_state().is_none());
475        let events = level.game_events.borrow();
476        assert_eq!(events.len(), 1);
477        assert_eq!(events[0].event, &vanilla_game_events::BLOCK_CHANGE);
478        assert_eq!(events[0].pos, pos);
479        assert_eq!(events[0].source_entity_id, None);
480        assert_eq!(events[0].affected_state, None);
481    }
482}