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::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_blocks;
12use steel_registry::vanilla_damage_types;
13use steel_registry::vanilla_dimension_types;
14use steel_utils::axis::Axis;
15use steel_utils::types::UpdateFlags;
16use steel_utils::{BlockPos, BlockStateId, Direction};
17
18use crate::behavior::block::BlockBehavior;
19use crate::behavior::context::BlockPlaceContext;
20use crate::entity::damage::DamageSource;
21use crate::entity::{Entity, InsideBlockEffectCollector, InsideBlockEffectType};
22use crate::portal::portal_shape::{PortalShape, nether_portal_config};
23use crate::world::{LevelReader, World};
24
25#[block_behavior]
27pub struct FireBlock {
28 block: BlockRef,
29}
30
31impl FireBlock {
32 #[must_use]
34 pub const fn new(block: BlockRef) -> Self {
35 Self { block }
36 }
37
38 pub(crate) fn in_portal_world(world: &World) -> bool {
43 world.dimension_type == &vanilla_dimension_types::OVERWORLD
44 || world.dimension_type == &vanilla_dimension_types::THE_NETHER
45 }
46
47 pub(crate) fn can_be_placed_at(
50 world: &Arc<World>,
51 pos: BlockPos,
52 forward_dir: Direction,
53 ) -> bool {
54 if !world.get_block_state(pos).is_air() {
55 return false;
56 }
57 Self::selected_fire_can_survive_at(world.as_ref(), pos)
58 || Self::is_portal(world, pos, forward_dir)
59 }
60
61 pub(crate) fn get_state(world: &dyn LevelReader, pos: BlockPos) -> BlockStateId {
64 if SoulFireBlock::can_survive_at(world, pos) {
65 vanilla_blocks::SOUL_FIRE.default_state()
66 } else {
67 vanilla_blocks::FIRE.default_state()
68 }
69 }
70
71 fn selected_fire_can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
72 SoulFireBlock::can_survive_at(world, pos) || Self::can_survive_at(world, pos)
73 }
74
75 fn can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
78 let below_pos = pos.below();
79 world.is_face_sturdy(world.get_block_state(below_pos), below_pos, Direction::Up)
80 }
82
83 fn is_portal(world: &Arc<World>, pos: BlockPos, forward_dir: Direction) -> bool {
86 if !Self::in_portal_world(world) {
87 return false;
88 }
89
90 let has_obsidian = Direction::ALL.iter().any(|&dir| {
91 world.get_block_state(pos.relative(dir)).get_block() == &vanilla_blocks::OBSIDIAN
92 });
93 if !has_obsidian {
94 return false;
95 }
96
97 let preferred_axis = if forward_dir.get_axis().is_horizontal() {
98 forward_dir.rotate_y_counter_clockwise().get_axis()
99 } else if rand::random::<bool>() {
100 Axis::X
101 } else {
102 Axis::Z
103 };
104
105 let config = nether_portal_config();
106 PortalShape::find_empty_portal_shape_with_axis(world, pos, preferred_axis, &config)
107 .is_some()
108 }
109
110 fn queue_entity_contact_effects(
111 effect_collector: &mut InsideBlockEffectCollector,
112 fire_damage: f32,
113 ) {
114 effect_collector.apply(InsideBlockEffectType::ClearFreeze);
115 effect_collector.apply(InsideBlockEffectType::FireIgnite);
116 effect_collector.run_after(
117 InsideBlockEffectType::FireIgnite,
118 Box::new(move |entity| {
119 if !entity.fire_immune()
120 && let Some(entity_world) = entity.level()
121 {
122 entity.hurt(
123 &entity_world,
124 &DamageSource::environment(&vanilla_damage_types::IN_FIRE),
125 fire_damage,
126 );
127 }
128 }),
129 );
130 }
131}
132
133impl BlockBehavior for FireBlock {
134 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
135 if SoulFireBlock::can_survive_at(context.world.as_ref(), context.place_pos()) {
136 Some(vanilla_blocks::SOUL_FIRE.default_state())
137 } else {
138 Some(self.block.default_state())
139 }
140 }
141
142 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
143 Self::can_survive_at(world, pos)
144 }
145
146 fn entity_inside(
147 &self,
148 _state: BlockStateId,
149 _world: &Arc<World>,
150 _pos: BlockPos,
151 _entity: &dyn Entity,
152 effect_collector: &mut InsideBlockEffectCollector,
153 _is_precise: bool,
154 ) {
155 Self::queue_entity_contact_effects(effect_collector, 1.0);
156 }
157
158 fn on_place(
159 &self,
160 state: BlockStateId,
161 world: &Arc<World>,
162 pos: BlockPos,
163 old_state: BlockStateId,
164 _moved_by_piston: bool,
165 ) {
166 if old_state.get_block() == state.get_block() {
168 return;
169 }
170
171 if Self::in_portal_world(world)
172 && let Some(shape) =
173 PortalShape::find_empty_portal_shape(world, pos, &nether_portal_config())
174 {
175 shape.place_portal_blocks(world);
176 return;
177 }
178
179 if !self.can_survive(state, world, pos) {
180 world.set_block(
181 pos,
182 vanilla_blocks::AIR.default_state(),
183 UpdateFlags::UPDATE_ALL,
184 );
185 }
186 }
187}
188
189#[block_behavior]
195pub struct SoulFireBlock {
196 block: BlockRef,
197}
198
199impl SoulFireBlock {
200 #[must_use]
202 pub const fn new(block: BlockRef) -> Self {
203 Self { block }
204 }
205
206 fn can_survive_at(world: &dyn LevelReader, pos: BlockPos) -> bool {
207 let block_below = world.get_block_state(pos.below()).get_block();
208 block_below.has_tag(&BlockTag::SOUL_FIRE_BASE_BLOCKS)
209 }
210}
211
212impl BlockBehavior for SoulFireBlock {
213 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
214 let state = self.block.default_state();
215 self.can_survive(state, context.world, context.place_pos())
216 .then_some(state)
217 }
218
219 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
220 Self::can_survive_at(world, pos)
221 }
222
223 fn entity_inside(
224 &self,
225 _state: BlockStateId,
226 _world: &Arc<World>,
227 _pos: BlockPos,
228 _entity: &dyn Entity,
229 effect_collector: &mut InsideBlockEffectCollector,
230 _is_precise: bool,
231 ) {
232 FireBlock::queue_entity_contact_effects(effect_collector, 2.0);
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use steel_registry::{
239 blocks::block_state_ext::BlockStateExt, init_vanilla_registry, vanilla_blocks,
240 };
241 use steel_utils::{BlockPos, BlockStateId};
242
243 use crate::test_support::TestLevel;
244
245 use super::FireBlock;
246
247 const POS: BlockPos = BlockPos::new(0, 64, 0);
248
249 fn level_with_support(support_state: BlockStateId) -> TestLevel {
250 TestLevel::default()
251 .with_min_y(0)
252 .with_block(POS.below(), support_state)
253 }
254
255 #[test]
256 fn get_state_selects_soul_fire_on_soul_fire_base_block() {
257 init_vanilla_registry();
258
259 let level = level_with_support(vanilla_blocks::SOUL_SAND.default_state());
260
261 assert_eq!(
262 FireBlock::get_state(&level, POS).get_block(),
263 &vanilla_blocks::SOUL_FIRE
264 );
265 assert!(FireBlock::selected_fire_can_survive_at(&level, POS));
266 }
267
268 #[test]
269 fn get_state_selects_regular_fire_otherwise() {
270 init_vanilla_registry();
271
272 let level = level_with_support(vanilla_blocks::STONE.default_state());
273
274 assert_eq!(
275 FireBlock::get_state(&level, POS).get_block(),
276 &vanilla_blocks::FIRE
277 );
278 }
279}