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_blocks, vanilla_custom_stats,
14    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
104impl BlockBehavior for CampfireBlock {
105    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
106        let waterlogged = context.is_water_source();
107        let below_state = context.world.get_block_state(context.place_pos().below());
108        Some(self.placement_state(waterlogged, below_state, context.horizontal_direction()))
109    }
110
111    fn use_item_on(
112        &self,
113        _state: BlockStateId,
114        world: &Arc<World>,
115        pos: BlockPos,
116        player: &Player,
117        _hand: InteractionHand,
118        _hit_result: &BlockHitResult,
119        inv: &mut InventoryAccess,
120    ) -> InteractionResult {
121        let accepted = inv.with_item(|stack| {
122            REGISTRY
123                .recipes
124                .find_match(
125                    &vanilla_recipe_types::CAMPFIRE_COOKING,
126                    &SingleItemRecipeInput::new(stack.clone()),
127                )
128                .is_some()
129        });
130        if !accepted {
131            return InteractionResult::TryEmptyHandInteraction;
132        }
133
134        let Some(block_entity) = world.get_block_entity(pos) else {
135            return InteractionResult::TryEmptyHandInteraction;
136        };
137        let Some(campfire) = block_entity.downcast_ref::<CampfireBlockEntity>() else {
138            return InteractionResult::TryEmptyHandInteraction;
139        };
140        let item = inv.with_item(|stack| stack.copy_with_count(1));
141        if !campfire.place_food(player, item) {
142            return InteractionResult::Consume;
143        }
144        if !player.has_infinite_materials() {
145            inv.with_item(|stack| stack.shrink(1));
146        }
147        player.award_custom_stat(&vanilla_custom_stats::INTERACT_WITH_CAMPFIRE);
148        InteractionResult::SuccessServer
149    }
150
151    fn update_shape(
152        &self,
153        state: BlockStateId,
154        world: &dyn ScheduledTickAccess,
155        pos: BlockPos,
156        direction: Direction,
157        _neighbor_pos: BlockPos,
158        neighbor_state: BlockStateId,
159    ) -> BlockStateId {
160        schedule_water_tick_if_waterlogged(state, world, pos);
161
162        if direction == Direction::Down {
163            state.set_value(SIGNAL_FIRE, Self::is_smoke_source(neighbor_state))
164        } else {
165            state
166        }
167    }
168
169    fn on_projectile_hit(
170        &self,
171        state: BlockStateId,
172        world: &Arc<World>,
173        hit: &ClipHitResult,
174        projectile: &dyn Projectile,
175    ) {
176        let Some(lit_state) = Self::projectile_lit_state(
177            state,
178            projectile.is_on_fire(),
179            projectile.projectile_may_interact(world, hit.block_pos),
180        ) else {
181            return;
182        };
183        world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
184    }
185
186    fn entity_inside(
187        &self,
188        state: BlockStateId,
189        world: &Arc<World>,
190        pos: BlockPos,
191        entity: &dyn Entity,
192        effect_collector: &mut InsideBlockEffectCollector,
193        is_precise: bool,
194    ) {
195        if let Some(damage) = self.contact_damage_amount(state, entity.is_living_entity()) {
196            entity.hurt(
197                world,
198                &DamageSource::environment(&vanilla_damage_types::CAMPFIRE),
199                damage,
200            );
201        }
202
203        self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
204    }
205
206    fn place_liquid(
207        &self,
208        level: &dyn LevelAccessor,
209        pos: BlockPos,
210        state: BlockStateId,
211        fluid_state: FluidState,
212    ) -> bool {
213        if state.try_get_value(WATERLOGGED) != Some(false)
214            || fluid_state.fluid_id != &vanilla_fluids::WATER
215        {
216            return false;
217        }
218
219        if state.get_value(LIT) {
220            level.play_block_sound(
221                &sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE,
222                pos,
223                1.0,
224                1.0,
225                None,
226            );
227            level.game_event(
228                &vanilla_game_events::BLOCK_CHANGE,
229                pos,
230                &GameEventContext::new(None, Some(state.set_value(LIT, false))),
231            );
232        }
233
234        level.set_block_state(
235            pos,
236            state.set_value(WATERLOGGED, true).set_value(LIT, false),
237            UpdateFlags::UPDATE_ALL,
238        );
239        schedule_placed_liquid_tick(level, pos, fluid_state);
240        true
241    }
242
243    fn new_block_entity(
244        &self,
245        level: Weak<World>,
246        pos: BlockPos,
247        state: BlockStateId,
248    ) -> BlockEntityCreation {
249        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
250            &vanilla_block_entity_types::CAMPFIRE,
251            level,
252            pos,
253            state,
254        ))
255    }
256
257    fn get_block_entity_ticker(
258        &self,
259        _world: &Arc<World>,
260        _state: BlockStateId,
261        block_entity_type: BlockEntityTypeRef,
262    ) -> Option<BlockEntityTicker> {
263        BlockEntityTicker::for_matching_entity_tick(
264            block_entity_type,
265            &vanilla_block_entity_types::CAMPFIRE,
266        )
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::test_support::TestLevel;
274    use steel_registry::{
275        blocks::block_state_ext::BlockStateExt, init_vanilla_registry, vanilla_blocks,
276    };
277
278    #[test]
279    fn lit_campfire_damages_living_entities() {
280        init_vanilla_registry();
281        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
282        let state = vanilla_blocks::CAMPFIRE
283            .default_state()
284            .set_value(LIT, true);
285
286        assert_eq!(campfire.contact_damage_amount(state, true), Some(1.0));
287    }
288
289    #[test]
290    fn unlit_campfire_does_not_damage_entities() {
291        init_vanilla_registry();
292        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
293        let state = vanilla_blocks::CAMPFIRE
294            .default_state()
295            .set_value(LIT, false);
296
297        assert_eq!(campfire.contact_damage_amount(state, true), None);
298    }
299
300    #[test]
301    fn campfire_does_not_damage_non_living_entities() {
302        init_vanilla_registry();
303        let campfire = CampfireBlock::new(&vanilla_blocks::SOUL_CAMPFIRE, false, 2);
304        let state = vanilla_blocks::SOUL_CAMPFIRE
305            .default_state()
306            .set_value(LIT, true);
307
308        assert_eq!(campfire.contact_damage_amount(state, false), None);
309    }
310
311    #[test]
312    fn burning_projectile_lights_only_dry_unlit_campfires() {
313        init_vanilla_registry();
314
315        let unlit = vanilla_blocks::CAMPFIRE
316            .default_state()
317            .set_value(LIT, false)
318            .set_value(WATERLOGGED, false);
319        let lit = unlit.set_value(LIT, true);
320        let waterlogged = unlit.set_value(WATERLOGGED, true);
321
322        assert_eq!(
323            CampfireBlock::projectile_lit_state(unlit, true, true),
324            Some(lit)
325        );
326        assert_eq!(
327            CampfireBlock::projectile_lit_state(unlit, false, true),
328            None
329        );
330        assert_eq!(
331            CampfireBlock::projectile_lit_state(unlit, true, false),
332            None
333        );
334        assert_eq!(CampfireBlock::projectile_lit_state(lit, true, true), None);
335        assert_eq!(
336            CampfireBlock::projectile_lit_state(waterlogged, true, true),
337            None
338        );
339    }
340
341    #[test]
342    fn placement_state_sets_facing_and_signal_fire() {
343        init_vanilla_registry();
344        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
345
346        let state = campfire.placement_state(
347            false,
348            vanilla_blocks::HAY_BLOCK.default_state(),
349            Direction::East,
350        );
351
352        assert_eq!(state.get_value(HORIZONTAL_FACING), Direction::East);
353        assert!(state.get_value(SIGNAL_FIRE));
354        assert!(state.get_value(LIT));
355        assert!(!state.get_value(WATERLOGGED));
356    }
357
358    #[test]
359    fn update_shape_recomputes_signal_fire_from_below() {
360        init_vanilla_registry();
361        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
362        let level = TestLevel::default();
363        let state = vanilla_blocks::CAMPFIRE
364            .default_state()
365            .set_value(SIGNAL_FIRE, false)
366            .set_value(WATERLOGGED, false);
367
368        let updated = campfire.update_shape(
369            state,
370            &level,
371            BlockPos::ZERO,
372            Direction::Down,
373            BlockPos::ZERO.below(),
374            vanilla_blocks::HAY_BLOCK.default_state(),
375        );
376
377        assert!(updated.get_value(SIGNAL_FIRE));
378    }
379
380    #[test]
381    fn water_placement_extinguishes_lit_campfire() {
382        init_vanilla_registry();
383        let level = TestLevel::default();
384        let campfire = CampfireBlock::new(&vanilla_blocks::CAMPFIRE, true, 1);
385        let state = vanilla_blocks::CAMPFIRE
386            .default_state()
387            .set_value(LIT, true)
388            .set_value(WATERLOGGED, false);
389        let pos = BlockPos::new(1, 2, 3);
390
391        assert!(campfire.place_liquid(
392            &level,
393            pos,
394            state,
395            FluidState::source(&vanilla_fluids::WATER),
396        ));
397
398        let placed = level
399            .last_placed_state()
400            .expect("campfire should be updated");
401        assert!(!placed.get_value(LIT));
402        assert!(placed.get_value(WATERLOGGED));
403        assert_eq!(
404            level
405                .block_sounds
406                .borrow()
407                .iter()
408                .map(|sound| sound.sound)
409                .collect::<Vec<_>>(),
410            vec![&sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE]
411        );
412        assert_eq!(
413            level
414                .scheduled_fluid_ticks
415                .borrow()
416                .iter()
417                .map(|tick| (tick.pos, tick.fluid, tick.delay))
418                .collect::<Vec<_>>(),
419            vec![(pos, &vanilla_fluids::WATER, 5)]
420        );
421        assert_eq!(
422            level
423                .game_events
424                .borrow()
425                .iter()
426                .map(|event| event.event)
427                .collect::<Vec<_>>(),
428            vec![&vanilla_game_events::BLOCK_CHANGE]
429        );
430    }
431}