Skip to main content

proof_engine/worldgen/
settlements.rs

1//! Settlement generation — L-system road networks + building placement.
2//!
3//! Places settlements at habitable locations near rivers and resources,
4//! then generates internal layout using L-systems for roads and
5//! zoning rules for buildings.
6
7use super::{Rng, Grid2D};
8use super::biomes::{BiomeMap, Biome};
9use super::rivers::RiverNetwork;
10
11/// Settlement size category.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum SettlementSize {
14    Hamlet,   // 5-20 buildings
15    Village,  // 20-80 buildings
16    Town,     // 80-300 buildings
17    City,     // 300-1000 buildings
18    Capital,  // 1000+ buildings
19}
20
21impl SettlementSize {
22    pub fn building_range(self) -> (usize, usize) {
23        match self {
24            Self::Hamlet  => (5, 20),
25            Self::Village => (20, 80),
26            Self::Town    => (80, 300),
27            Self::City    => (300, 1000),
28            Self::Capital => (1000, 3000),
29        }
30    }
31
32    pub fn population_range(self) -> (usize, usize) {
33        match self {
34            Self::Hamlet  => (10, 100),
35            Self::Village => (100, 500),
36            Self::Town    => (500, 5000),
37            Self::City    => (5000, 50000),
38            Self::Capital => (50000, 500000),
39        }
40    }
41}
42
43/// Building type.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum BuildingType {
46    House,
47    Farm,
48    Market,
49    Temple,
50    Barracks,
51    Wall,
52    Gate,
53    Tavern,
54    Smithy,
55    Library,
56    Palace,
57    Port,
58    Mine,
59    Warehouse,
60    Workshop,
61}
62
63/// A building in a settlement.
64#[derive(Debug, Clone)]
65pub struct Building {
66    pub building_type: BuildingType,
67    pub x: f32,
68    pub y: f32,
69    pub width: f32,
70    pub height: f32,
71    pub stories: u8,
72}
73
74/// A road segment.
75#[derive(Debug, Clone)]
76pub struct Road {
77    pub start: (f32, f32),
78    pub end: (f32, f32),
79    pub width: f32,
80    pub is_main: bool,
81}
82
83/// A complete settlement.
84#[derive(Debug, Clone)]
85pub struct Settlement {
86    pub id: u32,
87    pub name: String,
88    pub grid_x: usize,
89    pub grid_y: usize,
90    pub size: SettlementSize,
91    pub population: usize,
92    pub buildings: Vec<Building>,
93    pub roads: Vec<Road>,
94    pub biome: Biome,
95    pub near_river: bool,
96    pub near_coast: bool,
97    /// Civilization ID that owns this settlement.
98    pub owner_civ: Option<u32>,
99    /// Founding year.
100    pub founded_year: i32,
101    /// Resource score.
102    pub resources: f32,
103    /// Defense score.
104    pub defense: f32,
105}
106
107/// Place settlements on the map.
108pub fn place(
109    heightmap: &Grid2D,
110    biome_map: &BiomeMap,
111    rivers: &RiverNetwork,
112    max_settlements: usize,
113    rng: &mut Rng,
114) -> Vec<Settlement> {
115    let w = heightmap.width;
116    let h = heightmap.height;
117
118    // Score each cell for settlement suitability
119    let mut scores: Vec<(usize, usize, f32)> = Vec::new();
120    for y in 2..h - 2 {
121        for x in 2..w - 2 {
122            let biome = biome_map.biome_at(x, y);
123            if biome.is_water() { continue; }
124
125            let mut score = biome.habitability();
126
127            // Near river bonus
128            if rivers.is_river(x, y) || has_river_neighbor(rivers, x, y, w, h) {
129                score += 0.3;
130            }
131
132            // Flat terrain bonus
133            let (gx, gy) = heightmap.gradient(x, y);
134            let slope = (gx * gx + gy * gy).sqrt();
135            score += (1.0 - slope * 5.0).max(0.0) * 0.2;
136
137            // Resource bonus
138            score += biome.resources() * 0.2;
139
140            if score > 0.3 {
141                scores.push((x, y, score));
142            }
143        }
144    }
145
146    // Sort by score (best first)
147    scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
148
149    // Place settlements with minimum distance between them
150    let min_dist_sq = (w / 8).max(4).pow(2);
151    let mut placed: Vec<(usize, usize)> = Vec::new();
152    let mut settlements = Vec::new();
153    let mut next_id = 0u32;
154
155    for &(x, y, score) in &scores {
156        if settlements.len() >= max_settlements { break; }
157
158        // Check minimum distance
159        let too_close = placed.iter().any(|&(px, py)| {
160            let dx = x as i32 - px as i32;
161            let dy = y as i32 - py as i32;
162            (dx * dx + dy * dy) < min_dist_sq as i32
163        });
164        if too_close { continue; }
165
166        let size = if score > 0.85 {
167            SettlementSize::City
168        } else if score > 0.7 {
169            SettlementSize::Town
170        } else if score > 0.5 {
171            SettlementSize::Village
172        } else {
173            SettlementSize::Hamlet
174        };
175
176        let pop_range = size.population_range();
177        let population = rng.range_usize(pop_range.0, pop_range.1);
178
179        let biome = biome_map.biome_at(x, y);
180        let near_river = rivers.is_river(x, y) || has_river_neighbor(rivers, x, y, w, h);
181
182        let (buildings, roads) = generate_layout(size, rng);
183
184        settlements.push(Settlement {
185            id: next_id,
186            name: generate_name(rng),
187            grid_x: x,
188            grid_y: y,
189            size,
190            population,
191            buildings,
192            roads,
193            biome,
194            near_river,
195            near_coast: false, // TODO: compute from biome neighbors
196            owner_civ: None,
197            founded_year: 0,
198            resources: biome.resources(),
199            defense: 0.1,
200        });
201
202        placed.push((x, y));
203        next_id += 1;
204    }
205
206    settlements
207}
208
209fn has_river_neighbor(rivers: &RiverNetwork, x: usize, y: usize, w: usize, h: usize) -> bool {
210    for &(dx, dy) in &[(-1i32, 0), (1, 0), (0, -1), (0, 1)] {
211        let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as usize;
212        let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as usize;
213        if rivers.is_river(nx, ny) { return true; }
214    }
215    false
216}
217
218/// Generate settlement layout (buildings + roads) using L-system roads.
219fn generate_layout(size: SettlementSize, rng: &mut Rng) -> (Vec<Building>, Vec<Road>) {
220    let (min_b, max_b) = size.building_range();
221    let num_buildings = rng.range_usize(min_b, max_b);
222
223    let mut buildings = Vec::with_capacity(num_buildings);
224    let mut roads = Vec::new();
225
226    // Main road
227    let main_len = num_buildings as f32 * 0.3;
228    roads.push(Road {
229        start: (-main_len * 0.5, 0.0),
230        end: (main_len * 0.5, 0.0),
231        width: 2.0,
232        is_main: true,
233    });
234
235    // Cross roads (L-system branching)
236    let num_cross = (num_buildings / 20).max(1);
237    for i in 0..num_cross {
238        let t = (i as f32 + 0.5) / num_cross as f32;
239        let cx = -main_len * 0.5 + main_len * t;
240        let branch_len = main_len * rng.range_f32(0.2, 0.5);
241        roads.push(Road {
242            start: (cx, 0.0),
243            end: (cx, branch_len),
244            width: 1.5,
245            is_main: false,
246        });
247        roads.push(Road {
248            start: (cx, 0.0),
249            end: (cx, -branch_len),
250            width: 1.5,
251            is_main: false,
252        });
253    }
254
255    // Place buildings along roads
256    for i in 0..num_buildings {
257        let road_idx = i % roads.len();
258        let road = &roads[road_idx];
259        let t = rng.next_f32();
260        let rx = road.start.0 + (road.end.0 - road.start.0) * t;
261        let ry = road.start.1 + (road.end.1 - road.start.1) * t;
262        let offset = if rng.coin(0.5) { road.width + 1.0 } else { -(road.width + 1.0) };
263
264        let btype = pick_building_type(i, num_buildings, rng);
265        let (bw, bh) = building_size(btype);
266
267        buildings.push(Building {
268            building_type: btype,
269            x: rx + if road.start.0 == road.end.0 { offset } else { 0.0 },
270            y: ry + if road.start.1 == road.end.1 { offset } else { 0.0 },
271            width: bw,
272            height: bh,
273            stories: if matches!(btype, BuildingType::Palace | BuildingType::Temple | BuildingType::Library) { 3 } else { rng.range_u32(1, 3) as u8 },
274        });
275    }
276
277    (buildings, roads)
278}
279
280fn pick_building_type(index: usize, total: usize, rng: &mut Rng) -> BuildingType {
281    if index == 0 { return BuildingType::Market; }
282    if index == 1 { return BuildingType::Temple; }
283    if index == 2 && total > 50 { return BuildingType::Palace; }
284    match rng.range_u32(0, 10) {
285        0..=4 => BuildingType::House,
286        5 => BuildingType::Farm,
287        6 => BuildingType::Tavern,
288        7 => BuildingType::Smithy,
289        8 => BuildingType::Workshop,
290        _ => BuildingType::Warehouse,
291    }
292}
293
294fn building_size(btype: BuildingType) -> (f32, f32) {
295    match btype {
296        BuildingType::House     => (2.0, 2.0),
297        BuildingType::Farm      => (4.0, 3.0),
298        BuildingType::Market    => (5.0, 5.0),
299        BuildingType::Temple    => (4.0, 6.0),
300        BuildingType::Palace    => (8.0, 8.0),
301        BuildingType::Barracks  => (5.0, 4.0),
302        BuildingType::Tavern    => (3.0, 3.0),
303        BuildingType::Smithy    => (3.0, 2.5),
304        BuildingType::Library   => (4.0, 4.0),
305        BuildingType::Port      => (6.0, 3.0),
306        BuildingType::Mine      => (3.0, 3.0),
307        BuildingType::Warehouse => (4.0, 3.0),
308        BuildingType::Workshop  => (3.0, 3.0),
309        BuildingType::Wall      => (1.0, 1.0),
310        BuildingType::Gate      => (2.0, 2.0),
311    }
312}
313
314/// Generate a simple settlement name.
315fn generate_name(rng: &mut Rng) -> String {
316    let prefixes = ["Ash", "Oak", "Iron", "Storm", "Frost", "Shadow", "Gold", "Silver",
317        "Red", "Blue", "Green", "White", "Black", "Stone", "River", "Lake",
318        "Moon", "Sun", "Star", "Wind", "Fire", "Ice", "Dark", "Light"];
319    let suffixes = ["ford", "dale", "holm", "bury", "bridge", "gate", "keep",
320        "haven", "port", "vale", "fell", "crest", "wood", "field", "ton",
321        "wick", "march", "mire", "shore", "cliff"];
322
323    let prefix = prefixes[rng.next_u64() as usize % prefixes.len()];
324    let suffix = suffixes[rng.next_u64() as usize % suffixes.len()];
325    format!("{}{}", prefix, suffix)
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_settlement_name() {
334        let mut rng = Rng::new(42);
335        let name = generate_name(&mut rng);
336        assert!(!name.is_empty());
337    }
338
339    #[test]
340    fn test_layout_generation() {
341        let mut rng = Rng::new(42);
342        let (buildings, roads) = generate_layout(SettlementSize::Village, &mut rng);
343        assert!(!buildings.is_empty());
344        assert!(!roads.is_empty());
345    }
346}