src/worldgen.rs 7.2 K · 223 lines · raw · history

1 use std::collections::VecDeque;
2
3 use bevy::prelude::*;
4
5 use crate::{
6 noise::Noise,
7 world::{Terrain, WorldMap},
8 };
9
10 pub const WORLD_SIZE: UVec2 = UVec2::new(256, 256);
11 pub const DEFAULT_SEED: u32 = 1;
12
13 const ELEVATION_SCALE: f32 = 1.0 / 48.0;
14 const FERTILITY_SCALE: f32 = 1.0 / 16.0;
15 const DEEP_WATER_SHARE: f32 = 0.10;
16 const SHALLOW_WATER_SHARE: f32 = 0.06;
17 const SAND_SHARE: f32 = 0.04;
18 const STONE_SHARE: f32 = 0.14;
19 const SOIL_SHARE: f32 = 0.20;
20
21 pub struct WorldGenPlugin;
22
23 impl Plugin for WorldGenPlugin {
24 fn build(&self, app: &mut App) {
25 app.insert_resource(WorldSeed(DEFAULT_SEED))
26 .insert_resource(generate(WORLD_SIZE, DEFAULT_SEED))
27 .add_message::<WorldGenerated>()
28 .add_systems(Startup, |mut generated: MessageWriter<WorldGenerated>| {
29 generated.write(WorldGenerated);
30 })
31 .add_systems(Update, regenerate_on_key);
32 }
33 }
34
35 #[derive(Resource, Debug, Clone, Copy, PartialEq)]
36 pub struct WorldSeed(pub u32);
37
38 #[derive(Message, Debug, Clone, Copy)]
39 pub struct WorldGenerated;
40
41 fn regenerate_on_key(
42 keys: Res<ButtonInput<KeyCode>>,
43 mut seed: ResMut<WorldSeed>,
44 mut map: ResMut<WorldMap>,
45 mut generated: MessageWriter<WorldGenerated>,
46 ) {
47 if keys.just_pressed(KeyCode::KeyR) {
48 seed.0 = seed.0.wrapping_add(1);
49 *map = generate(map.size, seed.0);
50 generated.write(WorldGenerated);
51 }
52 }
53
54 pub fn generate(size: UVec2, seed: u32) -> WorldMap {
55 let elevation_noise = Noise::new(seed);
56 let fertility_noise = Noise::new(seed.wrapping_add(0x1000));
57 let mut map = WorldMap::filled(size, Terrain::Grass);
58 let positions: Vec<UVec2> = map.positions().collect();
59 let elevation: Vec<f32> = positions
60 .iter()
61 .map(|p| elevation_noise.fbm(p.as_vec2() * ELEVATION_SCALE, 5))
62 .collect();
63 let fertility: Vec<f32> = positions
64 .iter()
65 .map(|p| fertility_noise.fbm(p.as_vec2() * FERTILITY_SCALE, 3))
66 .collect();
67
68 let deep = quantile(&elevation, DEEP_WATER_SHARE);
69 let shallow = quantile(&elevation, DEEP_WATER_SHARE + SHALLOW_WATER_SHARE);
70 let sand = quantile(&elevation, DEEP_WATER_SHARE + SHALLOW_WATER_SHARE + SAND_SHARE);
71 let stone = quantile(&elevation, 1.0 - STONE_SHARE);
72 let soil = quantile(&fertility, 1.0 - SOIL_SHARE);
73
74 for ((&pos, &e), &f) in positions.iter().zip(&elevation).zip(&fertility) {
75 let terrain = if e < deep {
76 Terrain::DeepWater
77 } else if e < shallow {
78 Terrain::ShallowWater
79 } else if e < sand {
80 Terrain::Sand
81 } else if e >= stone {
82 Terrain::Stone
83 } else if f >= soil {
84 Terrain::Soil
85 } else {
86 Terrain::Grass
87 };
88 map.set(pos, terrain);
89 }
90 map
91 }
92
93 pub fn quantile(values: &[f32], share: f32) -> f32 {
94 let mut sorted = values.to_vec();
95 sorted.sort_by(f32::total_cmp);
96 let i = ((sorted.len() as f32 * share) as usize).min(sorted.len() - 1);
97 sorted[i]
98 }
99
100 pub fn largest_region(map: &WorldMap) -> Vec<UVec2> {
101 let mut seen = vec![false; map.size.element_product() as usize];
102 let index = |p: UVec2| (p.y * map.size.x + p.x) as usize;
103 let mut best = Vec::new();
104 for start in map.positions() {
105 if seen[index(start)] || map.move_cost(start.as_ivec2()).is_none() {
106 continue;
107 }
108 let region = flood(map, start, |p| {
109 let fresh = !seen[index(p)];
110 seen[index(p)] = true;
111 fresh
112 });
113 if region.len() > best.len() {
114 best = region;
115 }
116 }
117 best
118 }
119
120 fn flood(map: &WorldMap, start: UVec2, mut visit: impl FnMut(UVec2) -> bool) -> Vec<UVec2> {
121 let mut region = Vec::new();
122 let mut queue = VecDeque::from([start]);
123 visit(start);
124 while let Some(pos) = queue.pop_front() {
125 region.push(pos);
126 for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] {
127 let next = pos.as_ivec2() + step;
128 if map.move_cost(next).is_some() && visit(next.as_uvec2()) {
129 queue.push_back(next.as_uvec2());
130 }
131 }
132 }
133 region
134 }
135
136 pub fn spawn_tiles(map: &WorldMap, count: usize) -> Vec<UVec2> {
137 let center = map.size.as_vec2() / 2.0;
138 let Some(&origin) = largest_region(map)
139 .iter()
140 .min_by(|a, b| a.as_vec2().distance_squared(center).total_cmp(&b.as_vec2().distance_squared(center)))
141 else {
142 return Vec::new();
143 };
144 let mut seen = std::collections::HashSet::new();
145 flood(map, origin, |p| seen.insert(p))
146 .into_iter()
147 .step_by(2)
148 .take(count)
149 .collect()
150 }
151
152 #[cfg(test)]
153 mod tests {
154 use super::*;
155 use crate::pathfinding::find_path;
156
157 const SIZE: UVec2 = UVec2::new(96, 64);
158
159 fn share(map: &WorldMap, terrain: Terrain) -> f32 {
160 let n = map.positions().filter(|p| map.get(p.as_ivec2()) == Some(terrain)).count();
161 n as f32 / map.size.element_product() as f32
162 }
163
164 #[test]
165 fn same_seed_generates_same_world() {
166 let (a, b, c) = (generate(SIZE, 7), generate(SIZE, 7), generate(SIZE, 8));
167 let tiles = |m: &WorldMap| m.positions().map(|p| m.get(p.as_ivec2())).collect::<Vec<_>>();
168 assert_eq!(tiles(&a), tiles(&b));
169 assert_ne!(tiles(&a), tiles(&c));
170 }
171
172 // Elevation thresholds come from quantiles rather than fixed values, so every seed gets the
173 // same terrain mix instead of occasionally being all ocean or all mountain.
174 #[test]
175 fn terrain_shares_are_stable_across_seeds() {
176 for seed in 0..5 {
177 let map = generate(SIZE, seed);
178 assert!((share(&map, Terrain::DeepWater) - DEEP_WATER_SHARE).abs() < 0.02);
179 assert!((share(&map, Terrain::Stone) - STONE_SHARE).abs() < 0.02);
180 for terrain in Terrain::ALL {
181 assert!(share(&map, terrain) > 0.0, "seed {seed} lacks {terrain:?}");
182 }
183 }
184 }
185
186 #[test]
187 fn quantile_picks_value_at_share() {
188 let values: Vec<f32> = (0..10).rev().map(|v| v as f32).collect();
189 assert_eq!(quantile(&values, 0.0), 0.0);
190 assert_eq!(quantile(&values, 0.3), 3.0);
191 assert_eq!(quantile(&values, 1.0), 9.0);
192 }
193
194 #[test]
195 fn largest_region_picks_biggest_walkable_area() {
196 let mut map = WorldMap::filled(UVec2::new(6, 3), Terrain::Grass);
197 for y in 0..3 {
198 map.set(UVec2::new(2, y), Terrain::Stone);
199 }
200 let region = largest_region(&map);
201 assert_eq!(region.len(), 9);
202 assert!(region.iter().all(|p| p.x > 2));
203 }
204
205 // Colonists must start somewhere they can all reach each other and most of the map,
206 // not on a small island cut off by deep water or stone.
207 #[test]
208 fn spawn_tiles_are_distinct_walkable_and_mutually_reachable() {
209 for seed in 0..5 {
210 let map = generate(SIZE, seed);
211 let tiles = spawn_tiles(&map, 3);
212 assert_eq!(tiles.len(), 3);
213 let region = largest_region(&map);
214 for t in &tiles {
215 assert!(region.contains(t));
216 assert!(find_path(&map, tiles[0], *t).is_some());
217 }
218 let mut unique = tiles.clone();
219 unique.dedup();
220 assert_eq!(unique.len(), 3);
221 }
222 }
223 }