src/world.rs 15.3 K · 505 lines · raw · history

1 use serde::{Deserialize, Serialize};
2 use std::collections::HashSet;
3
4 use bevy::prelude::*;
5
6 pub const MIN_WEAR: f32 = 0.05;
7 pub const TILE_SIZE: f32 = 16.0;
8 pub const MAX_HEIGHT: f32 = 100.0;
9 pub const UPHILL_COST_PER_METER: f32 = 20.0;
10 pub const CHEAPEST_MOVE_COST: u32 = 60;
11
12 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13 pub enum Terrain {
14 Grass,
15 Soil,
16 Sand,
17 ShallowWater,
18 DeepWater,
19 Stone,
20 IronOre,
21 GoldOre,
22 Gravel,
23 Path,
24 Paved,
25 }
26
27 impl Terrain {
28 pub const ALL: [Terrain; 11] = [
29 Terrain::Grass,
30 Terrain::Soil,
31 Terrain::Sand,
32 Terrain::ShallowWater,
33 Terrain::DeepWater,
34 Terrain::Stone,
35 Terrain::IronOre,
36 Terrain::GoldOre,
37 Terrain::Gravel,
38 Terrain::Path,
39 Terrain::Paved,
40 ];
41
42 pub fn index(self) -> u16 {
43 self as u16
44 }
45
46 pub fn move_cost(self) -> Option<u32> {
47 match self {
48 Terrain::Paved => Some(CHEAPEST_MOVE_COST),
49 Terrain::Path => Some(80),
50 Terrain::Grass | Terrain::Soil => Some(100),
51 Terrain::Sand | Terrain::Gravel => Some(130),
52 Terrain::ShallowWater => Some(300),
53 Terrain::DeepWater | Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => None,
54 }
55 }
56
57 pub fn is_water(self) -> bool {
58 matches!(self, Terrain::ShallowWater | Terrain::DeepWater)
59 }
60
61 pub fn is_rock(self) -> bool {
62 matches!(self, Terrain::Stone | Terrain::IronOre | Terrain::GoldOre)
63 }
64
65 pub fn color(self) -> Srgba {
66 match self {
67 Terrain::Grass => Srgba::rgb_u8(92, 140, 64),
68 Terrain::Soil => Srgba::rgb_u8(122, 92, 60),
69 Terrain::Sand => Srgba::rgb_u8(214, 196, 140),
70 Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184),
71 Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140),
72 Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => Srgba::rgb_u8(118, 118, 124),
73 Terrain::Gravel => Srgba::rgb_u8(150, 146, 138),
74 Terrain::Path => Srgba::rgb_u8(160, 128, 86),
75 Terrain::Paved => Srgba::rgb_u8(168, 164, 156),
76 }
77 }
78
79 pub fn speck_color(self) -> Option<Srgba> {
80 match self {
81 Terrain::IronOre => Some(Srgba::rgb_u8(178, 104, 72)),
82 Terrain::GoldOre => Some(Srgba::rgb_u8(236, 196, 60)),
83 Terrain::Path => Some(Srgba::rgb_u8(130, 104, 70)),
84 _ => None,
85 }
86 }
87 }
88
89 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90 pub enum Plant {
91 Tree,
92 BerryBush,
93 Crop,
94 }
95
96 impl Plant {
97 pub const ALL: [Plant; 3] = [Plant::Tree, Plant::BerryBush, Plant::Crop];
98
99 pub fn extra_move_cost(self) -> u32 {
100 match self {
101 Plant::Tree => 100,
102 Plant::BerryBush => 60,
103 Plant::Crop => 30,
104 }
105 }
106
107 pub fn ripening_ticks(self) -> Option<f32> {
108 let days = match self {
109 Plant::Tree => return None,
110 Plant::BerryBush => 2.0,
111 Plant::Crop => 1.5,
112 };
113 Some(days * 24.0 * crate::sim::TICKS_PER_HOUR as f32)
114 }
115
116 pub fn is_ripe(self, growth: f32) -> bool {
117 self.ripening_ticks().is_some() && growth >= 1.0
118 }
119 }
120
121 #[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
122 pub struct TilePos(pub UVec2);
123
124 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
125 pub struct MapLayers {
126 size: UVec2,
127 terrain: Vec<(u8, u32)>,
128 plants: Vec<(u8, u32)>,
129 walls: Vec<(u8, u32)>,
130 roofs: Vec<(u8, u32)>,
131 growth: Vec<(u32, f32)>,
132 wear: Vec<(u32, f32)>,
133 }
134
135 fn run_length(values: impl Iterator<Item = u8>) -> Vec<(u8, u32)> {
136 let mut runs: Vec<(u8, u32)> = Vec::new();
137 for v in values {
138 match runs.last_mut() {
139 Some((last, count)) if *last == v => *count += 1,
140 _ => runs.push((v, 1)),
141 }
142 }
143 runs
144 }
145
146 #[derive(Message, Debug, Clone, Copy, PartialEq)]
147 pub struct TileChanged(pub UVec2);
148
149 #[derive(Resource, Clone, Debug)]
150 pub struct WorldMap {
151 pub size: UVec2,
152 tiles: Vec<Terrain>,
153 plants: Vec<Option<Plant>>,
154 heights: Vec<f32>,
155 walls: Vec<bool>,
156 roofs: Vec<bool>,
157 growth: Vec<f32>,
158 wear: Vec<f32>,
159 worn: HashSet<UVec2>,
160 growing: HashSet<UVec2>,
161 }
162
163 impl WorldMap {
164 pub fn filled(size: UVec2, terrain: Terrain) -> Self {
165 let n = size.element_product() as usize;
166 Self {
167 size,
168 tiles: vec![terrain; n],
169 plants: vec![None; n],
170 heights: vec![0.0; n],
171 walls: vec![false; n],
172 roofs: vec![false; n],
173 growth: vec![0.0; n],
174 wear: vec![0.0; n],
175 worn: HashSet::new(),
176 growing: HashSet::new(),
177 }
178 }
179
180 pub fn in_bounds(&self, pos: IVec2) -> bool {
181 pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y
182 }
183
184 fn index(&self, pos: UVec2) -> usize {
185 (pos.y * self.size.x + pos.x) as usize
186 }
187
188 pub fn get(&self, pos: IVec2) -> Option<Terrain> {
189 self.in_bounds(pos)
190 .then(|| self.tiles[self.index(pos.as_uvec2())])
191 }
192
193 pub fn set(&mut self, pos: UVec2, terrain: Terrain) {
194 let i = self.index(pos);
195 self.tiles[i] = terrain;
196 }
197
198 pub fn plant(&self, pos: IVec2) -> Option<Plant> {
199 self.in_bounds(pos)
200 .then(|| self.plants[self.index(pos.as_uvec2())])
201 .flatten()
202 }
203
204 pub fn set_plant(&mut self, pos: UVec2, plant: Option<Plant>) {
205 let i = self.index(pos);
206 self.plants[i] = plant;
207 if plant.is_some_and(|p| p.ripening_ticks().is_some()) {
208 self.growing.insert(pos);
209 } else {
210 self.growing.remove(&pos);
211 }
212 }
213
214 pub fn growing_plants(&self) -> Vec<UVec2> {
215 let mut tiles: Vec<UVec2> = self.growing.iter().copied().collect();
216 tiles.sort_by_key(|t| (t.y, t.x));
217 tiles
218 }
219
220 pub fn height(&self, pos: IVec2) -> Option<f32> {
221 self.in_bounds(pos)
222 .then(|| self.heights[self.index(pos.as_uvec2())])
223 }
224
225 pub fn set_height(&mut self, pos: UVec2, height: f32) {
226 let i = self.index(pos);
227 self.heights[i] = height;
228 }
229
230 pub fn is_wall(&self, pos: IVec2) -> bool {
231 self.in_bounds(pos) && self.walls[self.index(pos.as_uvec2())]
232 }
233
234 pub fn set_wall(&mut self, pos: UVec2, wall: bool) {
235 let i = self.index(pos);
236 self.walls[i] = wall;
237 }
238
239 pub fn growth(&self, pos: IVec2) -> f32 {
240 if self.in_bounds(pos) {
241 self.growth[self.index(pos.as_uvec2())]
242 } else {
243 0.0
244 }
245 }
246
247 pub fn set_growth(&mut self, pos: UVec2, growth: f32) {
248 let i = self.index(pos);
249 self.growth[i] = growth.clamp(0.0, 1.0);
250 }
251
252 pub fn wear(&self, pos: IVec2) -> f32 {
253 if self.in_bounds(pos) {
254 self.wear[self.index(pos.as_uvec2())]
255 } else {
256 0.0
257 }
258 }
259
260 pub fn set_wear(&mut self, pos: UVec2, wear: f32) {
261 let i = self.index(pos);
262 let wear = if wear < MIN_WEAR { 0.0 } else { wear };
263 self.wear[i] = wear;
264 if wear > 0.0 {
265 self.worn.insert(pos);
266 } else {
267 self.worn.remove(&pos);
268 }
269 }
270
271 pub fn layers(&self) -> MapLayers {
272 let sparse = |values: &[f32]| {
273 values
274 .iter()
275 .enumerate()
276 .filter(|(_, v)| **v != 0.0)
277 .map(|(i, v)| (i as u32, *v))
278 .collect()
279 };
280 MapLayers {
281 size: self.size,
282 terrain: run_length(self.tiles.iter().map(|t| t.index() as u8)),
283 plants: run_length(self.plants.iter().map(|p| p.map_or(0, |p| p as u8 + 1))),
284 walls: run_length(self.walls.iter().map(|&w| w as u8)),
285 roofs: run_length(self.roofs.iter().map(|&r| r as u8)),
286 growth: sparse(&self.growth),
287 wear: sparse(&self.wear),
288 }
289 }
290
291 pub fn restore(&mut self, layers: &MapLayers) -> Result<(), String> {
292 let n = self.tiles.len();
293 let unpacked = |runs: &[(u8, u32)]| {
294 let values: Vec<u8> = runs
295 .iter()
296 .flat_map(|&(v, count)| std::iter::repeat_n(v, count as usize))
297 .collect();
298 if values.len() == n {
299 Ok(values)
300 } else {
301 Err("map layer has the wrong size".to_string())
302 }
303 };
304 if layers.size != self.size {
305 return Err("map size differs".into());
306 }
307 let terrain = unpacked(&layers.terrain)?;
308 let plants = unpacked(&layers.plants)?;
309 let walls = unpacked(&layers.walls)?;
310 let roofs = unpacked(&layers.roofs)?;
311 for (i, pos) in self.positions().enumerate() {
312 let terrain = Terrain::ALL
313 .get(terrain[i] as usize)
314 .ok_or("unknown terrain")?;
315 self.set(pos, *terrain);
316 let plant = match plants[i] {
317 0 => None,
318 p => Some(*Plant::ALL.get(p as usize - 1).ok_or("unknown plant")?),
319 };
320 self.set_plant(pos, plant);
321 self.set_wall(pos, walls[i] != 0);
322 self.set_roof(pos, roofs[i] != 0);
323 self.set_growth(pos, 0.0);
324 self.set_wear(pos, 0.0);
325 }
326 let width = self.size.x;
327 let at = |i: u32| UVec2::new(i % width, i / width);
328 for &(i, growth) in &layers.growth {
329 self.set_growth(at(i), growth);
330 }
331 for &(i, wear) in &layers.wear {
332 self.set_wear(at(i), wear);
333 }
334 Ok(())
335 }
336
337 pub fn worn_tiles(&self) -> Vec<UVec2> {
338 let mut tiles: Vec<UVec2> = self.worn.iter().copied().collect();
339 tiles.sort_by_key(|t| (t.y, t.x));
340 tiles
341 }
342
343 pub fn ripe_plant(&self, pos: IVec2) -> Option<Plant> {
344 self.plant(pos).filter(|p| p.is_ripe(self.growth(pos)))
345 }
346
347 pub fn is_roofed(&self, pos: IVec2) -> bool {
348 self.in_bounds(pos) && self.roofs[self.index(pos.as_uvec2())]
349 }
350
351 pub fn set_roof(&mut self, pos: UVec2, roof: bool) {
352 let i = self.index(pos);
353 self.roofs[i] = roof;
354 }
355
356 pub fn move_cost(&self, pos: IVec2) -> Option<u32> {
357 if self.is_wall(pos) {
358 return None;
359 }
360 let terrain = self.get(pos)?.move_cost()?;
361 Some(terrain + self.plant(pos).map_or(0, Plant::extra_move_cost))
362 }
363
364 pub fn step_cost(&self, from: IVec2, to: IVec2) -> Option<u32> {
365 let climb = (self.height(to)? - self.height(from)?).max(0.0);
366 Some(self.move_cost(to)? + (climb * UPHILL_COST_PER_METER).round() as u32)
367 }
368
369 pub fn positions(&self) -> impl Iterator<Item = UVec2> + use<> {
370 let size = self.size;
371 (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y)))
372 }
373
374 pub fn world_size(&self) -> Vec2 {
375 self.size.as_vec2() * TILE_SIZE
376 }
377
378 pub fn tile_center(pos: UVec2) -> Vec2 {
379 (pos.as_vec2() + 0.5) * TILE_SIZE
380 }
381
382 pub fn tile_at(&self, world: Vec2) -> Option<UVec2> {
383 let pos = (world / TILE_SIZE).floor().as_ivec2();
384 self.in_bounds(pos).then(|| pos.as_uvec2())
385 }
386 }
387
388 pub fn hash2(x: u32, y: u32) -> u32 {
389 let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77);
390 h ^= h >> 15;
391 h = h.wrapping_mul(0x2C1B_3C6D);
392 h ^= h >> 12;
393 h = h.wrapping_mul(0x297A_2D39);
394 h ^ (h >> 15)
395 }
396
397 #[cfg(test)]
398 mod tests {
399 use super::*;
400
401 // Growth runs ten times a game hour; it visits only tracked growing plants because a
402 // full-map pass that often dominated frame time at high speeds.
403 #[test]
404 fn only_ripening_plants_are_tracked_as_growing() {
405 let mut map = WorldMap::filled(UVec2::splat(4), Terrain::Grass);
406 map.set_plant(UVec2::new(1, 1), Some(Plant::Tree));
407 map.set_plant(UVec2::new(2, 2), Some(Plant::BerryBush));
408 map.set_plant(UVec2::new(3, 3), Some(Plant::Crop));
409 assert_eq!(
410 map.growing_plants(),
411 vec![UVec2::new(2, 2), UVec2::new(3, 3)]
412 );
413 map.set_plant(UVec2::new(2, 2), None);
414 assert_eq!(map.growing_plants(), vec![UVec2::new(3, 3)]);
415 }
416
417 #[test]
418 fn get_out_of_bounds_is_none() {
419 let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
420 assert_eq!(map.get(IVec2::new(-1, 0)), None);
421 assert_eq!(map.get(IVec2::new(4, 0)), None);
422 assert_eq!(map.get(IVec2::new(0, 3)), None);
423 assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass));
424 }
425
426 #[test]
427 fn tiles_are_stored_row_major_from_bottom_left() {
428 let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
429 map.set(UVec2::new(1, 2), Terrain::Stone);
430 assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone);
431 }
432
433 #[test]
434 fn tile_at_inverts_tile_center() {
435 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
436 for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] {
437 assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos));
438 }
439 }
440
441 #[test]
442 fn tile_at_outside_world_is_none() {
443 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
444 assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None);
445 assert_eq!(map.tile_at(map.world_size()), None);
446 }
447
448 #[test]
449 fn trees_slow_movement_but_do_not_block_it() {
450 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
451 let open = map.move_cost(IVec2::ZERO).unwrap();
452 map.set_plant(UVec2::ZERO, Some(Plant::Tree));
453 assert!(map.move_cost(IVec2::ZERO).unwrap() > open);
454 }
455
456 #[test]
457 fn bushes_and_crops_ripen_but_trees_do_not() {
458 assert!(Plant::BerryBush.is_ripe(1.0));
459 assert!(!Plant::Crop.is_ripe(0.9));
460 assert!(!Plant::Tree.is_ripe(1.0));
461 }
462
463 // A* uses CHEAPEST_MOVE_COST in its distance estimate; if any terrain were cheaper the
464 // estimate would overshoot and colonists could take needlessly long routes.
465 #[test]
466 fn no_terrain_is_cheaper_than_the_pathfinding_estimate() {
467 for terrain in Terrain::ALL {
468 if let Some(cost) = terrain.move_cost() {
469 assert!(cost >= CHEAPEST_MOVE_COST, "{terrain:?}");
470 }
471 }
472 }
473
474 #[test]
475 fn walls_block_movement() {
476 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
477 map.set_wall(UVec2::ZERO, true);
478 assert_eq!(map.move_cost(IVec2::ZERO), None);
479 assert!(map.move_cost(IVec2::X).is_some());
480 }
481
482 #[test]
483 fn walking_uphill_costs_more_than_downhill_or_flat() {
484 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Grass);
485 map.set_height(UVec2::new(1, 0), 5.0);
486 let (low, high, flat) = (IVec2::new(0, 0), IVec2::new(1, 0), IVec2::new(2, 0));
487 let up = map.step_cost(low, high).unwrap();
488 let down = map.step_cost(high, low).unwrap();
489 assert!(up > down);
490 assert_eq!(down, map.step_cost(low, flat).unwrap());
491 }
492
493 #[test]
494 fn rock_and_water_classification() {
495 assert!(Terrain::IronOre.is_rock() && Terrain::IronOre.move_cost().is_none());
496 assert!(Terrain::ShallowWater.is_water() && !Terrain::ShallowWater.is_rock());
497 }
498
499 #[test]
500 fn terrain_index_matches_position_in_all() {
501 for (i, t) in Terrain::ALL.iter().enumerate() {
502 assert_eq!(t.index() as usize, i);
503 }
504 }
505 }