diff --git a/src/game/debug/terrain.rs b/src/game/debug/terrain.rs index 62ee70c..e8d94dc 100644 --- a/src/game/debug/terrain.rs +++ b/src/game/debug/terrain.rs @@ -80,24 +80,21 @@ fn debug_painter( } else { return; }; - - if key_input.just_pressed(KeyCode::Key1) { - brush.tile = 1; - } - if key_input.just_pressed(KeyCode::Key2) { - brush.tile = 2; - } - if key_input.just_pressed(KeyCode::Key3) { - brush.tile = 3; - } - if key_input.just_pressed(KeyCode::Key4) { - brush.tile = 4; - } - if key_input.just_pressed(KeyCode::Key5) { - brush.tile = 5; - } - if key_input.just_pressed(KeyCode::Key6) { - brush.tile = 6; + + for (index, key) in vec![ + KeyCode::Key1, + KeyCode::Key2, + KeyCode::Key3, + KeyCode::Key4, + KeyCode::Key5, + KeyCode::Key6, + KeyCode::Key7, + KeyCode::Key8, + KeyCode::Key9, + ].iter().enumerate() { + if key_input.just_pressed(*key) { + brush.tile = index as u8 + 1; + } } let origin = Vector2I::from(world_pos); @@ -116,8 +113,9 @@ fn debug_painter( for x in origin.x - (radius - 1)..origin.x + radius { let dx = (x - origin.x).abs(); let dy = (y - origin.y).abs(); + let draw = dx * dx + dy * dy <= (radius - 1) * (radius - 1); - if dx * dx + dy * dy <= (radius - 1) * (radius - 1) { + if draw { let pos: Vector2I = Vector2I { x, y }; debug_draw.line_colored( Vec3::from(pos) + Vec3::new(0.45, 0.45, 1.0), @@ -127,12 +125,7 @@ fn debug_painter( ); if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right) { - // 6 is special - if id == 6 { - terrain.mark_dirty(&pos) - } else { - terrain.set_texel(&pos, id, None) - } + terrain.set_texel(&pos, id, None) } } } diff --git a/src/terrain2d.rs b/src/terrain2d.rs index 67b19eb..a18e754 100644 --- a/src/terrain2d.rs +++ b/src/terrain2d.rs @@ -94,6 +94,8 @@ fn terrain_simulation(mut terrain: ResMut, frame_counter: Res = (rect.min.y..rect.max.y + 1).collect(); let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect(); @@ -106,104 +108,65 @@ fn terrain_simulation(mut terrain: ResMut, frame_counter: Res { - // Check if there is space below - { - let below_pos = global + Vector2I::DOWN; - if terrain.get_texel(&below_pos).map_or(true, |texel| { - TexelBehaviour2D::is_empty(&texel.id) - || TexelBehaviour2D::is_gas(&texel.id) - }) { - let below_id = - terrain.get_texel(&below_pos).map_or(0, |texel| texel.id); - terrain.set_texel(&below_pos, texel.id, Some(simulation_frame)); - terrain.set_texel(&global, below_id, Some(simulation_frame)); - continue; - } - } - - // Check if there is space to the side - let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT]; - if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 { - dirs.reverse(); - } - for dir in dirs.iter() { - let side_pos = global + *dir; - if terrain.get_texel(&side_pos).map_or(true, |texel| { - TexelBehaviour2D::is_empty(&texel.id) - || TexelBehaviour2D::is_gas(&texel.id) - }) { - let side_id = - terrain.get_texel(&side_pos).map_or(0, |texel| texel.id); - terrain.set_texel(&side_pos, texel.id, Some(simulation_frame)); - terrain.set_texel(&global, side_id, Some(simulation_frame)); - continue 'texel_loop; - }; - } - } - TexelForm::Gas => { - // Check if there is space above - { - let above_pos = global + Vector2I::UP; - if terrain - .get_texel(&above_pos) - .map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id)) - { - let above_id = - terrain.get_texel(&above_pos).map_or(0, |texel| texel.id); - terrain.set_texel(&above_pos, texel.id, Some(simulation_frame)); - terrain.set_texel(&global, above_id, Some(simulation_frame)); - continue; - } - } - - // Check if there is space to the side - let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT]; - if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 { - dirs.reverse(); - } - for dir in dirs.iter() { - let side_pos = global + *dir; - if terrain - .get_texel(&side_pos) - .map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id)) - { - let side_id = - terrain.get_texel(&side_pos).map_or(0, |texel| texel.id); - terrain.set_texel(&side_pos, texel.id, Some(simulation_frame)); - terrain.set_texel(&global, side_id, Some(simulation_frame)); - continue 'texel_loop; - }; - } - } - _ => (), - } + simulate_texel(global, &mut terrain, &frame_counter); } } } } } +fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &FrameCounter) { + let (_, behaviour) = match terrain.get_texel_behaviour(&global) { + (Some(texel), Some(behaviour)) => (texel, behaviour), + (_, _) => return, + }; + + let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1; + + // Gravity + if let Some(gravity) = behaviour.gravity { + let grav_offset = Vector2I::from(gravity); + let grav_pos = global + grav_offset; + + // Try falling + { + let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos); + if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) { + terrain.swap_texels(&global, &grav_pos, Some(simulation_frame)); + return; + } + } + + // Try "sliding" + let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT]; + if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 { + dirs.reverse(); + } + for dir in dirs.iter() { + let slide_pos = match behaviour.form { + TexelForm::Solid => grav_pos + *dir, + TexelForm::Liquid | TexelForm::Gas => global + *dir, + }; + let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos); + if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) { + terrain.swap_texels(&global, &slide_pos, Some(simulation_frame)); + return; + } + } + } +} + fn emit_terrain_events( mut terrain: ResMut, mut terrain_events: EventWriter, @@ -298,6 +261,17 @@ impl Terrain2D { .map_or(None, |chunk| chunk.get_texel(&global_to_local(global))) } + pub fn get_texel_behaviour( + &self, + global: &Vector2I, + ) -> (Option, Option) { + let texel = self.get_texel(global); + ( + texel, + texel.map_or(None, |t| TexelBehaviour2D::from_id(&t.id)), + ) + } + pub fn set_texel(&mut self, global: &Vector2I, id: TexelID, simulation_frame: Option) { let index = global_to_chunk_index(global); let changed = match self.index_to_chunk_mut(&index) { @@ -316,6 +290,18 @@ impl Terrain2D { self.mark_dirty(&(*global + Vector2I::LEFT)); } } + + pub fn swap_texels( + &mut self, + from_global: &Vector2I, + to_global: &Vector2I, + simulation_frame: Option, + ) { + let from = self.get_texel(from_global).map_or(0, |t| t.id); + let to = self.get_texel(to_global).map_or(0, |t| t.id); + self.set_texel(to_global, from, simulation_frame); + self.set_texel(from_global, to, simulation_frame); + } } pub fn local_to_texel_index(position: &Vector2I) -> Option { diff --git a/src/terrain2d/chunk2d.rs b/src/terrain2d/chunk2d.rs index 2f0ea94..63d3373 100644 --- a/src/terrain2d/chunk2d.rs +++ b/src/terrain2d/chunk2d.rs @@ -98,7 +98,7 @@ impl ChunkRect { pub struct Chunk2D { pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize], - // TODO: handle multiple dirty rects + // TODO: handle multiple dirty rects? pub dirty_rect: Option, } @@ -212,13 +212,18 @@ impl Chunk2D { local_to_texel_index(position).map(|i| &mut self.texels[i]) } - pub fn set_texel(&mut self, position: &Vector2I, id: TexelID, simulation_frame: Option) -> bool { + pub fn set_texel( + &mut self, + position: &Vector2I, + id: TexelID, + simulation_frame: Option, + ) -> bool { let i = local_to_texel_index(position).expect("Texel index out of range"); if self.texels[i].id != id { self.mark_dirty(position); } - let update_neighbours = - TexelBehaviour2D::is_solid(&self.texels[i].id) != TexelBehaviour2D::is_solid(&id); + let update_neighbours = TexelBehaviour2D::has_collision(&self.texels[i].id) + != TexelBehaviour2D::has_collision(&id); let changed = self.texels[i].id != id; self.texels[i].id = id; if let Some(simulation_frame) = simulation_frame { @@ -280,7 +285,8 @@ impl Chunk2D { | if local.x == 0 { 1 << 3 } else { 0 }; let mut sides: Vec; - if !TexelBehaviour2D::is_solid(&self.texels[i].id) { + let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id); + if !has_collision { sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize] .iter() .clone() @@ -289,7 +295,7 @@ impl Chunk2D { to: side.to + local, }) .collect(); - } else if TexelBehaviour2D::is_solid(&self.texels[i].id) && edge_mask != 0 { + } else if has_collision && edge_mask != 0 { sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2); for i in 0..MST_EDGE_CASE_MAP.len() { if edge_mask & (1 << i) != 0 { @@ -430,7 +436,7 @@ pub fn chunk_spawner( ..default() }, texture, - transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)), + transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 1.0)), ..default() }, ..default() diff --git a/src/terrain2d/terrain_gen2d.rs b/src/terrain2d/terrain_gen2d.rs index effb239..307697d 100644 --- a/src/terrain2d/terrain_gen2d.rs +++ b/src/terrain2d/terrain_gen2d.rs @@ -30,13 +30,13 @@ impl TerrainGen2D { let mut id = 0; if value > 0.35 { - id = 1; + id = 11; } if value > 0.42 { - id = 2; + id = 12; } if value > 0.9 { - id = 3; + id = 13; } chunk.set_texel(&local, id, None); diff --git a/src/terrain2d/texel_behaviour2d.rs b/src/terrain2d/texel_behaviour2d.rs index 469d790..acaa1db 100644 --- a/src/terrain2d/texel_behaviour2d.rs +++ b/src/terrain2d/texel_behaviour2d.rs @@ -1,3 +1,5 @@ +use crate::util::Vector2I; + use super::TexelID; use bevy::prelude::*; use lazy_static::lazy_static; @@ -7,32 +9,110 @@ lazy_static! { static ref ID_MAP: HashMap = { let mut result = HashMap::new(); - result.insert(1, TexelBehaviour2D { - color: Color::rgb(0.61, 0.49, 0.38), - ..default() - }); + result.insert( + 1, + TexelBehaviour2D { + name: String::from("loose sand"), + color: Color::rgb(0.61, 0.49, 0.38), + gravity: Some(TexelGravity::Down(100)), + has_collision: true, + ..default() + }, + ); - result.insert(2, TexelBehaviour2D { - color: Color::rgb(0.21, 0.19, 0.17), - ..default() - }); + result.insert( + 2, + TexelBehaviour2D { + name: String::from("loose stone"), + color: Color::rgb(0.21, 0.19, 0.17), + gravity: Some(TexelGravity::Down(100)), + has_collision: true, + ..default() + }, + ); - result.insert(3, TexelBehaviour2D { - color: Color::rgb(0.11, 0.11, 0.11), - ..default() - }); - - result.insert(4, TexelBehaviour2D { - color: Color::rgb(0.0, 0.0, 1.0), - form: TexelForm::Liquid, - ..default() - }); - - result.insert(5, TexelBehaviour2D { - color: Color::rgb(0.0, 1.0, 0.0), - form: TexelForm::Gas, - ..default() - }); + result.insert( + 3, + TexelBehaviour2D { + name: String::from("loose sturdy stone"), + color: Color::rgb(0.11, 0.11, 0.11), + gravity: Some(TexelGravity::Down(100)), + has_collision: true, + ..default() + }, + ); + + result.insert( + 4, + TexelBehaviour2D { + name: String::from("water"), + color: Color::rgba(0.0, 0.0, 1.0, 0.5), + form: TexelForm::Liquid, + gravity: Some(TexelGravity::Down(10)), + ..default() + }, + ); + + result.insert( + 5, + TexelBehaviour2D { + name: String::from("oil"), + color: Color::rgba(0.0, 1.0, 0.0, 0.5), + form: TexelForm::Gas, + gravity: Some(TexelGravity::Up(50)), + ..default() + }, + ); + + result.insert( + 6, + TexelBehaviour2D { + name: String::from("gas"), + color: Color::rgba(0.5, 0.5, 0.25, 0.5), + form: TexelForm::Liquid, + gravity: Some(TexelGravity::Down(5)), + ..default() + }, + ); + + result.insert( + 11, + TexelBehaviour2D { + name: String::from("sand"), + color: Color::rgb(0.61, 0.49, 0.38), + has_collision: true, + ..default() + }, + ); + + result.insert( + 12, + TexelBehaviour2D { + name: String::from("stone"), + color: Color::rgb(0.21, 0.19, 0.17), + has_collision: true, + ..default() + }, + ); + + result.insert( + 13, + TexelBehaviour2D { + name: String::from("sturdy stone"), + color: Color::rgb(0.11, 0.11, 0.11), + has_collision: true, + ..default() + }, + ); + + result.insert( + u8::MAX, + TexelBehaviour2D { + color: Color::BLACK, + has_collision: true, + ..default() + }, + ); result }; @@ -41,38 +121,86 @@ lazy_static! { #[derive(Clone, Copy, Default, PartialEq)] pub enum TexelForm { #[default] + // Solid materials, when affected by gravity, create pyramid-like piles Solid, + // Liquid materials, when affected by gravity, act like solids but also try to stabilise the surface level by traveling flat surfaces Liquid, + // Gas materials act like liquids, but also have density/pressure that causes them to disperse Gas, } -#[derive(Clone, Copy, Default)] -pub struct TexelBehaviour2D { - // pub flammability: Option, - // pub gravity: Option, - pub form: TexelForm, - pub color: Color, +#[derive(Clone, Copy, PartialEq)] +pub enum TexelGravity { + Down(u8), + Up(u8), } -// TODO: change form-based functions like is_solid to behaviour based (e.g. has_collision) +impl From for Vector2I { + fn from(gravity: TexelGravity) -> Self { + match gravity { + TexelGravity::Down(_) => Vector2I::DOWN, + TexelGravity::Up(_) => Vector2I::UP, + } + } +} + +#[derive(Clone)] +pub struct TexelBehaviour2D { + pub name: String, + pub color: Color, + pub form: TexelForm, + pub has_collision: bool, + pub gravity: Option, + pub toughness: Option, +} + +impl Default for TexelBehaviour2D { + fn default() -> Self { + TexelBehaviour2D { + name: "Unnamed material".to_string(), + color: Color::PINK, + form: TexelForm::Solid, + has_collision: false, + gravity: None, + toughness: None, + } + } +} + +// TODO: change form-based functions like is_solid to behaviour based (e.g. has_collision) impl TexelBehaviour2D { pub fn from_id(id: &TexelID) -> Option { - ID_MAP.get(id).copied() + ID_MAP.get(id).cloned() } pub fn is_empty(id: &TexelID) -> bool { ID_MAP.get(id).is_none() } - pub fn is_solid(id: &TexelID) -> bool { - ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Solid) + pub fn has_collision(id: &TexelID) -> bool { + ID_MAP.get(id).map_or(false, |b| b.has_collision) } - pub fn is_liquid(id: &TexelID) -> bool { - ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Liquid) - } + pub fn can_displace(from: &TexelBehaviour2D, to: &Option) -> bool { + let to = if let Some(to) = to { to } else { return true }; - pub fn is_gas(id: &TexelID) -> bool { - ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Gas) + match (from.form, to.form) { + (_, TexelForm::Solid) => false, + (_, _) => { + if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) { + match (from_grav, to_grav) { + (TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => { + from_grav > to_grav + } + (TexelGravity::Up(from_grav), TexelGravity::Up(to_grav)) => { + from_grav > to_grav + } + (_, _) => true, + } + } else { + true + } + } + } } }