src/world.rs 10.7 K · 368 lines · raw · history

1 use bevy::prelude::*;
2
3 pub const TILE_SIZE: f32 = 16.0;
4 pub const MAX_HEIGHT: f32 = 100.0;
5 pub const UPHILL_COST_PER_METER: f32 = 20.0;
6 pub const CHEAPEST_MOVE_COST: u32 = 60;
7
8 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9 pub enum Terrain {
10 Grass,
11 Soil,
12 Sand,
13 ShallowWater,
14 DeepWater,
15 Stone,
16 IronOre,
17 GoldOre,
18 Gravel,
19 Path,
20 Paved,
21 }
22
23 impl Terrain {
24 pub const ALL: [Terrain; 11] = [
25 Terrain::Grass,
26 Terrain::Soil,
27 Terrain::Sand,
28 Terrain::ShallowWater,
29 Terrain::DeepWater,
30 Terrain::Stone,
31 Terrain::IronOre,
32 Terrain::GoldOre,
33 Terrain::Gravel,
34 Terrain::Path,
35 Terrain::Paved,
36 ];
37
38 pub fn index(self) -> u16 {
39 self as u16
40 }
41
42 pub fn move_cost(self) -> Option<u32> {
43 match self {
44 Terrain::Paved => Some(CHEAPEST_MOVE_COST),
45 Terrain::Path => Some(80),
46 Terrain::Grass | Terrain::Soil => Some(100),
47 Terrain::Sand | Terrain::Gravel => Some(130),
48 Terrain::ShallowWater => Some(300),
49 Terrain::DeepWater | Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => None,
50 }
51 }
52
53 pub fn is_water(self) -> bool {
54 matches!(self, Terrain::ShallowWater | Terrain::DeepWater)
55 }
56
57 pub fn is_rock(self) -> bool {
58 matches!(self, Terrain::Stone | Terrain::IronOre | Terrain::GoldOre)
59 }
60
61 pub fn color(self) -> Srgba {
62 match self {
63 Terrain::Grass => Srgba::rgb_u8(92, 140, 64),
64 Terrain::Soil => Srgba::rgb_u8(122, 92, 60),
65 Terrain::Sand => Srgba::rgb_u8(214, 196, 140),
66 Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184),
67 Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140),
68 Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => Srgba::rgb_u8(118, 118, 124),
69 Terrain::Gravel => Srgba::rgb_u8(150, 146, 138),
70 Terrain::Path => Srgba::rgb_u8(160, 128, 86),
71 Terrain::Paved => Srgba::rgb_u8(168, 164, 156),
72 }
73 }
74
75 pub fn speck_color(self) -> Option<Srgba> {
76 match self {
77 Terrain::IronOre => Some(Srgba::rgb_u8(178, 104, 72)),
78 Terrain::GoldOre => Some(Srgba::rgb_u8(236, 196, 60)),
79 Terrain::Path => Some(Srgba::rgb_u8(130, 104, 70)),
80 _ => None,
81 }
82 }
83 }
84
85 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86 pub enum Plant {
87 Tree,
88 BerryBush,
89 Crop,
90 }
91
92 impl Plant {
93 pub fn extra_move_cost(self) -> u32 {
94 match self {
95 Plant::Tree => 100,
96 Plant::BerryBush => 60,
97 Plant::Crop => 30,
98 }
99 }
100
101 pub fn ripening_ticks(self) -> Option<f32> {
102 let days = match self {
103 Plant::Tree => return None,
104 Plant::BerryBush => 2.0,
105 Plant::Crop => 1.5,
106 };
107 Some(days * 24.0 * crate::sim::TICKS_PER_HOUR as f32)
108 }
109
110 pub fn is_ripe(self, growth: f32) -> bool {
111 self.ripening_ticks().is_some() && growth >= 1.0
112 }
113 }
114
115 #[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
116 pub struct TilePos(pub UVec2);
117
118 #[derive(Message, Debug, Clone, Copy, PartialEq)]
119 pub struct TileChanged(pub UVec2);
120
121 #[derive(Resource, Clone, Debug)]
122 pub struct WorldMap {
123 pub size: UVec2,
124 tiles: Vec<Terrain>,
125 plants: Vec<Option<Plant>>,
126 heights: Vec<f32>,
127 walls: Vec<bool>,
128 roofs: Vec<bool>,
129 growth: Vec<f32>,
130 wear: Vec<f32>,
131 }
132
133 impl WorldMap {
134 pub fn filled(size: UVec2, terrain: Terrain) -> Self {
135 let n = size.element_product() as usize;
136 Self {
137 size,
138 tiles: vec![terrain; n],
139 plants: vec![None; n],
140 heights: vec![0.0; n],
141 walls: vec![false; n],
142 roofs: vec![false; n],
143 growth: vec![0.0; n],
144 wear: vec![0.0; n],
145 }
146 }
147
148 pub fn in_bounds(&self, pos: IVec2) -> bool {
149 pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y
150 }
151
152 fn index(&self, pos: UVec2) -> usize {
153 (pos.y * self.size.x + pos.x) as usize
154 }
155
156 pub fn get(&self, pos: IVec2) -> Option<Terrain> {
157 self.in_bounds(pos)
158 .then(|| self.tiles[self.index(pos.as_uvec2())])
159 }
160
161 pub fn set(&mut self, pos: UVec2, terrain: Terrain) {
162 let i = self.index(pos);
163 self.tiles[i] = terrain;
164 }
165
166 pub fn plant(&self, pos: IVec2) -> Option<Plant> {
167 self.in_bounds(pos)
168 .then(|| self.plants[self.index(pos.as_uvec2())])
169 .flatten()
170 }
171
172 pub fn set_plant(&mut self, pos: UVec2, plant: Option<Plant>) {
173 let i = self.index(pos);
174 self.plants[i] = plant;
175 }
176
177 pub fn height(&self, pos: IVec2) -> Option<f32> {
178 self.in_bounds(pos)
179 .then(|| self.heights[self.index(pos.as_uvec2())])
180 }
181
182 pub fn set_height(&mut self, pos: UVec2, height: f32) {
183 let i = self.index(pos);
184 self.heights[i] = height;
185 }
186
187 pub fn is_wall(&self, pos: IVec2) -> bool {
188 self.in_bounds(pos) && self.walls[self.index(pos.as_uvec2())]
189 }
190
191 pub fn set_wall(&mut self, pos: UVec2, wall: bool) {
192 let i = self.index(pos);
193 self.walls[i] = wall;
194 }
195
196 pub fn growth(&self, pos: IVec2) -> f32 {
197 if self.in_bounds(pos) {
198 self.growth[self.index(pos.as_uvec2())]
199 } else {
200 0.0
201 }
202 }
203
204 pub fn set_growth(&mut self, pos: UVec2, growth: f32) {
205 let i = self.index(pos);
206 self.growth[i] = growth.clamp(0.0, 1.0);
207 }
208
209 pub fn wear(&self, pos: IVec2) -> f32 {
210 if self.in_bounds(pos) {
211 self.wear[self.index(pos.as_uvec2())]
212 } else {
213 0.0
214 }
215 }
216
217 pub fn set_wear(&mut self, pos: UVec2, wear: f32) {
218 let i = self.index(pos);
219 self.wear[i] = wear.max(0.0);
220 }
221
222 pub fn ripe_plant(&self, pos: IVec2) -> Option<Plant> {
223 self.plant(pos).filter(|p| p.is_ripe(self.growth(pos)))
224 }
225
226 pub fn is_roofed(&self, pos: IVec2) -> bool {
227 self.in_bounds(pos) && self.roofs[self.index(pos.as_uvec2())]
228 }
229
230 pub fn set_roof(&mut self, pos: UVec2, roof: bool) {
231 let i = self.index(pos);
232 self.roofs[i] = roof;
233 }
234
235 pub fn move_cost(&self, pos: IVec2) -> Option<u32> {
236 if self.is_wall(pos) {
237 return None;
238 }
239 let terrain = self.get(pos)?.move_cost()?;
240 Some(terrain + self.plant(pos).map_or(0, Plant::extra_move_cost))
241 }
242
243 pub fn step_cost(&self, from: IVec2, to: IVec2) -> Option<u32> {
244 let climb = (self.height(to)? - self.height(from)?).max(0.0);
245 Some(self.move_cost(to)? + (climb * UPHILL_COST_PER_METER).round() as u32)
246 }
247
248 pub fn positions(&self) -> impl Iterator<Item = UVec2> + use<> {
249 let size = self.size;
250 (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y)))
251 }
252
253 pub fn world_size(&self) -> Vec2 {
254 self.size.as_vec2() * TILE_SIZE
255 }
256
257 pub fn tile_center(pos: UVec2) -> Vec2 {
258 (pos.as_vec2() + 0.5) * TILE_SIZE
259 }
260
261 pub fn tile_at(&self, world: Vec2) -> Option<UVec2> {
262 let pos = (world / TILE_SIZE).floor().as_ivec2();
263 self.in_bounds(pos).then(|| pos.as_uvec2())
264 }
265 }
266
267 pub fn hash2(x: u32, y: u32) -> u32 {
268 let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77);
269 h ^= h >> 15;
270 h = h.wrapping_mul(0x2C1B_3C6D);
271 h ^= h >> 12;
272 h = h.wrapping_mul(0x297A_2D39);
273 h ^ (h >> 15)
274 }
275
276 #[cfg(test)]
277 mod tests {
278 use super::*;
279
280 #[test]
281 fn get_out_of_bounds_is_none() {
282 let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
283 assert_eq!(map.get(IVec2::new(-1, 0)), None);
284 assert_eq!(map.get(IVec2::new(4, 0)), None);
285 assert_eq!(map.get(IVec2::new(0, 3)), None);
286 assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass));
287 }
288
289 #[test]
290 fn tiles_are_stored_row_major_from_bottom_left() {
291 let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
292 map.set(UVec2::new(1, 2), Terrain::Stone);
293 assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone);
294 }
295
296 #[test]
297 fn tile_at_inverts_tile_center() {
298 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
299 for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] {
300 assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos));
301 }
302 }
303
304 #[test]
305 fn tile_at_outside_world_is_none() {
306 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
307 assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None);
308 assert_eq!(map.tile_at(map.world_size()), None);
309 }
310
311 #[test]
312 fn trees_slow_movement_but_do_not_block_it() {
313 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
314 let open = map.move_cost(IVec2::ZERO).unwrap();
315 map.set_plant(UVec2::ZERO, Some(Plant::Tree));
316 assert!(map.move_cost(IVec2::ZERO).unwrap() > open);
317 }
318
319 #[test]
320 fn bushes_and_crops_ripen_but_trees_do_not() {
321 assert!(Plant::BerryBush.is_ripe(1.0));
322 assert!(!Plant::Crop.is_ripe(0.9));
323 assert!(!Plant::Tree.is_ripe(1.0));
324 }
325
326 // A* uses CHEAPEST_MOVE_COST in its distance estimate; if any terrain were cheaper the
327 // estimate would overshoot and colonists could take needlessly long routes.
328 #[test]
329 fn no_terrain_is_cheaper_than_the_pathfinding_estimate() {
330 for terrain in Terrain::ALL {
331 if let Some(cost) = terrain.move_cost() {
332 assert!(cost >= CHEAPEST_MOVE_COST, "{terrain:?}");
333 }
334 }
335 }
336
337 #[test]
338 fn walls_block_movement() {
339 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
340 map.set_wall(UVec2::ZERO, true);
341 assert_eq!(map.move_cost(IVec2::ZERO), None);
342 assert!(map.move_cost(IVec2::X).is_some());
343 }
344
345 #[test]
346 fn walking_uphill_costs_more_than_downhill_or_flat() {
347 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Grass);
348 map.set_height(UVec2::new(1, 0), 5.0);
349 let (low, high, flat) = (IVec2::new(0, 0), IVec2::new(1, 0), IVec2::new(2, 0));
350 let up = map.step_cost(low, high).unwrap();
351 let down = map.step_cost(high, low).unwrap();
352 assert!(up > down);
353 assert_eq!(down, map.step_cost(low, flat).unwrap());
354 }
355
356 #[test]
357 fn rock_and_water_classification() {
358 assert!(Terrain::IronOre.is_rock() && Terrain::IronOre.move_cost().is_none());
359 assert!(Terrain::ShallowWater.is_water() && !Terrain::ShallowWater.is_rock());
360 }
361
362 #[test]
363 fn terrain_index_matches_position_in_all() {
364 for (i, t) in Terrain::ALL.iter().enumerate() {
365 assert_eq!(t.index() as usize, i);
366 }
367 }
368 }