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