Skip to main content

steel_core/fluid/fluids/
lava.rs

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