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