use bevy::prelude::*; pub const TILE_SIZE: f32 = 16.0; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Terrain { Grass, Soil, Sand, ShallowWater, DeepWater, Stone, } impl Terrain { pub const ALL: [Terrain; 6] = [ Terrain::Grass, Terrain::Soil, Terrain::Sand, Terrain::ShallowWater, Terrain::DeepWater, Terrain::Stone, ]; pub fn index(self) -> u16 { self as u16 } pub fn move_cost(self) -> Option { match self { Terrain::Grass | Terrain::Soil => Some(100), Terrain::Sand => Some(130), Terrain::ShallowWater => Some(300), Terrain::DeepWater | Terrain::Stone => None, } } pub fn color(self) -> Srgba { match self { Terrain::Grass => Srgba::rgb_u8(92, 140, 64), Terrain::Soil => Srgba::rgb_u8(122, 92, 60), Terrain::Sand => Srgba::rgb_u8(214, 196, 140), Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184), Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140), Terrain::Stone => Srgba::rgb_u8(118, 118, 124), } } } #[derive(Resource, Clone, Debug)] pub struct WorldMap { pub size: UVec2, tiles: Vec, } impl WorldMap { pub fn filled(size: UVec2, terrain: Terrain) -> Self { Self { size, tiles: vec![terrain; size.element_product() as usize], } } pub fn in_bounds(&self, pos: IVec2) -> bool { pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y } fn index(&self, pos: UVec2) -> usize { (pos.y * self.size.x + pos.x) as usize } pub fn get(&self, pos: IVec2) -> Option { self.in_bounds(pos) .then(|| self.tiles[self.index(pos.as_uvec2())]) } pub fn set(&mut self, pos: UVec2, terrain: Terrain) { let i = self.index(pos); self.tiles[i] = terrain; } pub fn move_cost(&self, pos: IVec2) -> Option { self.get(pos).and_then(Terrain::move_cost) } pub fn positions(&self) -> impl Iterator + use<> { let size = self.size; (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y))) } pub fn world_size(&self) -> Vec2 { self.size.as_vec2() * TILE_SIZE } pub fn tile_center(pos: UVec2) -> Vec2 { (pos.as_vec2() + 0.5) * TILE_SIZE } pub fn tile_at(&self, world: Vec2) -> Option { let pos = (world / TILE_SIZE).floor().as_ivec2(); self.in_bounds(pos).then(|| pos.as_uvec2()) } } pub fn hash2(x: u32, y: u32) -> u32 { let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77); h ^= h >> 15; h = h.wrapping_mul(0x2C1B_3C6D); h ^= h >> 12; h = h.wrapping_mul(0x297A_2D39); h ^ (h >> 15) } pub fn static_world() -> WorldMap { let size = UVec2::new(192, 128); let mut map = WorldMap::filled(size, Terrain::Grass); let lake = Vec2::new(140.0, 40.0); let mountain = Vec2::new(150.0, 105.0); for pos in map.positions() { let p = pos.as_vec2(); let river_x = 60.0 + 8.0 * (p.y / 14.0).sin(); let river_dist = (p.x - river_x).abs(); let lake_dist = p.distance(lake); let mountain_dist = ((p - mountain) / Vec2::new(36.0, 20.0)).length(); let terrain = if mountain_dist < 1.0 { Terrain::Stone } else if lake_dist < 9.0 { Terrain::DeepWater } else if lake_dist < 14.0 || river_dist < 2.5 { Terrain::ShallowWater } else if lake_dist < 17.0 || river_dist < 4.0 { Terrain::Sand } else if hash2(pos.x / 6, pos.y / 6) % 7 == 0 { Terrain::Soil } else { Terrain::Grass }; map.set(pos, terrain); } map } #[cfg(test)] mod tests { use super::*; #[test] fn get_out_of_bounds_is_none() { let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass); assert_eq!(map.get(IVec2::new(-1, 0)), None); assert_eq!(map.get(IVec2::new(4, 0)), None); assert_eq!(map.get(IVec2::new(0, 3)), None); assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass)); } #[test] fn tiles_are_stored_row_major_from_bottom_left() { let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass); map.set(UVec2::new(1, 2), Terrain::Stone); assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone); } #[test] fn tile_at_inverts_tile_center() { let map = static_world(); for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] { assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos)); } } #[test] fn tile_at_outside_world_is_none() { let map = static_world(); assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None); assert_eq!(map.tile_at(map.world_size()), None); } #[test] fn terrain_index_matches_position_in_all() { for (i, t) in Terrain::ALL.iter().enumerate() { assert_eq!(t.index() as usize, i); } } #[test] fn static_world_contains_every_terrain() { let map = static_world(); for t in Terrain::ALL { assert!(map.positions().any(|p| map.get(p.as_ivec2()) == Some(t)), "{t:?}"); } } }