src/world.rs 4.9 K · 182 lines · raw · history

1 use bevy::prelude::*;
2
3 pub const TILE_SIZE: f32 = 16.0;
4
5 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6 pub enum Terrain {
7 Grass,
8 Soil,
9 Sand,
10 ShallowWater,
11 DeepWater,
12 Stone,
13 }
14
15 impl Terrain {
16 pub const ALL: [Terrain; 6] = [
17 Terrain::Grass,
18 Terrain::Soil,
19 Terrain::Sand,
20 Terrain::ShallowWater,
21 Terrain::DeepWater,
22 Terrain::Stone,
23 ];
24
25 pub fn index(self) -> u16 {
26 self as u16
27 }
28
29 pub fn color(self) -> Srgba {
30 match self {
31 Terrain::Grass => Srgba::rgb_u8(92, 140, 64),
32 Terrain::Soil => Srgba::rgb_u8(122, 92, 60),
33 Terrain::Sand => Srgba::rgb_u8(214, 196, 140),
34 Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184),
35 Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140),
36 Terrain::Stone => Srgba::rgb_u8(118, 118, 124),
37 }
38 }
39 }
40
41 #[derive(Resource, Clone, Debug)]
42 pub struct WorldMap {
43 pub size: UVec2,
44 tiles: Vec<Terrain>,
45 }
46
47 impl WorldMap {
48 pub fn filled(size: UVec2, terrain: Terrain) -> Self {
49 Self {
50 size,
51 tiles: vec![terrain; size.element_product() as usize],
52 }
53 }
54
55 pub fn in_bounds(&self, pos: IVec2) -> bool {
56 pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y
57 }
58
59 fn index(&self, pos: UVec2) -> usize {
60 (pos.y * self.size.x + pos.x) as usize
61 }
62
63 pub fn get(&self, pos: IVec2) -> Option<Terrain> {
64 self.in_bounds(pos)
65 .then(|| self.tiles[self.index(pos.as_uvec2())])
66 }
67
68 pub fn set(&mut self, pos: UVec2, terrain: Terrain) {
69 let i = self.index(pos);
70 self.tiles[i] = terrain;
71 }
72
73 pub fn positions(&self) -> impl Iterator<Item = UVec2> + use<> {
74 let size = self.size;
75 (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y)))
76 }
77
78 pub fn world_size(&self) -> Vec2 {
79 self.size.as_vec2() * TILE_SIZE
80 }
81
82 pub fn tile_center(pos: UVec2) -> Vec2 {
83 (pos.as_vec2() + 0.5) * TILE_SIZE
84 }
85
86 pub fn tile_at(&self, world: Vec2) -> Option<UVec2> {
87 let pos = (world / TILE_SIZE).floor().as_ivec2();
88 self.in_bounds(pos).then(|| pos.as_uvec2())
89 }
90
91 }
92
93 pub fn hash2(x: u32, y: u32) -> u32 {
94 let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77);
95 h ^= h >> 15;
96 h = h.wrapping_mul(0x2C1B_3C6D);
97 h ^= h >> 12;
98 h = h.wrapping_mul(0x297A_2D39);
99 h ^ (h >> 15)
100 }
101
102 pub fn static_world() -> WorldMap {
103 let size = UVec2::new(192, 128);
104 let mut map = WorldMap::filled(size, Terrain::Grass);
105 let lake = Vec2::new(140.0, 40.0);
106 let mountain = Vec2::new(150.0, 105.0);
107
108 for pos in map.positions() {
109 let p = pos.as_vec2();
110 let river_x = 60.0 + 8.0 * (p.y / 14.0).sin();
111 let river_dist = (p.x - river_x).abs();
112 let lake_dist = p.distance(lake);
113 let mountain_dist = ((p - mountain) / Vec2::new(36.0, 20.0)).length();
114
115 let terrain = if mountain_dist < 1.0 {
116 Terrain::Stone
117 } else if lake_dist < 9.0 {
118 Terrain::DeepWater
119 } else if lake_dist < 14.0 || river_dist < 2.5 {
120 Terrain::ShallowWater
121 } else if lake_dist < 17.0 || river_dist < 4.0 {
122 Terrain::Sand
123 } else if hash2(pos.x / 6, pos.y / 6) % 7 == 0 {
124 Terrain::Soil
125 } else {
126 Terrain::Grass
127 };
128 map.set(pos, terrain);
129 }
130 map
131 }
132
133 #[cfg(test)]
134 mod tests {
135 use super::*;
136
137 #[test]
138 fn get_out_of_bounds_is_none() {
139 let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
140 assert_eq!(map.get(IVec2::new(-1, 0)), None);
141 assert_eq!(map.get(IVec2::new(4, 0)), None);
142 assert_eq!(map.get(IVec2::new(0, 3)), None);
143 assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass));
144 }
145
146 #[test]
147 fn tiles_are_stored_row_major_from_bottom_left() {
148 let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
149 map.set(UVec2::new(1, 2), Terrain::Stone);
150 assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone);
151 }
152
153 #[test]
154 fn tile_at_inverts_tile_center() {
155 let map = static_world();
156 for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] {
157 assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos));
158 }
159 }
160
161 #[test]
162 fn tile_at_outside_world_is_none() {
163 let map = static_world();
164 assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None);
165 assert_eq!(map.tile_at(map.world_size()), None);
166 }
167
168 #[test]
169 fn terrain_index_matches_position_in_all() {
170 for (i, t) in Terrain::ALL.iter().enumerate() {
171 assert_eq!(t.index() as usize, i);
172 }
173 }
174
175 #[test]
176 fn static_world_contains_every_terrain() {
177 let map = static_world();
178 for t in Terrain::ALL {
179 assert!(map.positions().any(|p| map.get(p.as_ivec2()) == Some(t)), "{t:?}");
180 }
181 }
182 }