summaryrefslogtreecommitdiff
path: root/src/town/plan.rs
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-11 15:24:37 -0800
committerskylar <me@casper>2026-11-11 15:24:37 -0800
commit9ccf951473e21a8b90c25161156fa09a9b227a7b (patch)
treee7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/town/plan.rs
parentd36c54d996f3c14ae811312dbbe621e12a39edde (diff)
Orrery 0.1.0: the galaxy, flight, cities, inventory and saves
Grows the derelict crawler into the whole game, and gets it ready to publish. - A galaxy of star systems joined by lanes, split between four peoples' territories, with a star map and a system map. Systems are modelled with Kepler orbits, moons inside their Hill spheres, belts past the frost line and stations at Lagrange points. - Flight around one place at a time: arcade flight with a throttle fallback where held keys can't be detected, mining rocks for ore, landing, docking at stations, and boarding wrecks that come and go by the calendar. - Your ship to walk about, and capital cities to land at: generated plans with walls, quarters and streets, building interiors, shops, taverns, scholars and armourers. - Fighting with a weapon and a gun ready, six damage types and how each monster takes them, and armour that guards, wears and can be mended. - Everything you own has a weight: a vim-keyed inventory, a pack and the ship's lockers, and trade in ore, salvage, arms and fuel. - Characters with a people and a ship of their own, saved as versioned JSON on every scene change and on quit, with a character sheet, a saves list and a pause menu wherever you are. - `orrery weapons`, `armour`, `galaxy`, `system` and `town` print the generators' output without the TUI. - Release: README install steps for macOS and Linux, a Makefile whose install also works with macOS's install, an MIT licence, and Cargo metadata with rust-version 1.88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/town/plan.rs')
-rw-r--r--src/town/plan.rs1230
1 files changed, 1230 insertions, 0 deletions
diff --git a/src/town/plan.rs b/src/town/plan.rs
new file mode 100644
index 0000000..90f5e32
--- /dev/null
+++ b/src/town/plan.rs
@@ -0,0 +1,1230 @@
+//! Laying out a capital.
+//!
+//! The wall is a rounded, slightly lumpy oval. A landing field lies outside
+//! its west gate, and one to three more gates open on the other sides. A
+//! road runs inside the wall all the way round, and avenues run from each
+//! gate to the market square in the middle.
+//!
+//! The city is shared out into quarters around points scattered inside the
+//! wall: a market around the square, a gate quarter by the landing field,
+//! then a grand quarter, a holy one, homes, a poor quarter, works, gardens.
+//! Then it's cut into blocks, from the outside in: each block is split in
+//! two by a street until it's small enough for its quarter, so streets meet
+//! at T-junctions and never quite line up, the way old cities grow. Poor
+//! quarters end up with small blocks and alleys, grand ones with big blocks
+//! and wide streets.
+//!
+//! A few blocks are kept whole: one for the palace, one for the temple,
+//! parks and plazas. Every other block gets buildings packed along the
+//! streets around it, doors facing out, with yards and courtyards left
+//! behind them. Then each building is given a use to suit its quarter, and
+//! the shops are chosen from buildings in the right places: the broker and
+//! the shipyard by the gate you came in by, the tavern by the square.
+//!
+//! The world has a say too (`Lay`): how many parks there are, how big their
+//! ponds, how thick the trees and what's scattered outside the walls. On
+//! an ocean world the city stands on an island, its landing field out on a
+//! spit of sand and its other roads running out to sea as causeways.
+
+use rand::RngExt;
+use rand::seq::{IndexedRandom, SliceRandom};
+
+use super::words;
+use super::*;
+use crate::seed::Rng;
+
+/// Columns west of the city wall, for the landing field and the road in.
+const FIELD: i32 = 26;
+/// The landing field's east edge.
+const PAD_RIGHT: i32 = 19;
+/// How far the wall's wobble can push it out, as a share of its radius.
+const REACH: f64 = 1.08;
+
+/// How a world bends a city's plan. Each is times what it'd be otherwise.
+struct Lay {
+ parks: f64,
+ trees: f64,
+ /// How big ponds are. None at all at 0.
+ ponds: f64,
+ /// The chance of something standing on any bit of ground outside the
+ /// walls (not times anything).
+ wild: f64,
+ /// Whether the sea comes up round the walls.
+ sea: bool,
+}
+
+fn lay(terrain: Terrain) -> Lay {
+ let lay = |parks, trees, ponds, wild| Lay { parks, trees, ponds, wild, sea: false };
+ match terrain {
+ // Few parks, small cisterns, little shade.
+ Terrain::Desert => lay(0.35, 0.5, 0.55, 0.04),
+ Terrain::Ice => lay(0.6, 0.6, 1.0, 0.06),
+ Terrain::Ocean => Lay { sea: true, ..lay(1.0, 1.0, 1.2, 0.05) },
+ // Parks everywhere, trees crowding every yard.
+ Terrain::Jungle => lay(3.0, 3.0, 1.0, 0.3),
+ // Nothing pools on airless rock.
+ Terrain::Barren | Terrain::Clouds => lay(0.6, 0.8, 0.0, 0.05),
+ Terrain::Forge => lay(0.7, 0.7, 0.7, 0.06),
+ }
+}
+
+fn lerp(a: f64, b: f64, t: f64) -> f64 {
+ a + (b - a) * t
+}
+
+/// The line of the city wall: a superellipse, wobbled.
+struct WallShape {
+ cx: f64,
+ cy: f64,
+ rx: f64,
+ ry: f64,
+ /// 2 is an ellipse; the higher, the squarer.
+ n: f64,
+ /// Sine waves on the radius: (how many round the wall, how big, where).
+ wobble: [(f64, f64, f64); 2],
+}
+
+impl WallShape {
+ fn inside(&self, p: Pos) -> bool {
+ let dx = (f64::from(p.x) + 0.5 - self.cx) / self.rx;
+ let dy = (f64::from(p.y) + 0.5 - self.cy) / self.ry;
+ let d = (dx.abs().powf(self.n) + dy.abs().powf(self.n)).powf(1.0 / self.n);
+ let angle = dy.atan2(dx);
+ let r =
+ 1.0 + self.wobble.iter().map(|&(k, amp, at)| amp * (k * angle + at).sin()).sum::<f64>();
+ d <= r
+ }
+}
+
+/// How big blocks get in a quarter before a street splits them: columns
+/// and rows.
+fn block_size(q: QuarterKind) -> (i32, i32) {
+ match q {
+ QuarterKind::Poor => (14, 6),
+ QuarterKind::Homes => (20, 8),
+ QuarterKind::Market => (18, 8),
+ QuarterKind::Gate => (20, 8),
+ QuarterKind::Grand => (30, 12),
+ QuarterKind::Holy => (24, 10),
+ QuarterKind::Works => (26, 10),
+ QuarterKind::Gardens => (26, 11),
+ }
+}
+
+/// How wide a street is, columns for one running north–south and rows for
+/// one running east–west. A row is twice as tall as a column is wide, so
+/// (2, 1) looks the same either way.
+fn street_width(q: QuarterKind, depth: u32, rng: &mut Rng) -> (i32, i32) {
+ use QuarterKind::*;
+ match (q, depth) {
+ (Poor, 0) => (2, 1),
+ (Poor, _) => (1, 1),
+ (_, 0) | (Grand | Market | Gate, 1) => (3, 2),
+ (Homes, 3..) if rng.random_bool(0.3) => (1, 1),
+ _ => (2, 1),
+ }
+}
+
+/// The sizes of building a quarter packs along its streets.
+struct Lots {
+ /// For doors facing north or south: columns along the street, and rows
+ /// deep.
+ wide: (i32, i32),
+ deep: (i32, i32),
+ /// For doors facing east or west: rows along the street, and columns deep.
+ tall: (i32, i32),
+ long: (i32, i32),
+ /// The chance of a gap after each building.
+ gap: f64,
+}
+
+fn lots(q: QuarterKind) -> Lots {
+ let lots = |wide, deep, tall, long, gap| Lots { wide, deep, tall, long, gap };
+ match q {
+ QuarterKind::Poor => lots((4, 6), (2, 3), (3, 3), (4, 6), 0.12),
+ QuarterKind::Homes => lots((5, 8), (2, 4), (3, 4), (5, 8), 0.3),
+ QuarterKind::Market => lots((6, 11), (3, 4), (3, 4), (6, 10), 0.15),
+ QuarterKind::Gate => lots((6, 11), (3, 4), (3, 4), (6, 10), 0.25),
+ QuarterKind::Grand => lots((8, 13), (3, 5), (3, 5), (8, 12), 0.45),
+ QuarterKind::Holy => lots((5, 9), (3, 4), (3, 4), (5, 9), 0.35),
+ QuarterKind::Works => lots((7, 13), (3, 5), (3, 5), (7, 12), 0.2),
+ QuarterKind::Gardens => lots((5, 9), (2, 4), (3, 4), (5, 8), 0.6),
+ }
+}
+
+/// What the buildings in a quarter tend to be, and how often.
+fn uses(q: QuarterKind, culture: Culture) -> Vec<(Kind, f64)> {
+ use Kind::*;
+ let baths = if matches!(culture, Culture::Qaari | Culture::Houses) { 1.0 } else { 0.0 };
+ match q {
+ QuarterKind::Market => vec![
+ (House, 3.0),
+ (Workshop, 2.5),
+ (Warehouse, 1.0),
+ (Tenement, 1.0),
+ (Shrine, 0.3),
+ (Bathhouse, 0.6 * baths),
+ ],
+ QuarterKind::Gate => {
+ vec![(Warehouse, 3.0), (House, 2.0), (Workshop, 1.5), (Tenement, 1.0), (Shrine, 0.2)]
+ }
+ QuarterKind::Homes => vec![
+ (House, 8.0),
+ (Tenement, 1.5),
+ (Shrine, 0.5),
+ (Workshop, 1.0),
+ (Bathhouse, 0.4 * baths),
+ ],
+ QuarterKind::Poor => vec![(Tenement, 5.0), (House, 3.0), (Workshop, 1.0), (Shrine, 0.6)],
+ QuarterKind::Grand => {
+ vec![(Manor, 6.0), (House, 1.5), (Shrine, 0.5), (Bathhouse, 0.6 * baths)]
+ }
+ QuarterKind::Holy => vec![(Shrine, 2.5), (House, 3.0), (Tenement, 1.0), (Workshop, 1.0)],
+ QuarterKind::Works => {
+ vec![(Workshop, 6.0), (Warehouse, 4.0), (Tenement, 2.0), (House, 1.0)]
+ }
+ QuarterKind::Gardens => {
+ vec![(House, 3.0), (Manor, 2.0), (Shrine, 1.0), (Bathhouse, 0.6 * baths)]
+ }
+ }
+}
+
+/// Whether a building this many columns and rows is big enough to be one
+/// of these.
+fn fits(kind: Kind, w: i32, h: i32) -> bool {
+ let area = w * h;
+ match kind {
+ Kind::Manor => area >= 21,
+ Kind::Warehouse | Kind::Bathhouse => area >= 18,
+ Kind::Tenement => area >= 15,
+ _ => true,
+ }
+}
+
+/// How likely each kind of building is to have its door barred to you.
+fn barred_chance(kind: Kind) -> f64 {
+ match kind {
+ Kind::House => 0.35,
+ Kind::Manor => 0.5,
+ Kind::Warehouse => 0.4,
+ Kind::Tenement => 0.15,
+ Kind::Workshop => 0.05,
+ _ => 0.0,
+ }
+}
+
+struct Planner<'a> {
+ t: Town,
+ rng: &'a mut Rng,
+ lay: Lay,
+ wall: WallShape,
+ size: f64,
+ main: i32,
+ square: Rect,
+ /// Quarter kinds and the points they're centred on.
+ points: Vec<(QuarterKind, Pos)>,
+ /// Blocks between streets, and whether each has been used for
+ /// something already.
+ blocks: Vec<(Rect, bool)>,
+ /// Parks, which get more trees.
+ parks: Vec<Rect>,
+ /// Market stalls, each with its keeper's place in front.
+ stalls: Vec<Pos>,
+ /// The grounds of the palace and the temple, which streets go round.
+ grounds: Vec<Rect>,
+}
+
+pub(super) fn build(
+ name: String,
+ culture: Culture,
+ land: Land,
+ seed: u64,
+ size: f64,
+ rng: &mut Rng,
+) -> Town {
+ let rx = lerp(60.0, 116.0, size) * rng.random_range(0.92..1.08);
+ let ry = rx * rng.random_range(0.40..0.50);
+ let width = FIELD + (2.0 * rx * REACH).ceil() as i32 + 6;
+ let height = (2.0 * ry * REACH).ceil() as i32 + 10;
+ let main = height / 2;
+ let mut wobble = || {
+ (
+ f64::from(rng.random_range(3..=7)),
+ rng.random_range(0.0..0.035),
+ rng.random_range(0.0..6.3),
+ )
+ };
+ let wobble = [wobble(), wobble()];
+ let wall = WallShape {
+ cx: f64::from(FIELD) + rx * REACH,
+ cy: f64::from(main) + 0.5,
+ rx,
+ ry,
+ n: rng.random_range(2.3..4.0),
+ wobble,
+ };
+ let cells = (width * height) as usize;
+ let t = Town {
+ name,
+ culture,
+ land,
+ seed,
+ site: Site::new(width, height, Tile::Ground, Tile::CityWall, true),
+ owner: vec![NO_OWNER; cells],
+ quarter_of: vec![NO_QUARTER; cells],
+ quarters: Vec::new(),
+ buildings: Vec::new(),
+ shops: Vec::new(),
+ ship: Pos::new(7, main - 1),
+ start: Pos::new(13, main),
+ square: Pos::new(0, main),
+ gates: Vec::new(),
+ };
+ let mut p = Planner {
+ t,
+ rng,
+ lay: lay(land.terrain),
+ wall,
+ size,
+ main,
+ square: Rect::new(0, 0, 0, 0),
+ points: Vec::new(),
+ blocks: Vec::new(),
+ parks: Vec::new(),
+ stalls: Vec::new(),
+ grounds: Vec::new(),
+ };
+ p.walls();
+ p.field();
+ let sides = p.gates();
+ p.market_square();
+ p.avenues(&sides);
+ p.quarters();
+ p.landmarks();
+ let inside = p.inside_bounds();
+ p.split(inside, 0);
+ for b in 0..p.t.buildings.len() {
+ let front = p.t.buildings[b].front();
+ p.link(front);
+ }
+ p.parks_and_plazas();
+ let blocks: Vec<Rect> = p.blocks.iter().filter(|(_, used)| !used).map(|&(r, _)| r).collect();
+ for r in blocks {
+ p.pack(r);
+ }
+ p.uses();
+ p.shops();
+ p.guardhouses();
+ if p.lay.sea {
+ p.sea();
+ }
+ p.dress();
+ p.people();
+ p.t
+}
+
+impl Planner<'_> {
+ fn tile(&self, p: Pos) -> Tile {
+ self.t.site.tile(p)
+ }
+
+ fn set(&mut self, p: Pos, tile: Tile) {
+ if self.t.site.contains(p) {
+ self.t.site.set(p, tile);
+ }
+ }
+
+ /// Paves over open ground: streets go round what's already built, and
+ /// round landmarks' grounds.
+ fn carve(&mut self, r: Rect) {
+ for p in r.cells() {
+ if self.tile(p) == Tile::Garden && !self.reserved(p) {
+ self.set(p, Tile::Street);
+ }
+ }
+ }
+
+ fn walls(&mut self) {
+ let site = &self.t.site;
+ let all: Vec<Pos> = site.positions().collect();
+ for &p in &all {
+ if self.wall.inside(p) {
+ let edge = p.neighbours().any(|n| !self.wall.inside(n));
+ self.set(p, if edge { Tile::CityWall } else { Tile::Garden });
+ }
+ }
+ // A road inside the wall, all the way round.
+ let ring: Vec<Pos> = all
+ .into_iter()
+ .filter(|&p| {
+ self.tile(p) == Tile::Garden
+ && p.neighbours().any(|n| self.tile(n) == Tile::CityWall)
+ })
+ .collect();
+ for p in ring {
+ self.set(p, Tile::Street);
+ }
+ }
+
+ /// The landing field, with your ship on it.
+ fn field(&mut self) {
+ let main = self.main;
+ self.t.site.fill(Rect::new(3, main - 5, PAD_RIGHT, main + 5), Tile::Pad);
+ let (sw, sh) = SHIP_SIZE;
+ let ship = self.t.ship;
+ self.t.site.fill(Rect::new(ship.x, ship.y, ship.x + sw - 1, ship.y + sh - 1), Tile::Ship);
+ }
+
+ /// Cuts a gate through the wall along each lane, paving the way to it
+ /// from outside, and returns the cell just inside the middle lane.
+ fn gate(&mut self, lanes: &[Pos], (dx, dy): (i32, i32)) -> Pos {
+ let mut inner = Vec::new();
+ for &start in lanes {
+ let mut p = start;
+ while self.t.site.contains(p) && self.tile(p) != Tile::CityWall {
+ if self.tile(p) == Tile::Ground {
+ self.set(p, Tile::Street);
+ }
+ p = p.offset(dx, dy);
+ }
+ while self.tile(p) == Tile::CityWall && self.t.site.contains(p) {
+ self.set(p, Tile::Gate);
+ p = p.offset(dx, dy);
+ }
+ inner.push(p);
+ }
+ inner[lanes.len() / 2]
+ }
+
+ /// The west gate, and one to three others. Returns which side each
+ /// gate is on, in the order of `Town::gates`.
+ fn gates(&mut self) -> Vec<Side> {
+ let main = self.main;
+ let west: Vec<Pos> = (main - 1..=main + 1).map(|y| Pos::new(PAD_RIGHT + 1, y)).collect();
+ let inner = self.gate(&west, (1, 0));
+ self.t.gates.push(inner);
+ let mut sides = vec![Side::North, Side::South, Side::East];
+ sides.shuffle(self.rng);
+ let extra = 1 + self.rng.random_range(0..=(self.size * 2.0).round() as usize);
+ sides.truncate(extra);
+ let (w, h) = (self.t.width(), self.t.height());
+ for &side in &sides {
+ let inner = match side {
+ Side::North | Side::South => {
+ let x = (self.wall.cx + self.wall.rx * self.rng.random_range(-0.45..0.45))
+ .round() as i32;
+ let (y, dy) = if side == Side::North { (0, 1) } else { (h - 1, -1) };
+ let lanes: Vec<Pos> = (x - 2..=x + 1).map(|x| Pos::new(x, y)).collect();
+ self.gate(&lanes, (0, dy))
+ }
+ _ => {
+ let y = (self.wall.cy + self.wall.ry * self.rng.random_range(-0.35..0.35))
+ .round() as i32;
+ let lanes: Vec<Pos> = (y - 1..=y + 1).map(|y| Pos::new(w - 1, y)).collect();
+ self.gate(&lanes, (-1, 0))
+ }
+ };
+ self.t.gates.push(inner);
+ }
+ let mut all = vec![Side::West];
+ all.extend(sides);
+ all
+ }
+
+ fn market_square(&mut self) {
+ let size = self.size;
+ let w = lerp(20.0, 36.0, size) as i32 + self.rng.random_range(0..5);
+ let h = lerp(7.0, 12.0, size) as i32 + self.rng.random_range(0..2);
+ let x = (self.wall.cx + self.wall.rx * self.rng.random_range(-0.12..0.12)).round() as i32;
+ let top = self.main - h / 2 + self.rng.random_range(-1..=1);
+ let r = Rect::new(x - w / 2, top, x - w / 2 + w - 1, top + h - 1);
+ for p in r.cells() {
+ if matches!(self.tile(p), Tile::Garden | Tile::Street) {
+ self.set(p, Tile::Square);
+ }
+ }
+ self.square = r;
+ self.t.square = Pos::new(x, self.main);
+ for dx in -1..=1 {
+ self.set(Pos::new(x + dx, self.main), Tile::Centrepiece);
+ }
+ }
+
+ /// Paves an L from `a` to `b`, `cols` wide where it runs north–south
+ /// and `rows` tall where it runs east–west.
+ fn road(&mut self, a: Pos, b: Pos, (cols, rows): (i32, i32), down_first: bool) {
+ let vertical = |x: i32, y0: i32, y1: i32| {
+ Rect::new(x - cols / 2, y0.min(y1), x - cols / 2 + cols - 1, y0.max(y1))
+ };
+ let across = |y: i32, x0: i32, x1: i32| {
+ Rect::new(x0.min(x1), y - rows / 2, x0.max(x1), y - rows / 2 + rows - 1)
+ };
+ if down_first {
+ self.carve(vertical(a.x, a.y, b.y));
+ self.carve(across(b.y, a.x, b.x));
+ } else {
+ self.carve(across(a.y, a.x, b.x));
+ self.carve(vertical(b.x, a.y, b.y));
+ }
+ }
+
+ fn avenues(&mut self, sides: &[Side]) {
+ let sq = self.square;
+ for (i, &side) in sides.iter().enumerate() {
+ let g = self.t.gates[i];
+ match side {
+ Side::West => self.road(g, Pos::new(sq.left + 1, self.main), (4, 3), false),
+ Side::North | Side::South => {
+ let y = if side == Side::North { sq.top + 1 } else { sq.bottom - 1 };
+ let to = Pos::new(g.x.clamp(sq.left + 2, sq.right - 2), y);
+ self.road(g, to, (4, 3), true);
+ }
+ Side::East => {
+ let to = Pos::new(sq.right - 2, g.y.clamp(sq.top + 1, sq.bottom - 1));
+ self.road(g, to, (4, 3), false);
+ }
+ }
+ }
+ }
+
+ fn inside_bounds(&self) -> Rect {
+ let inside: Vec<Pos> =
+ self.t.site.positions().filter(|&p| self.tile(p) != Tile::Ground).collect();
+ let xs = inside.iter().filter(|p| p.x > PAD_RIGHT + 1).map(|p| p.x);
+ let ys = inside.iter().map(|p| p.y);
+ Rect::new(
+ xs.clone().min().unwrap_or(0),
+ ys.clone().min().unwrap_or(0),
+ xs.max().unwrap_or(0),
+ ys.max().unwrap_or(0),
+ )
+ }
+
+ fn nearest_quarter(&self, p: Pos) -> usize {
+ (0..self.points.len()).min_by_key(|&i| distance2(self.points[i].1, p)).unwrap_or(0)
+ }
+
+ fn quarter_kind(&self, p: Pos) -> QuarterKind {
+ self.points[self.nearest_quarter(p)].0
+ }
+
+ fn quarters(&mut self) {
+ use QuarterKind::*;
+ let count = 4 + (self.size * 4.0).round() as usize;
+ let gate = self.t.gates[0];
+ self.points.push((Market, self.t.square));
+ self.points.push((Gate, gate.offset((self.wall.rx * 0.3) as i32, 0)));
+ let mut rest = vec![Homes, Works, Poor, Gardens];
+ rest.shuffle(self.rng);
+ let kinds = [Grand, Holy].into_iter().chain(rest).chain([Homes, Poor, Homes, Works]);
+ let inside: Vec<Pos> =
+ self.t.site.positions().filter(|&p| self.tile(p) == Tile::Garden).collect();
+ for kind in kinds.take(count - 2) {
+ // Of a dozen places, the one furthest from the others.
+ let tries: Vec<Pos> =
+ (0..12).filter_map(|_| inside.choose(self.rng).copied()).collect();
+ let far = tries.into_iter().max_by_key(|&p| {
+ self.points.iter().map(|&(_, q)| distance2(p, q)).min().unwrap_or(0)
+ });
+ if let Some(p) = far {
+ self.points.push((kind, p));
+ }
+ }
+ let mut sums = vec![(0i64, 0i64, 0i64); self.points.len()];
+ for p in self.t.site.positions().collect::<Vec<_>>() {
+ if self.tile(p) != Tile::Ground && p.x > PAD_RIGHT {
+ let q = self.nearest_quarter(p);
+ let i = self.t.site.index(p);
+ self.t.quarter_of[i] = q as u8;
+ sums[q] = (sums[q].0 + i64::from(p.x), sums[q].1 + i64::from(p.y), sums[q].2 + 1);
+ }
+ }
+ let culture = self.t.culture;
+ for (i, &(kind, at)) in self.points.iter().enumerate() {
+ let (sx, sy, n) = sums[i];
+ let centre = if n > 0 { Pos::new((sx / n) as i32, (sy / n) as i32) } else { at };
+ let mut name = words::quarter_name(culture, kind, self.rng);
+ for _ in 0..10 {
+ if !self.t.quarters.iter().any(|q| q.name == name) {
+ break;
+ }
+ name = words::quarter_name(culture, kind, self.rng);
+ }
+ self.t.quarters.push(Quarter { kind, name, centre });
+ }
+ }
+
+ /// Splits `r` with a street, and its halves, and theirs, until each is
+ /// small enough to be a block in its quarter.
+ fn split(&mut self, r: Rect, depth: u32) {
+ let kind = self.quarter_kind(r.centre());
+ let (max_w, max_h) = block_size(kind);
+ let (w, h) = (r.width(), r.height());
+ let (wide, tall) = (w > max_w, h > max_h);
+ let nearly = w <= max_w * 3 / 2 && h <= max_h * 3 / 2;
+ if !wide && !tall || w < 8 || h < 5 || depth > 2 && nearly && self.rng.random_bool(0.15) {
+ self.blocks.push((r, false));
+ return;
+ }
+ let upright = if wide && tall {
+ f64::from(w) > 2.0 * f64::from(h) * self.rng.random_range(0.8..1.25)
+ } else {
+ wide
+ };
+ let (cols, rows) = street_width(kind, depth, self.rng);
+ if upright {
+ let lo = r.left + w * 3 / 10;
+ let hi = (r.right - w * 3 / 10 - cols + 1).max(lo);
+ let x = self.rng.random_range(lo..=hi);
+ self.carve(Rect::new(x, r.top, x + cols - 1, r.bottom));
+ self.split(Rect::new(r.left, r.top, x - 1, r.bottom), depth + 1);
+ self.split(Rect::new(x + cols, r.top, r.right, r.bottom), depth + 1);
+ } else {
+ let lo = r.top + h * 3 / 10;
+ let hi = (r.bottom - h * 3 / 10 - rows + 1).max(lo);
+ let y = self.rng.random_range(lo..=hi);
+ self.carve(Rect::new(r.left, y, r.right, y + rows - 1));
+ self.split(Rect::new(r.left, r.top, r.right, y - 1), depth + 1);
+ self.split(Rect::new(r.left, y + rows, r.right, r.bottom), depth + 1);
+ }
+ }
+
+ /// Whether every cell of `r` is open ground: nothing built, no street
+ /// through it, all inside the wall, and not set aside for a landmark.
+ fn whole(&self, r: Rect) -> bool {
+ r.cells().all(|p| self.tile(p) == Tile::Garden && !self.reserved(p))
+ }
+
+ /// Whether `p` is in the grounds of a landmark.
+ fn reserved(&self, p: Pos) -> bool {
+ self.grounds.iter().any(|r| r.contains(p))
+ }
+
+ fn add_building(&mut self, kind: Kind, foot: Rect, door: Pos, facing: Side) -> usize {
+ let b = self.t.buildings.len();
+ for p in foot.cells() {
+ self.set(p, Tile::Roof);
+ let i = self.t.site.index(p);
+ self.t.owner[i] = b as u16;
+ }
+ self.set(door, Tile::Door { open: true });
+ let name = String::new();
+ self.t.buildings.push(Building {
+ kind,
+ left: foot.left,
+ top: foot.top,
+ right: foot.right,
+ bottom: foot.bottom,
+ door,
+ facing,
+ barred: false,
+ shop: None,
+ name,
+ });
+ b
+ }
+
+ /// Paves from `from` in direction `step` until it meets a street.
+ fn pave_to_street(&mut self, from: Pos, (dx, dy): (i32, i32)) {
+ let mut p = from;
+ for _ in 0..12 {
+ if self.tile(p) != Tile::Garden {
+ return;
+ }
+ self.set(p, Tile::Square);
+ p = p.offset(dx, dy);
+ }
+ }
+
+ /// Sets aside grounds for a great building before the streets are cut,
+ /// as near the middle of its quarter as they'll fit, with a street all
+ /// round them and a paved way from the door to it. Shrinks the building
+ /// if it won't fit anywhere.
+ fn landmark(&mut self, kind: Kind, home: QuarterKind, (mut w, mut h): (i32, i32)) {
+ let target =
+ self.points.iter().find(|(k, _)| *k == home).map_or(self.t.square, |&(_, p)| p);
+ let mut open: Vec<Pos> =
+ self.t.site.positions().filter(|&p| self.tile(p) == Tile::Garden).collect();
+ open.sort_by_key(|&p| distance2(p, target));
+ let around = |r: Rect| Rect::new(r.left - 2, r.top - 1, r.right + 2, r.bottom + 1);
+ loop {
+ let (gw, gh) = (w + 6, h + 2);
+ let at = |c: Pos| {
+ let (left, top) = (c.x - gw / 2, c.y - gh / 2);
+ Rect::new(left, top, left + gw - 1, top + gh - 1)
+ };
+ let found = open.iter().map(|&c| at(c)).find(|&grounds| {
+ self.whole(grounds)
+ && around(grounds).cells().all(|p| {
+ grounds.contains(p)
+ || !self.reserved(p)
+ && matches!(
+ self.tile(p),
+ Tile::Garden | Tile::Street | Tile::Square
+ )
+ })
+ });
+ if let Some(grounds) = found {
+ self.grounds.push(grounds);
+ self.carve(around(grounds));
+ let (left, top) = (grounds.left + 3, grounds.top + 1);
+ let foot = Rect::new(left, top, left + w - 1, top + h - 1);
+ // The door faces the square, or at least its side of the city.
+ let facing = if grounds.centre().y < self.main { Side::South } else { Side::North };
+ let x = foot.centre().x;
+ let door = Pos::new(x, if facing == Side::South { foot.bottom } else { foot.top });
+ let step = facing.step();
+ for dx in -1..=1 {
+ self.pave_to_street(door.offset(dx + step.0, step.1), step);
+ }
+ let front = door.offset(0, step.1);
+ for dx in [-3, 3] {
+ if self.tile(front.offset(dx, 0)) == Tile::Garden {
+ self.set(front.offset(dx, 0), Tile::Statue);
+ }
+ }
+ self.add_building(kind, foot, door, facing);
+ return;
+ }
+ if w <= 8 {
+ return;
+ }
+ w -= 2;
+ h = (h - 1).max(3);
+ }
+ }
+
+ fn landmarks(&mut self) {
+ let size = self.size;
+ let palace = (lerp(14.0, 22.0, size) as i32, lerp(5.0, 8.0, size) as i32);
+ let temple = (lerp(10.0, 16.0, size) as i32, lerp(4.0, 6.0, size) as i32);
+ self.landmark(Kind::Palace, QuarterKind::Grand, palace);
+ self.landmark(Kind::Temple, QuarterKind::Holy, temple);
+ }
+
+ /// The paved cells you can walk to from `from` without leaving the
+ /// streets.
+ fn paved_from(&self, from: Pos) -> Vec<bool> {
+ let site = &self.t.site;
+ let paved =
+ |p: Pos| site.contains(p) && self.tile(p).outdoors() && self.tile(p) != Tile::Garden;
+ let mut seen = vec![false; (site.width * site.height) as usize];
+ if !paved(from) {
+ return seen;
+ }
+ let mut queue = std::collections::VecDeque::from([from]);
+ seen[site.index(from)] = true;
+ while let Some(p) = queue.pop_front() {
+ for n in p.neighbours() {
+ if paved(n) && !seen[site.index(n)] {
+ seen[site.index(n)] = true;
+ queue.push_back(n);
+ }
+ }
+ }
+ seen
+ }
+
+ /// Makes sure you can walk by street from the west gate to `to`, paving
+ /// a road between the nearest two streets if not.
+ fn link(&mut self, to: Pos) {
+ let reached = self.paved_from(self.t.gates[0]);
+ let site = &self.t.site;
+ if reached[site.index(to)] {
+ return;
+ }
+ let cut_off = self.paved_from(to);
+ let ours: Vec<Pos> = site.positions().filter(|&p| reached[site.index(p)]).collect();
+ let theirs = site.positions().filter(|&p| cut_off[site.index(p)]);
+ let pair = theirs
+ .flat_map(|a| ours.iter().map(move |&b| (a, b)))
+ .min_by_key(|&(a, b)| distance2(a, b));
+ if let Some((a, b)) = pair {
+ let down_first = self.rng.random_bool(0.5);
+ self.road(a, b, (2, 1), down_first);
+ }
+ }
+
+ fn parks_and_plazas(&mut self) {
+ for i in 0..self.blocks.len() {
+ let (r, used) = self.blocks[i];
+ if used || r.width() < 6 || r.height() < 3 || r.cells().any(|p| self.reserved(p)) {
+ continue;
+ }
+ let q = self.quarter_kind(r.centre());
+ let park = match q {
+ QuarterKind::Gardens => 0.55,
+ QuarterKind::Market | QuarterKind::Gate => 0.0,
+ _ => 0.03,
+ };
+ let plaza = match q {
+ QuarterKind::Gardens | QuarterKind::Grand => 0.0,
+ _ if r.area() > 200 => 0.0,
+ _ => 0.03,
+ };
+ if self.rng.random_bool((park * self.lay.parks).min(0.95)) {
+ self.blocks[i].1 = true;
+ self.park(r);
+ } else if self.rng.random_bool(plaza) {
+ self.blocks[i].1 = true;
+ self.plaza(r);
+ }
+ }
+ }
+
+ /// Puts something in the middle of a park or plaza, if the middle is
+ /// open ground inside the walls.
+ fn centrepiece(&mut self, at: Pos, tile: Tile) {
+ if matches!(self.tile(at), Tile::Garden | Tile::Street | Tile::Square) {
+ self.set(at, tile);
+ }
+ }
+
+ /// Paths crossing, a pond if there's room, and trees later.
+ fn park(&mut self, r: Rect) {
+ self.parks.push(r);
+ let c = r.centre();
+ let path = |p: &mut Planner, cell: Pos| {
+ if p.tile(cell) == Tile::Garden {
+ p.set(cell, Tile::Street);
+ }
+ };
+ for x in r.left..=r.right {
+ path(self, Pos::new(x, c.y));
+ }
+ for y in r.top..=r.bottom {
+ path(self, Pos::new(c.x, y));
+ path(self, Pos::new(c.x + 1, y));
+ }
+ let size = self.lay.ponds;
+ if size > 0.0 && r.width() >= 14 && r.height() >= 6 {
+ // In one corner, between the paths.
+ let qx =
+ if self.rng.random_bool(0.5) { (r.left + c.x) / 2 } else { (c.x + r.right) / 2 };
+ let qy =
+ if self.rng.random_bool(0.5) { (r.top + c.y) / 2 } else { (c.y + r.bottom) / 2 };
+ let (ax, ay) = (f64::from(r.width()) / 7.0 * size, f64::from(r.height()) / 5.0 * size);
+ for p in r.cells() {
+ let dx = f64::from(p.x - qx) / ax.max(1.0);
+ let dy = f64::from(p.y - qy) / ay.max(0.8);
+ if dx * dx + dy * dy <= 1.0 && self.tile(p) == Tile::Garden {
+ self.set(p, Tile::Water);
+ }
+ }
+ }
+ let centre = if self.rng.random_bool(0.5) { Tile::Statue } else { Tile::Well };
+ self.centrepiece(c, centre);
+ }
+
+ /// A little square with a well or a statue in the middle.
+ fn plaza(&mut self, r: Rect) {
+ for p in r.cells() {
+ if self.tile(p) == Tile::Garden {
+ self.set(p, Tile::Square);
+ }
+ }
+ let wells = matches!(self.t.culture, Culture::Qaari | Culture::Drift);
+ let centre = if wells != self.rng.random_bool(0.25) { Tile::Well } else { Tile::Statue };
+ self.centrepiece(r.centre(), centre);
+ }
+
+ /// Packs buildings along the streets round a block, the long sides
+ /// first, then the ends.
+ fn pack(&mut self, r: Rect) {
+ let lots = lots(self.quarter_kind(r.centre()));
+ let mut long = [Side::North, Side::South];
+ let mut ends = [Side::West, Side::East];
+ long.shuffle(self.rng);
+ ends.shuffle(self.rng);
+ for side in long.into_iter().chain(ends) {
+ self.pack_side(r, side, &lots);
+ }
+ }
+
+ fn pack_side(&mut self, r: Rect, side: Side, lots: &Lots) {
+ let upright = matches!(side, Side::North | Side::South);
+ let (len, cap) = if upright {
+ (r.width(), (r.height() + 1) / 2)
+ } else {
+ (r.height(), (r.width() + 1) / 2)
+ };
+ let (along, deep) = if upright { (lots.wide, lots.deep) } else { (lots.tall, lots.long) };
+ let min_deep = if upright { 2 } else { 3 };
+ let mut u = 0;
+ while u < len {
+ let want = self.rng.random_range(along.0..=along.1);
+ let depth = self.rng.random_range(deep.0..=deep.1).min(cap);
+ if depth < min_deep {
+ return;
+ }
+ let mut placed = None;
+ for n in (along.0.min(want)..=want).rev() {
+ let foot = match side {
+ Side::North => {
+ Rect::new(r.left + u, r.top, r.left + u + n - 1, r.top + depth - 1)
+ }
+ Side::South => {
+ Rect::new(r.left + u, r.bottom - depth + 1, r.left + u + n - 1, r.bottom)
+ }
+ Side::West => {
+ Rect::new(r.left, r.top + u, r.left + depth - 1, r.top + u + n - 1)
+ }
+ Side::East => {
+ Rect::new(r.right - depth + 1, r.top + u, r.right, r.top + u + n - 1)
+ }
+ };
+ let inside = foot.right <= r.right && foot.bottom <= r.bottom;
+ if inside
+ && self.whole(foot)
+ && let Some(door) = self.door_for(foot, side)
+ {
+ self.add_building(Kind::House, foot, door, side);
+ placed = Some(n);
+ break;
+ }
+ }
+ match placed {
+ Some(n) => {
+ let gap = if self.rng.random_bool(lots.gap) {
+ if self.rng.random_bool(0.25) { 2 } else { 1 }
+ } else {
+ 0
+ };
+ u += n + gap;
+ }
+ None => u += 1,
+ }
+ }
+ }
+
+ /// A door on the `side` edge of a footprint, away from the corners,
+ /// that opens onto a street.
+ fn door_for(&mut self, foot: Rect, side: Side) -> Option<Pos> {
+ let (dx, dy) = side.step();
+ let doors: Vec<Pos> = match side {
+ Side::North | Side::South => {
+ let y = if side == Side::North { foot.top } else { foot.bottom };
+ (foot.left + 1..foot.right).map(|x| Pos::new(x, y)).collect()
+ }
+ Side::West | Side::East => {
+ let x = if side == Side::West { foot.left } else { foot.right };
+ (foot.top + 1..foot.bottom).map(|y| Pos::new(x, y)).collect()
+ }
+ };
+ let good: Vec<Pos> =
+ doors.into_iter().filter(|d| self.tile(d.offset(dx, dy)).paved()).collect();
+ good.choose(self.rng).copied()
+ }
+
+ /// Decides what each ordinary building is, by its quarter and size, and
+ /// names it.
+ fn uses(&mut self) {
+ let culture = self.t.culture;
+ for b in 0..self.t.buildings.len() {
+ let building = &self.t.buildings[b];
+ let (w, h) = (building.width(), building.height());
+ let mut kind = building.kind;
+ if kind == Kind::House {
+ let q = self.quarter_kind(building.centre());
+ let options = uses(q, culture);
+ kind = options
+ .choose_weighted(self.rng, |(_, weight)| *weight)
+ .map_or(Kind::House, |(k, _)| *k);
+ if !fits(kind, w, h) {
+ kind = Kind::House;
+ }
+ }
+ let barred = self.rng.random_bool(barred_chance(kind));
+ let name = words::building_name(kind, culture, self.rng);
+ let building = &mut self.t.buildings[b];
+ building.kind = kind;
+ building.barred = barred;
+ building.name = name;
+ let door = building.door;
+ self.set(door, Tile::Door { open: !barred });
+ }
+ }
+
+ fn make(&mut self, b: usize, kind: Kind) {
+ let culture = self.t.culture;
+ let name = words::building_name(kind, culture, self.rng);
+ let building = &mut self.t.buildings[b];
+ building.kind = kind;
+ building.barred = false;
+ building.name = name;
+ let door = building.door;
+ self.set(door, Tile::Door { open: true });
+ }
+
+ /// Picks one of the ordinary buildings nearest to `near`, and wide
+ /// enough for a sign `sign` long.
+ /// An ordinary building near `near` with room for the sign over its
+ /// door, and not just down the street from another shop of the same
+ /// kind.
+ fn site_near(&mut self, near: Pos, kind: ShopKind) -> Option<usize> {
+ let need = kind.sign().len() as i32 + 2;
+ let rivals: Vec<Pos> = self
+ .t
+ .shops
+ .iter()
+ .filter(|s| s.kind == kind)
+ .map(|s| self.t.buildings[s.building].door)
+ .collect();
+ let mut options: Vec<usize> = (0..self.t.buildings.len())
+ .filter(|&b| {
+ let building = &self.t.buildings[b];
+ let ordinary = matches!(
+ building.kind,
+ Kind::House | Kind::Workshop | Kind::Warehouse | Kind::Tenement | Kind::Shrine
+ );
+ ordinary
+ && building.width() >= need
+ && building.height() >= 3
+ && rivals.iter().all(|&r| distance2(r, building.door) > 24 * 24)
+ })
+ .collect();
+ options.sort_by_key(|&b| distance2(self.t.buildings[b].door, near));
+ options.truncate(4);
+ options.choose(self.rng).copied()
+ }
+
+ fn shops(&mut self) {
+ let culture = self.t.culture;
+ let anchor = |p: &Planner, kinds: &[QuarterKind]| {
+ let quarters: Vec<&Quarter> =
+ p.t.quarters.iter().filter(|q| kinds.contains(&q.kind)).collect();
+ quarters.first().map(|q| q.centre)
+ };
+ let gate = self.t.gates[0].offset(8, 0);
+ let square = self.t.square;
+ let temple = self.t.buildings.iter().find(|b| b.kind == Kind::Temple).map(|b| b.front());
+ let scholar = anchor(self, &[QuarterKind::Holy]).or(temple).unwrap_or(square);
+ let homes = anchor(self, &[QuarterKind::Homes, QuarterKind::Market]).unwrap_or(square);
+ let works = anchor(self, &[QuarterKind::Works, QuarterKind::Poor]).unwrap_or(gate);
+ let mut wanted = vec![
+ (ShopKind::Broker, gate),
+ (ShopKind::Shipyard, gate),
+ (ShopKind::Tavern, square),
+ (ShopKind::Doctor, homes),
+ (ShopKind::Scholar, scholar),
+ (ShopKind::Armoury, works),
+ ];
+ let mut quiet: Vec<Pos> = self
+ .t
+ .quarters
+ .iter()
+ .filter(|q| {
+ matches!(q.kind, QuarterKind::Poor | QuarterKind::Homes | QuarterKind::Works)
+ })
+ .map(|q| q.centre)
+ .collect();
+ // More taverns in bigger cities, each in a different quarter while
+ // there are quarters to go round.
+ quiet.shuffle(self.rng);
+ let extra = (self.size * 3.0).round() as usize;
+ wanted.extend(quiet.iter().cycle().take(extra).map(|&at| (ShopKind::Tavern, at)));
+ for (kind, near) in wanted {
+ let Some(b) = self.site_near(near, kind) else { continue };
+ let mut name = words::shop_name(kind, culture, self.rng);
+ for _ in 0..10 {
+ if !self.t.shops.iter().any(|s| s.name == name) {
+ break;
+ }
+ name = words::shop_name(kind, culture, self.rng);
+ }
+ let shop = self.t.shops.len();
+ self.t.shops.push(Shop {
+ kind,
+ name: name.clone(),
+ keeper: culture.person(self.rng),
+ building: b,
+ });
+ self.make(b, Kind::Shop(kind));
+ self.t.buildings[b].shop = Some(shop);
+ self.t.buildings[b].name = name;
+ }
+ }
+
+ /// Each gate has a guardhouse just inside it.
+ fn guardhouses(&mut self) {
+ for g in self.t.gates.clone() {
+ let near = (0..self.t.buildings.len())
+ .filter(|&b| {
+ let kind = self.t.buildings[b].kind;
+ !matches!(kind, Kind::Shop(_) | Kind::Palace | Kind::Temple | Kind::Guardhouse)
+ })
+ .min_by_key(|&b| distance2(self.t.buildings[b].door, g));
+ if let Some(b) = near.filter(|&b| distance2(self.t.buildings[b].door, g) < 40 * 40) {
+ self.make(b, Kind::Guardhouse);
+ }
+ }
+ }
+
+ /// Floods the ground outside the walls, but for a shore round them and
+ /// the landing field, and the road between. The roads to the other
+ /// gates run on out to sea.
+ fn sea(&mut self) {
+ let site = &self.t.site;
+ let (main, cx) = (self.main, self.wall.cx);
+ let shore = |p: Pos| {
+ let t = site.tile(p);
+ let west_road = t == Tile::Street && (p.y - main).abs() <= 1 && f64::from(p.x) < cx;
+ !matches!(t, Tile::Ground | Tile::Street) || west_road
+ };
+ // How far each bit of ground is from the shore, a step at a time.
+ let mut far = vec![i32::MAX; site.tiles.len()];
+ let mut queue = std::collections::VecDeque::new();
+ for p in site.positions() {
+ if shore(p) {
+ far[site.index(p)] = 0;
+ queue.push_back(p);
+ }
+ }
+ while let Some(p) = queue.pop_front() {
+ let d = far[site.index(p)];
+ for n in p.neighbours() {
+ if site.contains(n) && site.tile(n) == Tile::Ground && far[site.index(n)] > d + 1 {
+ far[site.index(n)] = d + 1;
+ queue.push_back(n);
+ }
+ }
+ }
+ // A beach two or three wide, wider and narrower along the coast.
+ let (a, b) = (self.rng.random_range(0.0..6.3), self.rng.random_range(0.0..6.3));
+ let flood: Vec<Pos> = site
+ .positions()
+ .filter(|&p| {
+ let (x, y) = (f64::from(p.x), f64::from(p.y));
+ let beach = 2.2 + 1.3 * (x * 0.23 + a).sin() * (y * 0.41 + b).sin();
+ site.tile(p) == Tile::Ground && f64::from(far[site.index(p)]) > beach
+ })
+ .collect();
+ for p in flood {
+ self.set(p, Tile::Sea);
+ }
+ }
+
+ /// Trees, stalls and lamps.
+ fn dress(&mut self) {
+ let all: Vec<Pos> = self.t.site.positions().collect();
+ let main = self.main;
+ // The Cant and the Drift leave pipes and scrap about; everyone plants.
+ let clutter = matches!(self.t.culture, Culture::Cant | Culture::Drift);
+ for &p in &all {
+ let tile = self.tile(p);
+ let roadside =
+ p.neighbours().any(|n| self.tile(n).paved() || self.tile(n) == Tile::Pad);
+ if tile == Tile::Ground && !roadside && (p.y - main).abs() > 1 {
+ if self.rng.random_bool(self.lay.wild) {
+ self.set(p, Tile::Wild);
+ }
+ } else if tile == Tile::Garden && !roadside {
+ let park = self.parks.iter().any(|r| r.contains(p));
+ let grounds = p.neighbours().any(|n| {
+ self.t.building_at(n).is_some_and(|b| {
+ matches!(self.t.buildings[b].kind, Kind::Palace | Kind::Temple)
+ })
+ });
+ let chance = if park {
+ 0.16
+ } else if grounds {
+ 0.3
+ } else {
+ 0.07
+ };
+ if self.rng.random_bool((chance * self.lay.trees).min(0.6)) {
+ let planted = !clutter || park || grounds || self.rng.random_bool(0.4);
+ self.set(p, if planted { Tile::Tree } else { Tile::Decor });
+ }
+ }
+ }
+ // Stalls along the top and bottom of the square, facing in, keeping
+ // clear of the streets that come into it.
+ let sq = self.square;
+ for (y, front) in [(sq.top + 1, 1), (sq.bottom - 1, -1)] {
+ let edge = if front == 1 { sq.top - 1 } else { sq.bottom + 1 };
+ let mut x = sq.left + 2;
+ while x + 2 <= sq.right - 2 {
+ let clear = (x - 1..=x + 3).all(|x| !self.tile(Pos::new(x, edge)).paved())
+ && (x..=x + 2).all(|x| self.tile(Pos::new(x, y)) == Tile::Square)
+ && (y - main).abs() > 1;
+ if clear && self.rng.random_bool(0.7) {
+ for dx in 0..3 {
+ self.set(Pos::new(x + dx, y), Tile::Stall);
+ }
+ self.stalls.push(Pos::new(x + 1, y + front));
+ x += 6;
+ } else {
+ x += 2;
+ }
+ }
+ }
+ for p in [
+ Pos::new(sq.left, sq.top),
+ Pos::new(sq.right, sq.top),
+ Pos::new(sq.left, sq.bottom),
+ Pos::new(sq.right, sq.bottom),
+ ] {
+ let doorstep = p.neighbours().any(|n| matches!(self.tile(n), Tile::Door { .. }));
+ if self.tile(p) == Tile::Square && !doorstep {
+ self.set(p, Tile::Lamp);
+ }
+ }
+ }
+
+ fn person(&mut self, pos: Pos, role: Role, stays: bool) {
+ let culture = self.t.culture;
+ let says = *words::lines(role, culture).choose(self.rng).unwrap_or(&"...");
+ self.t.site.people.push(Person { pos, role, name: None, says, stays });
+ }
+
+ fn people(&mut self) {
+ for at in self.stalls.clone() {
+ if self.t.site.free(at) {
+ self.person(at, Role::Stallholder, true);
+ }
+ }
+ // Guards just inside each gate, against the wall.
+ for g in self.t.gates.clone() {
+ let mut spots: Vec<Pos> = Rect::new(g.x - 3, g.y - 3, g.x + 3, g.y + 3)
+ .cells()
+ .filter(|&p| {
+ self.tile(p).outdoors()
+ && self.tile(p) != Tile::Gate
+ && self.t.site.free(p)
+ && p.neighbours().any(|n| self.tile(n) == Tile::CityWall)
+ && p.neighbours().all(|n| self.tile(n) != Tile::Gate)
+ })
+ .collect();
+ spots.shuffle(self.rng);
+ for p in spots.into_iter().take(2) {
+ self.person(p, Role::Guard, true);
+ }
+ }
+ let streets: Vec<Pos> = self.t.site.positions().filter(|&p| self.tile(p).paved()).collect();
+ let count = (streets.len() / 55).clamp(16, 180);
+ let crowd = (self.square.area() / 30) as usize;
+ let square: Vec<Pos> =
+ streets.iter().copied().filter(|&p| self.tile(p) == Tile::Square).collect();
+ let mut placed = 0;
+ let mut tries = 0;
+ while placed < count + crowd && tries < 20 * (count + crowd) {
+ tries += 1;
+ let pool = if placed < crowd { &square } else { &streets };
+ let Some(&p) = pool.choose(self.rng) else { break };
+ if !self.t.site.free(p) {
+ continue;
+ }
+ let q = self.quarter_kind(p);
+ let roll = self.rng.random_range(0.0..1.0);
+ let role = match q {
+ QuarterKind::Homes | QuarterKind::Poor if roll < 0.2 => Role::Child,
+ QuarterKind::Works | QuarterKind::Gate if roll < 0.25 => Role::Porter,
+ QuarterKind::Holy if roll < 0.3 => Role::Pilgrim,
+ _ => Role::Passerby,
+ };
+ self.person(p, role, false);
+ placed += 1;
+ }
+ }
+}