1use super::Rng;
7use std::collections::{BinaryHeap, HashSet, VecDeque};
8use std::cmp::Ordering;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct BiomeId(pub u8);
15
16impl BiomeId {
17 pub const OCEAN: BiomeId = BiomeId(0);
18 pub const COAST: BiomeId = BiomeId(1);
19 pub const DESERT: BiomeId = BiomeId(2);
20 pub const SAVANNA: BiomeId = BiomeId(3);
21 pub const TROPICAL_FOREST: BiomeId = BiomeId(4);
22 pub const GRASSLAND: BiomeId = BiomeId(5);
23 pub const SHRUBLAND: BiomeId = BiomeId(6);
24 pub const TEMPERATE_FOREST:BiomeId = BiomeId(7);
25 pub const BOREAL_FOREST: BiomeId = BiomeId(8);
26 pub const TUNDRA: BiomeId = BiomeId(9);
27 pub const SNOW: BiomeId = BiomeId(10);
28 pub const MOUNTAIN: BiomeId = BiomeId(11);
29}
30
31#[derive(Debug, Clone)]
33pub struct BiomeParams {
34 pub id: BiomeId,
35 pub name: &'static str,
36 pub glyph_char: char,
37 pub color: (u8, u8, u8),
39 pub temperature_min: f32,
40 pub temperature_max: f32,
41 pub moisture_min: f32,
42 pub moisture_max: f32,
43 pub elevation_min: f32,
44 pub elevation_max: f32,
45}
46
47impl BiomeParams {
48 pub fn all() -> Vec<BiomeParams> {
49 vec![
50 BiomeParams { id: BiomeId::OCEAN, name: "Ocean", glyph_char: '~', color: (30, 80, 160), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: -1.0, elevation_max: 0.0 },
51 BiomeParams { id: BiomeId::COAST, name: "Coast", glyph_char: '≈', color: (60, 120, 180), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.0, elevation_max: 0.1 },
52 BiomeParams { id: BiomeId::DESERT, name: "Desert", glyph_char: '∴', color: (210, 185, 130), temperature_min: 0.5, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 0.2, elevation_min: 0.0, elevation_max: 1.0 },
53 BiomeParams { id: BiomeId::SAVANNA, name: "Savanna", glyph_char: ',', color: (180, 160, 90), temperature_min: 0.4, temperature_max: 1.0, moisture_min: 0.2, moisture_max: 0.4, elevation_min: 0.0, elevation_max: 1.0 },
54 BiomeParams { id: BiomeId::TROPICAL_FOREST, name: "Tropical Forest", glyph_char: '♣', color: (30, 110, 40), temperature_min: 0.4, temperature_max: 1.0, moisture_min: 0.6, moisture_max: 1.0, elevation_min: 0.0, elevation_max: 1.0 },
55 BiomeParams { id: BiomeId::GRASSLAND, name: "Grassland", glyph_char: '"', color: (120, 170, 60), temperature_min: 0.0, temperature_max: 0.7, moisture_min: 0.3, moisture_max: 0.6, elevation_min: 0.0, elevation_max: 1.0 },
56 BiomeParams { id: BiomeId::SHRUBLAND, name: "Shrubland", glyph_char: '\'',color: (150, 140, 80), temperature_min: 0.0, temperature_max: 0.7, moisture_min: 0.1, moisture_max: 0.4, elevation_min: 0.0, elevation_max: 1.0 },
57 BiomeParams { id: BiomeId::TEMPERATE_FOREST,name: "Temperate Forest", glyph_char: '♠', color: (60, 130, 60), temperature_min: -0.3, temperature_max: 0.5, moisture_min: 0.4, moisture_max: 0.8, elevation_min: 0.0, elevation_max: 0.8 },
58 BiomeParams { id: BiomeId::BOREAL_FOREST, name: "Boreal Forest", glyph_char: '▲', color: (40, 100, 60), temperature_min: -0.6, temperature_max: 0.1, moisture_min: 0.3, moisture_max: 0.7, elevation_min: 0.0, elevation_max: 0.8 },
59 BiomeParams { id: BiomeId::TUNDRA, name: "Tundra", glyph_char: '-', color: (160, 160, 140), temperature_min: -1.0, temperature_max: -0.3,moisture_min: 0.1, moisture_max: 0.5, elevation_min: 0.0, elevation_max: 0.8 },
60 BiomeParams { id: BiomeId::SNOW, name: "Snow", glyph_char: '*', color: (230, 240, 255), temperature_min: -1.0, temperature_max: -0.3,moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.0, elevation_max: 1.0 },
61 BiomeParams { id: BiomeId::MOUNTAIN, name: "Mountain", glyph_char: '^', color: (130, 120, 120), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.7, elevation_max: 1.0 },
62 ]
63 }
64}
65
66pub struct BiomeClassifier {
70 params: Vec<BiomeParams>,
71}
72
73impl BiomeClassifier {
74 pub fn new() -> Self {
75 Self { params: BiomeParams::all() }
76 }
77
78 pub fn classify(&self, temperature: f32, moisture: f32, elevation: f32) -> BiomeId {
80 if elevation < 0.0 { return BiomeId::OCEAN; }
82 if elevation < 0.1 { return BiomeId::COAST; }
83 if elevation > 0.75 {
85 if temperature < -0.2 { return BiomeId::SNOW; }
86 return BiomeId::MOUNTAIN;
87 }
88 if temperature < -0.4 {
90 if moisture < 0.15 { return BiomeId::SNOW; }
91 return BiomeId::TUNDRA;
92 }
93 if temperature < 0.0 {
94 if moisture < 0.3 { return BiomeId::TUNDRA; }
95 return BiomeId::BOREAL_FOREST;
96 }
97 if temperature < 0.4 {
99 if moisture < 0.2 { return BiomeId::SHRUBLAND; }
100 if moisture < 0.5 { return BiomeId::GRASSLAND; }
101 return BiomeId::TEMPERATE_FOREST;
102 }
103 if moisture < 0.15 { return BiomeId::DESERT; }
105 if moisture < 0.35 { return BiomeId::SAVANNA; }
106 if moisture < 0.6 { return BiomeId::GRASSLAND; }
107 BiomeId::TROPICAL_FOREST
108 }
109
110 pub fn params_for(&self, id: BiomeId) -> Option<&BiomeParams> {
111 self.params.iter().find(|p| p.id == id)
112 }
113}
114
115impl Default for BiomeClassifier {
116 fn default() -> Self { Self::new() }
117}
118
119#[derive(Debug, Clone)]
123pub struct WorldCell {
124 pub elevation: f32,
125 pub temperature: f32,
126 pub moisture: f32,
127 pub biome: BiomeId,
128 pub river_id: Option<u32>,
129 pub road_id: Option<u32>,
130 pub settlement_id: Option<u32>,
131}
132
133impl Default for WorldCell {
134 fn default() -> Self {
135 Self {
136 elevation: 0.0,
137 temperature: 0.0,
138 moisture: 0.5,
139 biome: BiomeId::OCEAN,
140 river_id: None,
141 road_id: None,
142 settlement_id: None,
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
151pub struct WorldMap {
152 pub width: usize,
153 pub height: usize,
154 pub cells: Vec<WorldCell>,
155}
156
157impl WorldMap {
158 pub fn new(width: usize, height: usize) -> Self {
159 Self { width, height, cells: vec![WorldCell::default(); width * height] }
160 }
161
162 pub fn idx(&self, x: usize, y: usize) -> usize { y * self.width + x }
163
164 pub fn get(&self, x: usize, y: usize) -> &WorldCell {
165 &self.cells[self.idx(x, y)]
166 }
167
168 pub fn get_mut(&mut self, x: usize, y: usize) -> &mut WorldCell {
169 let i = self.idx(x, y);
170 &mut self.cells[i]
171 }
172
173 pub fn in_bounds(&self, x: i32, y: i32) -> bool {
174 x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
175 }
176
177 pub fn elevation_at(&self, x: usize, y: usize) -> f32 {
178 self.get(x, y).elevation
179 }
180
181 pub fn lowest_neighbour(&self, x: usize, y: usize) -> Option<(usize, usize)> {
183 let mut best = (x, y);
184 let mut best_e = self.get(x, y).elevation;
185 for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
186 let nx = x as i32 + dx;
187 let ny = y as i32 + dy;
188 if !self.in_bounds(nx, ny) { continue; }
189 let e = self.get(nx as usize, ny as usize).elevation;
190 if e < best_e { best_e = e; best = (nx as usize, ny as usize); }
191 }
192 if best == (x, y) { None } else { Some(best) }
193 }
194}
195
196fn hash_noise(ix: i64, iy: i64) -> f32 {
200 let mut h = ix.wrapping_mul(1619).wrapping_add(iy.wrapping_mul(31337));
201 h = (h ^ (h >> 16)).wrapping_mul(0x45d9f3b);
202 h = (h ^ (h >> 16)).wrapping_mul(0x45d9f3b);
203 h = h ^ (h >> 16);
204 (h as f32).abs() / i64::MAX as f32
205}
206
207fn smoothstep(t: f32) -> f32 { t * t * (3.0 - 2.0 * t) }
208
209fn lerp(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
210
211fn value_noise(x: f32, y: f32) -> f32 {
213 let ix = x.floor() as i64;
214 let iy = y.floor() as i64;
215 let fx = x - x.floor();
216 let fy = y - y.floor();
217 let sx = smoothstep(fx);
218 let sy = smoothstep(fy);
219 let v00 = hash_noise(ix, iy);
220 let v10 = hash_noise(ix + 1, iy);
221 let v01 = hash_noise(ix, iy + 1);
222 let v11 = hash_noise(ix + 1, iy + 1);
223 lerp(lerp(v00, v10, sx), lerp(v01, v11, sx), sy)
224}
225
226fn fbm_warped(x: f32, y: f32, octaves: u32, warp_strength: f32) -> f32 {
228 let wx = value_noise(x * 1.3 + 7.1, y * 1.3 + 2.7) * warp_strength;
230 let wy = value_noise(x * 1.3 + 1.2, y * 1.3 + 9.3) * warp_strength;
231 let mut value = 0.0_f32;
232 let mut amplitude = 0.5_f32;
233 let mut frequency = 1.0_f32;
234 let mut max_val = 0.0_f32;
235 for _ in 0..octaves {
236 value += amplitude * value_noise((x + wx) * frequency, (y + wy) * frequency);
237 max_val += amplitude;
238 amplitude *= 0.5;
239 frequency *= 2.0;
240 }
241 value / max_val
242}
243
244pub struct HeightmapWorld {
249 pub octaves: u32,
250 pub scale: f32,
251 pub warp_strength: f32,
252 pub sea_level: f32,
253 pub continent_falloff: f32, }
255
256impl Default for HeightmapWorld {
257 fn default() -> Self {
258 Self { octaves: 6, scale: 0.005, warp_strength: 0.4, sea_level: 0.42, continent_falloff: 0.8 }
259 }
260}
261
262impl HeightmapWorld {
263 pub fn new(octaves: u32, scale: f32, warp_strength: f32, sea_level: f32) -> Self {
264 Self { octaves, scale, warp_strength, sea_level, continent_falloff: 0.7 }
265 }
266
267 pub fn apply(&self, map: &mut WorldMap, seed: u64) {
269 let w = map.width as f32;
270 let h = map.height as f32;
271 let seed_f = (seed % 10000) as f32 * 0.1;
272
273 for y in 0..map.height {
274 for x in 0..map.width {
275 let nx = x as f32 * self.scale + seed_f;
276 let ny = y as f32 * self.scale + seed_f * 0.7;
277 let raw = fbm_warped(nx, ny, self.octaves, self.warp_strength);
278
279 let dx = (x as f32 / w - 0.5) * 2.0;
281 let dy = (y as f32 / h - 0.5) * 2.0;
282 let dist = (dx * dx + dy * dy).sqrt().min(1.0);
283 let mask = 1.0 - dist.powf(1.5) * self.continent_falloff;
284
285 let elevation = (raw * mask * 2.0 - 1.0).clamp(-1.0, 1.0);
286 map.get_mut(x, y).elevation = elevation;
287 }
288 }
289 }
290}
291
292pub struct ClimateSimulator {
296 pub lapse_rate: f32, pub ocean_moisture: f32, pub rain_shadow_decay: f32, }
300
301impl Default for ClimateSimulator {
302 fn default() -> Self {
303 Self { lapse_rate: 0.6, ocean_moisture: 0.7, rain_shadow_decay: 0.4 }
304 }
305}
306
307impl ClimateSimulator {
308 pub fn new(lapse_rate: f32, ocean_moisture: f32, rain_shadow_decay: f32) -> Self {
309 Self { lapse_rate, ocean_moisture, rain_shadow_decay }
310 }
311
312 pub fn apply(&self, map: &mut WorldMap) {
314 let h = map.height as f32;
315 let w = map.width;
316 let ht = map.height;
317
318 for y in 0..ht {
320 let lat_norm = y as f32 / h; let lat_temp = (std::f32::consts::PI * lat_norm).cos(); for x in 0..w {
323 let elev = map.get(x, y).elevation.max(0.0);
324 let temp = lat_temp - elev * self.lapse_rate;
325 map.get_mut(x, y).temperature = temp.clamp(-1.0, 1.0);
326 }
327 }
328
329 let elev_snapshot: Vec<f32> = map.cells.iter().map(|c| c.elevation).collect();
332
333 for y in 0..ht {
334 let mut moisture = 0.2_f32; for x in 0..w {
336 let elev = elev_snapshot[y * w + x];
337 if elev < 0.0 {
338 moisture = (moisture + self.ocean_moisture * 0.1).min(1.0);
340 } else {
341 let rainfall = moisture * (0.05 + elev * self.rain_shadow_decay * 0.1);
343 moisture = (moisture - rainfall).max(0.0);
344 }
346 map.get_mut(x, y).moisture = moisture.clamp(0.0, 1.0);
347 }
348 }
349
350 let is_ocean: Vec<bool> = map.cells.iter().map(|c| c.elevation < 0.0).collect();
353 let mut ocean_prox = vec![0.0_f32; w * ht];
354 let mut queue: VecDeque<(usize, usize, u32)> = VecDeque::new();
356 let mut visited_op = vec![false; w * ht];
357 for y in 0..ht {
358 for x in 0..w {
359 if is_ocean[y * w + x] {
360 ocean_prox[y * w + x] = 1.0;
361 queue.push_back((x, y, 0));
362 visited_op[y * w + x] = true;
363 }
364 }
365 }
366 let radius = 15u32;
367 while let Some((cx, cy, dist)) = queue.pop_front() {
368 if dist >= radius { continue; }
369 for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
370 let nx = cx as i32 + dx;
371 let ny = cy as i32 + dy;
372 if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= ht { continue; }
373 let ni = ny as usize * w + nx as usize;
374 if !visited_op[ni] {
375 visited_op[ni] = true;
376 ocean_prox[ni] = 1.0 - (dist + 1) as f32 / radius as f32;
377 queue.push_back((nx as usize, ny as usize, dist + 1));
378 }
379 }
380 }
381 for y in 0..ht {
383 for x in 0..w {
384 let i = y * w + x;
385 let m = map.cells[i].moisture + ocean_prox[i] * self.ocean_moisture * 0.3;
386 map.cells[i].moisture = m.clamp(0.0, 1.0);
387 }
388 }
389 }
390}
391
392pub struct RiverSystem {
396 pub num_rivers: usize,
397 pub min_source_elev: f32,
398 pub erosion_amount: f32,
399}
400
401impl Default for RiverSystem {
402 fn default() -> Self { Self { num_rivers: 12, min_source_elev: 0.5, erosion_amount: 0.02 } }
403}
404
405impl RiverSystem {
406 pub fn new(num_rivers: usize, min_source_elev: f32, erosion_amount: f32) -> Self {
407 Self { num_rivers, min_source_elev, erosion_amount }
408 }
409
410 pub fn apply(&self, map: &mut WorldMap, rng: &mut Rng) {
412 let w = map.width;
413 let h = map.height;
414
415 let mut candidates: Vec<(usize, usize)> = Vec::new();
417 for y in 0..h {
418 for x in 0..w {
419 if map.get(x, y).elevation >= self.min_source_elev {
420 candidates.push((x, y));
421 }
422 }
423 }
424 rng.shuffle(&mut candidates);
425
426 for river_idx in 0..self.num_rivers.min(candidates.len()) {
427 let (mut cx, mut cy) = candidates[river_idx];
428 let river_id = river_idx as u32 + 1;
429 let mut visited_r = HashSet::new();
430 let mut steps = 0usize;
431
432 loop {
433 if visited_r.contains(&(cx, cy)) { break; }
434 visited_r.insert((cx, cy));
435 map.get_mut(cx, cy).river_id = Some(river_id);
436 let e = map.get(cx, cy).elevation;
438 map.get_mut(cx, cy).elevation = (e - self.erosion_amount).max(-0.05);
439 let m = map.get(cx, cy).moisture;
441 map.get_mut(cx, cy).moisture = (m + 0.1).min(1.0);
442
443 steps += 1;
444 if steps > w + h { break; } match map.lowest_neighbour(cx, cy) {
448 Some((nx, ny)) => {
449 if map.get(nx, ny).elevation < 0.0 {
450 map.get_mut(nx, ny).river_id = Some(river_id);
452 break;
453 }
454 cx = nx; cy = ny;
455 }
456 None => break,
457 }
458 }
459 }
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum SettlementKind {
468 Village,
469 Town,
470 City,
471 Capital,
472}
473
474impl SettlementKind {
475 pub fn base_population(&self) -> u32 {
476 match self {
477 SettlementKind::Village => 200,
478 SettlementKind::Town => 2_000,
479 SettlementKind::City => 20_000,
480 SettlementKind::Capital => 100_000,
481 }
482 }
483
484 pub fn glyph(&self) -> char {
485 match self {
486 SettlementKind::Village => 'v',
487 SettlementKind::Town => 'T',
488 SettlementKind::City => 'C',
489 SettlementKind::Capital => '★',
490 }
491 }
492}
493
494#[derive(Debug, Clone)]
496pub struct Settlement {
497 pub id: u32,
498 pub position: (usize, usize),
499 pub kind: SettlementKind,
500 pub population: u32,
501 pub name: String,
502 pub faction_id: Option<u32>,
503}
504
505impl Settlement {
506 pub fn new(id: u32, x: usize, y: usize, kind: SettlementKind, name: String) -> Self {
507 let base = kind.base_population();
508 Self {
509 id,
510 position: (x, y),
511 kind,
512 population: base,
513 name,
514 faction_id: None,
515 }
516 }
517}
518
519pub struct SettlementPlacer {
523 pub num_settlements: usize,
524 pub min_separation: f32, pub capital_count: usize,
526 pub city_count: usize,
527 pub town_count: usize,
528}
529
530impl Default for SettlementPlacer {
531 fn default() -> Self {
532 Self { num_settlements: 20, min_separation: 15.0, capital_count: 1, city_count: 3, town_count: 6 }
533 }
534}
535
536impl SettlementPlacer {
537 fn fertility(cell: &WorldCell) -> f32 {
539 if cell.elevation < 0.05 { return 0.0; } if cell.elevation > 0.65 { return 0.0; } let river_bonus = if cell.river_id.is_some() { 0.3 } else { 0.0 };
542 let temp_score = 1.0 - (cell.temperature - 0.3).abs();
543 let moist_score = cell.moisture.min(0.8);
544 (temp_score * 0.4 + moist_score * 0.4 + river_bonus).max(0.0)
545 }
546
547 pub fn place(&self, map: &mut WorldMap, names: &mut NameGenerator, rng: &mut Rng) -> Vec<Settlement> {
548 let w = map.width;
549 let h = map.height;
550
551 let mut scored: Vec<(f32, usize, usize)> = (0..h).flat_map(|y| {
553 (0..w).map(move |x| (0.0_f32, x, y))
554 }).collect();
555 for (score, x, y) in &mut scored {
556 *score = Self::fertility(map.get(*x, *y));
557 }
558 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
559
560 let mut settlements = Vec::new();
561 let mut placed_positions: Vec<(usize, usize)> = Vec::new();
562
563 let mut kinds: Vec<SettlementKind> = Vec::new();
565 for _ in 0..self.capital_count { kinds.push(SettlementKind::Capital); }
566 for _ in 0..self.city_count { kinds.push(SettlementKind::City); }
567 for _ in 0..self.town_count { kinds.push(SettlementKind::Town); }
568 while kinds.len() < self.num_settlements { kinds.push(SettlementKind::Village); }
569
570 let sep2 = self.min_separation * self.min_separation;
571 let mut kind_idx = 0;
572
573 for (_, x, y) in &scored {
574 if settlements.len() >= self.num_settlements { break; }
575 let x = *x; let y = *y;
576 let too_close = placed_positions.iter().any(|&(px, py)| {
578 let dx = px as f32 - x as f32;
579 let dy = py as f32 - y as f32;
580 dx * dx + dy * dy < sep2
581 });
582 if too_close { continue; }
583
584 let kind = kinds.get(kind_idx).copied().unwrap_or(SettlementKind::Village);
585 kind_idx += 1;
586 let name = names.generate(rng);
587 let id = settlements.len() as u32 + 1;
588 let pop_jitter = rng.range_f32(0.7, 1.4);
589 let mut s = Settlement::new(id, x, y, kind, name);
590 s.population = (s.population as f32 * pop_jitter) as u32;
591 s.faction_id = Some(rng.range_usize(4) as u32 + 1);
592
593 map.get_mut(x, y).settlement_id = Some(id);
594 placed_positions.push((x, y));
595 settlements.push(s);
596 }
597
598 settlements
599 }
600}
601
602pub struct RoadNetwork {
606 pub road_cost_slope: f32, }
608
609impl Default for RoadNetwork {
610 fn default() -> Self { Self { road_cost_slope: 5.0 } }
611}
612
613#[derive(Debug, Clone)]
615struct AStarNode {
616 cost: f32,
617 x: usize,
618 y: usize,
619}
620
621impl PartialEq for AStarNode {
622 fn eq(&self, other: &Self) -> bool { self.cost == other.cost }
623}
624impl Eq for AStarNode {}
625impl PartialOrd for AStarNode {
626 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
627}
628impl Ord for AStarNode {
629 fn cmp(&self, other: &Self) -> Ordering {
630 other.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal)
632 }
633}
634
635impl RoadNetwork {
636 pub fn new(road_cost_slope: f32) -> Self { Self { road_cost_slope } }
637
638 pub fn find_path(&self, map: &WorldMap, ax: usize, ay: usize, bx: usize, by: usize) -> Vec<(usize, usize)> {
641 let w = map.width;
642 let h = map.height;
643 let start = ay * w + ax;
644 let goal = by * w + bx;
645
646 let mut dist = vec![f32::INFINITY; w * h];
647 let mut prev = vec![usize::MAX; w * h];
648 dist[start] = 0.0;
649
650 let mut heap: BinaryHeap<AStarNode> = BinaryHeap::new();
651 heap.push(AStarNode { cost: 0.0, x: ax, y: ay });
652
653 while let Some(AStarNode { cost, x, y }) = heap.pop() {
654 let idx = y * w + x;
655 if idx == goal {
656 let mut path = Vec::new();
658 let mut cur = goal;
659 while cur != usize::MAX {
660 path.push((cur % w, cur / w));
661 cur = prev[cur];
662 }
663 path.reverse();
664 return path;
665 }
666 if cost > dist[idx] { continue; }
667
668 for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
669 let nx = x as i32 + dx;
670 let ny = y as i32 + dy;
671 if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
672 let (nx, ny) = (nx as usize, ny as usize);
673 let ni = ny * w + nx;
674 let elev_change = (map.get(nx, ny).elevation - map.get(x, y).elevation).abs();
675 let move_cost = 1.0 + elev_change * self.road_cost_slope;
676 let new_cost = dist[idx] + move_cost;
677 if new_cost < dist[ni] {
678 dist[ni] = new_cost;
679 prev[ni] = idx;
680 let hdx = nx as f32 - bx as f32;
682 let hdy = ny as f32 - by as f32;
683 let h_val = (hdx * hdx + hdy * hdy).sqrt();
684 heap.push(AStarNode { cost: new_cost + h_val, x: nx, y: ny });
685 }
686 }
687 }
688 Vec::new() }
690
691 pub fn shortest_trade_route(
693 &self,
694 map: &WorldMap,
695 settlements: &[Settlement],
696 a_id: u32,
697 b_id: u32,
698 ) -> Vec<(usize, usize)> {
699 let a = settlements.iter().find(|s| s.id == a_id);
700 let b = settlements.iter().find(|s| s.id == b_id);
701 match (a, b) {
702 (Some(sa), Some(sb)) => {
703 let (ax, ay) = sa.position;
704 let (bx, by) = sb.position;
705 self.find_path(map, ax, ay, bx, by)
706 }
707 _ => Vec::new(),
708 }
709 }
710
711 pub fn build_roads(&self, map: &mut WorldMap, settlements: &[Settlement], rng: &mut Rng) {
713 if settlements.len() < 2 { return; }
714 let n = settlements.len();
716 let mut connected = vec![false; n];
717 connected[0] = true;
718 let mut road_id = 1u32;
719
720 for _ in 1..n {
721 let mut best_cost = f32::INFINITY;
722 let mut best_pair = (0, 1);
723 for i in 0..n {
724 if !connected[i] { continue; }
725 for j in 0..n {
726 if connected[j] { continue; }
727 let (ax, ay) = settlements[i].position;
728 let (bx, by) = settlements[j].position;
729 let dx = ax as f32 - bx as f32;
730 let dy = ay as f32 - by as f32;
731 let d = (dx * dx + dy * dy).sqrt();
732 if d < best_cost { best_cost = d; best_pair = (i, j); }
733 }
734 }
735 let (i, j) = best_pair;
736 connected[j] = true;
737 let (ax, ay) = settlements[i].position;
738 let (bx, by) = settlements[j].position;
739 let path = self.find_path(map, ax, ay, bx, by);
740 for (px, py) in path {
741 map.get_mut(px, py).road_id = Some(road_id);
742 }
743 road_id += 1;
744 }
745 let _ = rng.next_u64();
747 }
748}
749
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub enum Culture {
755 Norse,
756 Arabic,
757 Japanese,
758 Latin,
759 Fantasy,
760}
761
762pub struct NameGenerator {
764 culture: Culture,
765}
766
767impl NameGenerator {
768 pub fn new(culture: Culture) -> Self { Self { culture } }
769
770 pub fn generate(&self, rng: &mut Rng) -> String {
772 let (pre, mid, suf) = self.syllables();
773 let p = *rng.pick(pre).unwrap_or(&"Ka");
774 let s = *rng.pick(suf).unwrap_or(&"ar");
775 if rng.chance(0.55) || mid.is_empty() {
776 capitalize_first(&format!("{p}{s}"))
777 } else {
778 let m = *rng.pick(mid).unwrap_or(&"an");
779 capitalize_first(&format!("{p}{m}{s}"))
780 }
781 }
782
783 pub fn generate_with_seed(&self, seed: u64) -> String {
784 let mut rng = Rng::new(seed);
785 self.generate(&mut rng)
786 }
787
788 fn syllables(&self) -> (&[&'static str], &[&'static str], &[&'static str]) {
789 match self.culture {
790 Culture::Norse => (NORSE_PRE, NORSE_MID, NORSE_SUF),
791 Culture::Arabic => (ARABIC_PRE, ARABIC_MID, ARABIC_SUF),
792 Culture::Japanese => (JAPANESE_PRE, JAPANESE_MID, JAPANESE_SUF),
793 Culture::Latin => (LATIN_PRE, LATIN_MID, LATIN_SUF),
794 Culture::Fantasy => (FANTASY_PRE, FANTASY_MID, FANTASY_SUF),
795 }
796 }
797}
798
799fn capitalize_first(s: &str) -> String {
800 let mut c = s.chars();
801 match c.next() {
802 None => String::new(),
803 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
804 }
805}
806
807const NORSE_PRE: &[&str] = &["Thor","Bjorn","Sigurd","Ulf","Ragnar","Gunnar","Erik","Leif","Ivar","Sven","Haldor","Vidar"];
808const NORSE_MID: &[&str] = &["gar","mund","ald","helm","ulf","ric","win","frid","run","ing","rod","bald"];
809const NORSE_SUF: &[&str] = &["son","sen","sson","dottir","ir","ar","heim","borg","fjord","dal","vik","stad"];
810
811const ARABIC_PRE: &[&str] = &["Al","Abd","Khalid","Omar","Yusuf","Ahmad","Hamid","Tariq","Walid","Faisal","Jamil","Nabil"];
812const ARABIC_MID: &[&str] = &["al","ibn","bin","ab","um","al","din","ud","ur","ul","im","is"];
813const ARABIC_SUF: &[&str] = &["ah","i","an","un","in","oon","een","at","iya","iyya","awi","awi"];
814
815const JAPANESE_PRE: &[&str] = &["Hiro","Taka","Yoshi","Masa","Nori","Tat","Kei","Shin","Haru","Aki","Tomo","Kazu"];
816const JAPANESE_MID: &[&str] = &["no","na","yo","ka","ta","ma","mi","mu","ki","ku","ro","to"];
817const JAPANESE_SUF: &[&str] = &["shi","ko","ro","ki","mi","ka","to","ru","no","ta","su","ya"];
818
819const LATIN_PRE: &[&str] = &["Marc","Gaius","Lucius","Publius","Quintus","Titus","Sextus","Aulus","Gnaeus","Decimus","Marcus","Julius"];
820const LATIN_MID: &[&str] = &["aes","ius","ell","inn","iss","ull","orr","err","uss","elius","anus","atus"];
821const LATIN_SUF: &[&str] = &["us","um","ia","ius","ae","ix","ax","ius","anus","inus","ulus","atus"];
822
823const FANTASY_PRE: &[&str] = &["Aer","Zyl","Vael","Xan","Myr","Thal","Aen","Ith","Sel","Nox","Kyr","Dra"];
824const FANTASY_MID: &[&str] = &["an","ael","iel","ion","ias","eld","ath","ill","orn","ith","eth","ash"];
825const FANTASY_SUF: &[&str] = &["iel","ias","ion","ath","ael","ial","uen","ean","ian","iel","ath","orn"];
826
827#[derive(Debug, Clone, PartialEq, Eq)]
831pub enum EventKind {
832 War,
833 Plague,
834 NaturalDisaster,
835 FoundedSettlement,
836 RulerChanged,
837 Discovery,
838 Trade,
839 Rebellion,
840}
841
842#[derive(Debug, Clone)]
844pub struct HistoryEvent {
845 pub year: i32,
846 pub kind: EventKind,
847 pub affected_settlements: Vec<u32>,
848 pub description: String,
849}
850
851pub struct WorldHistory {
853 pub events: Vec<HistoryEvent>,
854}
855
856impl WorldHistory {
857 pub fn new() -> Self { Self { events: Vec::new() } }
858
859 pub fn generate(
861 &mut self,
862 settlements: &[Settlement],
863 years: u32,
864 rng: &mut Rng,
865 ) {
866 let mut year = 1i32;
867 let end_year = year + years as i32;
868 let n_settle = settlements.len();
869
870 while year < end_year {
871 let events_this_year = rng.range_usize(3);
872 for _ in 0..=events_this_year {
873 if year >= end_year { break; }
874 let kind_roll = rng.range_usize(8);
875 let kind = match kind_roll {
876 0 => EventKind::War,
877 1 => EventKind::Plague,
878 2 => EventKind::NaturalDisaster,
879 3 => EventKind::FoundedSettlement,
880 4 => EventKind::RulerChanged,
881 5 => EventKind::Discovery,
882 6 => EventKind::Trade,
883 _ => EventKind::Rebellion,
884 };
885
886 let n_affected = if n_settle == 0 { 0 } else {
887 rng.range_usize(n_settle.min(3)) + 1
888 };
889 let affected: Vec<u32> = if n_settle > 0 {
890 let mut ids: Vec<u32> = settlements.iter().map(|s| s.id).collect();
891 rng.shuffle(&mut ids);
892 ids.into_iter().take(n_affected).collect()
893 } else {
894 Vec::new()
895 };
896
897 let description = Self::describe(&kind, &affected, settlements, year, rng);
898 self.events.push(HistoryEvent { year, kind, affected_settlements: affected, description });
899 }
900 year += rng.range_i32(1, 10);
902 }
903 }
904
905 fn describe(
906 kind: &EventKind,
907 affected: &[u32],
908 settlements: &[Settlement],
909 year: i32,
910 rng: &mut Rng,
911 ) -> String {
912 let settle_name = |id: u32| -> &str {
913 settlements.iter().find(|s| s.id == id).map(|s| s.name.as_str()).unwrap_or("Unknown")
914 };
915
916 let first = affected.first().copied().unwrap_or(0);
917 let second = affected.get(1).copied();
918
919 match kind {
920 EventKind::War => {
921 let a = settle_name(first);
922 let b = second.map(settle_name).unwrap_or("foreign powers");
923 format!("Year {year}: War erupted between {a} and {b}.")
924 }
925 EventKind::Plague => {
926 let a = settle_name(first);
927 format!("Year {year}: A plague ravaged {a}, killing thousands.")
928 }
929 EventKind::NaturalDisaster => {
930 let disasters = &["earthquake","flood","volcanic eruption","drought","wildfire"];
931 let d = rng.pick(disasters).copied().unwrap_or("storm");
932 let a = settle_name(first);
933 format!("Year {year}: A great {d} struck near {a}.")
934 }
935 EventKind::FoundedSettlement => {
936 let a = settle_name(first);
937 format!("Year {year}: The settlement of {a} was founded.")
938 }
939 EventKind::RulerChanged => {
940 let a = settle_name(first);
941 format!("Year {year}: A new ruler came to power in {a}.")
942 }
943 EventKind::Discovery => {
944 let things = &["ancient ruins","a new trade route","a rich vein of ore","a sacred spring","a lost tome"];
945 let thing = rng.pick(things).copied().unwrap_or("something remarkable");
946 let a = settle_name(first);
947 format!("Year {year}: Explorers from {a} discovered {thing}.")
948 }
949 EventKind::Trade => {
950 let a = settle_name(first);
951 let b = second.map(settle_name).unwrap_or("distant lands");
952 format!("Year {year}: A prosperous trade agreement was forged between {a} and {b}.")
953 }
954 EventKind::Rebellion => {
955 let a = settle_name(first);
956 format!("Year {year}: The people of {a} rose in rebellion against their rulers.")
957 }
958 }
959 }
960}
961
962impl Default for WorldHistory {
963 fn default() -> Self { Self::new() }
964}
965
966pub struct WorldBuilder {
970 pub width: usize,
971 pub height: usize,
972 pub seed: u64,
973}
974
975impl WorldBuilder {
976 pub fn new(width: usize, height: usize, seed: u64) -> Self {
977 Self { width, height, seed }
978 }
979
980 pub fn build(&self) -> (WorldMap, Vec<Settlement>, WorldHistory) {
982 let mut rng = Rng::new(self.seed);
983 let mut map = WorldMap::new(self.width, self.height);
984
985 let heightmap = HeightmapWorld::default();
987 heightmap.apply(&mut map, self.seed);
988
989 let climate = ClimateSimulator::default();
991 climate.apply(&mut map);
992
993 let rivers = RiverSystem::default();
995 rivers.apply(&mut map, &mut rng);
996
997 let classifier = BiomeClassifier::new();
999 for y in 0..self.height {
1000 for x in 0..self.width {
1001 let (e, t, m) = {
1002 let c = map.get(x, y);
1003 (c.elevation, c.temperature, c.moisture)
1004 };
1005 map.get_mut(x, y).biome = classifier.classify(t, m, e);
1006 }
1007 }
1008
1009 let culture = [Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy];
1011 let c = culture[rng.range_usize(5)];
1012 let mut name_gen = NameGenerator::new(c);
1013 let mut placer = SettlementPlacer::default();
1014 placer.num_settlements = 20;
1015 let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1016
1017 let roads = RoadNetwork::default();
1019 roads.build_roads(&mut map, &settlements, &mut rng);
1020
1021 let mut history = WorldHistory::new();
1023 history.generate(&settlements, 500, &mut rng);
1024
1025 (map, settlements, history)
1026 }
1027}
1028
1029#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 fn small_map() -> WorldMap {
1036 let mut map = WorldMap::new(64, 48);
1037 let hm = HeightmapWorld::default();
1038 hm.apply(&mut map, 42);
1039 map
1040 }
1041
1042 #[test]
1043 fn heightmap_has_land_and_ocean() {
1044 let map = small_map();
1045 let has_land = map.cells.iter().any(|c| c.elevation > 0.0);
1046 let has_ocean = map.cells.iter().any(|c| c.elevation < 0.0);
1047 assert!(has_land, "expected some land cells");
1048 assert!(has_ocean, "expected some ocean cells");
1049 }
1050
1051 #[test]
1052 fn climate_assigns_temperature() {
1053 let mut map = small_map();
1054 let climate = ClimateSimulator::default();
1055 climate.apply(&mut map);
1056 let any_nonzero = map.cells.iter().any(|c| c.temperature != 0.0);
1057 assert!(any_nonzero);
1058 }
1059
1060 #[test]
1061 fn climate_assigns_moisture() {
1062 let mut map = small_map();
1063 let climate = ClimateSimulator::default();
1064 climate.apply(&mut map);
1065 let any_moisture = map.cells.iter().any(|c| c.moisture > 0.0);
1066 assert!(any_moisture);
1067 }
1068
1069 #[test]
1070 fn rivers_carve_some_cells() {
1071 let mut map = small_map();
1072 let climate = ClimateSimulator::default();
1073 climate.apply(&mut map);
1074 let mut rng = Rng::new(99);
1075 let rivers = RiverSystem { num_rivers: 3, min_source_elev: 0.4, erosion_amount: 0.01 };
1076 rivers.apply(&mut map, &mut rng);
1077 let river_cells = map.cells.iter().filter(|c| c.river_id.is_some()).count();
1078 assert!(river_cells > 0, "expected river cells");
1079 }
1080
1081 #[test]
1082 fn biome_classifier_covers_all_ids() {
1083 let clf = BiomeClassifier::new();
1084 let biomes = [
1086 clf.classify(-0.8, 0.1, 0.5),
1087 clf.classify( 0.8, 0.05, 0.3),
1088 clf.classify( 0.2, 0.6, 0.3),
1089 clf.classify(-0.5, 0.4, 0.4),
1090 clf.classify( 0.6, 0.7, 0.3),
1091 ];
1092 assert!(biomes.iter().any(|b| *b != BiomeId::OCEAN));
1093 }
1094
1095 #[test]
1096 fn settlement_placer_places_settlements() {
1097 let mut rng = Rng::new(7);
1098 let mut map = small_map();
1099 let climate = ClimateSimulator::default();
1100 climate.apply(&mut map);
1101 let mut name_gen = NameGenerator::new(Culture::Fantasy);
1102 let mut placer = SettlementPlacer::default();
1103 placer.num_settlements = 5;
1104 let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1105 assert!(!settlements.is_empty(), "should place at least one settlement");
1106 }
1107
1108 #[test]
1109 fn settlement_names_non_empty() {
1110 let mut rng = Rng::new(1234);
1111 let gen = NameGenerator::new(Culture::Norse);
1112 for _ in 0..5 {
1113 let name = gen.generate(&mut rng);
1114 assert!(!name.is_empty(), "name should not be empty");
1115 }
1116 }
1117
1118 #[test]
1119 fn road_network_finds_path() {
1120 let map = small_map();
1121 let roads = RoadNetwork::default();
1122 let path = roads.find_path(&map, 5, 5, 20, 20);
1123 assert!(!path.is_empty(), "A* should find a path");
1124 }
1125
1126 #[test]
1127 fn world_history_generates_events() {
1128 let mut rng = Rng::new(42);
1129 let settlements = vec![
1130 Settlement::new(1, 10, 10, SettlementKind::Town, "Testville".into()),
1131 Settlement::new(2, 30, 30, SettlementKind::City, "Cityburg".into()),
1132 Settlement::new(3, 50, 10, SettlementKind::Village, "Hamlet".into()),
1133 ];
1134 let mut history = WorldHistory::new();
1135 history.generate(&settlements, 100, &mut rng);
1136 assert!(!history.events.is_empty(), "should generate events");
1137 }
1138
1139 #[test]
1140 fn world_history_event_years_monotone() {
1141 let mut rng = Rng::new(55);
1142 let settlements = vec![Settlement::new(1, 5, 5, SettlementKind::Village, "A".into())];
1143 let mut history = WorldHistory::new();
1144 history.generate(&settlements, 200, &mut rng);
1145 let years: Vec<i32> = history.events.iter().map(|e| e.year).collect();
1146 let sorted = years.windows(2).all(|w| w[0] <= w[1]);
1147 assert!(sorted, "events should be in chronological order");
1148 }
1149
1150 #[test]
1151 fn world_builder_full_pipeline() {
1152 let builder = WorldBuilder::new(32, 24, 42);
1153 let (map, settlements, history) = builder.build();
1154 assert_eq!(map.width, 32);
1155 assert_eq!(map.height, 24);
1156 assert!(!history.events.is_empty());
1157 }
1158
1159 #[test]
1160 fn name_generator_all_cultures() {
1161 let mut rng = Rng::new(7);
1162 for culture in &[Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy] {
1163 let gen = NameGenerator::new(*culture);
1164 let name = gen.generate(&mut rng);
1165 assert!(!name.is_empty(), "Culture {:?} produced empty name", culture);
1166 }
1167 }
1168
1169 #[test]
1170 fn biome_params_all_twelve() {
1171 let params = BiomeParams::all();
1172 assert_eq!(params.len(), 12);
1173 }
1174
1175 #[test]
1176 fn world_map_idx_roundtrip() {
1177 let map = WorldMap::new(50, 40);
1178 for y in 0..40 {
1179 for x in 0..50 {
1180 let i = map.idx(x, y);
1181 assert_eq!(i % 50, x);
1182 assert_eq!(i / 50, y);
1183 }
1184 }
1185 }
1186}