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