steel_core/behavior/blocks/portal/
fire.rs1use std::sync::Arc;
7use steel_macros::block_behavior;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::level_events;
11use steel_registry::vanilla_block_tags::BlockTag;
12use steel_registry::vanilla_blocks;
13use steel_registry::vanilla_damage_types;
14use steel_registry::vanilla_dimension_types;
15use steel_utils::axis::Axis;
16use steel_utils::types::UpdateFlags;
17use steel_utils::{BlockPos, BlockStateId, Direction};
18
19use crate::behavior::block::BlockBehavior;
20use crate::behavior::context::BlockPlaceContext;
21use crate::entity::damage::DamageSource;
22use crate::entity::{Entity, InsideBlockEffectCollector, InsideBlockEffectType};
23use crate::player::Player;
24use crate::portal::portal_shape::{PortalShape, nether_portal_config};
25use crate::world::{LevelReader, ScheduledTickAccess, World};
26#[block_behavior]
28pub struct FireBlock {
29 block: BlockRef,
30}
31
32impl FireBlock {
33 #[must_use]
35 pub const fn new(block: BlockRef) -> Self {
36 Self { block }
37 }
38
39 pub(crate) fn in_portal_world(world: &World) -> bool {
44 world.dimension_type == &vanilla_dimension_types::OVERWORLD
45 || world.dimension_type == &vanilla_dimension_types::THE_NETHER
46 }
47
48 pub(crate) fn can_be_placed_at(
51 world: &Arc<World>,
52 pos: BlockPos,
53 forward_dir: Direction,
54 ) -> bool {
55 if !world.get_block_state(pos).is_air() {
56 return false;
57 }
58 Self::selected_fire_can_survive_at(world.as_ref(), pos)
59 || Self::is_portal(world, pos, forward_dir)
60 }
61
62 pub(crate) fn get_state(world: &dyn LevelReader, pos: BlockPos) -> BlockStateId {
65 if SoulFireBlock::can_survive_at(world, pos) {
66 vanilla_blocks::SOUL_FIRE.default_state()
67 } else {
68 vanilla_blocks::FIRE.default_state()
69 }
70 }
71
72 fn selected_fire_can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
73 SoulFireBlock::can_survive_at(world, pos) || Self::can_survive_at(world, pos)
74 }
75
76 fn can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
79 let below_pos = pos.below();
80 world.is_face_sturdy(world.get_block_state(below_pos), below_pos, Direction::Up)
81 }
83
84 fn is_portal(world: &Arc<World>, pos: BlockPos, forward_dir: Direction) -> bool {
87 if !Self::in_portal_world(world) {
88 return false;
89 }
90
91 let has_obsidian = Direction::ALL.iter().any(|&dir| {
92 world.get_block_state(pos.relative(dir)).get_block() == &vanilla_blocks::OBSIDIAN
93 });
94 if !has_obsidian {
95 return false;
96 }
97
98 let preferred_axis = if forward_dir.get_axis().is_horizontal() {
99 forward_dir.rotate_y_counter_clockwise().get_axis()
100 } else if rand::random::<bool>() {
101 Axis::X
102 } else {
103 Axis::Z
104 };
105
106 let config = nether_portal_config();
107 PortalShape::find_empty_portal_shape_with_axis(world, pos, preferred_axis, &config)
108 .is_some()
109 }
110
111 fn queue_entity_contact_effects(
112 effect_collector: &mut InsideBlockEffectCollector,
113 fire_damage: f32,
114 ) {
115 effect_collector.apply(InsideBlockEffectType::ClearFreeze);
116 effect_collector.apply(InsideBlockEffectType::FireIgnite);
117 effect_collector.run_after(
118 InsideBlockEffectType::FireIgnite,
119 Box::new(move |entity| {
120 if !entity.fire_immune()
121 && let Some(entity_world) = entity.level()
122 {
123 entity.hurt(
124 &entity_world,
125 &DamageSource::environment(&vanilla_damage_types::IN_FIRE),
126 fire_damage,
127 );
128 }
129 }),
130 );
131 }
132}
133
134impl BlockBehavior for FireBlock {
135 fn update_shape(
138 &self,
139 state: BlockStateId,
140 world: &dyn ScheduledTickAccess,
141 pos: BlockPos,
142 direction: Direction,
143 _neighbor_pos: BlockPos,
144 _neighbor_state: BlockStateId,
145 ) -> BlockStateId {
146 if direction == Direction::Down && !self.can_survive(state, world, pos) {
147 return vanilla_blocks::AIR.default_state();
148 }
149 state
150 }
151 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
152 if SoulFireBlock::can_survive_at(context.world.as_ref(), context.place_pos()) {
153 Some(vanilla_blocks::SOUL_FIRE.default_state())
154 } else {
155 Some(self.block.default_state())
156 }
157 }
158
159 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
160 Self::can_survive_at(world, pos)
161 }
162
163 fn entity_inside(
164 &self,
165 _state: BlockStateId,
166 _world: &Arc<World>,
167 _pos: BlockPos,
168 _entity: &dyn Entity,
169 effect_collector: &mut InsideBlockEffectCollector,
170 _is_precise: bool,
171 ) {
172 Self::queue_entity_contact_effects(effect_collector, 1.0);
173 }
174
175 fn on_place(
176 &self,
177 state: BlockStateId,
178 world: &Arc<World>,
179 pos: BlockPos,
180 old_state: BlockStateId,
181 _moved_by_piston: bool,
182 ) {
183 if old_state.get_block() == state.get_block() {
185 return;
186 }
187
188 if Self::in_portal_world(world)
189 && let Some(shape) =
190 PortalShape::find_empty_portal_shape(world, pos, &nether_portal_config())
191 {
192 shape.place_portal_blocks(world);
193 return;
194 }
195
196 if !self.can_survive(state, world, pos) {
197 world.set_block(
198 pos,
199 vanilla_blocks::AIR.default_state(),
200 UpdateFlags::UPDATE_ALL,
201 );
202 }
203 }
204 fn player_will_destroy(
205 &self,
206 state: BlockStateId,
207 world: &Arc<World>,
208 pos: BlockPos,
209 _player: &Player,
210 ) -> BlockStateId {
211 world.level_event(level_events::SOUND_EXTINGUISH_FIRE, pos, 0, None);
212 state
213 }
214}
215
216#[block_behavior]
221pub struct SoulFireBlock {
222 block: BlockRef,
223}
224
225impl SoulFireBlock {
226 #[must_use]
228 pub const fn new(block: BlockRef) -> Self {
229 Self { block }
230 }
231
232 fn can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
233 let block_below = world.get_block_state(pos.below()).get_block();
234 block_below.has_tag(&BlockTag::SOUL_FIRE_BASE_BLOCKS)
235 }
236}
237
238impl BlockBehavior for SoulFireBlock {
239 fn update_shape(
241 &self,
242 state: BlockStateId,
243 world: &dyn ScheduledTickAccess,
244 pos: BlockPos,
245 direction: Direction,
246 _neighbor_pos: BlockPos,
247 _neighbor_state: BlockStateId,
248 ) -> BlockStateId {
249 if direction == Direction::Down && !self.can_survive(state, world, pos) {
250 return vanilla_blocks::AIR.default_state();
251 }
252 state
253 }
254
255 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
256 let state = self.block.default_state();
257 self.can_survive(state, context.world, context.place_pos())
258 .then_some(state)
259 }
260
261 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
262 Self::can_survive_at(world, pos)
263 }
264
265 fn entity_inside(
266 &self,
267 _state: BlockStateId,
268 _world: &Arc<World>,
269 _pos: BlockPos,
270 _entity: &dyn Entity,
271 effect_collector: &mut InsideBlockEffectCollector,
272 _is_precise: bool,
273 ) {
274 FireBlock::queue_entity_contact_effects(effect_collector, 2.0);
275 }
276 fn player_will_destroy(
277 &self,
278 state: BlockStateId,
279 world: &Arc<World>,
280 pos: BlockPos,
281 _player: &Player,
282 ) -> BlockStateId {
283 world.level_event(level_events::SOUND_EXTINGUISH_FIRE, pos, 0, None);
284 state
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use steel_registry::{
291 blocks::block_state_ext::BlockStateExt, init_vanilla_registry, vanilla_blocks,
292 };
293 use steel_utils::{BlockPos, BlockStateId, Direction};
294
295 use crate::behavior::block::BlockBehavior;
296 use crate::test_support::TestLevel;
297
298 use super::{FireBlock, SoulFireBlock};
299
300 const POS: BlockPos = BlockPos::new(0, 64, 0);
301
302 fn level_with_support(support_state: BlockStateId) -> TestLevel {
303 TestLevel::default()
304 .with_min_y(0)
305 .with_block(POS.below(), support_state)
306 }
307
308 #[test]
309 fn get_state_selects_soul_fire_on_soul_fire_base_block() {
310 init_vanilla_registry();
311
312 let level = level_with_support(vanilla_blocks::SOUL_SAND.default_state());
313
314 assert_eq!(
315 FireBlock::get_state(&level, POS).get_block(),
316 &vanilla_blocks::SOUL_FIRE
317 );
318 assert!(FireBlock::selected_fire_can_survive_at(&level, POS));
319 }
320
321 #[test]
322 fn get_state_selects_regular_fire_otherwise() {
323 init_vanilla_registry();
324
325 let level = level_with_support(vanilla_blocks::STONE.default_state());
326
327 assert_eq!(
328 FireBlock::get_state(&level, POS).get_block(),
329 &vanilla_blocks::FIRE
330 );
331 }
332 #[test]
333 fn update_shape_removes_unsupported_fire() {
334 init_vanilla_registry();
335 let behavior = FireBlock::new(&vanilla_blocks::FIRE);
336 let state = vanilla_blocks::FIRE.default_state();
337 let level = TestLevel::default();
338 let result = behavior.update_shape(
339 state,
340 &level,
341 POS,
342 Direction::Down,
343 POS.below(),
344 vanilla_blocks::AIR.default_state(),
345 );
346 assert_eq!(result.get_block(), &vanilla_blocks::AIR);
347 }
348
349 #[test]
350 fn update_shape_removes_unsupported_soul_fire() {
351 init_vanilla_registry();
352 let behavior = SoulFireBlock::new(&vanilla_blocks::SOUL_FIRE);
353 let state = vanilla_blocks::SOUL_FIRE.default_state();
354 let level = TestLevel::default();
355 let result = behavior.update_shape(
356 state,
357 &level,
358 POS,
359 Direction::Down,
360 POS.below(),
361 vanilla_blocks::AIR.default_state(),
362 );
363 assert_eq!(result.get_block(), &vanilla_blocks::AIR);
364 }
365}