src/worldgen.rs 17.9 K · 545 lines · raw · history

1 use std::{
2 cmp::Reverse,
3 collections::{BinaryHeap, VecDeque},
4 };
5
6 use bevy::prelude::*;
7
8 use crate::{
9 actions::Action,
10 noise::Noise,
11 rng::SimRng,
12 world::{MAX_HEIGHT, Plant, Terrain, WorldMap, hash2},
13 };
14
15 pub const WORLD_SIZE: UVec2 = UVec2::new(256, 256);
16 pub const DEFAULT_SEED: u32 = 1;
17
18 const ELEVATION_SCALE: f32 = 1.0 / 48.0;
19 const FERTILITY_SCALE: f32 = 1.0 / 16.0;
20 const DEEP_WATER_SHARE: f32 = 0.10;
21 const SHALLOW_WATER_SHARE: f32 = 0.06;
22 const SAND_SHARE: f32 = 0.04;
23 const STONE_SHARE: f32 = 0.14;
24 const SOIL_SHARE: f32 = 0.20;
25 const ORE_SCALE: f32 = 1.0 / 6.0;
26 const IRON_SHARE_OF_ROCK: f32 = 0.10;
27 const GOLD_SHARE_OF_ROCK: f32 = 0.03;
28 const FOREST_SCALE: f32 = 1.0 / 32.0;
29 const FOREST_SHARE: f32 = 0.35;
30 const FOREST_TREE_PERCENT: u32 = 45;
31 const LONE_TREE_PERCENT: u32 = 3;
32 const BERRY_BUSH_PERCENT: u32 = 2;
33 const RIVER_COUNT: usize = 4;
34 const RIVER_CANDIDATES: usize = 40;
35 const RIVER_SOURCE_SPACING: f32 = 40.0;
36
37 pub struct WorldGenPlugin;
38
39 impl Plugin for WorldGenPlugin {
40 fn build(&self, app: &mut App) {
41 app.insert_resource(WorldSeed(DEFAULT_SEED))
42 .insert_resource(generate(WORLD_SIZE, DEFAULT_SEED))
43 .add_message::<WorldGenerated>()
44 .add_systems(Startup, |mut generated: MessageWriter<WorldGenerated>| {
45 generated.write(WorldGenerated);
46 })
47 .add_systems(Update, regenerate);
48 }
49 }
50
51 #[derive(Resource, Debug, Clone, Copy, PartialEq)]
52 pub struct WorldSeed(pub u32);
53
54 #[derive(Message, Debug, Clone, Copy)]
55 pub struct WorldGenerated;
56
57 fn regenerate(
58 mut actions: MessageReader<Action>,
59 mut seed: ResMut<WorldSeed>,
60 mut map: ResMut<WorldMap>,
61 mut generated: MessageWriter<WorldGenerated>,
62 ) {
63 if actions.read().any(|a| *a == Action::NewWorld) {
64 seed.0 = seed.0.wrapping_add(1);
65 *map = generate(map.size, seed.0);
66 generated.write(WorldGenerated);
67 }
68 }
69
70 pub fn generate(size: UVec2, seed: u32) -> WorldMap {
71 let mut map = WorldMap::filled(size, Terrain::Grass);
72 let positions: Vec<UVec2> = map.positions().collect();
73 let sample = |offset: u32, scale: f32, octaves: u32| -> Vec<f32> {
74 let noise = Noise::new(seed.wrapping_add(offset));
75 positions
76 .iter()
77 .map(|p| noise.fbm(p.as_vec2() * scale, octaves))
78 .collect()
79 };
80 let elevation = sample(0, ELEVATION_SCALE, 5);
81 let fertility = sample(0x1000, FERTILITY_SCALE, 3);
82 let iron = sample(0x2000, ORE_SCALE, 2);
83 let gold = sample(0x3000, ORE_SCALE, 2);
84 let forest = sample(0x4000, FOREST_SCALE, 3);
85
86 let (low, high) = (quantile(&elevation, 0.0), quantile(&elevation, 1.0));
87 for (&pos, &e) in positions.iter().zip(&elevation) {
88 map.set_height(pos, (e - low) / (high - low).max(f32::EPSILON) * MAX_HEIGHT);
89 }
90
91 let deep = quantile(&elevation, DEEP_WATER_SHARE);
92 let shallow = quantile(&elevation, DEEP_WATER_SHARE + SHALLOW_WATER_SHARE);
93 let sand = quantile(
94 &elevation,
95 DEEP_WATER_SHARE + SHALLOW_WATER_SHARE + SAND_SHARE,
96 );
97 let stone = quantile(&elevation, 1.0 - STONE_SHARE);
98 let soil = quantile(&fertility, 1.0 - SOIL_SHARE);
99 let is_rock = |i: usize| elevation[i] >= stone;
100 let rock_values = |values: &[f32]| -> Vec<f32> {
101 (0..values.len())
102 .filter(|&i| is_rock(i))
103 .map(|i| values[i])
104 .collect()
105 };
106 let iron_min = quantile(&rock_values(&iron), 1.0 - IRON_SHARE_OF_ROCK);
107 let gold_min = quantile(&rock_values(&gold), 1.0 - GOLD_SHARE_OF_ROCK);
108
109 for (i, &pos) in positions.iter().enumerate() {
110 let e = elevation[i];
111 let terrain = if e < deep {
112 Terrain::DeepWater
113 } else if e < shallow {
114 Terrain::ShallowWater
115 } else if e < sand {
116 Terrain::Sand
117 } else if is_rock(i) && gold[i] >= gold_min {
118 Terrain::GoldOre
119 } else if is_rock(i) && iron[i] >= iron_min {
120 Terrain::IronOre
121 } else if is_rock(i) {
122 Terrain::Stone
123 } else if fertility[i] >= soil {
124 Terrain::Soil
125 } else {
126 Terrain::Grass
127 };
128 map.set(pos, terrain);
129 }
130
131 for river in pick_rivers(&map, seed) {
132 carve_river(&mut map, &river);
133 }
134
135 let forest_min = quantile(&forest, 1.0 - FOREST_SHARE);
136 for (i, &pos) in positions.iter().enumerate() {
137 let fertile = matches!(
138 map.get(pos.as_ivec2()),
139 Some(Terrain::Grass | Terrain::Soil)
140 );
141 let percent = if forest[i] >= forest_min {
142 FOREST_TREE_PERCENT
143 } else {
144 LONE_TREE_PERCENT
145 };
146 let roll = hash2(pos.x ^ seed, pos.y) % 100;
147 if fertile && roll < percent {
148 map.set_plant(pos, Some(Plant::Tree));
149 } else if fertile && roll >= 100 - BERRY_BUSH_PERCENT {
150 map.set_plant(pos, Some(Plant::BerryBush));
151 map.set_growth(pos, (hash2(pos.y ^ seed, pos.x) % 140) as f32 / 100.0);
152 }
153 }
154 map
155 }
156
157 fn mountain_feet(map: &WorldMap) -> Vec<UVec2> {
158 map.positions()
159 .filter(|p| {
160 let p = p.as_ivec2();
161 map.move_cost(p).is_some()
162 && !map.get(p).unwrap().is_water()
163 && [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y]
164 .iter()
165 .any(|&d| map.get(p + d).is_some_and(Terrain::is_rock))
166 })
167 .collect()
168 }
169
170 pub fn pick_rivers(map: &WorldMap, seed: u32) -> Vec<Vec<UVec2>> {
171 let feet = mountain_feet(map);
172 if feet.is_empty() {
173 return Vec::new();
174 }
175 let flow = FlowField::new(map);
176 let mut rng = SimRng::new(u64::from(seed) ^ 0x5249_5645);
177 let mut rivers: Vec<Vec<UVec2>> = (0..RIVER_CANDIDATES)
178 .map(|_| feet[rng.range_i32(0..feet.len() as i32) as usize])
179 .map(|source| flow.trace(source))
180 .collect();
181 rivers.sort_by_key(|river| Reverse(river.len()));
182 let mut picked: Vec<Vec<UVec2>> = Vec::new();
183 for river in rivers {
184 if picked.len() == RIVER_COUNT {
185 break;
186 }
187 let spaced = picked
188 .iter()
189 .all(|other| other[0].as_vec2().distance(river[0].as_vec2()) >= RIVER_SOURCE_SPACING);
190 if spaced {
191 picked.push(river);
192 }
193 }
194 picked
195 }
196
197 pub struct FlowField {
198 size: UVec2,
199 downstream: Vec<Option<UVec2>>,
200 }
201
202 impl FlowField {
203 pub fn new(map: &WorldMap) -> Self {
204 let n = map.size.element_product() as usize;
205 let index = |p: UVec2| (p.y * map.size.x + p.x) as usize;
206 let mut downstream = vec![None; n];
207 let mut done = vec![false; n];
208 let mut open = BinaryHeap::new();
209 for pos in map.positions() {
210 let p = pos.as_ivec2();
211 let terrain = map.get(p).unwrap();
212 let at_edge =
213 pos.x == 0 || pos.y == 0 || pos.x == map.size.x - 1 || pos.y == map.size.y - 1;
214 if terrain.is_water() || (at_edge && !terrain.is_rock()) {
215 done[index(pos)] = true;
216 open.push(Reverse((map.height(p).unwrap().to_bits(), pos.x, pos.y)));
217 }
218 }
219 while let Some(Reverse((level, x, y))) = open.pop() {
220 let current = UVec2::new(x, y);
221 for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] {
222 let next = current.as_ivec2() + step;
223 if !map.get(next).is_some_and(|t| !t.is_rock()) || done[index(next.as_uvec2())] {
224 continue;
225 }
226 let next = next.as_uvec2();
227 done[index(next)] = true;
228 downstream[index(next)] = Some(current);
229 let filled = map
230 .height(next.as_ivec2())
231 .unwrap()
232 .max(f32::from_bits(level));
233 open.push(Reverse((filled.to_bits(), next.x, next.y)));
234 }
235 }
236 Self {
237 size: map.size,
238 downstream,
239 }
240 }
241
242 fn downstream(&self, pos: UVec2) -> Option<UVec2> {
243 self.downstream[(pos.y * self.size.x + pos.x) as usize]
244 }
245
246 pub fn trace(&self, source: UVec2) -> Vec<UVec2> {
247 let mut path = vec![source];
248 while let Some(next) = self.downstream(*path.last().unwrap()) {
249 path.push(next);
250 }
251 path
252 }
253 }
254
255 fn carve_river(map: &mut WorldMap, path: &[UVec2]) {
256 for (i, &pos) in path.iter().enumerate() {
257 let mut tiles = vec![pos.as_ivec2()];
258 if i >= path.len() / 3 {
259 tiles.push(pos.as_ivec2() + IVec2::X);
260 }
261 if i >= path.len() * 2 / 3 {
262 tiles.push(pos.as_ivec2() + IVec2::Y);
263 }
264 for tile in tiles {
265 if map
266 .get(tile)
267 .is_some_and(|t| !t.is_rock() && t != Terrain::DeepWater)
268 {
269 map.set(tile.as_uvec2(), Terrain::ShallowWater);
270 }
271 }
272 }
273 }
274
275 pub fn quantile(values: &[f32], share: f32) -> f32 {
276 let mut sorted = values.to_vec();
277 sorted.sort_by(f32::total_cmp);
278 let i = ((sorted.len() as f32 * share) as usize).min(sorted.len() - 1);
279 sorted[i]
280 }
281
282 pub fn largest_region(map: &WorldMap) -> Vec<UVec2> {
283 let mut seen = vec![false; map.size.element_product() as usize];
284 let index = |p: UVec2| (p.y * map.size.x + p.x) as usize;
285 let mut best = Vec::new();
286 for start in map.positions() {
287 if seen[index(start)] || map.move_cost(start.as_ivec2()).is_none() {
288 continue;
289 }
290 let region = flood(map, start, |p| {
291 let fresh = !seen[index(p)];
292 seen[index(p)] = true;
293 fresh
294 });
295 if region.len() > best.len() {
296 best = region;
297 }
298 }
299 best
300 }
301
302 fn flood(map: &WorldMap, start: UVec2, mut visit: impl FnMut(UVec2) -> bool) -> Vec<UVec2> {
303 let mut region = Vec::new();
304 let mut queue = VecDeque::from([start]);
305 visit(start);
306 while let Some(pos) = queue.pop_front() {
307 region.push(pos);
308 for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] {
309 let next = pos.as_ivec2() + step;
310 if map.move_cost(next).is_some() && visit(next.as_uvec2()) {
311 queue.push_back(next.as_uvec2());
312 }
313 }
314 }
315 region
316 }
317
318 pub fn spawn_tiles(map: &WorldMap, count: usize) -> Vec<UVec2> {
319 let center = map.size.as_vec2() / 2.0;
320 let Some(&origin) = largest_region(map).iter().min_by(|a, b| {
321 a.as_vec2()
322 .distance_squared(center)
323 .total_cmp(&b.as_vec2().distance_squared(center))
324 }) else {
325 return Vec::new();
326 };
327 let mut seen = std::collections::HashSet::new();
328 flood(map, origin, |p| seen.insert(p))
329 .into_iter()
330 .step_by(2)
331 .take(count)
332 .collect()
333 }
334
335 #[cfg(test)]
336 mod tests {
337 use super::*;
338 use crate::pathfinding::find_path;
339
340 const SIZE: UVec2 = UVec2::new(96, 64);
341
342 fn share(map: &WorldMap, terrain: Terrain) -> f32 {
343 let n = map
344 .positions()
345 .filter(|p| map.get(p.as_ivec2()) == Some(terrain))
346 .count();
347 n as f32 / map.size.element_product() as f32
348 }
349
350 #[test]
351 fn same_seed_generates_same_world() {
352 let (a, b, c) = (generate(SIZE, 7), generate(SIZE, 7), generate(SIZE, 8));
353 let tiles = |m: &WorldMap| {
354 m.positions()
355 .map(|p| {
356 let p = p.as_ivec2();
357 (m.get(p), m.plant(p), m.height(p).map(f32::to_bits))
358 })
359 .collect::<Vec<_>>()
360 };
361 assert_eq!(tiles(&a), tiles(&b));
362 assert_ne!(tiles(&a), tiles(&c));
363 }
364
365 // Elevation thresholds come from quantiles rather than fixed values, so every seed gets the
366 // same terrain mix instead of occasionally being all ocean or all mountain.
367 #[test]
368 fn terrain_shares_are_stable_across_seeds() {
369 for seed in 0..5 {
370 let map = generate(SIZE, seed);
371 assert!((share(&map, Terrain::DeepWater) - DEEP_WATER_SHARE).abs() < 0.02);
372 let rock: f32 = [Terrain::Stone, Terrain::IronOre, Terrain::GoldOre]
373 .iter()
374 .map(|&t| share(&map, t))
375 .sum();
376 assert!((rock - STONE_SHARE).abs() < 0.02);
377 let generated =
378 |t: &Terrain| !matches!(t, Terrain::Gravel | Terrain::Path | Terrain::Paved);
379 for terrain in Terrain::ALL.into_iter().filter(generated) {
380 assert!(share(&map, terrain) > 0.0, "seed {seed} lacks {terrain:?}");
381 }
382 }
383 }
384
385 #[test]
386 fn gold_is_rarer_than_iron() {
387 let map = generate(SIZE, 3);
388 assert!(share(&map, Terrain::GoldOre) < share(&map, Terrain::IronOre));
389 }
390
391 #[test]
392 fn mountains_are_the_highest_ground() {
393 let map = generate(SIZE, 4);
394 let mean_height = |pred: fn(Terrain) -> bool| {
395 let hs: Vec<f32> = map
396 .positions()
397 .map(|p| p.as_ivec2())
398 .filter(|&p| pred(map.get(p).unwrap()))
399 .map(|p| map.height(p).unwrap())
400 .collect();
401 hs.iter().sum::<f32>() / hs.len() as f32
402 };
403 assert!(mean_height(Terrain::is_rock) > mean_height(|t| t == Terrain::Grass));
404 assert!(mean_height(|t| t == Terrain::Grass) > mean_height(|t| t == Terrain::DeepWater));
405 assert!(
406 map.positions()
407 .all(|p| (0.0..=MAX_HEIGHT).contains(&map.height(p.as_ivec2()).unwrap()))
408 );
409 }
410
411 #[test]
412 fn worlds_have_berry_bushes_some_of_them_ripe() {
413 let map = generate(SIZE, 2);
414 let bushes: Vec<_> = map
415 .positions()
416 .map(|p| p.as_ivec2())
417 .filter(|&p| map.plant(p) == Some(Plant::BerryBush))
418 .collect();
419 assert!(bushes.len() > 20);
420 assert!(bushes.iter().any(|&p| map.ripe_plant(p).is_some()));
421 assert!(bushes.iter().any(|&p| map.ripe_plant(p).is_none()));
422 }
423
424 #[test]
425 fn trees_only_grow_on_grass_and_soil() {
426 let map = generate(SIZE, 2);
427 let trees: Vec<_> = map
428 .positions()
429 .map(|p| p.as_ivec2())
430 .filter(|&p| map.plant(p) == Some(Plant::Tree))
431 .collect();
432 assert!(!trees.is_empty());
433 assert!(
434 trees
435 .iter()
436 .all(|&p| matches!(map.get(p), Some(Terrain::Grass | Terrain::Soil)))
437 );
438 }
439
440 fn slope_map() -> WorldMap {
441 let mut map = WorldMap::filled(UVec2::new(10, 5), Terrain::Grass);
442 for pos in map.positions() {
443 map.set_height(pos, pos.x as f32);
444 if pos.x == 0 {
445 map.set(pos, Terrain::DeepWater);
446 }
447 }
448 map
449 }
450
451 #[test]
452 fn rivers_flow_downhill_into_water() {
453 let map = slope_map();
454 let path = FlowField::new(&map).trace(UVec2::new(8, 2));
455 assert_eq!(
456 map.get(path.last().unwrap().as_ivec2()),
457 Some(Terrain::DeepWater)
458 );
459 assert!(path.windows(2).all(|w| w[1].x < w[0].x));
460 }
461
462 // Rivers are carved into the terrain after rock is placed; flowing through mountains would
463 // leave walkable water channels cutting through what should be solid stone.
464 #[test]
465 fn rivers_route_around_rock() {
466 let mut map = slope_map();
467 map.set(UVec2::new(5, 2), Terrain::Stone);
468 let path = FlowField::new(&map).trace(UVec2::new(8, 2));
469 assert!(
470 path.iter()
471 .all(|p| !map.get(p.as_ivec2()).unwrap().is_rock())
472 );
473 assert_eq!(
474 map.get(path.last().unwrap().as_ivec2()),
475 Some(Terrain::DeepWater)
476 );
477 }
478
479 // Noise terrain is full of small dips. Flow directions come from a priority flood that
480 // spills out of dips at their lowest rim, so rivers cross them instead of ending in (or
481 // meandering around inside) every depression.
482 #[test]
483 fn rivers_cross_depressions_to_reach_water() {
484 let mut map = slope_map();
485 map.set_height(UVec2::new(5, 2), 0.5);
486 let path = FlowField::new(&map).trace(UVec2::new(8, 2));
487 assert_eq!(
488 map.get(path.last().unwrap().as_ivec2()),
489 Some(Terrain::DeepWater)
490 );
491 assert!(path.len() <= 9);
492 }
493
494 #[test]
495 fn generated_worlds_have_rivers() {
496 let map = generate(WORLD_SIZE, 1);
497 assert!(share(&map, Terrain::ShallowWater) > SHALLOW_WATER_SHARE + 0.005);
498 }
499
500 #[test]
501 fn longest_candidate_rivers_are_picked() {
502 let map = generate(WORLD_SIZE, 1);
503 let rivers = pick_rivers(&map, 1);
504 assert!(!rivers.is_empty() && rivers.len() <= RIVER_COUNT);
505 assert!(rivers.windows(2).all(|w| w[0].len() >= w[1].len()));
506 }
507
508 #[test]
509 fn quantile_picks_value_at_share() {
510 let values: Vec<f32> = (0..10).rev().map(|v| v as f32).collect();
511 assert_eq!(quantile(&values, 0.0), 0.0);
512 assert_eq!(quantile(&values, 0.3), 3.0);
513 assert_eq!(quantile(&values, 1.0), 9.0);
514 }
515
516 #[test]
517 fn largest_region_picks_biggest_walkable_area() {
518 let mut map = WorldMap::filled(UVec2::new(6, 3), Terrain::Grass);
519 for y in 0..3 {
520 map.set(UVec2::new(2, y), Terrain::Stone);
521 }
522 let region = largest_region(&map);
523 assert_eq!(region.len(), 9);
524 assert!(region.iter().all(|p| p.x > 2));
525 }
526
527 // Colonists must start somewhere they can all reach each other and most of the map,
528 // not on a small island cut off by deep water or stone.
529 #[test]
530 fn spawn_tiles_are_distinct_walkable_and_mutually_reachable() {
531 for seed in 0..5 {
532 let map = generate(SIZE, seed);
533 let tiles = spawn_tiles(&map, 3);
534 assert_eq!(tiles.len(), 3);
535 let region = largest_region(&map);
536 for t in &tiles {
537 assert!(region.contains(t));
538 assert!(find_path(&map, tiles[0], *t).is_some());
539 }
540 let mut unique = tiles.clone();
541 unique.dedup();
542 assert_eq!(unique.len(), 3);
543 }
544 }
545 }