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#[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 #[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 #[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 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}