src/world.rs 12.3 K · 414 lines · raw · history

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