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 let h_here = ((x as f32 - bx as f32).powi(2) + (y as f32 - by as f32).powi(2)).sqrt();
670 if cost - h_here > dist[idx] + 1e-4 { continue; }
671
672 for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
673 let nx = x as i32 + dx;
674 let ny = y as i32 + dy;
675 if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
676 let (nx, ny) = (nx as usize, ny as usize);
677 let ni = ny * w + nx;
678 let elev_change = (map.get(nx, ny).elevation - map.get(x, y).elevation).abs();
679 let move_cost = 1.0 + elev_change * self.road_cost_slope;
680 let new_cost = dist[idx] + move_cost;
681 if new_cost < dist[ni] {
682 dist[ni] = new_cost;
683 prev[ni] = idx;
684 let hdx = nx as f32 - bx as f32;
686 let hdy = ny as f32 - by as f32;
687 let h_val = (hdx * hdx + hdy * hdy).sqrt();
688 heap.push(AStarNode { cost: new_cost + h_val, x: nx, y: ny });
689 }
690 }
691 }
692 Vec::new() }
694
695 pub fn shortest_trade_route(
697 &self,
698 map: &WorldMap,
699 settlements: &[Settlement],
700 a_id: u32,
701 b_id: u32,
702 ) -> Vec<(usize, usize)> {
703 let a = settlements.iter().find(|s| s.id == a_id);
704 let b = settlements.iter().find(|s| s.id == b_id);
705 match (a, b) {
706 (Some(sa), Some(sb)) => {
707 let (ax, ay) = sa.position;
708 let (bx, by) = sb.position;
709 self.find_path(map, ax, ay, bx, by)
710 }
711 _ => Vec::new(),
712 }
713 }
714
715 pub fn build_roads(&self, map: &mut WorldMap, settlements: &[Settlement], rng: &mut Rng) {
717 if settlements.len() < 2 { return; }
718 let n = settlements.len();
720 let mut connected = vec![false; n];
721 connected[0] = true;
722 let mut road_id = 1u32;
723
724 for _ in 1..n {
725 let mut best_cost = f32::INFINITY;
726 let mut best_pair = (0, 1);
727 for i in 0..n {
728 if !connected[i] { continue; }
729 for j in 0..n {
730 if connected[j] { continue; }
731 let (ax, ay) = settlements[i].position;
732 let (bx, by) = settlements[j].position;
733 let dx = ax as f32 - bx as f32;
734 let dy = ay as f32 - by as f32;
735 let d = (dx * dx + dy * dy).sqrt();
736 if d < best_cost { best_cost = d; best_pair = (i, j); }
737 }
738 }
739 let (i, j) = best_pair;
740 connected[j] = true;
741 let (ax, ay) = settlements[i].position;
742 let (bx, by) = settlements[j].position;
743 let path = self.find_path(map, ax, ay, bx, by);
744 for (px, py) in path {
745 map.get_mut(px, py).road_id = Some(road_id);
746 }
747 road_id += 1;
748 }
749 let _ = rng.next_u64();
751 }
752}
753
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
758pub enum Culture {
759 Norse,
760 Arabic,
761 Japanese,
762 Latin,
763 Fantasy,
764}
765
766pub struct NameGenerator {
768 culture: Culture,
769}
770
771impl NameGenerator {
772 pub fn new(culture: Culture) -> Self { Self { culture } }
773
774 pub fn generate(&self, rng: &mut Rng) -> String {
776 let (pre, mid, suf) = self.syllables();
777 let p = *rng.pick(pre).unwrap_or(&"Ka");
778 let s = *rng.pick(suf).unwrap_or(&"ar");
779 if rng.chance(0.55) || mid.is_empty() {
780 capitalize_first(&format!("{p}{s}"))
781 } else {
782 let m = *rng.pick(mid).unwrap_or(&"an");
783 capitalize_first(&format!("{p}{m}{s}"))
784 }
785 }
786
787 pub fn generate_with_seed(&self, seed: u64) -> String {
788 let mut rng = Rng::new(seed);
789 self.generate(&mut rng)
790 }
791
792 fn syllables(&self) -> (&[&'static str], &[&'static str], &[&'static str]) {
793 match self.culture {
794 Culture::Norse => (NORSE_PRE, NORSE_MID, NORSE_SUF),
795 Culture::Arabic => (ARABIC_PRE, ARABIC_MID, ARABIC_SUF),
796 Culture::Japanese => (JAPANESE_PRE, JAPANESE_MID, JAPANESE_SUF),
797 Culture::Latin => (LATIN_PRE, LATIN_MID, LATIN_SUF),
798 Culture::Fantasy => (FANTASY_PRE, FANTASY_MID, FANTASY_SUF),
799 }
800 }
801}
802
803fn capitalize_first(s: &str) -> String {
804 let mut c = s.chars();
805 match c.next() {
806 None => String::new(),
807 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
808 }
809}
810
811const NORSE_PRE: &[&str] = &["Thor","Bjorn","Sigurd","Ulf","Ragnar","Gunnar","Erik","Leif","Ivar","Sven","Haldor","Vidar"];
812const NORSE_MID: &[&str] = &["gar","mund","ald","helm","ulf","ric","win","frid","run","ing","rod","bald"];
813const NORSE_SUF: &[&str] = &["son","sen","sson","dottir","ir","ar","heim","borg","fjord","dal","vik","stad"];
814
815const ARABIC_PRE: &[&str] = &["Al","Abd","Khalid","Omar","Yusuf","Ahmad","Hamid","Tariq","Walid","Faisal","Jamil","Nabil"];
816const ARABIC_MID: &[&str] = &["al","ibn","bin","ab","um","al","din","ud","ur","ul","im","is"];
817const ARABIC_SUF: &[&str] = &["ah","i","an","un","in","oon","een","at","iya","iyya","awi","awi"];
818
819const JAPANESE_PRE: &[&str] = &["Hiro","Taka","Yoshi","Masa","Nori","Tat","Kei","Shin","Haru","Aki","Tomo","Kazu"];
820const JAPANESE_MID: &[&str] = &["no","na","yo","ka","ta","ma","mi","mu","ki","ku","ro","to"];
821const JAPANESE_SUF: &[&str] = &["shi","ko","ro","ki","mi","ka","to","ru","no","ta","su","ya"];
822
823const LATIN_PRE: &[&str] = &["Marc","Gaius","Lucius","Publius","Quintus","Titus","Sextus","Aulus","Gnaeus","Decimus","Marcus","Julius"];
824const LATIN_MID: &[&str] = &["aes","ius","ell","inn","iss","ull","orr","err","uss","elius","anus","atus"];
825const LATIN_SUF: &[&str] = &["us","um","ia","ius","ae","ix","ax","ius","anus","inus","ulus","atus"];
826
827const FANTASY_PRE: &[&str] = &["Aer","Zyl","Vael","Xan","Myr","Thal","Aen","Ith","Sel","Nox","Kyr","Dra"];
828const FANTASY_MID: &[&str] = &["an","ael","iel","ion","ias","eld","ath","ill","orn","ith","eth","ash"];
829const FANTASY_SUF: &[&str] = &["iel","ias","ion","ath","ael","ial","uen","ean","ian","iel","ath","orn"];
830
831#[derive(Debug, Clone, PartialEq, Eq)]
835pub enum EventKind {
836 War,
837 Plague,
838 NaturalDisaster,
839 FoundedSettlement,
840 RulerChanged,
841 Discovery,
842 Trade,
843 Rebellion,
844}
845
846#[derive(Debug, Clone)]
848pub struct HistoryEvent {
849 pub year: i32,
850 pub kind: EventKind,
851 pub affected_settlements: Vec<u32>,
852 pub description: String,
853}
854
855pub struct WorldHistory {
857 pub events: Vec<HistoryEvent>,
858}
859
860impl WorldHistory {
861 pub fn new() -> Self { Self { events: Vec::new() } }
862
863 pub fn generate(
865 &mut self,
866 settlements: &[Settlement],
867 years: u32,
868 rng: &mut Rng,
869 ) {
870 let mut year = 1i32;
871 let end_year = year + years as i32;
872 let n_settle = settlements.len();
873
874 while year < end_year {
875 let events_this_year = rng.range_usize(3);
876 for _ in 0..=events_this_year {
877 if year >= end_year { break; }
878 let kind_roll = rng.range_usize(8);
879 let kind = match kind_roll {
880 0 => EventKind::War,
881 1 => EventKind::Plague,
882 2 => EventKind::NaturalDisaster,
883 3 => EventKind::FoundedSettlement,
884 4 => EventKind::RulerChanged,
885 5 => EventKind::Discovery,
886 6 => EventKind::Trade,
887 _ => EventKind::Rebellion,
888 };
889
890 let n_affected = if n_settle == 0 { 0 } else {
891 rng.range_usize(n_settle.min(3)) + 1
892 };
893 let affected: Vec<u32> = if n_settle > 0 {
894 let mut ids: Vec<u32> = settlements.iter().map(|s| s.id).collect();
895 rng.shuffle(&mut ids);
896 ids.into_iter().take(n_affected).collect()
897 } else {
898 Vec::new()
899 };
900
901 let description = Self::describe(&kind, &affected, settlements, year, rng);
902 self.events.push(HistoryEvent { year, kind, affected_settlements: affected, description });
903 }
904 year += rng.range_i32(1, 10);
906 }
907 }
908
909 fn describe(
910 kind: &EventKind,
911 affected: &[u32],
912 settlements: &[Settlement],
913 year: i32,
914 rng: &mut Rng,
915 ) -> String {
916 let settle_name = |id: u32| -> &str {
917 settlements.iter().find(|s| s.id == id).map(|s| s.name.as_str()).unwrap_or("Unknown")
918 };
919
920 let first = affected.first().copied().unwrap_or(0);
921 let second = affected.get(1).copied();
922
923 match kind {
924 EventKind::War => {
925 let a = settle_name(first);
926 let b = second.map(settle_name).unwrap_or("foreign powers");
927 format!("Year {year}: War erupted between {a} and {b}.")
928 }
929 EventKind::Plague => {
930 let a = settle_name(first);
931 format!("Year {year}: A plague ravaged {a}, killing thousands.")
932 }
933 EventKind::NaturalDisaster => {
934 let disasters = &["earthquake","flood","volcanic eruption","drought","wildfire"];
935 let d = rng.pick(disasters).copied().unwrap_or("storm");
936 let a = settle_name(first);
937 format!("Year {year}: A great {d} struck near {a}.")
938 }
939 EventKind::FoundedSettlement => {
940 let a = settle_name(first);
941 format!("Year {year}: The settlement of {a} was founded.")
942 }
943 EventKind::RulerChanged => {
944 let a = settle_name(first);
945 format!("Year {year}: A new ruler came to power in {a}.")
946 }
947 EventKind::Discovery => {
948 let things = &["ancient ruins","a new trade route","a rich vein of ore","a sacred spring","a lost tome"];
949 let thing = rng.pick(things).copied().unwrap_or("something remarkable");
950 let a = settle_name(first);
951 format!("Year {year}: Explorers from {a} discovered {thing}.")
952 }
953 EventKind::Trade => {
954 let a = settle_name(first);
955 let b = second.map(settle_name).unwrap_or("distant lands");
956 format!("Year {year}: A prosperous trade agreement was forged between {a} and {b}.")
957 }
958 EventKind::Rebellion => {
959 let a = settle_name(first);
960 format!("Year {year}: The people of {a} rose in rebellion against their rulers.")
961 }
962 }
963 }
964}
965
966impl Default for WorldHistory {
967 fn default() -> Self { Self::new() }
968}
969
970pub struct WorldBuilder {
974 pub width: usize,
975 pub height: usize,
976 pub seed: u64,
977}
978
979impl WorldBuilder {
980 pub fn new(width: usize, height: usize, seed: u64) -> Self {
981 Self { width, height, seed }
982 }
983
984 pub fn build(&self) -> (WorldMap, Vec<Settlement>, WorldHistory) {
986 let mut rng = Rng::new(self.seed);
987 let mut map = WorldMap::new(self.width, self.height);
988
989 let heightmap = HeightmapWorld::default();
991 heightmap.apply(&mut map, self.seed);
992
993 let climate = ClimateSimulator::default();
995 climate.apply(&mut map);
996
997 let rivers = RiverSystem::default();
999 rivers.apply(&mut map, &mut rng);
1000
1001 let classifier = BiomeClassifier::new();
1003 for y in 0..self.height {
1004 for x in 0..self.width {
1005 let (e, t, m) = {
1006 let c = map.get(x, y);
1007 (c.elevation, c.temperature, c.moisture)
1008 };
1009 map.get_mut(x, y).biome = classifier.classify(t, m, e);
1010 }
1011 }
1012
1013 let culture = [Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy];
1015 let c = culture[rng.range_usize(5)];
1016 let mut name_gen = NameGenerator::new(c);
1017 let mut placer = SettlementPlacer::default();
1018 placer.num_settlements = 20;
1019 let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1020
1021 let roads = RoadNetwork::default();
1023 roads.build_roads(&mut map, &settlements, &mut rng);
1024
1025 let mut history = WorldHistory::new();
1027 history.generate(&settlements, 500, &mut rng);
1028
1029 (map, settlements, history)
1030 }
1031}
1032
1033#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 fn small_map() -> WorldMap {
1040 let mut map = WorldMap::new(64, 48);
1041 let hm = HeightmapWorld::default();
1042 hm.apply(&mut map, 42);
1043 map
1044 }
1045
1046 #[test]
1047 fn heightmap_has_land_and_ocean() {
1048 let map = small_map();
1049 let has_land = map.cells.iter().any(|c| c.elevation > 0.0);
1050 let has_ocean = map.cells.iter().any(|c| c.elevation < 0.0);
1051 assert!(has_land, "expected some land cells");
1052 assert!(has_ocean, "expected some ocean cells");
1053 }
1054
1055 #[test]
1056 fn climate_assigns_temperature() {
1057 let mut map = small_map();
1058 let climate = ClimateSimulator::default();
1059 climate.apply(&mut map);
1060 let any_nonzero = map.cells.iter().any(|c| c.temperature != 0.0);
1061 assert!(any_nonzero);
1062 }
1063
1064 #[test]
1065 fn climate_assigns_moisture() {
1066 let mut map = small_map();
1067 let climate = ClimateSimulator::default();
1068 climate.apply(&mut map);
1069 let any_moisture = map.cells.iter().any(|c| c.moisture > 0.0);
1070 assert!(any_moisture);
1071 }
1072
1073 #[test]
1074 fn rivers_carve_some_cells() {
1075 let mut map = small_map();
1076 let climate = ClimateSimulator::default();
1077 climate.apply(&mut map);
1078 let mut rng = Rng::new(99);
1079 let rivers = RiverSystem { num_rivers: 3, min_source_elev: 0.4, erosion_amount: 0.01 };
1080 rivers.apply(&mut map, &mut rng);
1081 let river_cells = map.cells.iter().filter(|c| c.river_id.is_some()).count();
1082 assert!(river_cells > 0, "expected river cells");
1083 }
1084
1085 #[test]
1086 fn biome_classifier_covers_all_ids() {
1087 let clf = BiomeClassifier::new();
1088 let biomes = [
1090 clf.classify(-0.8, 0.1, 0.5),
1091 clf.classify( 0.8, 0.05, 0.3),
1092 clf.classify( 0.2, 0.6, 0.3),
1093 clf.classify(-0.5, 0.4, 0.4),
1094 clf.classify( 0.6, 0.7, 0.3),
1095 ];
1096 assert!(biomes.iter().any(|b| *b != BiomeId::OCEAN));
1097 }
1098
1099 #[test]
1100 fn settlement_placer_places_settlements() {
1101 let mut rng = Rng::new(7);
1102 let mut map = small_map();
1103 let climate = ClimateSimulator::default();
1104 climate.apply(&mut map);
1105 let mut name_gen = NameGenerator::new(Culture::Fantasy);
1106 let mut placer = SettlementPlacer::default();
1107 placer.num_settlements = 5;
1108 let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1109 assert!(!settlements.is_empty(), "should place at least one settlement");
1110 }
1111
1112 #[test]
1113 fn settlement_names_non_empty() {
1114 let mut rng = Rng::new(1234);
1115 let gen = NameGenerator::new(Culture::Norse);
1116 for _ in 0..5 {
1117 let name = gen.generate(&mut rng);
1118 assert!(!name.is_empty(), "name should not be empty");
1119 }
1120 }
1121
1122 #[test]
1123 fn road_network_finds_path() {
1124 let map = small_map();
1125 let roads = RoadNetwork::default();
1126 let path = roads.find_path(&map, 5, 5, 20, 20);
1127 assert!(!path.is_empty(), "A* should find a path");
1128 }
1129
1130 #[test]
1131 fn world_history_generates_events() {
1132 let mut rng = Rng::new(42);
1133 let settlements = vec![
1134 Settlement::new(1, 10, 10, SettlementKind::Town, "Testville".into()),
1135 Settlement::new(2, 30, 30, SettlementKind::City, "Cityburg".into()),
1136 Settlement::new(3, 50, 10, SettlementKind::Village, "Hamlet".into()),
1137 ];
1138 let mut history = WorldHistory::new();
1139 history.generate(&settlements, 100, &mut rng);
1140 assert!(!history.events.is_empty(), "should generate events");
1141 }
1142
1143 #[test]
1144 fn world_history_event_years_monotone() {
1145 let mut rng = Rng::new(55);
1146 let settlements = vec![Settlement::new(1, 5, 5, SettlementKind::Village, "A".into())];
1147 let mut history = WorldHistory::new();
1148 history.generate(&settlements, 200, &mut rng);
1149 let years: Vec<i32> = history.events.iter().map(|e| e.year).collect();
1150 let sorted = years.windows(2).all(|w| w[0] <= w[1]);
1151 assert!(sorted, "events should be in chronological order");
1152 }
1153
1154 #[test]
1155 fn world_builder_full_pipeline() {
1156 let builder = WorldBuilder::new(32, 24, 42);
1157 let (map, settlements, history) = builder.build();
1158 assert_eq!(map.width, 32);
1159 assert_eq!(map.height, 24);
1160 assert!(!history.events.is_empty());
1161 }
1162
1163 #[test]
1164 fn name_generator_all_cultures() {
1165 let mut rng = Rng::new(7);
1166 for culture in &[Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy] {
1167 let gen = NameGenerator::new(*culture);
1168 let name = gen.generate(&mut rng);
1169 assert!(!name.is_empty(), "Culture {:?} produced empty name", culture);
1170 }
1171 }
1172
1173 #[test]
1174 fn biome_params_all_twelve() {
1175 let params = BiomeParams::all();
1176 assert_eq!(params.len(), 12);
1177 }
1178
1179 #[test]
1180 fn world_map_idx_roundtrip() {
1181 let map = WorldMap::new(50, 40);
1182 for y in 0..40 {
1183 for x in 0..50 {
1184 let i = map.idx(x, y);
1185 assert_eq!(i % 50, x);
1186 assert_eq!(i / 50, y);
1187 }
1188 }
1189 }
1190}