feat(citygen): City::validate and seed sweep tests
Also guarantees one building per district and repairs interiors split by crossing partitions, both found by the new validation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
use super::{GenCtx, Stage, StageRunner, StepOut};
|
||||
use crate::geom::{Pos, Rect};
|
||||
use crate::params::GenError;
|
||||
use crate::pathing::{bfs_nearest, components};
|
||||
use crate::tile::Terrain;
|
||||
use crate::world::{Building, Connector, ConnectorKind, StratumKind};
|
||||
use rand::Rng;
|
||||
@@ -105,6 +106,45 @@ impl InteriorsStage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Guarantees every walkable tile inside the footprint reaches every other by carving
|
||||
/// doors through inner walls (never the perimeter).
|
||||
fn ensure_connected(ctx: &mut GenCtx, k: StratumKind, fp: Rect) {
|
||||
let size = ctx.size();
|
||||
let inner = fp.inset(1);
|
||||
for _ in 0..8 {
|
||||
let comps = components(size, |p| fp.contains(p) && ctx.city.walkable(k, p));
|
||||
if comps.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
let main = &comps[0];
|
||||
let mut in_main = vec![false; size.area()];
|
||||
for p in main {
|
||||
in_main[size.index(*p)] = true;
|
||||
}
|
||||
let start = comps[1][0];
|
||||
let path = bfs_nearest(
|
||||
size,
|
||||
start,
|
||||
|p| {
|
||||
inner.contains(p)
|
||||
&& (ctx.city.walkable(k, p) || ctx.terrain(k, p) == Terrain::Wall)
|
||||
},
|
||||
|p| in_main[size.index(p)],
|
||||
16,
|
||||
);
|
||||
match path {
|
||||
Some(path) => {
|
||||
for p in path {
|
||||
if ctx.terrain(k, p) == Terrain::Wall && inner.contains(p) {
|
||||
ctx.set_terrain(k, p, Terrain::Door);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a door on a border wall facing `outside_ok` terrain; returns the lobby tile.
|
||||
fn exterior_door(
|
||||
ctx: &mut GenCtx,
|
||||
@@ -228,6 +268,7 @@ impl InteriorsStage {
|
||||
continue;
|
||||
}
|
||||
Self::partition(ctx, rng, k, fp.inset(1), 4);
|
||||
Self::ensure_connected(ctx, k, fp);
|
||||
match k {
|
||||
StratumKind::Street => {
|
||||
let lobby = Self::exterior_door(
|
||||
|
||||
@@ -103,8 +103,10 @@ impl LotsStage {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// A district's first lot is always a building, so no district ends up empty.
|
||||
let has_building = ctx.city.buildings.iter().any(|b| b.district == district.id);
|
||||
let plaza_p = spec.plaza * (1.3 - density);
|
||||
if chance(rng, plaza_p) {
|
||||
if has_building && chance(rng, plaza_p) {
|
||||
let parking = matches!(
|
||||
district.theme,
|
||||
crate::world::Theme::CorpCore | crate::world::Theme::Industrial
|
||||
|
||||
Reference in New Issue
Block a user