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