feat(citygen): building interiors, doors, windows, shafts
Blocks now keep a one-tile margin from water, the map edge and other districts and get an alley ring; lots are cut as back-to-back rows so every building has an outdoor-facing wall for its door. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
359
crates/citygen/src/generate/interiors.rs
Normal file
359
crates/citygen/src/generate/interiors.rs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
//! Stage 7: rooms, doors, windows and internal shafts.
|
||||||
|
|
||||||
|
use super::{GenCtx, Stage, StageRunner, StepOut};
|
||||||
|
use crate::geom::{Pos, Rect};
|
||||||
|
use crate::params::GenError;
|
||||||
|
use crate::tile::Terrain;
|
||||||
|
use crate::world::{Building, Connector, ConnectorKind, StratumKind};
|
||||||
|
use rand::Rng;
|
||||||
|
use rand::seq::IndexedRandom;
|
||||||
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct InteriorsStage {
|
||||||
|
next: usize,
|
||||||
|
rng: Option<ChaCha8Rng>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_corner(r: Rect, p: Pos) -> bool {
|
||||||
|
(p.x == r.x || p.x == r.right() - 1) && (p.y == r.y || p.y == r.bottom() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The neighbour of a border tile that lies inside the footprint.
|
||||||
|
fn inward(r: Rect, p: Pos) -> Pos {
|
||||||
|
if p.x == r.x {
|
||||||
|
p.offset(1, 0)
|
||||||
|
} else if p.x == r.right() - 1 {
|
||||||
|
p.offset(-1, 0)
|
||||||
|
} else if p.y == r.y {
|
||||||
|
p.offset(0, 1)
|
||||||
|
} else {
|
||||||
|
p.offset(0, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outward(r: Rect, p: Pos) -> Pos {
|
||||||
|
let i = inward(r, p);
|
||||||
|
Pos::new(2 * p.x - i.x, 2 * p.y - i.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteriorsStage {
|
||||||
|
/// Recursive partitions: a wall line with one door, on floor tiles only.
|
||||||
|
fn partition(ctx: &mut GenCtx, rng: &mut ChaCha8Rng, k: StratumKind, inner: Rect, depth: u8) {
|
||||||
|
let can_v = inner.w >= 7;
|
||||||
|
let can_h = inner.h >= 7;
|
||||||
|
if depth == 0 || (!can_v && !can_h) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let vertical = if can_v && can_h {
|
||||||
|
inner.w >= inner.h
|
||||||
|
} else {
|
||||||
|
can_v
|
||||||
|
};
|
||||||
|
if vertical {
|
||||||
|
let s = rng.random_range(inner.x + 3..=inner.right() - 4);
|
||||||
|
for y in inner.y..inner.bottom() {
|
||||||
|
let p = Pos::new(s, y);
|
||||||
|
if ctx.terrain(k, p) == Terrain::Floor {
|
||||||
|
ctx.set_terrain(k, p, Terrain::Wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let door = Pos::new(s, rng.random_range(inner.y..inner.bottom()));
|
||||||
|
if ctx.terrain(k, door) == Terrain::Wall {
|
||||||
|
ctx.set_terrain(k, door, Terrain::Door);
|
||||||
|
}
|
||||||
|
Self::partition(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
Rect::new(inner.x, inner.y, s - inner.x, inner.h),
|
||||||
|
depth - 1,
|
||||||
|
);
|
||||||
|
Self::partition(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
Rect::new(s + 1, inner.y, inner.right() - s - 1, inner.h),
|
||||||
|
depth - 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let s = rng.random_range(inner.y + 3..=inner.bottom() - 4);
|
||||||
|
for x in inner.x..inner.right() {
|
||||||
|
let p = Pos::new(x, s);
|
||||||
|
if ctx.terrain(k, p) == Terrain::Floor {
|
||||||
|
ctx.set_terrain(k, p, Terrain::Wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let door = Pos::new(rng.random_range(inner.x..inner.right()), s);
|
||||||
|
if ctx.terrain(k, door) == Terrain::Wall {
|
||||||
|
ctx.set_terrain(k, door, Terrain::Door);
|
||||||
|
}
|
||||||
|
Self::partition(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
Rect::new(inner.x, inner.y, inner.w, s - inner.y),
|
||||||
|
depth - 1,
|
||||||
|
);
|
||||||
|
Self::partition(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
Rect::new(inner.x, s + 1, inner.w, inner.bottom() - s - 1),
|
||||||
|
depth - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a door on a border wall facing `outside_ok` terrain; returns the lobby tile.
|
||||||
|
fn exterior_door(
|
||||||
|
ctx: &mut GenCtx,
|
||||||
|
rng: &mut ChaCha8Rng,
|
||||||
|
k: StratumKind,
|
||||||
|
fp: Rect,
|
||||||
|
outside_ok: impl Fn(Terrain) -> bool,
|
||||||
|
prefer: Option<Terrain>,
|
||||||
|
) -> Option<Pos> {
|
||||||
|
let candidates: Vec<Pos> = fp
|
||||||
|
.border()
|
||||||
|
.filter(|p| !is_corner(fp, *p) && ctx.terrain(k, *p) == Terrain::Wall)
|
||||||
|
.filter(|p| {
|
||||||
|
let o = outward(fp, *p);
|
||||||
|
ctx.city.size.contains(o) && outside_ok(ctx.terrain(k, o))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let preferred: Vec<Pos> = match prefer {
|
||||||
|
Some(t) => candidates
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|p| ctx.terrain(k, outward(fp, *p)) == t)
|
||||||
|
.collect(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
let pool = if preferred.is_empty() {
|
||||||
|
&candidates
|
||||||
|
} else {
|
||||||
|
&preferred
|
||||||
|
};
|
||||||
|
let door = *pool.choose(rng).expect("non-empty pool");
|
||||||
|
ctx.set_terrain(k, door, Terrain::Door);
|
||||||
|
let lobby = inward(fp, door);
|
||||||
|
if !ctx.city.walkable(k, lobby) {
|
||||||
|
ctx.set_terrain(k, lobby, Terrain::Floor);
|
||||||
|
}
|
||||||
|
Some(lobby)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows(ctx: &mut GenCtx, fp: Rect) {
|
||||||
|
let k = StratumKind::Street;
|
||||||
|
let spots: Vec<Pos> = fp
|
||||||
|
.border()
|
||||||
|
.filter(|p| !is_corner(fp, *p) && ctx.terrain(k, *p) == Terrain::Wall)
|
||||||
|
.filter(|p| (p.x + p.y) % 2 == 0)
|
||||||
|
.filter(|p| {
|
||||||
|
let o = outward(fp, *p);
|
||||||
|
ctx.tile(k, o).is_some_and(|t| t.is_outdoor_ground())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for p in spots {
|
||||||
|
ctx.set_terrain(k, p, Terrain::Window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places the internal stair or lift and registers its connector.
|
||||||
|
fn shaft(ctx: &mut GenCtx, b: &Building) {
|
||||||
|
let fp = b.footprint;
|
||||||
|
let inner = fp.inset(1);
|
||||||
|
let strata: Vec<StratumKind> = b.span.iter().collect();
|
||||||
|
let open_everywhere = |ctx: &GenCtx, p: Pos| {
|
||||||
|
strata.iter().all(|k| {
|
||||||
|
let t = ctx.terrain(*k, p);
|
||||||
|
t != Terrain::Wall && t != Terrain::Void && ctx.city.walkable(*k, p)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let centre = fp.center();
|
||||||
|
let mut best = inner
|
||||||
|
.iter()
|
||||||
|
.filter(|p| open_everywhere(ctx, *p))
|
||||||
|
.min_by_key(|p| p.dist2(centre));
|
||||||
|
if best.is_none() {
|
||||||
|
let c = if inner.is_empty() {
|
||||||
|
centre
|
||||||
|
} else {
|
||||||
|
inner.center()
|
||||||
|
};
|
||||||
|
for k in &strata {
|
||||||
|
if *k != StratumKind::Heights {
|
||||||
|
ctx.set_terrain(*k, c, Terrain::Floor);
|
||||||
|
for n in c.neighbors4() {
|
||||||
|
if inner.contains(n) && ctx.terrain(*k, n) == Terrain::Wall {
|
||||||
|
ctx.set_terrain(*k, n, Terrain::Floor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best = Some(c);
|
||||||
|
}
|
||||||
|
let pos = best.expect("set above");
|
||||||
|
let kind = if strata.len() == 2 {
|
||||||
|
ConnectorKind::Stair
|
||||||
|
} else {
|
||||||
|
ConnectorKind::Lift
|
||||||
|
};
|
||||||
|
let terrain = match kind {
|
||||||
|
ConnectorKind::Stair => Terrain::Stair,
|
||||||
|
_ => Terrain::Lift,
|
||||||
|
};
|
||||||
|
for k in &strata {
|
||||||
|
ctx.set_terrain(*k, pos, terrain);
|
||||||
|
ctx.set_feature(*k, pos, None);
|
||||||
|
}
|
||||||
|
ctx.city.connectors.push(Connector {
|
||||||
|
kind,
|
||||||
|
pos,
|
||||||
|
serves: strata,
|
||||||
|
public: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn furnish(&mut self, ctx: &mut GenCtx, i: usize) {
|
||||||
|
let b = ctx.city.buildings[i].clone();
|
||||||
|
let rng = self.rng.as_mut().expect("begin sets rng");
|
||||||
|
let fp = b.footprint;
|
||||||
|
for k in b.span.iter() {
|
||||||
|
if k == StratumKind::Heights {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Self::partition(ctx, rng, k, fp.inset(1), 4);
|
||||||
|
match k {
|
||||||
|
StratumKind::Street => {
|
||||||
|
let lobby = Self::exterior_door(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
fp,
|
||||||
|
|t| {
|
||||||
|
matches!(
|
||||||
|
t,
|
||||||
|
Terrain::Road | Terrain::Sidewalk | Terrain::Alley | Terrain::Plaza
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Some(Terrain::Sidewalk),
|
||||||
|
);
|
||||||
|
if let Some(l) = lobby {
|
||||||
|
ctx.scratch.lobbies.push((b.id, l));
|
||||||
|
}
|
||||||
|
Self::windows(ctx, fp);
|
||||||
|
}
|
||||||
|
StratumKind::Skyways => {
|
||||||
|
Self::exterior_door(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
fp,
|
||||||
|
|t| matches!(t, Terrain::Walkway | Terrain::Bridge),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
StratumKind::Undercity => {
|
||||||
|
Self::exterior_door(
|
||||||
|
ctx,
|
||||||
|
rng,
|
||||||
|
k,
|
||||||
|
fp,
|
||||||
|
|t| matches!(t, Terrain::Tunnel | Terrain::Rail | Terrain::Grate),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.span.len() >= 2 {
|
||||||
|
Self::shaft(ctx, &b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StageRunner for InteriorsStage {
|
||||||
|
fn begin(&mut self, ctx: &mut GenCtx) -> Result<(), GenError> {
|
||||||
|
self.rng = Some(ctx.rng(Stage::Interiors));
|
||||||
|
self.next = 0;
|
||||||
|
ctx.scratch.lobbies.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&mut self, ctx: &mut GenCtx) -> Result<StepOut, GenError> {
|
||||||
|
let n = ctx.city.buildings.len();
|
||||||
|
if self.next >= n {
|
||||||
|
return Ok(StepOut::Finished);
|
||||||
|
}
|
||||||
|
self.furnish(ctx, self.next);
|
||||||
|
self.next += 1;
|
||||||
|
Ok(StepOut::Continue(self.next as f32 / n as f32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::generate::testutil::small;
|
||||||
|
use crate::params::WaterMode;
|
||||||
|
use crate::pathing::components;
|
||||||
|
use crate::tile::Terrain;
|
||||||
|
use crate::world::StratumKind;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_building_has_a_street_door_and_internal_connectivity() {
|
||||||
|
let c = small(61, WaterMode::Bay, 7);
|
||||||
|
let st = c.stratum(StratumKind::Street).unwrap();
|
||||||
|
for b in &c.buildings {
|
||||||
|
let doors: Vec<_> = b
|
||||||
|
.footprint
|
||||||
|
.border()
|
||||||
|
.filter(|p| st.tiles[*p].terrain == Terrain::Door)
|
||||||
|
.collect();
|
||||||
|
assert!(!doors.is_empty(), "{:?} has no door", b.id);
|
||||||
|
assert!(doors.iter().any(|d| {
|
||||||
|
d.neighbors4()
|
||||||
|
.any(|n| st.tiles.get(n).is_some_and(|t| t.is_outdoor_ground()))
|
||||||
|
}));
|
||||||
|
let inside = b.footprint;
|
||||||
|
let comps = components(st.tiles.size(), |p| {
|
||||||
|
inside.contains(p) && st.tiles[p].is_walkable()
|
||||||
|
});
|
||||||
|
assert_eq!(comps.len(), 1, "{:?} interior split", b.id);
|
||||||
|
if b.span.len() >= 2 {
|
||||||
|
let conn = c
|
||||||
|
.connectors
|
||||||
|
.iter()
|
||||||
|
.find(|k| inside.contains(k.pos) && !k.public)
|
||||||
|
.expect("internal connector");
|
||||||
|
for s in b.span.iter() {
|
||||||
|
assert!(conn.serves.contains(&s));
|
||||||
|
assert!(c.walkable(s, conn.pos), "{:?} {s:?} {:?}", b.id, conn.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rooms_respect_minimum_size_and_big_buildings_get_walls() {
|
||||||
|
let c = small(61, WaterMode::None, 6);
|
||||||
|
let st = c.stratum(StratumKind::Street).unwrap();
|
||||||
|
let big = c
|
||||||
|
.buildings
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.footprint.w >= 12 && b.footprint.h >= 9)
|
||||||
|
.expect("a big building");
|
||||||
|
let walls_inside = big
|
||||||
|
.footprint
|
||||||
|
.inset(1)
|
||||||
|
.iter()
|
||||||
|
.filter(|p| st.tiles[*p].terrain == Terrain::Wall)
|
||||||
|
.count();
|
||||||
|
assert!(walls_inside > 0, "big building should have internal walls");
|
||||||
|
assert!(st.tiles.count(|t| t.terrain == Terrain::Window) > 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,41 +18,55 @@ pub(crate) struct LotsStage {
|
|||||||
rng: Option<ChaCha8Rng>,
|
rng: Option<ChaCha8Rng>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a block into lots no smaller than `min` and no larger than twice that.
|
/// Splits a run `[lo, hi)` into pieces of length in `[min, 2*min)` with jittered cuts.
|
||||||
fn bsp(rng: &mut ChaCha8Rng, rect: Rect, min: (i32, i32), out: &mut Vec<Rect>) {
|
fn split_run(rng: &mut ChaCha8Rng, lo: i32, hi: i32, min: i32, out: &mut Vec<(i32, i32)>) {
|
||||||
let (min_w, min_h) = min;
|
let len = hi - lo;
|
||||||
let split_w = rect.w > 2 * min_w;
|
if len < 2 * min {
|
||||||
let split_h = rect.h > 2 * min_h;
|
out.push((lo, hi));
|
||||||
if !split_w && !split_h {
|
|
||||||
out.push(rect);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let vertical = if split_w && split_h {
|
|
||||||
rect.w as f32 / min_w as f32 >= rect.h as f32 / min_h as f32
|
|
||||||
} else {
|
|
||||||
split_w
|
|
||||||
};
|
|
||||||
let jitter: f32 = rng.random_range(0.4..0.6);
|
let jitter: f32 = rng.random_range(0.4..0.6);
|
||||||
if vertical {
|
let s = (lo + (len as f32 * jitter) as i32).clamp(lo + min, hi - min);
|
||||||
let s =
|
split_run(rng, lo, s, min, out);
|
||||||
(rect.x + (rect.w as f32 * jitter) as i32).clamp(rect.x + min_w, rect.right() - min_w);
|
split_run(rng, s, hi, min, out);
|
||||||
bsp(rng, Rect::new(rect.x, rect.y, s - rect.x, rect.h), min, out);
|
}
|
||||||
bsp(
|
|
||||||
rng,
|
/// Splits a block into lots: at most two back-to-back rows (or columns), each cut into
|
||||||
Rect::new(s, rect.y, rect.right() - s, rect.h),
|
/// strips along the long axis, so every lot touches the block perimeter.
|
||||||
min,
|
fn bsp(rng: &mut ChaCha8Rng, rect: Rect, min: (i32, i32), out: &mut Vec<Rect>) {
|
||||||
out,
|
let (min_w, min_h) = min;
|
||||||
);
|
if rect.w >= rect.h {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
if rect.h >= 2 * min_h {
|
||||||
|
let s = rect.y + rect.h / 2;
|
||||||
|
rows.push((rect.y, s));
|
||||||
|
rows.push((s, rect.bottom()));
|
||||||
} else {
|
} else {
|
||||||
let s =
|
rows.push((rect.y, rect.bottom()));
|
||||||
(rect.y + (rect.h as f32 * jitter) as i32).clamp(rect.y + min_h, rect.bottom() - min_h);
|
}
|
||||||
bsp(rng, Rect::new(rect.x, rect.y, rect.w, s - rect.y), min, out);
|
for (y0, y1) in rows {
|
||||||
bsp(
|
let mut cols = Vec::new();
|
||||||
rng,
|
split_run(rng, rect.x, rect.right(), min_w, &mut cols);
|
||||||
Rect::new(rect.x, s, rect.w, rect.bottom() - s),
|
for (x0, x1) in cols {
|
||||||
min,
|
out.push(Rect::new(x0, y0, x1 - x0, y1 - y0));
|
||||||
out,
|
}
|
||||||
);
|
}
|
||||||
|
} else {
|
||||||
|
let mut cols = Vec::new();
|
||||||
|
if rect.w >= 2 * min_w {
|
||||||
|
let s = rect.x + rect.w / 2;
|
||||||
|
cols.push((rect.x, s));
|
||||||
|
cols.push((s, rect.right()));
|
||||||
|
} else {
|
||||||
|
cols.push((rect.x, rect.right()));
|
||||||
|
}
|
||||||
|
for (x0, x1) in cols {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
split_run(rng, rect.y, rect.bottom(), min_h, &mut rows);
|
||||||
|
for (y0, y1) in rows {
|
||||||
|
out.push(Rect::new(x0, y0, x1 - x0, y1 - y0));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +241,7 @@ mod tests {
|
|||||||
fn buildings_fit_blocks_and_do_not_overlap() {
|
fn buildings_fit_blocks_and_do_not_overlap() {
|
||||||
let c = small(41, WaterMode::Bay, 7);
|
let c = small(41, WaterMode::Bay, 7);
|
||||||
assert!(
|
assert!(
|
||||||
c.buildings.len() > 40,
|
c.buildings.len() > 30,
|
||||||
"only {} buildings",
|
"only {} buildings",
|
||||||
c.buildings.len()
|
c.buildings.len()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod arterials;
|
pub mod arterials;
|
||||||
pub mod districts;
|
pub mod districts;
|
||||||
|
pub mod interiors;
|
||||||
pub mod lots;
|
pub mod lots;
|
||||||
pub mod streets;
|
pub mod streets;
|
||||||
pub mod terrain;
|
pub mod terrain;
|
||||||
@@ -231,6 +232,7 @@ pub(crate) fn runner_for(stage: Stage) -> Box<dyn StageRunner> {
|
|||||||
Stage::Streets => Box::new(streets::StreetsStage::default()),
|
Stage::Streets => Box::new(streets::StreetsStage::default()),
|
||||||
Stage::Lots => Box::new(lots::LotsStage::default()),
|
Stage::Lots => Box::new(lots::LotsStage::default()),
|
||||||
Stage::Verticals => Box::new(verticals::VerticalsStage::default()),
|
Stage::Verticals => Box::new(verticals::VerticalsStage::default()),
|
||||||
|
Stage::Interiors => Box::new(interiors::InteriorsStage::default()),
|
||||||
_ => Box::new(NoopStage),
|
_ => Box::new(NoopStage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,10 +284,21 @@ impl Carver<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A tile a block may not use: water, another district, anything already painted, or a
|
||||||
|
/// tile touching water, the map edge or another district (so a street ring always fits
|
||||||
|
/// around the block).
|
||||||
fn is_bad(&self, p: Pos) -> bool {
|
fn is_bad(&self, p: Pos) -> bool {
|
||||||
!self.ctx.is_land(p)
|
if !self.ctx.is_land(p)
|
||||||
|| !self.ctx.in_district(p, self.d)
|
|| !self.ctx.in_district(p, self.d)
|
||||||
|| self.ctx.terrain(StratumKind::Street, p) != Terrain::Void
|
|| self.ctx.terrain(StratumKind::Street, p) != Terrain::Void
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
p.neighbors4().any(|q| {
|
||||||
|
!self.ctx.city.size.contains(q)
|
||||||
|
|| !self.ctx.is_land(q)
|
||||||
|
|| !self.ctx.in_district(q, self.d)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drops edge rows/columns that are entirely unusable.
|
/// Drops edge rows/columns that are entirely unusable.
|
||||||
@@ -448,6 +459,12 @@ impl StreetsStage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Every block gets a walkable ring so each perimeter lot can open a door.
|
||||||
|
for b in &blocks {
|
||||||
|
for p in b.rect.grow(1).border() {
|
||||||
|
paint_cell(carver.ctx, p, district.id, Terrain::Alley);
|
||||||
|
}
|
||||||
|
}
|
||||||
carver.ctx.scratch.blocks.extend(blocks);
|
carver.ctx.scratch.blocks.extend(blocks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,10 +377,7 @@ mod tests {
|
|||||||
for b in &c.buildings {
|
for b in &c.buildings {
|
||||||
let p = b.footprint.center();
|
let p = b.footprint.center();
|
||||||
if b.span.contains(StratumKind::Skyways) {
|
if b.span.contains(StratumKind::Skyways) {
|
||||||
assert!(matches!(
|
assert!(sk.tiles[p].is_building_part() || sk.tiles[p].is_connector());
|
||||||
sk.tiles[p].terrain,
|
|
||||||
Terrain::Floor | Terrain::Wall
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
// Air above a low roof may carry an elevated walkway.
|
// Air above a low roof may carry an elevated walkway.
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -389,7 +386,10 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
if b.span.contains(StratumKind::Heights) {
|
if b.span.contains(StratumKind::Heights) {
|
||||||
assert_eq!(hi.tiles[p].terrain, Terrain::Rooftop);
|
assert!(matches!(
|
||||||
|
hi.tiles[p].terrain,
|
||||||
|
Terrain::Rooftop | Terrain::Lift | Terrain::Stair
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
hi.tiles[p].terrain,
|
hi.tiles[p].terrain,
|
||||||
|
|||||||
Reference in New Issue
Block a user