Skip to main content

steel_core/fluid/fluids/
lava.rs

1//! Lava fluid callbacks.
2//!
3//! Implements `FluidBehavior` and `FlowingFluid` for sharing base spread logic.
4
5use std::sync::Arc;
6
7use glam::DVec3;
8use steel_registry::REGISTRY;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::blocks::properties::Direction;
11use steel_registry::items::ItemRef;
12use steel_registry::level_events;
13use steel_registry::sound_event::SoundEventRef;
14use steel_registry::sound_events;
15use steel_registry::vanilla_blocks;
16use steel_registry::vanilla_game_rules::LAVA_SOURCE_CONVERSION;
17use steel_registry::vanilla_items;
18use steel_utils::BlockPos;
19use steel_utils::BlockStateId;
20use steel_utils::types::UpdateFlags;
21
22use crate::entity::{Entity, InsideBlockEffectCollector, InsideBlockEffectType};
23use crate::fluid::{FlowingFluid, FluidBehavior, get_flow as flowing_fluid_flow};
24use crate::fluid::{
25    FluidRef, FluidState, FluidStateExt, get_fluid_state, get_height, is_lava_fluid,
26    is_water_fluid, lava_id, liquid_block_state,
27};
28use crate::world::World;
29const NORMAL_LAVA_ENTITY_FLOW_SCALE: f64 = 0.002_333_333_333_333_333_5;
30const FAST_LAVA_ENTITY_FLOW_SCALE: f64 = 0.007;
31
32/// Lava fluid implementation.
33///
34/// Implements [`FluidBehavior`] with lava-specific parameters and
35/// behaviors (world-dependent spread, uphill delay, lava/water chemistry,
36/// fizz sounds).
37pub struct LavaFluid;
38
39impl LavaFluid {
40    /// Returns true if this world uses fast lava (nether-like).
41    fn is_fast_lava(world: &Arc<World>) -> bool {
42        world.dimension_type.fast_lava
43    }
44
45    /// Returns the lava current scale for entity fluid pushing.
46    pub(crate) fn entity_flow_scale(world: &Arc<World>) -> f64 {
47        if Self::is_fast_lava(world) {
48            FAST_LAVA_ENTITY_FLOW_SCALE
49        } else {
50            NORMAL_LAVA_ENTITY_FLOW_SCALE
51        }
52    }
53}
54
55impl FluidBehavior for LavaFluid {
56    fn pickup_sound(&self) -> Option<SoundEventRef> {
57        Some(&sound_events::ITEM_BUCKET_FILL_LAVA)
58    }
59
60    fn create_legacy_block(&self, fluid_state: FluidState) -> BlockStateId {
61        liquid_block_state(&vanilla_blocks::LAVA, fluid_state)
62    }
63
64    fn bucket(&self) -> ItemRef {
65        &vanilla_items::LAVA_BUCKET
66    }
67
68    fn fluid_type(&self) -> FluidRef {
69        lava_id()
70    }
71
72    fn is_same(&self, fluid: FluidRef) -> bool {
73        is_lava_fluid(fluid)
74    }
75
76    fn tick_delay(&self, world: &Arc<World>) -> i32 {
77        if Self::is_fast_lava(world) { 10 } else { 30 }
78    }
79
80    fn drop_off(&self, world: &Arc<World>) -> u8 {
81        if Self::is_fast_lava(world) { 1 } else { 2 }
82    }
83
84    fn slope_find_distance(&self, world: &Arc<World>) -> u8 {
85        if Self::is_fast_lava(world) { 4 } else { 2 }
86    }
87
88    fn explosion_resistance(&self) -> f32 {
89        100.0
90    }
91
92    fn can_convert_to_source(&self, world: &Arc<World>) -> bool {
93        world.get_game_rule(&LAVA_SOURCE_CONVERSION)
94    }
95
96    fn get_flow(&self, world: &Arc<World>, pos: BlockPos, fluid_state: FluidState) -> DVec3 {
97        flowing_fluid_flow(world, pos, fluid_state)
98    }
99
100    /// Lava can be replaced if its effective height >= 0.444 and the replacer is water.
101    ///
102    /// Uses `get_height()` (not raw `amount / 9.0`) so that falling lava
103    /// (same fluid directly above → height = 1.0) is also correctly handled.
104    fn can_be_replaced_with(
105        &self,
106        fluid_state: FluidState,
107        world: &Arc<World>,
108        pos: BlockPos,
109        other_fluid: FluidRef,
110        _direction: Direction,
111    ) -> bool {
112        get_height(world, pos, fluid_state) >= 0.444_444_45 && is_water_fluid(other_fluid)
113    }
114
115    /// Uphill lava spreads 4× slower with 3/4 probability.
116    ///
117    /// "Uphill" means the target position (`new_state`) has a greater effective
118    /// height than the source (`old_state`). Uses `get_height()` so that a
119    /// falling lava source (height = 1.0) is correctly treated as "tall".
120    fn get_spread_delay(
121        &self,
122        world: &Arc<World>,
123        pos: BlockPos,
124        old_state: FluidState,
125        new_state: FluidState,
126    ) -> i32 {
127        let base = self.tick_delay(world);
128        if !old_state.is_empty()
129            && !new_state.is_empty()
130            && !old_state.falling
131            && !new_state.falling
132            && get_height(world, pos, new_state) > get_height(world, pos, old_state)
133            && rand::random_range(0u32..4) != 0
134        {
135            base * 4
136        } else {
137            base
138        }
139    }
140
141    /// Plays the fizz sound. Lava does NOT drop block items (unlike water).
142    fn before_destroying_block(&self, world: &Arc<World>, pos: BlockPos, _state: BlockStateId) {
143        world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
144    }
145
146    /// Clears freezing, ignites, then applies lava damage.
147    fn entity_inside(
148        &self,
149        _world: &Arc<World>,
150        _pos: BlockPos,
151        _entity: &dyn Entity,
152        effect_collector: &mut InsideBlockEffectCollector,
153    ) {
154        effect_collector.apply(InsideBlockEffectType::ClearFreeze);
155        effect_collector.apply(InsideBlockEffectType::LavaIgnite);
156        effect_collector.run_after(
157            InsideBlockEffectType::LavaIgnite,
158            Box::new(Entity::lava_hurt),
159        );
160    }
161
162    fn tick(
163        &self,
164        world: &Arc<World>,
165        pos: BlockPos,
166        block_state: BlockStateId,
167        fluid_state: FluidState,
168    ) {
169        self.base_tick(world, pos, block_state, fluid_state);
170    }
171
172    fn spread(
173        &self,
174        world: &Arc<World>,
175        pos: BlockPos,
176        block_state: BlockStateId,
177        fluid_state: FluidState,
178    ) {
179        self.base_spread(world, pos, block_state, fluid_state);
180    }
181}
182
183// Marker impl to provide base FlowingFluid logic
184impl FlowingFluid for LavaFluid {
185    fn spread_to(
186        &self,
187        world: &Arc<World>,
188        pos: BlockPos,
189        fluid_state: FluidState,
190        direction: Direction,
191    ) {
192        if direction == Direction::Down {
193            let below_fluid = get_fluid_state(world, pos);
194            if below_fluid.is_water() {
195                // Fizz always plays when lava meets water going down,
196                // regardless of whether stone is formed.
197                world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
198
199                // Stone only forms when the target is a pure water LiquidBlock,
200                // not a waterlogged block (stairs, slabs, etc.).
201                let below_block = world.get_block_state(pos).get_block();
202                if below_block == &vanilla_blocks::WATER {
203                    world.set_block(
204                        pos,
205                        REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE),
206                        UpdateFlags::UPDATE_ALL_IMMEDIATE,
207                    );
208                }
209                return;
210            }
211        }
212
213        self.base_spread_to(world, pos, fluid_state);
214    }
215}