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