diff --git a/assets/world/style/colors.ron b/assets/world/style/colors.ron index b46018a8c7..78e282b410 100644 --- a/assets/world/style/colors.ron +++ b/assets/world/style/colors.ron @@ -64,6 +64,7 @@ cave_roof: (38, 21, 79), dirt: (69, 48, 15), scaffold: (195, 190, 212), + lava: (184, 39, 0), vein: (222, 140, 39), ), site: ( diff --git a/common/net/src/msg/compression.rs b/common/net/src/msg/compression.rs index e6666503a2..f48f22fb63 100644 --- a/common/net/src/msg/compression.rs +++ b/common/net/src/msg/compression.rs @@ -557,7 +557,7 @@ impl VoxelImageDecoding for TriPngEncoding Rgb { r: 0, g: 0, b: 0 }, + Air | Water | Lava => Rgb { r: 0, g: 0, b: 0 }, Rock => Rgb { r: 93, g: 110, diff --git a/common/src/comp/fluid_dynamics.rs b/common/src/comp/fluid_dynamics.rs index 6529efc82c..6e233545e3 100644 --- a/common/src/comp/fluid_dynamics.rs +++ b/common/src/comp/fluid_dynamics.rs @@ -2,18 +2,45 @@ use super::body::{object, Body}; use super::{Density, Ori, Vel}; use crate::{ - consts::{AIR_DENSITY, WATER_DENSITY}, + consts::{AIR_DENSITY, LAVA_DENSITY, WATER_DENSITY}, util::{Dir, Plane, Projection}, }; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; use vek::*; +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum LiquidKind { + Water, + Lava, +} + +impl LiquidKind { + /// If an entity is in multiple overlapping liquid blocks, which one takes + /// precedence? (should be a rare edge case, since checkerboard patterns of + /// water and lava shouldn't show up in worldgen) + pub fn merge(self, other: LiquidKind) -> LiquidKind { + use LiquidKind::{Lava, Water}; + match (self, other) { + (Water, Water) => Water, + (Water, Lava) => Lava, + (Lava, _) => Lava, + } + } +} + /// Fluid medium in which the entity exists #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Fluid { - Air { vel: Vel, elevation: f32 }, - Water { vel: Vel, depth: f32 }, + Air { + vel: Vel, + elevation: f32, + }, + Liquid { + kind: LiquidKind, + vel: Vel, + depth: f32, + }, } impl Fluid { @@ -21,7 +48,14 @@ impl Fluid { pub fn density(&self) -> Density { match self { Self::Air { .. } => Density(AIR_DENSITY), - Self::Water { .. } => Density(WATER_DENSITY), + Self::Liquid { + kind: LiquidKind::Water, + .. + } => Density(WATER_DENSITY), + Self::Liquid { + kind: LiquidKind::Lava, + .. + } => Density(LAVA_DENSITY), } } @@ -53,14 +87,14 @@ impl Fluid { pub fn flow_vel(&self) -> Vel { match self { Self::Air { vel, .. } => *vel, - Self::Water { vel, .. } => *vel, + Self::Liquid { vel, .. } => *vel, } } // Very simple but useful in reducing mental overhead pub fn relative_flow(&self, vel: &Vel) -> Vel { Vel(self.flow_vel().0 - vel.0) } - pub fn is_liquid(&self) -> bool { matches!(self, Fluid::Water { .. }) } + pub fn is_liquid(&self) -> bool { matches!(self, Fluid::Liquid { .. }) } pub fn elevation(&self) -> Option { match self { @@ -71,7 +105,7 @@ impl Fluid { pub fn depth(&self) -> Option { match self { - Fluid::Water { depth, .. } => Some(*depth), + Fluid::Liquid { depth, .. } => Some(*depth), _ => None, } } diff --git a/common/src/consts.rs b/common/src/consts.rs index e4a047e5a8..4cbaa9598e 100644 --- a/common/src/consts.rs +++ b/common/src/consts.rs @@ -13,6 +13,9 @@ pub const FRIC_GROUND: f32 = 0.15; // kg/m³ pub const AIR_DENSITY: f32 = 1.225; pub const WATER_DENSITY: f32 = 999.1026; +// LAVA_DENSITY is unsourced, estimated as "roughly three times higher" than +// water +pub const LAVA_DENSITY: f32 = 3000.0; pub const IRON_DENSITY: f32 = 7870.0; // pub const HUMAN_DENSITY: f32 = 1010.0; // real value pub const HUMAN_DENSITY: f32 = 990.0; // value we use to make humanoids gently float diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 3a5db4cf69..4327137fda 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -1,5 +1,8 @@ use super::SpriteKind; -use crate::{comp::tool::ToolKind, make_case_elim}; +use crate::{ + comp::{fluid_dynamics::LiquidKind, tool::ToolKind}, + make_case_elim, +}; use enum_iterator::IntoEnumIterator; use hashbrown::HashMap; use lazy_static::lazy_static; @@ -33,6 +36,7 @@ make_case_elim!( // being *very* fast). Rock = 0x10, WeakRock = 0x11, // Explodable + Lava = 0x12, // 0x12 <= x < 0x20 is reserved for future rocks Grass = 0x20, // Note: *not* the same as grass sprites Snow = 0x21, @@ -63,6 +67,15 @@ impl BlockKind { #[inline] pub const fn is_liquid(&self) -> bool { self.is_fluid() && !self.is_air() } + #[inline] + pub const fn liquid_kind(&self) -> Option { + Some(match self { + BlockKind::Water => LiquidKind::Water, + BlockKind::Lava => LiquidKind::Lava, + _ => return None, + }) + } + /// Determine whether the block is filled (i.e: fully solid). Right now, /// this is the opposite of being a fluid. #[inline] @@ -168,6 +181,9 @@ impl Block { #[inline] pub fn get_glow(&self) -> Option { + if matches!(self.kind, BlockKind::Lava) { + return Some(24); + } match self.get_sprite()? { SpriteKind::StreetLamp | SpriteKind::StreetLampTall => Some(24), SpriteKind::Ember => Some(20), @@ -217,7 +233,7 @@ impl Block { pub fn is_solid(&self) -> bool { self.get_sprite() .map(|s| s.solid_height().is_some()) - .unwrap_or(true) + .unwrap_or(!matches!(self.kind, BlockKind::Lava)) } /// Can this block be exploded? If so, what 'power' is required to do so? diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index b504dbc4a2..3275bb60bb 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -1,8 +1,8 @@ use common::{ comp::{ - fluid_dynamics::Fluid, Buff, BuffCategory, BuffChange, BuffEffect, BuffId, BuffKind, - BuffSource, Buffs, Energy, Health, HealthChange, HealthSource, Inventory, ModifierKind, - PhysicsState, Stats, + fluid_dynamics::{Fluid, LiquidKind}, + Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs, + Energy, Health, HealthChange, HealthSource, Inventory, ModifierKind, PhysicsState, Stats, }, event::{EventBus, ServerEvent}, resources::DeltaTime, @@ -45,34 +45,47 @@ impl<'a> System<'a> for Sys { // Set to false to avoid spamming server buffs.set_event_emission(false); stats.set_event_emission(false); - for (entity, mut buff_comp, energy, mut stat, health) in ( + for (entity, mut buff_comp, energy, mut stat, health, physics_state) in ( &read_data.entities, &mut buffs, &read_data.energies, &mut stats, &read_data.healths, + read_data.physics_states.maybe(), ) .join() { + let in_fluid = physics_state.and_then(|p| p.in_fluid); + + if matches!( + in_fluid, + Some(Fluid::Liquid { + kind: LiquidKind::Lava, + .. + }) + ) && !buff_comp.contains(BuffKind::Burning) + { + server_emitter.emit(ServerEvent::Buff { + entity, + buff_change: BuffChange::Add(Buff::new( + BuffKind::Burning, + BuffData::new(200.0, None), + vec![BuffCategory::Natural], + BuffSource::World, + )), + }); + } + let (buff_comp_kinds, buff_comp_buffs): ( &HashMap>, &mut HashMap, ) = buff_comp.parts(); let mut expired_buffs = Vec::::new(); + // For each buff kind present on entity, if the buff kind queues, only ticks // duration of strongest buff of that kind, else it ticks durations of all buffs // of that kind. Any buffs whose durations expire are marked expired. for (kind, ids) in buff_comp_kinds.iter() { - // Only get the physics state component if the entity has the burning buff, as - // we don't need it for any other conditions yet - let in_fluid = if matches!(kind, BuffKind::Burning) { - read_data - .physics_states - .get(entity) - .and_then(|p| p.in_fluid) - } else { - None - }; if kind.queues() { if let Some((Some(buff), id)) = ids.get(0).map(|id| (buff_comp_buffs.get_mut(id), id)) @@ -257,7 +270,15 @@ fn tick_buff( } if let Some(remaining_time) = &mut buff.time { // Extinguish Burning buff when in water - if matches!(buff.kind, BuffKind::Burning) && matches!(in_fluid, Some(Fluid::Water { .. })) { + if matches!(buff.kind, BuffKind::Burning) + && matches!( + in_fluid, + Some(Fluid::Liquid { + kind: LiquidKind::Water, + .. + }) + ) + { *remaining_time = Duration::default(); } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 5852454f5f..81d1cec1c5 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1,7 +1,7 @@ use common::{ comp::{ body::ship::figuredata::{VoxelCollider, VOXEL_COLLIDER_MANIFEST}, - fluid_dynamics::{Fluid, Wings}, + fluid_dynamics::{Fluid, LiquidKind, Wings}, BeamSegment, Body, CharacterState, Collider, Density, Mass, Mounting, Ori, PhysicsState, Pos, PosVelDefer, PreviousPhysCache, Projectile, Scale, Shockwave, Stats, Sticky, Vel, }, @@ -905,13 +905,15 @@ impl<'a> PhysicsData<'a> { .terrain .get(pos.0.map(|e| e.floor() as i32)) .ok() - .and_then(|vox| vox.is_liquid().then_some(1.0)) - .map(|depth| Fluid::Water { - depth, - vel: Vel::zero(), + .and_then(|vox| { + vox.liquid_kind().map(|kind| Fluid::Liquid { + kind, + depth: 1.0, + vel: Vel::zero(), + }) }) .or_else(|| match physics_state.in_fluid { - Some(Fluid::Water { .. }) | None => Some(Fluid::Air { + Some(Fluid::Liquid { .. }) | None => Some(Fluid::Air { elevation: pos.0.z, vel: Vel::default(), }), @@ -1541,24 +1543,27 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( }); // Find liquid immersion and wall collision all in one round of iteration - let mut max_liquid_z = None::; + let mut liquid = None::<(LiquidKind, f32)>; let mut wall_dir_collisions = [false; 4]; near_iter.for_each(|(i, j, k)| { let block_pos = player_voxel_pos + Vec3::new(i, j, k); if let Some(block) = terrain.get(block_pos).ok().copied() { // Check for liquid blocks - if block.is_liquid() { + if let Some(block_liquid) = block.liquid_kind() { let liquid_aabb = Aabb { min: block_pos.map(|e| e as f32), // The liquid part of a liquid block always extends 1 block high. max: block_pos.map(|e| e as f32) + Vec3::one(), }; if player_aabb.collides_with_aabb(liquid_aabb) { - max_liquid_z = Some(match max_liquid_z { - Some(z) => z.max(liquid_aabb.max.z), - None => liquid_aabb.max.z, - }); + liquid = match liquid { + Some((kind, max_liquid_z)) => Some(( + kind.merge(block_liquid), + max_liquid_z.max(liquid_aabb.max.z), + )), + None => Some((block_liquid, liquid_aabb.max.z)), + }; } } // Check for walls @@ -1594,23 +1599,24 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( physics_state.ground_vel = ground_vel; } - physics_state.in_fluid = max_liquid_z - .map(|max_z| max_z - pos.0.z) // NOTE: assumes min_z == 0.0 - .map(|depth| { - physics_state + physics_state.in_fluid = liquid + .map(|(kind, max_z)| (kind, max_z - pos.0.z)) // NOTE: assumes min_z == 0.0 + .map(|(kind, depth)| { + (kind, physics_state .in_liquid() // This is suboptimal because it doesn't check for true depth, // so it can cause problems for situations like swimming down // a river and spawning or teleporting in(/to) water .map(|old_depth| (old_depth + old_pos.z - pos.0.z).max(depth)) - .unwrap_or(depth) + .unwrap_or(depth)) }) - .map(|depth| Fluid::Water { + .map(|(kind, depth)| Fluid::Liquid { + kind, depth, vel: Vel::zero(), }) .or_else(|| match physics_state.in_fluid { - Some(Fluid::Water { .. }) | None => Some(Fluid::Air { + Some(Fluid::Liquid { .. }) | None => Some(Fluid::Air { elevation: pos.0.z, vel: Vel::default(), }), diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index f448f71f79..56592205df 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -34,6 +34,7 @@ pub struct Colors { pub cave_roof: (u8, u8, u8), pub dirt: (u8, u8, u8), pub scaffold: (u8, u8, u8), + pub lava: (u8, u8, u8), pub vein: (u8, u8, u8), } @@ -210,12 +211,14 @@ pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) { //make pits for z in cave_base - pit_depth..cave_base { if pit_condition && (cave_roof - cave_base) > 10 { + let kind = if z < (cave_base - pit_depth) + (3 * pit_depth / 4) { + BlockKind::Lava + } else { + BlockKind::Air + }; canvas.set( Vec3::new(wpos2d.x, wpos2d.y, z), - Block::new( - BlockKind::Air, - noisy_color(info.index().colors.layer.scaffold.into(), 8), - ), + Block::new(kind, noisy_color(info.index().colors.layer.lava.into(), 8)), ); } }