summaryrefslogtreecommitdiff
path: root/src/town
diff options
context:
space:
mode:
Diffstat (limited to 'src/town')
-rw-r--r--src/town/interior.rs1112
-rw-r--r--src/town/mod.rs850
-rw-r--r--src/town/plan.rs1230
-rw-r--r--src/town/words.rs633
4 files changed, 3825 insertions, 0 deletions
diff --git a/src/town/interior.rs b/src/town/interior.rs
new file mode 100644
index 0000000..4e09333
--- /dev/null
+++ b/src/town/interior.rs
@@ -0,0 +1,1112 @@
+//! Inside a building.
+//!
+//! Inside is drawn at a much closer scale than the street: a house three
+//! rows deep outside is eleven rows deep inside. You come in by a door on
+//! the same side as the one you bumped outside, at the same place along
+//! the wall.
+//!
+//! Rooms come from splitting the floor with walls, each wall with a
+//! doorway, so every room can be reached. Most buildings have a front room
+//! as wide as the building with smaller rooms behind it: a shop's counter,
+//! a tavern's taproom, a temple's nave, a palace's throne room. Houses are
+//! split any which way. Each room is furnished for what it's for, with a
+//! check after every piece that nothing has been cut off, and then the
+//! people go in.
+//!
+//! "Back" is the side away from the door, wherever that is. Rooms are
+//! furnished in a frame turned to match: `u` runs along the back wall and
+//! `v` away from it, so a counter or an altar always stands at the back.
+
+use rand::RngExt;
+use rand::seq::{IndexedRandom, SliceRandom};
+
+use super::words;
+use super::*;
+use crate::seed::Rng;
+
+/// Furniture.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Thing {
+ Table,
+ Chair,
+ Bed,
+ Hearth,
+ Stove,
+ Shelf,
+ Chest,
+ Crate,
+ Barrel,
+ Altar,
+ Pew,
+ Pillar,
+ Statue,
+ Throne,
+ Plant,
+ /// Hot water, in a bathhouse.
+ Bath,
+ Anvil,
+ Forge,
+ Loom,
+ Workbench,
+ /// Something to sit on, in a changing room or a steam room.
+ Bench,
+ /// Weapons, in a guardroom.
+ Rack,
+ Lamp,
+ /// Pieces of ship, in a shipyard.
+ Engine,
+}
+
+impl Thing {
+ /// Seats can be walked over; people sit on them.
+ pub fn walkable(self) -> bool {
+ matches!(self, Thing::Chair | Thing::Pew | Thing::Bench | Thing::Throne)
+ }
+}
+
+/// What a room is for.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Purpose {
+ Living,
+ Bedroom,
+ Kitchen,
+ Store,
+ /// A room of one's own, in a tenement.
+ Lodging,
+ Taproom,
+ Parlour,
+ Cellar,
+ /// A shop's front room.
+ Counter,
+ Ward,
+ Stacks,
+ Assay,
+ Yard,
+ /// Where an armourer makes and mends.
+ Smithy,
+ Hall,
+ Dining,
+ Library,
+ Bedchamber,
+ Gallery,
+ Throne,
+ Council,
+ Treasury,
+ Nave,
+ Vestry,
+ Cell,
+ Shrine,
+ Workroom,
+ Storehouse,
+ Office,
+ Changing,
+ Baths,
+ Steam,
+ Guardroom,
+ Bunks,
+}
+
+struct Room {
+ rect: Rect,
+ purpose: Purpose,
+}
+
+pub struct Interior {
+ pub building: usize,
+ pub culture: Culture,
+ pub kind: Kind,
+ pub site: Site,
+ rooms: Vec<Room>,
+ /// Which room each cell is in, `NO_ROOM` for walls and doorways.
+ room_of: Vec<u8>,
+ /// Where you stand when you come in, just inside the door.
+ pub start: Pos,
+ pub exit: Pos,
+ /// The shop whose counter this is.
+ pub shop: Option<usize>,
+}
+
+const NO_ROOM: u8 = u8::MAX;
+
+impl Interior {
+ pub fn purpose_at(&self, p: Pos) -> Option<Purpose> {
+ if !self.site.contains(p) {
+ return None;
+ }
+ self.rooms.get(usize::from(self.room_of[self.site.index(p)])).map(|r| r.purpose)
+ }
+
+ pub fn room_count(&self) -> usize {
+ self.rooms.len()
+ }
+
+ /// The shop, if `p` is its counter or its keeper.
+ pub fn shop_at(&self, p: Pos) -> Option<usize> {
+ let shop = self.shop?;
+ let keeper = self.site.person_at(p).map(|i| self.site.people[i].role);
+ (self.site.tile(p) == Tile::Counter || keeper == Some(Role::Keeper(shop))).then_some(shop)
+ }
+}
+
+/// A room, turned so its back wall is at `v = -1`.
+#[derive(Clone, Copy)]
+struct Frame {
+ r: Rect,
+ back: Side,
+}
+
+impl Frame {
+ fn len_u(self) -> i32 {
+ if matches!(self.back, Side::North | Side::South) {
+ self.r.width()
+ } else {
+ self.r.height()
+ }
+ }
+
+ fn len_v(self) -> i32 {
+ if matches!(self.back, Side::North | Side::South) {
+ self.r.height()
+ } else {
+ self.r.width()
+ }
+ }
+
+ fn at(self, u: i32, v: i32) -> Pos {
+ let r = self.r;
+ match self.back {
+ Side::North => Pos::new(r.left + u, r.top + v),
+ Side::South => Pos::new(r.left + u, r.bottom - v),
+ Side::West => Pos::new(r.left + v, r.top + u),
+ Side::East => Pos::new(r.right - v, r.top + u),
+ }
+ }
+}
+
+enum Layout {
+ /// Split any which way, into this many rooms or so.
+ Split(usize, usize),
+ /// A front room as wide as the building, `share` of its depth, with
+ /// this many rooms side by side behind it.
+ Hall(f64, usize, usize),
+ Single,
+}
+
+/// How a building of this kind is laid out: the plan, what the front room
+/// is for, and what the rooms behind it are for, in order. If there are
+/// more rooms than purposes, the last purpose repeats.
+fn layout(kind: Kind) -> (Layout, Purpose, &'static [Purpose]) {
+ use Purpose::*;
+ match kind {
+ Kind::House => (Layout::Split(1, 4), Living, &[Bedroom, Kitchen, Bedroom, Store, Bedroom]),
+ Kind::Tenement => (Layout::Split(4, 9), Kitchen, &[Lodging]),
+ Kind::Manor => (
+ Layout::Hall(0.45, 3, 6),
+ Hall,
+ &[Dining, Library, Bedchamber, Gallery, Kitchen, Bedchamber, Store],
+ ),
+ Kind::Palace => (
+ Layout::Hall(0.55, 4, 8),
+ Throne,
+ &[Council, Treasury, Library, Bedchamber, Dining, Gallery, Kitchen, Guardroom],
+ ),
+ Kind::Temple => (Layout::Hall(0.72, 1, 3), Nave, &[Vestry, Cell, Cell]),
+ Kind::Shrine => (Layout::Single, Shrine, &[]),
+ Kind::Workshop => (Layout::Hall(0.62, 1, 2), Workroom, &[Store, Bedroom]),
+ Kind::Warehouse => (Layout::Hall(0.8, 1, 1), Storehouse, &[Office]),
+ Kind::Bathhouse => (Layout::Hall(0.32, 1, 2), Changing, &[Baths, Steam]),
+ Kind::Guardhouse => (Layout::Split(2, 3), Guardroom, &[Bunks, Store]),
+ Kind::Shop(ShopKind::Broker) => (Layout::Hall(0.55, 1, 2), Counter, &[Assay, Store]),
+ Kind::Shop(ShopKind::Doctor) => (Layout::Hall(0.5, 1, 2), Counter, &[Ward, Store]),
+ Kind::Shop(ShopKind::Scholar) => (Layout::Hall(0.5, 1, 2), Counter, &[Stacks]),
+ Kind::Shop(ShopKind::Tavern) => {
+ (Layout::Hall(0.65, 1, 3), Taproom, &[Parlour, Cellar, Kitchen])
+ }
+ Kind::Shop(ShopKind::Shipyard) => (Layout::Hall(0.45, 1, 2), Counter, &[Yard, Store]),
+ Kind::Shop(ShopKind::Armoury) => (Layout::Hall(0.5, 1, 2), Counter, &[Smithy, Store]),
+ }
+}
+
+struct Builder {
+ culture: Culture,
+ kind: Kind,
+ rng: Rng,
+ site: Site,
+ rooms: Vec<Room>,
+ start: Pos,
+ exit: Pos,
+ /// Which side the back wall is on.
+ back: Side,
+ shop: Option<usize>,
+ keeper: Option<String>,
+ /// How many cells you can walk to from the door, to check nothing gets
+ /// cut off as the furniture goes in.
+ reachable: usize,
+}
+
+pub(super) fn build(t: &Town, b: usize) -> Interior {
+ let building = &t.buildings[b];
+ let kind = building.kind;
+ let mut rng = seed::rng(seed::derive(t.seed, "interior", b as u64));
+ let scale = if matches!(kind, Kind::Palace | Kind::Temple | Kind::Manor) { 3.6 } else { 3.4 };
+ let grow = |n: i32, lo: i32, hi: i32| ((f64::from(n) * scale).round() as i32 + 1).clamp(lo, hi);
+ let shopfront = matches!(kind, Kind::Shop(_));
+ let width = grow(building.width(), if shopfront { 17 } else { 13 }, 77);
+ let height = grow(building.height(), if shopfront { 11 } else { 9 }, 29);
+ let width = width + rng.random_range(0..=2);
+
+ // The door in, at the same place along the wall as outside.
+ let along = |at: i32, lo: i32, hi: i32, len: i32| {
+ let k = if hi > lo { f64::from(at - lo) / f64::from(hi - lo) } else { 0.5 };
+ ((1.0 + k * f64::from(len - 3)).round() as i32).clamp(2, len - 3)
+ };
+ let (bx, by) = (building.door.x, building.door.y);
+ let exit = match building.facing {
+ Side::North => Pos::new(along(bx, building.left, building.right, width), 0),
+ Side::South => Pos::new(along(bx, building.left, building.right, width), height - 1),
+ Side::West => Pos::new(0, along(by, building.top, building.bottom, height)),
+ Side::East => Pos::new(width - 1, along(by, building.top, building.bottom, height)),
+ };
+ let (dx, dy) = building.facing.step();
+ let start = exit.offset(-dx, -dy);
+
+ let mut site = Site::new(width, height, Tile::Wall, Tile::Wall, false);
+ site.fill(Rect::new(1, 1, width - 2, height - 2), Tile::Floor);
+ site.set(exit, Tile::Exit);
+ let shop = building.shop;
+ let mut builder = Builder {
+ culture: t.culture,
+ kind,
+ rng,
+ site,
+ rooms: Vec::new(),
+ start,
+ exit,
+ back: building.facing.opposite(),
+ shop,
+ keeper: shop.map(|s| t.shops[s].keeper.clone()),
+ reachable: 0,
+ };
+ builder.divide();
+ builder.reachable = builder.count_reachable();
+ for room in 0..builder.rooms.len() {
+ builder.furnish(room);
+ }
+ builder.residents();
+
+ let Builder { site, rooms, .. } = builder;
+ let mut room_of = vec![NO_ROOM; (width * height) as usize];
+ for (i, room) in rooms.iter().enumerate() {
+ for p in room.rect.cells() {
+ room_of[site.index(p)] = i as u8;
+ }
+ }
+ Interior { building: b, culture: t.culture, kind, site, rooms, room_of, start, exit, shop }
+}
+
+impl Builder {
+ fn tile(&self, p: Pos) -> Tile {
+ self.site.tile(p)
+ }
+
+ fn is_door(&self, p: Pos) -> bool {
+ matches!(self.tile(p), Tile::Doorway | Tile::Exit)
+ }
+
+ /// Whether a wall across `r` at `at` would block a doorway that opens
+ /// onto the end of it.
+ fn can_wall(&self, r: Rect, upright: bool, at: i32) -> bool {
+ let ends = if upright {
+ [Pos::new(at, r.top - 1), Pos::new(at, r.bottom + 1)]
+ } else {
+ [Pos::new(r.left - 1, at), Pos::new(r.right + 1, at)]
+ };
+ !ends.iter().any(|&p| self.is_door(p))
+ }
+
+ /// Builds a wall across `r`, upright (a column) or not (a row), with a
+ /// doorway in it somewhere in `doors`. Returns the rooms either side.
+ fn wall(
+ &mut self,
+ r: Rect,
+ upright: bool,
+ at: i32,
+ doors: std::ops::RangeInclusive<i32>,
+ ) -> (Rect, Rect) {
+ let cells: Vec<Pos> = if upright {
+ (r.top..=r.bottom).map(|y| Pos::new(at, y)).collect()
+ } else {
+ (r.left..=r.right).map(|x| Pos::new(x, at)).collect()
+ };
+ for &p in &cells {
+ self.site.set(p, Tile::Wall);
+ }
+ let options: Vec<Pos> = cells
+ .iter()
+ .copied()
+ .filter(|p| {
+ let k = if upright { p.y } else { p.x };
+ doors.contains(&k)
+ })
+ .collect();
+ let door = options.choose(&mut self.rng).or(cells.get(cells.len() / 2)).copied();
+ if let Some(door) = door {
+ self.site.set(door, Tile::Doorway);
+ }
+ if upright {
+ (
+ Rect::new(r.left, r.top, at - 1, r.bottom),
+ Rect::new(at + 1, r.top, r.right, r.bottom),
+ )
+ } else {
+ (
+ Rect::new(r.left, r.top, r.right, at - 1),
+ Rect::new(r.left, at + 1, r.right, r.bottom),
+ )
+ }
+ }
+
+ /// Splits the floor into rooms.
+ fn divide(&mut self) {
+ let floor = Rect::new(1, 1, self.site.width - 2, self.site.height - 2);
+ let (plan, front, behind) = layout(self.kind);
+ let mut rects = Vec::new();
+ match plan {
+ Layout::Single => rects.push(floor),
+ Layout::Split(lo, hi) => {
+ let area = floor.area() as usize;
+ let most = (area / 40).max(1);
+ let target = self.rng.random_range(lo..=hi).min(most).max(lo.min(most));
+ self.split(floor, target, &mut rects);
+ }
+ Layout::Hall(share, lo, hi) => self.hall(floor, share, (lo, hi), &mut rects),
+ }
+ // The front room is the one you walk into.
+ let first = rects.iter().position(|r| r.contains(self.start)).unwrap_or(0);
+ let front_rect = rects.remove(first);
+ self.rooms.push(Room { rect: front_rect, purpose: front });
+ for (i, rect) in rects.into_iter().enumerate() {
+ let purpose = behind.get(i).or(behind.last()).copied().unwrap_or(front);
+ self.rooms.push(Room { rect, purpose });
+ }
+ }
+
+ fn split(&mut self, r: Rect, rooms: usize, out: &mut Vec<Rect>) {
+ let (min_w, min_h) = (4, 3);
+ if rooms <= 1 {
+ out.push(r);
+ return;
+ }
+ let wide =
+ f64::from(r.width()) > 2.0 * f64::from(r.height()) * self.rng.random_range(0.7..1.4);
+ for upright in [wide, !wide] {
+ let (lo, hi) = if upright {
+ (r.left + min_w, r.right - min_w)
+ } else {
+ (r.top + min_h, r.bottom - min_h)
+ };
+ let options: Vec<i32> = (lo..=hi).filter(|&at| self.can_wall(r, upright, at)).collect();
+ let Some(&at) = options.choose(&mut self.rng) else { continue };
+ let span = if upright { r.top + 1..=r.bottom - 1 } else { r.left + 1..=r.right - 1 };
+ let (a, b) = self.wall(r, upright, at, span);
+ // Share the rooms out by size.
+ let share = a.area() as f64 / (a.area() + b.area()).max(1) as f64;
+ let first = ((rooms as f64 * share).round() as usize).clamp(1, rooms - 1);
+ self.split(a, first, out);
+ self.split(b, rooms - first, out);
+ return;
+ }
+ out.push(r);
+ }
+
+ fn hall(&mut self, floor: Rect, share: f64, (lo, hi): (usize, usize), out: &mut Vec<Rect>) {
+ let f = Frame { r: floor, back: self.back };
+ let (len_u, len_v) = (f.len_u(), f.len_v());
+ let upright = matches!(self.back, Side::West | Side::East);
+ // The back rooms take `v` from 0 to `k - 1`, then comes the wall,
+ // then the front room, at least four deep for a counter.
+ let least = if upright { 4 } else { 2 };
+ if len_v < least + 5 {
+ out.push(floor);
+ return;
+ }
+ let k = ((f64::from(len_v) * (1.0 - share)).round() as i32).clamp(least, len_v - 5);
+ let at = if upright { f.at(0, k).x } else { f.at(0, k).y };
+ // Temples and palaces keep the middle of the wall for the altar or
+ // throne, so their doorway is off to one side.
+ let off_centre = matches!(self.kind, Kind::Temple | Kind::Palace);
+ let span = if upright {
+ floor.top + 1..=floor.bottom - 1
+ } else {
+ floor.left + 1..=floor.right - 1
+ };
+ let span = if off_centre {
+ let (a, b) = (*span.start(), *span.end());
+ let quarter = ((b - a) / 5).max(1);
+ if self.rng.random_bool(0.5) { a..=a + quarter } else { b - quarter..=b }
+ } else {
+ span
+ };
+ let (one, two) = self.wall(floor, upright, at, span);
+ let (back_rect, front_rect) =
+ if one.contains(f.at(0, 0)) { (one, two) } else { (two, one) };
+ out.push(front_rect);
+ // Rooms side by side behind: split across `u`.
+ let min_u = if upright { 3 } else { 5 };
+ let most = (len_u / (min_u + 1)).max(1) as usize;
+ let n = self.rng.random_range(lo..=hi).min(most);
+ let mut rest = back_rect;
+ for i in (1..n).rev() {
+ // Cut off one room from the far end of `rest`, sized to leave
+ // the others about as big.
+ let (from, to) =
+ if upright { (rest.top, rest.bottom) } else { (rest.left, rest.right) };
+ let size = (to - from + 1) / (i as i32 + 1);
+ let ideal = to - size;
+ let options: Vec<i32> = (ideal - 2..=ideal + 2)
+ .filter(|&a| a - from >= min_u && to - a >= min_u)
+ .filter(|&a| self.can_wall(rest, !upright, a))
+ .collect();
+ let Some(&a) = options.choose(&mut self.rng) else { break };
+ let span = if upright { rest.left..=rest.right } else { rest.top..=rest.bottom };
+ let (near, far) = self.wall(rest, !upright, a, span);
+ out.push(far);
+ rest = near;
+ }
+ out.push(rest);
+ }
+
+ fn count_reachable(&self) -> usize {
+ self.site.reachable(self.start).iter().filter(|&&seen| seen).count()
+ }
+
+ /// Puts a piece of furniture on a bare floor cell, unless it would
+ /// crowd a doorway or cut off part of the building.
+ fn put(&mut self, p: Pos, thing: Thing) -> bool {
+ if self.tile(p) != Tile::Floor || p.neighbours().any(|n| self.is_door(n)) || p == self.start
+ {
+ return false;
+ }
+ self.site.set(p, Tile::Furniture(thing));
+ if thing.walkable() {
+ return true;
+ }
+ let now = self.count_reachable();
+ if now + 1 == self.reachable {
+ self.reachable = now;
+ true
+ } else {
+ self.site.set(p, Tile::Floor);
+ false
+ }
+ }
+
+ fn frame(&self, room: usize) -> Frame {
+ Frame { r: self.rooms[room].rect, back: self.back }
+ }
+
+ fn floor_of(&self, room: usize) -> Vec<Pos> {
+ self.rooms[room].rect.cells().filter(|&p| self.tile(p) == Tile::Floor).collect()
+ }
+
+ /// Bare floor against the walls, in a random order.
+ fn by_wall(&mut self, room: usize) -> Vec<Pos> {
+ let mut cells: Vec<Pos> = self
+ .floor_of(room)
+ .into_iter()
+ .filter(|&p| {
+ [(0, -1), (0, 1), (-1, 0), (1, 0)]
+ .iter()
+ .any(|&(dx, dy)| self.tile(p.offset(dx, dy)) == Tile::Wall)
+ })
+ .collect();
+ cells.shuffle(&mut self.rng);
+ cells
+ }
+
+ /// Bare floor out in the room, away from the walls, in a random order.
+ fn middle(&mut self, room: usize) -> Vec<Pos> {
+ let mut cells: Vec<Pos> = self
+ .floor_of(room)
+ .into_iter()
+ .filter(|&p| p.neighbours().all(|n| self.tile(n) != Tile::Wall))
+ .collect();
+ cells.shuffle(&mut self.rng);
+ cells
+ }
+
+ fn scatter(&mut self, cells: Vec<Pos>, thing: Thing, n: usize) -> usize {
+ let mut placed = 0;
+ for p in cells {
+ if placed == n {
+ break;
+ }
+ if self.put(p, thing) {
+ placed += 1;
+ }
+ }
+ placed
+ }
+
+ fn against(&mut self, room: usize, thing: Thing, lo: usize, hi: usize) {
+ let n = self.rng.random_range(lo..=hi);
+ let cells = self.by_wall(room);
+ self.scatter(cells, thing, n);
+ }
+
+ fn out_in(&mut self, room: usize, thing: Thing, lo: usize, hi: usize) {
+ let n = self.rng.random_range(lo..=hi);
+ let cells = self.middle(room);
+ self.scatter(cells, thing, n);
+ }
+
+ fn rug(&mut self, room: usize, chance: f64) {
+ if !self.rng.random_bool(chance) {
+ return;
+ }
+ let r = self.rooms[room].rect;
+ if r.width() < 6 || r.height() < 4 {
+ return;
+ }
+ let inner = Rect::new(r.left + 2, r.top + 1, r.right - 2, r.bottom - 1);
+ for p in inner.cells() {
+ if self.tile(p) == Tile::Floor {
+ self.site.set(p, Tile::Rug);
+ }
+ }
+ }
+
+ /// A table a few cells long out in the room, chairs round it.
+ fn table(&mut self, room: usize, seats: usize) -> Vec<Pos> {
+ let cells = self.middle(room);
+ for c in cells.into_iter().take(6) {
+ let len = self.rng.random_range(1..=3);
+ if !(0..len).all(|i| self.tile(c.offset(i, 0)) == Tile::Floor) {
+ continue;
+ }
+ let mut placed = 0;
+ for i in 0..len {
+ if self.put(c.offset(i, 0), Thing::Table) {
+ placed += 1;
+ }
+ }
+ if placed == 0 {
+ continue;
+ }
+ let mut around: Vec<Pos> = (0..placed)
+ .flat_map(|i| [c.offset(i, -1), c.offset(i, 1)])
+ .chain([c.offset(-1, 0), c.offset(placed, 0)])
+ .collect();
+ around.shuffle(&mut self.rng);
+ let mut chairs = Vec::new();
+ for p in around.into_iter().take(seats.max(2)) {
+ if self.put(p, Thing::Chair) {
+ chairs.push(p);
+ }
+ }
+ return chairs;
+ }
+ Vec::new()
+ }
+
+ /// A long table down the middle of the room, chairs either side.
+ fn long_table(&mut self, room: usize) -> Vec<Pos> {
+ let r = self.rooms[room].rect;
+ let c = r.centre();
+ let mut chairs = Vec::new();
+ if r.width() < 7 || r.height() < 3 {
+ return chairs;
+ }
+ for x in r.left + 2..=r.right - 2 {
+ if self.put(Pos::new(x, c.y), Thing::Table) {
+ for dy in [-1, 1] {
+ if self.put(Pos::new(x, c.y + dy), Thing::Chair) {
+ chairs.push(Pos::new(x, c.y + dy));
+ }
+ }
+ }
+ }
+ chairs
+ }
+
+ /// Rows of something across the room, every `every` rows back from the
+ /// back wall, leaving aisles at the ends and, if `aisle`, one down the
+ /// middle.
+ fn rows(&mut self, room: usize, thing: Thing, first: i32, every: i32, aisle: bool) -> Vec<Pos> {
+ let f = self.frame(room);
+ let (len_u, len_v) = (f.len_u(), f.len_v());
+ let mid = len_u / 2;
+ let mut placed = Vec::new();
+ let mut v = first;
+ while v < len_v - 2 {
+ for u in 1..len_u - 1 {
+ if aisle && (u - mid).abs() <= 1 {
+ continue;
+ }
+ let p = f.at(u, v);
+ if self.put(p, thing) {
+ placed.push(p);
+ }
+ }
+ v += every;
+ }
+ placed
+ }
+
+ /// A runner from the front of the room to the back, down the middle.
+ fn runner(&mut self, room: usize) {
+ let f = self.frame(room);
+ let mid = f.len_u() / 2;
+ for v in 0..f.len_v() {
+ for u in [mid - 1, mid, mid + 1] {
+ let p = f.at(u, v);
+ if self.tile(p) == Tile::Floor {
+ self.site.set(p, Tile::Rug);
+ }
+ }
+ }
+ }
+
+ /// The keeper's corner at the back of the room: shelves against the
+ /// back wall, a counter in front of them, the keeper between, and the
+ /// counter turned back to the wall at either end so nobody walks round
+ /// it. Keeps clear of doorways in the back wall. Returns where the
+ /// keeper stands.
+ fn nook(&mut self, room: usize, share: f64) -> Option<Pos> {
+ let f = self.frame(room);
+ let (len_u, len_v) = (f.len_u(), f.len_v());
+ if len_v < 4 || len_u < 5 {
+ return None;
+ }
+ // Doorways in the back wall, and the side walls by the back.
+ let blocked: Vec<i32> =
+ (0..len_u).filter(|&u| self.is_door(f.at(u, -1)) || self.is_door(f.at(u, 0))).collect();
+ let side_doors = (0..3).any(|v| self.is_door(f.at(-1, v)))
+ || (0..3).any(|v| self.is_door(f.at(len_u, v)));
+ // The longest run of the back wall with no doorway within a cell.
+ let mut best = (0, -1);
+ let mut start = 0;
+ for u in 0..=len_u {
+ let clear = u < len_u && !blocked.iter().any(|&b| (b - u).abs() <= 1);
+ if !clear {
+ if u - 1 - start > best.1 - best.0 {
+ best = (start, u - 1);
+ }
+ start = u + 1;
+ }
+ }
+ let (lo, hi) = best;
+ // Room for a cap at each end that isn't against a side wall.
+ let lo = if lo == 0 && !side_doors { 0 } else { lo + 1 };
+ let hi = if hi == len_u - 1 && !side_doors { hi } else { hi - 1 };
+ let room_for = hi - lo + 1;
+ if room_for < 3 {
+ return None;
+ }
+ let want = ((f64::from(len_u) * share).round() as i32).clamp(3, room_for);
+ let a = self.rng.random_range(lo..=hi - want + 1);
+ let b = a + want - 1;
+ let mid = (a + b) / 2;
+ for u in a - 1..=b + 1 {
+ if u >= 0 && u < len_u {
+ self.site.set(f.at(u, 1), Tile::Counter);
+ }
+ }
+ for u in [a - 1, b + 1] {
+ if u >= 0 && u < len_u {
+ self.site.set(f.at(u, 0), Tile::Counter);
+ }
+ }
+ for u in a..=b {
+ self.site.set(f.at(u, 0), if u == mid { Tile::Floor } else { Tile::Shelf });
+ }
+ self.reachable = self.count_reachable();
+ Some(f.at(mid, 0))
+ }
+
+ /// A free floor cell in the room that you can walk to.
+ fn spot(&mut self, room: usize, seen: &[bool]) -> Option<Pos> {
+ let mut cells: Vec<Pos> = self.rooms[room]
+ .rect
+ .cells()
+ .filter(|&p| {
+ matches!(self.tile(p), Tile::Floor | Tile::Rug)
+ && seen[self.site.index(p)]
+ && self.site.person_at(p).is_none()
+ && p != self.start
+ && !p.neighbours().any(|n| n == self.exit)
+ })
+ .collect();
+ cells.shuffle(&mut self.rng);
+ cells.first().copied()
+ }
+
+ fn add(&mut self, pos: Pos, role: Role, stays: bool) {
+ let named = matches!(
+ role,
+ Role::Resident | Role::Ruler | Role::Priest | Role::Worker | Role::Courtier
+ );
+ let name = match role {
+ Role::Keeper(_) => self.keeper.clone(),
+ _ if named => Some(self.culture.person(&mut self.rng)),
+ _ => None,
+ };
+ let says = *words::lines(role, self.culture).choose(&mut self.rng).unwrap_or(&"...");
+ self.site.people.push(Person { pos, role, name, says, stays });
+ }
+
+ /// Up to `n` people wandering the room.
+ fn crowd(&mut self, room: usize, role: Role, n: std::ops::RangeInclusive<usize>) {
+ let n = self.rng.random_range(n);
+ let seen = self.site.reachable(self.start);
+ for _ in 0..n {
+ if let Some(p) = self.spot(room, &seen) {
+ self.add(p, role, false);
+ }
+ }
+ }
+
+ /// Up to `n` people sitting in these seats, staying put.
+ fn seat(&mut self, seats: &[Pos], role: Role, n: std::ops::RangeInclusive<usize>) {
+ let n = self.rng.random_range(n);
+ let mut seats: Vec<Pos> =
+ seats.iter().copied().filter(|&p| self.site.person_at(p).is_none()).collect();
+ seats.shuffle(&mut self.rng);
+ for p in seats.into_iter().take(n) {
+ self.add(p, role, true);
+ }
+ }
+
+ /// Puts `thing` at the middle of the back wall, and returns the cell in
+ /// front of it.
+ fn at_back(&mut self, room: usize, thing: Thing) -> Option<Pos> {
+ let f = self.frame(room);
+ let mid = f.len_u() / 2;
+ for u in [mid, mid - 1, mid + 1, mid - 2, mid + 2] {
+ let p = f.at(u, 0);
+ if self.put(p, thing) {
+ return Some(f.at(u, 1));
+ }
+ }
+ None
+ }
+
+ fn furnish(&mut self, room: usize) {
+ use Purpose::*;
+ use Thing::*;
+ let purpose = self.rooms[room].purpose;
+ let area = self.rooms[room].rect.area() as usize;
+ let culture = self.culture;
+ match purpose {
+ Living => {
+ self.against(room, Hearth, 1, 1);
+ self.table(room, 4);
+ self.against(room, Shelf, 1, 2);
+ self.against(room, Chest, 0, 1);
+ self.out_in(room, Plant, 0, 1);
+ self.rug(room, 0.3);
+ }
+ Bedroom => {
+ self.against(room, Bed, 1, 2);
+ self.against(room, Chest, 0, 1);
+ self.rug(room, 0.3);
+ }
+ Lodging => {
+ self.against(room, Bed, 1, 1);
+ self.against(room, Chest, 0, 1);
+ self.crowd(room, Role::Resident, 0..=1);
+ }
+ Kitchen => {
+ self.against(room, Stove, 1, 1);
+ self.against(room, Barrel, 1, 3);
+ self.against(room, Shelf, 0, 1);
+ self.table(room, 2);
+ if self.kind == Kind::Tenement {
+ self.crowd(room, Role::Resident, 1..=2);
+ }
+ }
+ Store => {
+ self.against(room, Crate, 2, 5);
+ self.against(room, Barrel, 1, 3);
+ }
+ Taproom => {
+ if let Some(at) = self.nook(room, 0.45) {
+ self.add(at, Role::Keeper(self.shop.unwrap_or(0)), true);
+ }
+ self.against(room, Hearth, 1, 1);
+ let mut seats = Vec::new();
+ for _ in 0..(area / 28).clamp(2, 8) {
+ seats.extend(self.table(room, 4));
+ }
+ let n = self.rng.random_range(3..=(seats.len().max(3)));
+ self.seat(&seats, Role::Patron, n..=n);
+ self.crowd(room, Role::Patron, 0..=2);
+ }
+ Parlour => {
+ let mut seats = self.table(room, 4);
+ if area > 40 {
+ seats.extend(self.table(room, 4));
+ }
+ self.seat(&seats, Role::Patron, 1..=3);
+ self.against(room, Shelf, 0, 1);
+ }
+ Cellar => {
+ self.rows(room, Barrel, 1, 2, false);
+ self.against(room, Crate, 1, 3);
+ }
+ Counter => {
+ if let Some(at) = self.nook(room, 0.75) {
+ self.add(at, Role::Keeper(self.shop.unwrap_or(0)), true);
+ }
+ match self.kind {
+ Kind::Shop(ShopKind::Doctor) => {
+ let cells = self.by_wall(room);
+ let seats: Vec<Pos> =
+ cells.into_iter().filter(|&p| self.put(p, Chair)).take(3).collect();
+ self.seat(&seats, Role::Patient, 0..=1);
+ self.out_in(room, Plant, 0, 1);
+ }
+ Kind::Shop(ShopKind::Scholar) => {
+ self.against(room, Shelf, 2, 5);
+ self.out_in(room, Lamp, 1, 1);
+ }
+ Kind::Shop(ShopKind::Broker) => self.against(room, Crate, 2, 4),
+ Kind::Shop(ShopKind::Shipyard) => {
+ self.out_in(room, Engine, 1, 2);
+ self.against(room, Crate, 1, 3);
+ }
+ Kind::Shop(ShopKind::Armoury) => self.against(room, Rack, 2, 5),
+ _ => {}
+ }
+ }
+ Ward => {
+ let n = (self.rooms[room].rect.width() / 3).clamp(2, 8) as usize;
+ self.against(room, Bed, n, n);
+ self.crowd(room, Role::Patient, 1..=3);
+ self.against(room, Chest, 0, 1);
+ }
+ Stacks => {
+ self.rows(room, Shelf, 1, 2, true);
+ self.crowd(room, Role::Reader, 0..=2);
+ }
+ Assay => {
+ self.out_in(room, Workbench, 1, 2);
+ self.against(room, Crate, 2, 4);
+ self.crowd(room, Role::Worker, 0..=1);
+ }
+ Smithy => {
+ self.against(room, Forge, 1, 1);
+ self.out_in(room, Anvil, 1, 2);
+ self.against(room, Rack, 1, 2);
+ self.out_in(room, Workbench, 0, 1);
+ self.crowd(room, Role::Worker, 0..=1);
+ }
+ Yard => {
+ self.out_in(room, Engine, 2, 4);
+ self.against(room, Crate, 2, 5);
+ self.out_in(room, Anvil, 0, 1);
+ self.crowd(room, Role::Worker, 1..=3);
+ }
+ Hall => {
+ self.runner(room);
+ self.against(room, Statue, 2, 4);
+ self.against(room, Plant, 1, 3);
+ self.against(room, Lamp, 1, 2);
+ }
+ Dining => {
+ let seats = self.long_table(room);
+ self.against(room, Shelf, 0, 1);
+ if self.rng.random_bool(0.4) {
+ self.seat(&seats, Role::Resident, 1..=1);
+ }
+ self.crowd(room, Role::Servant, 0..=1);
+ }
+ Library => {
+ self.against(room, Shelf, 4, 12);
+ let seats = self.table(room, 2);
+ self.seat(&seats, Role::Reader, 0..=1);
+ }
+ Bedchamber => {
+ self.against(room, Bed, 1, 1);
+ self.against(room, Chest, 1, 1);
+ self.against(room, Lamp, 0, 1);
+ self.out_in(room, Plant, 0, 1);
+ self.rug(room, 0.7);
+ }
+ Gallery => {
+ self.against(room, Statue, 3, 6);
+ self.out_in(room, Plant, 1, 2);
+ }
+ Purpose::Throne => {
+ self.runner(room);
+ let f = self.frame(room);
+ let mid = f.len_u() / 2;
+ // The throne at the end of the runner, lamps beside it.
+ let throne = f.at(mid, 0);
+ if self.tile(throne) == Tile::Rug {
+ self.site.set(throne, Tile::Floor);
+ }
+ if self.put(throne, Thing::Throne) {
+ self.add(throne, Role::Ruler, true);
+ }
+ for u in [mid - 2, mid + 2] {
+ self.put(f.at(u, 0), Lamp);
+ }
+ // Pillars down both sides, guards along the runner.
+ let mut v = 2;
+ while v < f.len_v() - 2 {
+ for u in [mid - 4, mid + 4] {
+ if u > 0 && u < f.len_u() - 1 {
+ self.put(f.at(u, v), Pillar);
+ }
+ }
+ v += 3;
+ }
+ let mut guards = 0;
+ for v in (2..f.len_v() - 1).step_by(4) {
+ for u in [mid - 2, mid + 2] {
+ let p = f.at(u, v);
+ if guards < 6 && self.tile(p).walkable() && self.site.person_at(p).is_none()
+ {
+ self.add(p, Role::Guard, true);
+ guards += 1;
+ }
+ }
+ }
+ self.crowd(room, Role::Courtier, 2..=4);
+ }
+ Council => {
+ let seats = self.long_table(room);
+ self.seat(&seats, Role::Courtier, 1..=4);
+ self.against(room, Shelf, 0, 2);
+ }
+ Treasury => {
+ self.against(room, Chest, 4, 9);
+ self.crowd(room, Role::Guard, 1..=1);
+ }
+ Nave => {
+ let altar = self.at_back(room, Altar);
+ let f = self.frame(room);
+ let mid = f.len_u() / 2;
+ for u in [mid - 2, mid + 2] {
+ self.put(f.at(u, 0), Lamp);
+ }
+ if let Some(at) = altar {
+ self.add(at, Role::Priest, true);
+ }
+ let pews = self.rows(room, Pew, 3, 2, true);
+ let n = self.rng.random_range(2..=(pews.len() / 3).max(3));
+ self.seat(&pews, Role::Worshipper, n..=n);
+ self.crowd(room, Role::Pilgrim, 0..=2);
+ }
+ Vestry => {
+ self.against(room, Shelf, 1, 2);
+ self.against(room, Chest, 1, 1);
+ self.table(room, 1);
+ }
+ Cell => {
+ self.against(room, Bed, 1, 1);
+ self.against(room, Chest, 0, 1);
+ if self.rng.random_bool(0.5) {
+ self.crowd(room, Role::Priest, 1..=1);
+ }
+ }
+ Shrine => {
+ if let Some(at) = self.at_back(room, Altar) {
+ self.add(at, Role::Priest, true);
+ }
+ let f = self.frame(room);
+ let mid = f.len_u() / 2;
+ for u in [mid - 2, mid + 2] {
+ self.put(f.at(u, 0), Lamp);
+ }
+ let benches = self.rows(room, Bench, 3, 3, true);
+ self.seat(&benches, Role::Worshipper, 0..=2);
+ self.crowd(room, Role::Pilgrim, 0..=2);
+ }
+ Workroom => {
+ match culture {
+ Culture::Cant => {
+ self.against(room, Forge, 1, 1);
+ self.out_in(room, Anvil, 1, 2);
+ self.against(room, Rack, 0, 1);
+ }
+ Culture::Qaari => {
+ self.out_in(room, Loom, 1, 2);
+ self.against(room, Barrel, 1, 2);
+ }
+ Culture::Houses => {
+ self.out_in(room, Workbench, 1, 3);
+ self.against(room, Shelf, 1, 2);
+ }
+ Culture::Drift => {
+ self.out_in(room, Engine, 1, 1);
+ self.out_in(room, Workbench, 1, 2);
+ }
+ }
+ self.against(room, Workbench, 1, 2);
+ self.against(room, Crate, 0, 2);
+ self.crowd(room, Role::Worker, 1..=3);
+ }
+ Storehouse => {
+ let goods = if self.rng.random_bool(0.6) { Crate } else { Barrel };
+ self.rows(room, goods, 1, 2, true);
+ self.crowd(room, Role::Watchman, 1..=1);
+ }
+ Office => {
+ let seats = self.table(room, 1);
+ self.against(room, Shelf, 1, 1);
+ self.seat(&seats, Role::Worker, 0..=1);
+ }
+ Changing => {
+ self.against(room, Bench, 2, 5);
+ self.against(room, Chest, 1, 2);
+ self.crowd(room, Role::Worker, 1..=1);
+ }
+ Baths => {
+ let r = self.rooms[room].rect;
+ let pool = Rect::new(r.left + 3, r.top + 2, r.right - 3, r.bottom - 2);
+ if pool.width() >= 2 && pool.height() >= 1 {
+ for p in pool.cells() {
+ self.put(p, Bath);
+ }
+ }
+ self.crowd(room, Role::Bather, 2..=5);
+ }
+ Steam => {
+ self.against(room, Stove, 1, 1);
+ let cells = self.by_wall(room);
+ let seats: Vec<Pos> =
+ cells.into_iter().filter(|&p| self.put(p, Bench)).take(5).collect();
+ self.seat(&seats, Role::Bather, 1..=3);
+ }
+ Guardroom => {
+ let seats = self.table(room, 4);
+ self.seat(&seats, Role::Guard, 1..=3);
+ self.against(room, Rack, 1, 3);
+ self.crowd(room, Role::Guard, 0..=2);
+ }
+ Bunks => self.against(room, Bed, 3, 8),
+ }
+ }
+
+ /// The people who live in a house or a great house.
+ fn residents(&mut self) {
+ let (role, n, servants) = match self.kind {
+ Kind::House => (Role::Resident, self.rng.random_range(1..=4), 0),
+ Kind::Manor => {
+ (Role::Resident, self.rng.random_range(1..=2), self.rng.random_range(1..=3))
+ }
+ Kind::Palace => (Role::Servant, self.rng.random_range(1..=3), 0),
+ Kind::Workshop => (Role::Worker, self.rng.random_range(0..=1), 0),
+ _ => return,
+ };
+ let rooms = self.rooms.len();
+ for (role, n) in [(role, n), (Role::Servant, servants)] {
+ for _ in 0..n {
+ let room = self.rng.random_range(0..rooms);
+ self.crowd(room, role, 1..=1);
+ }
+ }
+ }
+}
diff --git a/src/town/mod.rs b/src/town/mod.rs
new file mode 100644
index 0000000..fd17b20
--- /dev/null
+++ b/src/town/mod.rs
@@ -0,0 +1,850 @@
+//! Capital cities: where you walk when you land somewhere people live.
+//!
+//! A capital comes at two scales. Outside is the whole city, seen from
+//! high up: a wall around it, a landing field beyond the west gate, avenues
+//! to a market square, and streets and alleys between blocks of buildings,
+//! each one a small roof with a door. Bump a door and you go in, and the
+//! building becomes a map of its own at a much closer scale: rooms,
+//! furniture, and the people who live or work there.
+//!
+//! - `plan` lays out the city: walls, gates, quarters, streets, landmarks
+//! and the buildings, and which of them are shops.
+//! - `interior` builds what's inside a building when you go in.
+//! - `words` holds what everyone says, and what things are called.
+//!
+//! A city is generated from its place, so it's the same city every time you
+//! land, and the more people live on a world the bigger its capital is.
+//! Interiors come from the building, so a house has the same rooms each
+//! visit too.
+//!
+//! The world it stands on shapes it as well (see `Land`): the ground, what
+//! grows in its parks and what stands outside its walls, how many parks
+//! there are, and, on an ocean world, the sea all round. The weather on the
+//! day you land only changes how it looks.
+
+mod interior;
+mod plan;
+pub mod words;
+
+pub use interior::{Interior, Purpose, Thing};
+
+use rand::seq::IndexedRandom;
+
+use crate::dungeon::Pos;
+use crate::names::Culture;
+use crate::seed;
+use crate::space::survey::{self, FREEZING, Temperature};
+use crate::space::system::{PlaceKind, System, Terrain};
+
+/// Columns and rows your ship takes up on the pad.
+pub const SHIP_SIZE: (i32, i32) = (5, 3);
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Tile {
+ /// Outside the walls.
+ Ground,
+ /// The landing field.
+ Pad,
+ /// Your ship, standing on the pad.
+ Ship,
+ CityWall,
+ Gate,
+ Street,
+ /// Paving: the market square, plazas, the forecourts of great buildings.
+ Square,
+ /// Open ground inside the walls: yards, courtyards, parks.
+ Garden,
+ /// A pond.
+ Water,
+ /// The open sea round a city on an ocean world.
+ Sea,
+ /// Pipes, scrap: in the way, just for looks. Cultures that don't leave
+ /// things lying about plant trees instead.
+ Decor,
+ /// A tree, or whatever grows instead on this world.
+ Tree,
+ /// Something outside the walls: a boulder, a cactus, an ice spire, a
+ /// vent. Each world has two sorts (see `Town::wild_sort`).
+ Wild,
+ /// The well, fountain or furnace in the middle of the market square.
+ Centrepiece,
+ /// A market stall's awning.
+ Stall,
+ Statue,
+ Well,
+ Lamp,
+ /// A building, seen from the street. Which one is in `Town::building_at`.
+ Roof,
+ /// A building's street door. Bump it to go in, unless it's barred.
+ Door {
+ open: bool,
+ },
+ /// A building's walls, from inside.
+ Wall,
+ Floor,
+ /// The way between two rooms.
+ Doorway,
+ /// The door back out to the street.
+ Exit,
+ /// A shop's counter, with the keeper behind it.
+ Counter,
+ /// The shop's wares, behind the counter.
+ Shelf,
+ Rug,
+ Furniture(Thing),
+}
+
+impl Tile {
+ pub fn walkable(self) -> bool {
+ match self {
+ Tile::Ground
+ | Tile::Pad
+ | Tile::Gate
+ | Tile::Street
+ | Tile::Square
+ | Tile::Garden
+ | Tile::Floor
+ | Tile::Doorway
+ | Tile::Rug => true,
+ Tile::Furniture(thing) => thing.walkable(),
+ _ => false,
+ }
+ }
+
+ /// Open air inside the walls, where townsfolk wander.
+ pub fn outdoors(self) -> bool {
+ matches!(self, Tile::Gate | Tile::Street | Tile::Square | Tile::Garden)
+ }
+
+ /// A street or square: what a building's door has to open onto.
+ fn paved(self) -> bool {
+ matches!(self, Tile::Street | Tile::Square)
+ }
+}
+
+/// A rectangle of cells, edges included.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct Rect {
+ left: i32,
+ top: i32,
+ right: i32,
+ bottom: i32,
+}
+
+impl Rect {
+ const fn new(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
+ Rect { left, top, right, bottom }
+ }
+
+ fn width(self) -> i32 {
+ self.right - self.left + 1
+ }
+
+ fn height(self) -> i32 {
+ self.bottom - self.top + 1
+ }
+
+ fn area(self) -> i32 {
+ self.width().max(0) * self.height().max(0)
+ }
+
+ fn centre(self) -> Pos {
+ Pos::new((self.left + self.right) / 2, (self.top + self.bottom) / 2)
+ }
+
+ fn contains(self, p: Pos) -> bool {
+ p.x >= self.left && p.x <= self.right && p.y >= self.top && p.y <= self.bottom
+ }
+
+ fn cells(self) -> impl Iterator<Item = Pos> {
+ (self.top..=self.bottom)
+ .flat_map(move |y| (self.left..=self.right).map(move |x| Pos::new(x, y)))
+ }
+}
+
+/// One of the four ways a door can face.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Side {
+ North,
+ South,
+ West,
+ East,
+}
+
+impl Side {
+ pub fn step(self) -> (i32, i32) {
+ match self {
+ Side::North => (0, -1),
+ Side::South => (0, 1),
+ Side::West => (-1, 0),
+ Side::East => (1, 0),
+ }
+ }
+
+ fn opposite(self) -> Side {
+ match self {
+ Side::North => Side::South,
+ Side::South => Side::North,
+ Side::West => Side::East,
+ Side::East => Side::West,
+ }
+ }
+}
+
+/// A map you can walk around, and the people on it. The city's streets are
+/// one; each building's inside is another.
+pub struct Site {
+ pub width: i32,
+ pub height: i32,
+ tiles: Vec<Tile>,
+ pub people: Vec<Person>,
+ /// What's off the edge of the map.
+ edge: Tile,
+ /// Whether people here keep to the open air (the streets) or to the
+ /// floors (indoors).
+ streets: bool,
+}
+
+impl Site {
+ fn new(width: i32, height: i32, fill: Tile, edge: Tile, streets: bool) -> Site {
+ Site {
+ width,
+ height,
+ tiles: vec![fill; (width * height) as usize],
+ people: Vec::new(),
+ edge,
+ streets,
+ }
+ }
+
+ pub fn contains(&self, p: Pos) -> bool {
+ p.x >= 0 && p.y >= 0 && p.x < self.width && p.y < self.height
+ }
+
+ fn index(&self, p: Pos) -> usize {
+ (p.y * self.width + p.x) as usize
+ }
+
+ /// The tile at `p`; off the map is the edge, so nothing walks off it.
+ pub fn tile(&self, p: Pos) -> Tile {
+ if self.contains(p) { self.tiles[self.index(p)] } else { self.edge }
+ }
+
+ fn set(&mut self, p: Pos, tile: Tile) {
+ let i = self.index(p);
+ self.tiles[i] = tile;
+ }
+
+ fn fill(&mut self, r: Rect, tile: Tile) {
+ for p in r.cells() {
+ if self.contains(p) {
+ self.set(p, tile);
+ }
+ }
+ }
+
+ pub fn positions(&self) -> impl Iterator<Item = Pos> + use<> {
+ Rect::new(0, 0, self.width - 1, self.height - 1).cells()
+ }
+
+ pub fn person_at(&self, p: Pos) -> Option<usize> {
+ self.people.iter().position(|person| person.pos == p)
+ }
+
+ /// Whether someone could step onto `p` right now.
+ pub fn free(&self, p: Pos) -> bool {
+ self.tile(p).walkable() && self.person_at(p).is_none()
+ }
+
+ /// Where people here may wander.
+ fn roams(&self, tile: Tile) -> bool {
+ if self.streets { tile.outdoors() } else { tile.walkable() }
+ }
+
+ /// Steps someone who's wandering aside onto `to`, where you were, as you
+ /// walk through them, if they'd go there. Otherwise they stay where
+ /// they are, and you share the cell until they wander off.
+ pub fn make_way(&mut self, who: usize, to: Pos) {
+ if !self.people[who].stays && self.roams(self.tile(to)) && self.free(to) {
+ self.people[who].pos = to;
+ }
+ }
+
+ /// Moves someone a step at random, if they're the wandering kind,
+ /// staying off `avoid` (where you're standing).
+ pub fn wander(&mut self, who: usize, rng: &mut seed::Rng, avoid: Pos) {
+ if self.people[who].stays {
+ return;
+ }
+ let from = self.people[who].pos;
+ let steps: Vec<Pos> = from
+ .neighbours()
+ .filter(|&p| p != avoid && self.roams(self.tile(p)) && self.free(p))
+ .collect();
+ if let Some(&to) = steps.choose(rng) {
+ self.people[who].pos = to;
+ }
+ }
+
+ /// Every cell you can walk to from `from`.
+ pub fn reachable(&self, from: Pos) -> Vec<bool> {
+ let mut seen = vec![false; self.tiles.len()];
+ if !self.contains(from) {
+ return seen;
+ }
+ let mut queue = std::collections::VecDeque::from([from]);
+ seen[self.index(from)] = true;
+ while let Some(p) = queue.pop_front() {
+ for n in p.neighbours() {
+ if self.contains(n) && self.tile(n).walkable() && !seen[self.index(n)] {
+ seen[self.index(n)] = true;
+ queue.push_back(n);
+ }
+ }
+ }
+ seen
+ }
+}
+
+/// What someone in a city is doing there, which decides what they say and
+/// whether they move about.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Role {
+ Passerby,
+ Child,
+ Porter,
+ Pilgrim,
+ Stallholder,
+ Guard,
+ /// The keeper of the shop with this index in `Town::shops`.
+ Keeper(usize),
+ Resident,
+ Servant,
+ Ruler,
+ Courtier,
+ Priest,
+ Worshipper,
+ Worker,
+ Patron,
+ Patient,
+ Bather,
+ Watchman,
+ Reader,
+}
+
+/// Someone in the streets or indoors.
+pub struct Person {
+ pub pos: Pos,
+ pub role: Role,
+ /// Residents, keepers and the like have names; passers-by don't.
+ pub name: Option<String>,
+ pub says: &'static str,
+ /// Stallholders, keepers, worshippers in their pews: they stay put.
+ pub stays: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ShopKind {
+ Broker,
+ Doctor,
+ Scholar,
+ Tavern,
+ Shipyard,
+ /// Weapons, bought and sold.
+ Armoury,
+}
+
+impl ShopKind {
+ pub const ALL: [ShopKind; 6] = [
+ ShopKind::Broker,
+ ShopKind::Doctor,
+ ShopKind::Scholar,
+ ShopKind::Tavern,
+ ShopKind::Shipyard,
+ ShopKind::Armoury,
+ ];
+
+ /// What's painted on the roof.
+ pub fn sign(self) -> &'static str {
+ match self {
+ ShopKind::Broker => "ORE",
+ ShopKind::Doctor => "DOCTOR",
+ ShopKind::Scholar => "SCHOLAR",
+ ShopKind::Tavern => "TAVERN",
+ ShopKind::Shipyard => "SHIPYARD",
+ ShopKind::Armoury => "ARMS",
+ }
+ }
+
+ /// "the doctor", for giving directions.
+ pub fn trade(self) -> &'static str {
+ match self {
+ ShopKind::Broker => "an ore broker",
+ ShopKind::Doctor => "a doctor",
+ ShopKind::Scholar => "a scholar",
+ ShopKind::Tavern => "a tavern",
+ ShopKind::Shipyard => "a shipyard",
+ ShopKind::Armoury => "an armourer",
+ }
+ }
+}
+
+/// What a building is for.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Kind {
+ Shop(ShopKind),
+ House,
+ /// Lots of small rooms, lots of people.
+ Tenement,
+ /// A great house, in the grand quarter.
+ Manor,
+ /// Where the city's ruler sits. One to a city.
+ Palace,
+ /// The city's great temple. One to a city.
+ Temple,
+ Shrine,
+ Workshop,
+ Warehouse,
+ Bathhouse,
+ Guardhouse,
+}
+
+/// One building, as it stands in the street.
+pub struct Building {
+ pub kind: Kind,
+ /// Its footprint on the city map.
+ pub left: i32,
+ pub top: i32,
+ pub right: i32,
+ pub bottom: i32,
+ /// On the footprint's edge, facing the street.
+ pub door: Pos,
+ pub facing: Side,
+ pub barred: bool,
+ /// Index into `Town::shops`, if it's a shop.
+ pub shop: Option<usize>,
+ /// "the Sand Archive", "a house", "House Vastre".
+ pub name: String,
+}
+
+impl Building {
+ /// The street cell in front of the door.
+ pub fn front(&self) -> Pos {
+ let (dx, dy) = self.facing.step();
+ self.door.offset(dx, dy)
+ }
+
+ pub fn centre(&self) -> Pos {
+ Pos::new((self.left + self.right) / 2, (self.top + self.bottom) / 2)
+ }
+
+ pub fn width(&self) -> i32 {
+ self.right - self.left + 1
+ }
+
+ pub fn height(&self) -> i32 {
+ self.bottom - self.top + 1
+ }
+
+ #[cfg(test)]
+ fn rect(&self) -> Rect {
+ Rect::new(self.left, self.top, self.right, self.bottom)
+ }
+}
+
+pub struct Shop {
+ pub kind: ShopKind,
+ /// "the Sand Archive".
+ pub name: String,
+ pub keeper: String,
+ pub building: usize,
+}
+
+/// The kinds of neighbourhood a city is made of. They decide how big the
+/// blocks and buildings are, and what the buildings are.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum QuarterKind {
+ /// Around the square.
+ Market,
+ /// Inside the west gate, by the landing field.
+ Gate,
+ Homes,
+ Poor,
+ /// Where the palace and the great houses are.
+ Grand,
+ /// Around the temple.
+ Holy,
+ Works,
+ Gardens,
+}
+
+pub struct Quarter {
+ pub kind: QuarterKind,
+ /// "the Spice Souk".
+ pub name: String,
+ /// Where it's centred, for the map.
+ pub centre: Pos,
+}
+
+pub struct Town {
+ /// "Erg Khaseem".
+ pub name: String,
+ pub culture: Culture,
+ /// The world it's built on, and the weather today.
+ pub land: Land,
+ seed: u64,
+ /// The streets, and the people in them.
+ pub site: Site,
+ /// Which building each cell is part of, `NONE` for none.
+ owner: Vec<u16>,
+ /// Which quarter each cell inside the walls is in, `NONE` outside.
+ quarter_of: Vec<u8>,
+ pub quarters: Vec<Quarter>,
+ pub buildings: Vec<Building>,
+ pub shops: Vec<Shop>,
+ /// The top left of your ship, which is `SHIP_SIZE` big.
+ pub ship: Pos,
+ /// Where you step off your ship.
+ pub start: Pos,
+ /// The middle of the market square.
+ pub square: Pos,
+ /// The middle of each gate, the west gate first.
+ pub gates: Vec<Pos>,
+}
+
+const NO_OWNER: u16 = u16::MAX;
+const NO_QUARTER: u8 = u8::MAX;
+
+impl Town {
+ pub fn width(&self) -> i32 {
+ self.site.width
+ }
+
+ pub fn height(&self) -> i32 {
+ self.site.height
+ }
+
+ pub fn tile(&self, p: Pos) -> Tile {
+ self.site.tile(p)
+ }
+
+ /// Which of its world's two sorts of `Tile::Wild` stands at `p`.
+ pub fn wild_sort(&self, p: Pos) -> usize {
+ let h = seed::derive(self.seed, "wild", (p.x as u64) << 32 | p.y as u64);
+ usize::from(h.is_multiple_of(3))
+ }
+
+ /// How much redder, greener and bluer this city's ground is than
+ /// others on the same kind of world, from 0.92 to 1.08 times, so no two
+ /// look quite alike.
+ pub fn tint(&self) -> [f64; 3] {
+ std::array::from_fn(|i| {
+ let h = seed::derive(self.seed, "tint", i as u64);
+ 0.92 + 0.16 * (h >> 11) as f64 / (1u64 << 53) as f64
+ })
+ }
+
+ /// The building whose footprint `p` is in, door included.
+ pub fn building_at(&self, p: Pos) -> Option<usize> {
+ if !self.site.contains(p) {
+ return None;
+ }
+ let owner = self.owner[self.site.index(p)];
+ (owner != NO_OWNER).then_some(usize::from(owner))
+ }
+
+ /// The building whose street door is at `p`.
+ pub fn door_at(&self, p: Pos) -> Option<usize> {
+ self.building_at(p).filter(|&b| self.buildings[b].door == p)
+ }
+
+ pub fn quarter_at(&self, p: Pos) -> Option<&Quarter> {
+ if !self.site.contains(p) {
+ return None;
+ }
+ self.quarters.get(usize::from(self.quarter_of[self.site.index(p)]))
+ }
+
+ /// Goes into a building: builds its inside, the same every time.
+ pub fn enter(&self, building: usize) -> Interior {
+ interior::build(self, building)
+ }
+
+ /// The shop of this kind nearest to `from`.
+ pub fn nearest_shop(&self, kind: ShopKind, from: Pos) -> Option<&Shop> {
+ self.shops
+ .iter()
+ .filter(|s| s.kind == kind)
+ .min_by_key(|s| distance2(self.buildings[s.building].door, from))
+ }
+}
+
+/// Squared distance as it looks on screen, where rows are twice as tall as
+/// columns are wide.
+pub fn distance2(a: Pos, b: Pos) -> i32 {
+ (a.x - b.x).pow(2) + (2 * (a.y - b.y)).pow(2)
+}
+
+/// The seed for the capital of `place` in a system.
+pub fn seed_for(system_seed: u64, place: usize) -> u64 {
+ seed::derive(system_seed, "town", place as u64)
+}
+
+/// What the capital `generate` would build is called, without building it.
+pub fn name(seed: u64, culture: Culture) -> String {
+ culture.place(&mut seed::rng(seed))
+}
+
+/// How big a capital is, from 0 to 1, for a world of this many people.
+pub fn size_for(population: Option<u64>) -> f64 {
+ let people = population.unwrap_or(10_000_000).max(1) as f64;
+ ((people.log10() - 5.3) / 4.3).clamp(0.0, 1.0)
+}
+
+/// Builds the capital for `seed`, `size` big (see `size_for`), on `land`.
+/// Only the land's terrain changes the plan: the weather doesn't move
+/// anything.
+pub fn generate(seed: u64, culture: Culture, size: f64, land: Land) -> Town {
+ let mut rng = seed::rng(seed);
+ // The name comes first, so `name` can find it alone.
+ let name = culture.place(&mut rng);
+ plan::build(name, culture, land, seed, size.clamp(0.0, 1.0), &mut rng)
+}
+
+/// What the world a city stands on does to it: what the ground is, and
+/// what the weather's doing to it today.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Land {
+ pub terrain: Terrain,
+ /// From 0 to 1: how hard it's frozen, on a world that isn't always.
+ pub frost: f64,
+ /// From 0 to 1: how parched the gardens are.
+ pub drought: f64,
+}
+
+/// What's in a city's ponds today.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Pond {
+ Water,
+ Ice,
+ /// Dried to cracked mud.
+ Dry,
+ Lava,
+}
+
+impl Land {
+ /// `terrain`, on a day `temperature` warm. Only worlds with air and
+ /// water see frost come and go and gardens wither: frost from 4°C
+ /// down, white by -10°C; drought from 30°C up, worst by 50°C.
+ pub fn new(terrain: Terrain, temperature: Option<Temperature>) -> Land {
+ let seasons = matches!(terrain, Terrain::Desert | Terrain::Ocean | Terrain::Jungle);
+ let c = temperature.map_or(15.0, |t| t.now - FREEZING);
+ let (frost, drought) = if seasons {
+ (((4.0 - c) / 14.0).clamp(0.0, 1.0), ((c - 30.0) / 20.0).clamp(0.0, 1.0))
+ } else {
+ (0.0, 0.0)
+ };
+ Land { terrain, frost, drought }
+ }
+
+ /// The land under the capital of `place` on `day`, if it's a planet or
+ /// a moon.
+ pub fn of(s: &System, place: usize, day: f64) -> Option<Land> {
+ let kind = match s.places[place].kind {
+ PlaceKind::Planet(p) => s.planets[p].kind,
+ PlaceKind::Moon(p, m) => s.planets[p].moons[m].kind,
+ PlaceKind::Belt | PlaceKind::Station(_) => return None,
+ };
+ Some(Land::new(kind.terrain, survey::survey(s, place, day).temperature))
+ }
+
+ pub fn pond(self) -> Pond {
+ match self.terrain {
+ Terrain::Forge => Pond::Lava,
+ Terrain::Ice => Pond::Ice,
+ Terrain::Barren | Terrain::Clouds => Pond::Dry,
+ _ if self.frost > 0.6 => Pond::Ice,
+ _ if self.drought > 0.8 => Pond::Dry,
+ _ => Pond::Water,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const TERRAINS: [Terrain; 6] = [
+ Terrain::Desert,
+ Terrain::Ice,
+ Terrain::Ocean,
+ Terrain::Jungle,
+ Terrain::Barren,
+ Terrain::Forge,
+ ];
+
+ fn land(terrain: Terrain) -> Land {
+ Land::new(terrain, None)
+ }
+
+ /// A mild day on a jungle world.
+ fn green() -> Land {
+ land(Terrain::Jungle)
+ }
+
+ fn towns() -> impl Iterator<Item = Town> {
+ (0..12).flat_map(|seed| {
+ Culture::ALL.into_iter().map(move |c| {
+ let terrain = TERRAINS[(seed as usize + c as usize) % TERRAINS.len()];
+ generate(seed, c, (seed % 4) as f64 / 3.0, land(terrain))
+ })
+ })
+ }
+
+ #[test]
+ fn a_capitals_name_is_known_before_its_built() {
+ for seed in 0..40 {
+ for culture in Culture::ALL {
+ assert_eq!(name(seed, culture), generate(seed, culture, 0.5, green()).name);
+ }
+ }
+ }
+
+ #[test]
+ fn the_same_seed_builds_the_same_city() {
+ let a = generate(7, Culture::Houses, 0.6, green());
+ let b = generate(7, Culture::Houses, 0.6, green());
+ assert_eq!(a.site.tiles, b.site.tiles);
+ assert_eq!(a.buildings.len(), b.buildings.len());
+ }
+
+ #[test]
+ fn the_weather_changes_how_a_city_looks_not_where_anything_is() {
+ let day = |now: f64| Temperature { now: FREEZING + now, coldest: 0.0, hottest: 0.0 };
+ let cold = Land::new(Terrain::Ocean, Some(day(-12.0)));
+ let hot = Land::new(Terrain::Ocean, Some(day(48.0)));
+ assert_eq!(cold.pond(), Pond::Ice);
+ assert_eq!(hot.pond(), Pond::Dry);
+ assert_eq!(Land::new(Terrain::Ocean, Some(day(15.0))).pond(), Pond::Water);
+ let a = generate(4, Culture::Qaari, 0.5, cold);
+ let b = generate(4, Culture::Qaari, 0.5, hot);
+ assert_eq!(a.site.tiles, b.site.tiles);
+ // Ice and forge worlds have no seasons to speak of.
+ assert_eq!(Land::new(Terrain::Ice, Some(day(-180.0))).frost, 0.0);
+ assert_eq!(Land::new(Terrain::Forge, Some(day(300.0))).drought, 0.0);
+ }
+
+ #[test]
+ fn the_world_shapes_the_city() {
+ let count =
+ |t: &Town, tile: Tile| t.site.positions().filter(|&p| t.tile(p) == tile).count();
+ let mut trees = [0; 6];
+ let mut wild = [0; 6];
+ let mut ponds = [0; 6];
+ for seed in 0..6 {
+ for (i, &terrain) in TERRAINS.iter().enumerate() {
+ let t = generate(seed, Culture::Houses, 0.5, land(terrain));
+ trees[i] += count(&t, Tile::Tree);
+ wild[i] += count(&t, Tile::Wild);
+ ponds[i] += count(&t, Tile::Water);
+ let sea = count(&t, Tile::Sea);
+ if terrain == Terrain::Ocean {
+ assert!(sea > count(&t, Tile::Ground), "{}: mostly sea outside", t.name);
+ // The landing field is on dry land, and the road from it.
+ assert!(t.site.reachable(t.start)[t.site.index(t.gates[0])]);
+ } else {
+ assert_eq!(sea, 0);
+ }
+ }
+ }
+ let [desert, ice, _, jungle, barren, _] = [0, 1, 2, 3, 4, 5];
+ assert!(trees[jungle] > 2 * trees[desert], "jungle {trees:?}");
+ assert!(trees[ice] < trees[jungle]);
+ assert!(wild[jungle] > 3 * wild[desert], "jungle {wild:?}");
+ assert_eq!(ponds[barren], 0, "nothing pools on airless rock");
+ assert!(ponds[desert] < ponds[jungle], "{ponds:?}");
+ }
+
+ #[test]
+ fn no_two_cities_are_quite_the_same_colour() {
+ let a = generate(1, Culture::Drift, 0.3, green()).tint();
+ let b = generate(2, Culture::Drift, 0.3, green()).tint();
+ assert_ne!(a, b);
+ assert!(a.iter().chain(&b).all(|k| (0.92..=1.08).contains(k)));
+ }
+
+ #[test]
+ fn bigger_worlds_have_bigger_capitals() {
+ let small = generate(3, Culture::Qaari, size_for(Some(300_000)), green());
+ let big = generate(3, Culture::Qaari, size_for(Some(3_000_000_000)), green());
+ let area = |t: &Town| t.width() * t.height();
+ assert!(area(&big) * 2 > 5 * area(&small));
+ assert!(big.buildings.len() > 2 * small.buildings.len());
+ assert!(small.buildings.len() > 60, "even a small capital is a city");
+ }
+
+ #[test]
+ fn every_door_opens_onto_a_street_you_can_reach() {
+ for t in towns() {
+ let seen = t.site.reachable(t.start);
+ for (i, b) in t.buildings.iter().enumerate() {
+ assert_eq!(t.building_at(b.door), Some(i));
+ assert!(t.tile(b.front()).paved(), "{}: door of {} opens onto nothing", t.name, i);
+ assert!(seen[t.site.index(b.front())], "{}: can't reach {}", t.name, b.name);
+ assert_eq!(t.tile(b.door), Tile::Door { open: !b.barred });
+ for p in b.rect().cells() {
+ assert_eq!(t.building_at(p), Some(i), "buildings don't overlap");
+ }
+ }
+ for p in &t.site.people {
+ assert!(t.tile(p.pos).walkable());
+ assert!(seen[t.site.index(p.pos)], "{}: someone's stuck at {:?}", t.name, p.pos);
+ }
+ }
+ }
+
+ #[test]
+ fn every_city_has_every_shop_and_a_palace_and_temple() {
+ for t in towns() {
+ for kind in ShopKind::ALL {
+ let n = t.shops.iter().filter(|s| s.kind == kind).count();
+ if kind == ShopKind::Tavern { assert!(n >= 1) } else { assert_eq!(n, 1) }
+ }
+ for (i, shop) in t.shops.iter().enumerate() {
+ let b = &t.buildings[shop.building];
+ assert_eq!(b.shop, Some(i));
+ assert!(!b.barred);
+ assert!(
+ b.width() >= shop.kind.sign().len() as i32 + 2,
+ "{}: the sign fits",
+ t.name
+ );
+ }
+ for kind in [Kind::Palace, Kind::Temple] {
+ let n = t.buildings.iter().filter(|b| b.kind == kind).count();
+ assert_eq!(n, 1, "{}: {kind:?}", t.name);
+ }
+ let kinds: std::collections::HashSet<_> =
+ t.buildings.iter().map(|b| std::mem::discriminant(&b.kind)).collect();
+ assert!(kinds.len() >= 8, "{}: a city has all sorts of buildings", t.name);
+ }
+ }
+
+ #[test]
+ fn passers_by_stay_outdoors() {
+ let mut t = generate(5, Culture::Drift, 0.4, green());
+ let mut rng = seed::rng(1);
+ for _ in 0..300 {
+ for who in 0..t.site.people.len() {
+ t.site.wander(who, &mut rng, t.start);
+ }
+ }
+ for p in &t.site.people {
+ assert!(t.tile(p.pos).outdoors() || p.stays, "{:?} wandered off the street", p.pos);
+ }
+ let mut places: Vec<Pos> = t.site.people.iter().map(|p| p.pos).collect();
+ places.sort_by_key(|p| (p.x, p.y));
+ places.dedup();
+ assert_eq!(places.len(), t.site.people.len(), "no two people in one place");
+ }
+}
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;
+ }
+ }
+}
diff --git a/src/town/words.rs b/src/town/words.rs
new file mode 100644
index 0000000..251f2b2
--- /dev/null
+++ b/src/town/words.rs
@@ -0,0 +1,633 @@
+//! What things in a city are called, and what people there say.
+//!
+//! Names use the same templates as `names`: `{w}` is a new word in the
+//! culture's tongue, `{n}` a number, `{r}` a Roman numeral.
+
+use super::{Kind, Land, Pond, Purpose, QuarterKind, Role, ShopKind, Thing, Tile};
+use crate::names::{Culture, pick};
+use crate::seed::Rng;
+use crate::space::system::Terrain;
+
+fn fill(culture: Culture, templates: &'static [&'static str], rng: &mut Rng) -> String {
+ culture.expand(pick(templates, rng), rng)
+}
+
+pub fn quarter_name(culture: Culture, kind: QuarterKind, rng: &mut Rng) -> String {
+ use QuarterKind::*;
+ let templates: &'static [&'static str] = match (culture, kind) {
+ (Culture::Qaari, Market) => &["the {w} Souk", "the Great Souk", "the Spice Souk"],
+ (Culture::Qaari, Gate) => &["the Caravan Yards", "the {w} Caravanserai", "the Gate of {w}"],
+ (Culture::Qaari, Homes) => &["the {w} Quarter", "the Lanes of {w}", "the Old Wells"],
+ (Culture::Qaari, Poor) => &["the Lower Wells", "the Dust Quarter", "the {w} Warren"],
+ (Culture::Qaari, Grand) => &["the Rise of {w}", "the Shaded Heights", "the Palace Rise"],
+ (Culture::Qaari, Holy) => {
+ &["the Shrine Steps", "the Deep Well Quarter", "the {w} Sanctuary"]
+ }
+ (Culture::Qaari, Works) => {
+ &["the Dyers' Lanes", "the Glassblowers' Quarter", "the Tanneries"]
+ }
+ (Culture::Qaari, Gardens) => &["the Palm Gardens", "the {w} Oasis", "the Date Groves"],
+ (Culture::Cant, Market) => &["the Exchange Floor", "Barter Deck {n}", "the {w} Market"],
+ (Culture::Cant, Gate) => &["the Intake", "Gate Sector {n}", "the Receiving Yards"],
+ (Culture::Cant, Homes) => &["Hab Sector {n}", "the {w} Habs", "Dormitory Row {r}"],
+ (Culture::Cant, Poor) => &["the Undercroft", "the Sump", "Sector {n}-Low"],
+ (Culture::Cant, Grand) => &["the Primarch's Tier", "the Upper Spire", "the {w} Tier"],
+ (Culture::Cant, Holy) => &["the Litany Steps", "the Engine Precinct", "the Fane Sector"],
+ (Culture::Cant, Works) => &["the Foundry Ward", "Pipe Row", "the {w} Works"],
+ (Culture::Cant, Gardens) => {
+ &["the Coolant Pools", "the Fungus Vaults", "the Moss Terraces"]
+ }
+ (Culture::Houses, Market) => &["the {w} Market", "the Marble Exchange", "the Silk Arcade"],
+ (Culture::Houses, Gate) => {
+ &["the Gate of {w}", "the Customs Quarter", "the Carriage Yards"]
+ }
+ (Culture::Houses, Homes) => &["the {w} Quarter", "Lantern Row", "the {w} Walk"],
+ (Culture::Houses, Poor) => &["the Stews", "Tallow Lane", "the {w} Rookery"],
+ (Culture::Houses, Grand) => &["the Archon's Hill", "the {w} Heights", "the Court Quarter"],
+ (Culture::Houses, Holy) => {
+ &["the Cathedral Close", "the {w} Cloisters", "the Choir Quarter"]
+ }
+ (Culture::Houses, Works) => &["the Perfumers' Quarter", "the Ateliers", "Glass Street"],
+ (Culture::Houses, Gardens) => &["the {w} Gardens", "the Pleasure Gardens", "the Orangery"],
+ (Culture::Drift, Market) => &["the Swap", "the {w} Bazaar", "the Hanging Market"],
+ (Culture::Drift, Gate) => &["the Moorings", "the {w} Berths", "the Dockside"],
+ (Culture::Drift, Homes) => &["the Bunks", "{w} Row", "the Hull Quarter"],
+ (Culture::Drift, Poor) => &["the Scrap", "the Leaky End", "the {w} Tangle"],
+ (Culture::Drift, Grand) => &["Captains' Row", "the High Masts", "the {w} Deck"],
+ (Culture::Drift, Holy) => &["the Lantern Quarter", "the Rememberings", "the {w} Vigil"],
+ (Culture::Drift, Works) => &["the Splice", "the Sail Lofts", "the {w} Yards"],
+ (Culture::Drift, Gardens) => {
+ &["the Green Tanks", "the {w} Hydroponics", "the Garden Hulls"]
+ }
+ };
+ fill(culture, templates, rng)
+}
+
+/// Who works in a workshop.
+fn trades(culture: Culture) -> &'static [&'static str] {
+ match culture {
+ Culture::Qaari => &["weaver", "potter", "glassblower", "dyer", "coppersmith", "rugmaker"],
+ Culture::Cant => &["welder", "wire-drawer", "gear-cutter", "plater", "cogwright"],
+ Culture::Houses => &["perfumer", "jeweller", "tailor", "luthier", "mask-maker", "glazier"],
+ Culture::Drift => &["sailmaker", "splicer", "tinker", "chartmaker", "ropemaker"],
+ }
+}
+
+/// What a building is called: "a house", "House Vastre", "the Qasr Khaseem".
+pub fn building_name(kind: Kind, culture: Culture, rng: &mut Rng) -> String {
+ use Culture::*;
+ let templates: &'static [&'static str] = match (kind, culture) {
+ (Kind::Shop(shop), _) => return shop_name(shop, culture, rng),
+ (Kind::House, _) => &["a house"],
+ (Kind::Tenement, Qaari) => &["a courtyard tenement"],
+ (Kind::Tenement, Cant) => &["a hab-block"],
+ (Kind::Tenement, Houses) => &["a lodging house"],
+ (Kind::Tenement, Drift) => &["a bunkhouse"],
+ (Kind::Manor, Qaari) => &["the House of {w}", "the {w} Mansion"],
+ (Kind::Manor, Cant) => &["the {w} Habitation", "Residence {w}-{n}"],
+ (Kind::Manor, Houses) => &["House {w}", "the {w} Townhouse"],
+ (Kind::Manor, Drift) => &["the {w} Berth", "Captain {w}'s"],
+ (Kind::Palace, Qaari) => &["the Qasr {w}"],
+ (Kind::Palace, Cant) => &["the Primarch's Spire"],
+ (Kind::Palace, Houses) => &["the Archon's Palace"],
+ (Kind::Palace, Drift) => &["the Speaker's Hulk"],
+ (Kind::Temple, Qaari) => &["the Deep Well of {w}"],
+ (Kind::Temple, Cant) => &["the Fane of the Engine"],
+ (Kind::Temple, Houses) => &["the Basilica of {w}"],
+ (Kind::Temple, Drift) => &["the Lantern of the Lost"],
+ (Kind::Shrine, Qaari) => &["a well-shrine"],
+ (Kind::Shrine, Cant) => &["a litany-chapel"],
+ (Kind::Shrine, Houses) => &["a chapel"],
+ (Kind::Shrine, Drift) => &["a waystone shrine"],
+ (Kind::Workshop, _) => {
+ let trade = pick(trades(culture), rng);
+ return format!("a {trade}'s workshop");
+ }
+ (Kind::Warehouse, Qaari) => &["a caravan store"],
+ (Kind::Warehouse, Cant) => &["a depot"],
+ (Kind::Warehouse, Houses) => &["a warehouse"],
+ (Kind::Warehouse, Drift) => &["a cargo shed"],
+ (Kind::Bathhouse, Qaari) => &["the {w} Baths", "a bathhouse"],
+ (Kind::Bathhouse, _) => &["the {w} Baths", "a bathhouse"],
+ (Kind::Guardhouse, Qaari) => &["a gatehouse"],
+ (Kind::Guardhouse, Cant) => &["an enforcer post"],
+ (Kind::Guardhouse, Houses) => &["a guardhouse"],
+ (Kind::Guardhouse, Drift) => &["the watch-house"],
+ };
+ fill(culture, templates, rng)
+}
+
+pub fn shop_name(kind: ShopKind, culture: Culture, rng: &mut Rng) -> String {
+ use Culture::*;
+ use ShopKind::*;
+ let templates: &'static [&'static str] = match (kind, culture) {
+ (Broker, Qaari) => &["the {w} Assay", "the Ore Souk of {w}"],
+ (Broker, Cant) => &["the Ore-Tithe of {w}", "Assay Station {n}"],
+ (Broker, Houses) => &["the {w} Exchange", "the Royal Assayer"],
+ (Broker, Drift) => &["{w}'s Scales", "the Weigh-In"],
+ (Doctor, Qaari) => &["the House of Salves", "the {w} Infirmary"],
+ (Doctor, Cant) => &["the Mending Shrine", "Repair Bay {n}"],
+ (Doctor, Houses) => &["Physician {w}'s", "the {w} Hospice"],
+ (Doctor, Drift) => &["the Patchwright", "the Sick Bay"],
+ (Scholar, Qaari) => &["the Sand Archive", "the House of {w}'s Scrolls"],
+ (Scholar, Cant) => &["the Index of Relics", "Archive {r}"],
+ (Scholar, Houses) => &["the {w} Athenaeum", "the {w} Collection"],
+ (Scholar, Drift) => &["the Rememberer", "the Log-Keeper"],
+ (Tavern, Qaari) => &["the {w} Cistern", "the Thirsty Dune", "the {w} Tea House"],
+ (Tavern, Cant) => &["the Oil Hall", "the Burnt Gasket", "Refreshment Unit {n}"],
+ (Tavern, Houses) => &["the {w} Salon", "the Gilded {w}", "the Masquerade"],
+ (Tavern, Drift) => &["the Slack Line", "the {w} Mooring", "the Last Burn"],
+ (Shipyard, Qaari) => &["the Caravan-Wrights", "the {w} Yards"],
+ (Shipyard, Cant) => &["Hull Foundry {w}", "Drydock {n}"],
+ (Shipyard, Houses) => &["the Royal Yards", "the {w} Shipwrights"],
+ (Shipyard, Drift) => &["the Splicers", "{w}'s Slip"],
+ (Armoury, Qaari) => &["the {w} Blade-Souk", "the House of Edges"],
+ (Armoury, Cant) => &["Armoury {r}", "the Arc-Smithy of {w}"],
+ (Armoury, Houses) => &["the {w} Armourers", "the Duellists' Gallery"],
+ (Armoury, Drift) => &["{w}'s Iron", "the Gun Locker"],
+ };
+ fill(culture, templates, rng)
+}
+
+/// What a ruler is called.
+pub fn ruler_title(culture: Culture) -> &'static str {
+ match culture {
+ Culture::Qaari => "the Well-Warden",
+ Culture::Cant => "the Primarch",
+ Culture::Houses => "the Archon",
+ Culture::Drift => "the Speaker",
+ }
+}
+
+/// "A guard", for people without a name.
+pub fn role_name(role: Role, culture: Culture) -> &'static str {
+ match role {
+ Role::Passerby => "A passer-by",
+ Role::Child => "A child",
+ Role::Porter => "A porter",
+ Role::Pilgrim => "A pilgrim",
+ Role::Stallholder => "A stallholder",
+ Role::Guard => match culture {
+ Culture::Cant => "An enforcer",
+ _ => "A guard",
+ },
+ Role::Keeper(_) => "The keeper",
+ Role::Resident => "Someone who lives here",
+ Role::Servant => "A servant",
+ Role::Ruler => ruler_title(culture),
+ Role::Courtier => "A courtier",
+ Role::Priest => match culture {
+ Culture::Qaari => "A well-priest",
+ Culture::Cant => "A tech-cantor",
+ Culture::Houses => "A priest",
+ Culture::Drift => "A lamplighter",
+ },
+ Role::Worshipper => "A worshipper",
+ Role::Worker => "A worker",
+ Role::Patron => "A drinker",
+ Role::Patient => "A patient",
+ Role::Bather => "A bather",
+ Role::Watchman => "A watchman",
+ Role::Reader => "A reader",
+ }
+}
+
+/// What someone says when you bump into them.
+pub fn lines(role: Role, culture: Culture) -> &'static [&'static str] {
+ use Culture::*;
+ match (role, culture) {
+ (Role::Passerby, _) => chatter(culture),
+ (Role::Stallholder, _) => cries(culture),
+ (Role::Child, Qaari) => &[
+ "Is that your ship? Is it faster than a sandstorm?",
+ "I can hold my breath through a whole dust-wind.",
+ "My brother says wrecks have ghosts in them.",
+ ],
+ (Role::Child, Cant) => &[
+ "I am in my apprentice cycle. I'm not supposed to talk to you.",
+ "Do your engines pray? Ours do.",
+ "I counted all the bolts on the wall. There are four thousand and six.",
+ ],
+ (Role::Child, Houses) => &[
+ "Mother says spacers are common. Are you common?",
+ "I'm going to be an Archon when I grow up. Or a pirate.",
+ "Bet you can't catch me!",
+ ],
+ (Role::Child, Drift) => &[
+ "Have you been to the stars? Which one's the best?",
+ "I was born on a ship, you know. I'm only visiting.",
+ "Tag! You're it!",
+ ],
+ (Role::Porter, _) => &[
+ "Mind your back! Heavy load!",
+ "Two more crates and I can eat.",
+ "Out of the way, it's on my head, not yours.",
+ "They pay by the load, not by the step. Shift.",
+ ],
+ (Role::Pilgrim, _) => &[
+ "I've walked forty days to get here.",
+ "They say a prayer here is heard twice.",
+ "Have you come to pray too? You don't look it.",
+ "I promised my mother I'd come. She's been dead nine years.",
+ ],
+ (Role::Guard, Qaari) => &[
+ "Keep your hands where the sun can see them.",
+ "No blades drawn in the souk, offworlder.",
+ "The gate shuts when the third wind blows. Be in, or be out.",
+ ],
+ (Role::Guard, Cant) => &[
+ "Halt. Proceed. You were not stopped.",
+ "Unit. Your papers are in order. Probably.",
+ "Patrol cycle four hundred and twelve. All nominal.",
+ ],
+ (Role::Guard, Houses) => &[
+ "Move along, spacer. Nothing to see but your betters.",
+ "The Archon's peace is kept here.",
+ "Draw steel in this city and you'll leave it lighter.",
+ ],
+ (Role::Guard, Drift) => &[
+ "Easy, spacer. Nobody's looking for trouble.",
+ "Watch shift. Twelve hours of nothing, mostly.",
+ "If anyone asks, I was awake.",
+ ],
+ (Role::Keeper(_), _) => &["What can I do for you?"],
+ (Role::Resident, Qaari) => &[
+ "You'll take water? Everyone who comes in takes water.",
+ "My grandmother built this house with her own hands.",
+ "Shut the door behind you, the sand gets in.",
+ "A guest! Sit, sit. Not there. There.",
+ ],
+ (Role::Resident, Cant) => &[
+ "Unauthorised presence in a hab-unit. I'll allow it.",
+ "The walls hum at night. It's comforting.",
+ "Visitors are logged. You have been logged.",
+ "My shift starts in an hour. Be brief.",
+ ],
+ (Role::Resident, Houses) => &[
+ "We weren't expecting callers. Do forgive the state of things.",
+ "Mind the portraits. Every one of them is watching you.",
+ "If you're selling something, we already have two.",
+ "Oh! A spacer. How thrilling. Please don't touch anything.",
+ ],
+ (Role::Resident, Drift) => &[
+ "Come in, come in. Nobody locks a door in the Drift.",
+ "It's small, but it doesn't move. I'm still getting used to that.",
+ "Got any news from out there?",
+ "Sit anywhere. Everything's somebody's, nothing's anyone's.",
+ ],
+ (Role::Servant, _) => &[
+ "The household is not receiving.",
+ "I'm to polish the silver before the bell.",
+ "You didn't see me sitting down.",
+ "Wipe your feet. Please. I just did that floor.",
+ ],
+ (Role::Ruler, Qaari) => &[
+ "Every drop in this city is counted, offworlder, and I count them.",
+ "Sit in my shade. Few strangers are offered it.",
+ "The deep erg gives and the deep erg takes. I only keep the ledger.",
+ ],
+ (Role::Ruler, Cant) => &[
+ "The Engine wills, and I implement. State your function.",
+ "You are a variable. I dislike variables.",
+ "Efficiency is devotion. You are wasting both.",
+ ],
+ (Role::Ruler, Houses) => &[
+ "We so rarely receive spacers. How novel.",
+ "Kneel, if you know how. No? Then bow. No? Then stand there.",
+ "My ancestors held this city for eleven hundred years. I intend to beat that.",
+ ],
+ (Role::Ruler, Drift) => &[
+ "I don't rule. Someone has to hold the tiller, that's all.",
+ "The council meets when it meets. Sit, if you like.",
+ "Every ship that docks here is family until it isn't.",
+ ],
+ (Role::Courtier, _) => &[
+ "Petitions are heard at noon. It is never noon.",
+ "I've been waiting for an audience since the last eclipse.",
+ "Don't stand there. That's where the last ambassador stood.",
+ "Have you heard who dined with whom? No? Then what use are you?",
+ ],
+ (Role::Priest, Qaari) => &[
+ "The deep well answers every question with silence. Silence is an answer.",
+ "Drink, and remember who dug the well before you.",
+ "Water remembers. That is why we pray over it.",
+ ],
+ (Role::Priest, Cant) => &[
+ "Blessed be the torque. Blessed be the tolerance.",
+ "Every machine is a prayer that works.",
+ "The Engine turns. Will you turn with it?",
+ ],
+ (Role::Priest, Houses) => &[
+ "The saints loved this city once. We are trying to remind them.",
+ "Light a candle, if you have a coin. Light two, if you have a sin.",
+ "Even the Archon kneels here. Briefly.",
+ ],
+ (Role::Priest, Drift) => &[
+ "Every lantern here is someone who didn't come home.",
+ "We remember the lost so the void doesn't have to.",
+ "If you board wrecks, light one for whoever you find.",
+ ],
+ (Role::Worshipper, _) => &[
+ "Shh.",
+ "...and keep the void from the hull, and the hull from the void...",
+ "I'm praying. Go away. Kindly.",
+ "Pray with us, or step softly.",
+ ],
+ (Role::Worker, _) => &[
+ "Mind the edge, it's sharp.",
+ "Commissions take a month. Two if you're rude.",
+ "Every piece made by hand. My hands. Look at them.",
+ "Pass me that. No, the other one.",
+ ],
+ (Role::Patron, _) => &[
+ "Buy a round and I'll tell you a story. Two rounds, a true one.",
+ "This is my stool. It knows me.",
+ "The wrecks out past the belt? Cursed, the lot of them.",
+ "One more and I'll go home. I said that last night too.",
+ "You've got the look of someone who boards wrecks.",
+ ],
+ (Role::Patient, _) => &[
+ "Don't mind me. I'm dying, slightly.",
+ "The doctor says I'll mend. The doctor says that to everyone.",
+ "Got bitten by something aboard a wreck. Don't ask what.",
+ ],
+ (Role::Bather, _) => &[
+ "Ah. Heaven is hot water.",
+ "Close the door, you're letting the steam out.",
+ "Best thing about a planet: baths.",
+ ],
+ (Role::Watchman, _) => &[
+ "Nothing in here but crates. And me.",
+ "If you're thinking of stealing, think harder.",
+ "I count the crates every hour. Still forty-one.",
+ ],
+ (Role::Reader, _) => &[
+ "Shh. Some of us are reading.",
+ "Did you know the old empire had nine words for vacuum?",
+ "I've been on this page for three days.",
+ ],
+ }
+}
+
+/// What passers-by say when you bump into them.
+pub fn chatter(culture: Culture) -> &'static [&'static str] {
+ match culture {
+ Culture::Qaari => &[
+ "Water and shade to you, offworlder.",
+ "The caravan from the deep erg is three days late. Three!",
+ "Don't drink from the public cistern after dusk. Old law.",
+ "My cousin crews an ore hauler. He says the belt sings at night.",
+ "You smell of ship. Go to the tavern; everyone does.",
+ "The glass storms took the east road again.",
+ ],
+ Culture::Cant => &[
+ "Unit. Your hull is unblessed. Have it seen to.",
+ "The litany runs at the third bell. Attendance is noted.",
+ "Every bolt remembers the hand that turned it.",
+ "Salvage is sacrament. Do not waste it.",
+ "I was a pump-servitor before my promotion. I miss the pumps.",
+ "Do not touch the pipes. The pipes are listening.",
+ ],
+ Culture::Houses => &[
+ "Mind your boots on the marble, spacer.",
+ "A House is hosting again. Nobody was invited, so everybody will go.",
+ "They say the Archon's heir has taken up mining. Imagine.",
+ "The fountain was a gift. Everything here was a gift. Everything has a price.",
+ "Do you dance? No? Pity.",
+ "Never bow lower than the person you're bowing to.",
+ ],
+ Culture::Drift => &[
+ "Hey, spacer. Long burn?",
+ "Planets make me dizzy. Too much down.",
+ "Trade you a song for a ration bar.",
+ "My mother was born between stars. So was I. It's strange, being here.",
+ "Somebody's always leaving. That's the Drift.",
+ "If you see a blue hull with a broken mast, that's my aunt. Wave.",
+ ],
+ }
+}
+
+/// What stallholders call out.
+pub fn cries(culture: Culture) -> &'static [&'static str] {
+ match culture {
+ Culture::Qaari => &[
+ "Dates! Sun-dates, sweet as a promise!",
+ "Spiced locust, two for a ¤!",
+ "Rugs! Woven by hand, by grandmother's hand!",
+ "Water-skins, never leaked, never once!",
+ ],
+ Culture::Cant => &[
+ "Bearings, blessed and greased!",
+ "Lubricant, consecrated this very morning!",
+ "Prayer-wire by the metre!",
+ "Spare parts for spare souls!",
+ ],
+ Culture::Houses => &[
+ "Candied petals, fit for a court!",
+ "Silks from the Gardens!",
+ "Scent of the old capital, one drop a ¤!",
+ "Masks for the season's balls!",
+ ],
+ Culture::Drift => &[
+ "Patches, hull or heart!",
+ "Songs, fresh from the void!",
+ "Ration bars, hardly expired!",
+ "Charts of places that might still be there!",
+ ],
+ }
+}
+
+pub fn room_name(purpose: Purpose, culture: Culture) -> &'static str {
+ use Purpose::*;
+ match purpose {
+ Living => "the front room",
+ Bedroom => "a bedroom",
+ Kitchen => "the kitchen",
+ Store => "a storeroom",
+ Lodging => "someone's room",
+ Taproom => "the taproom",
+ Parlour => "the back room",
+ Cellar => "the cellar",
+ Counter => "the shop",
+ Ward => "the ward",
+ Stacks => "the stacks",
+ Assay => "the assay room",
+ Yard => "the yard",
+ Smithy => "the smithy",
+ Hall => "the hall",
+ Dining => "the dining room",
+ Library => "the library",
+ Bedchamber => "a bedchamber",
+ Gallery => "the gallery",
+ Throne => "the throne room",
+ Council => "the council chamber",
+ Treasury => "the treasury",
+ Nave => match culture {
+ Culture::Qaari => "the well-court",
+ Culture::Cant => "the engine hall",
+ Culture::Houses => "the nave",
+ Culture::Drift => "the lantern hall",
+ },
+ Vestry => "the vestry",
+ Cell => "a cell",
+ Shrine => "the shrine",
+ Workroom => "the workshop",
+ Storehouse => "the store",
+ Office => "the office",
+ Changing => "the changing room",
+ Baths => "the baths",
+ Steam => "the steam room",
+ Guardroom => "the guardroom",
+ Bunks => "the bunk room",
+ }
+}
+
+/// What bumping into a piece of furniture tells you.
+pub fn thing_about(thing: Thing, culture: Culture) -> &'static str {
+ use Culture::*;
+ use Thing::*;
+ match (thing, culture) {
+ (Table, _) => "A table, ringed with old cups.",
+ (Chair | Pew | Bench | Throne, _) => "A seat.",
+ (Bed, Qaari) => "A sleeping mat on a low frame, piled with rugs.",
+ (Bed, Cant) => "A cot of welded mesh. Exactly as comfortable as required.",
+ (Bed, Houses) => "A canopied bed, its curtains heavy with dust.",
+ (Bed, Drift) => "A hammock-bed, rigged as if the floor might go away.",
+ (Hearth, Cant) => "A heating coil glows behind a grille.",
+ (Hearth, _) => "A fire crackles in the hearth.",
+ (Stove, _) => "A stove, still warm, smelling of whatever was cooked last.",
+ (Shelf, _) => "Shelves crammed with books, jars and keepsakes.",
+ (Chest, _) => "A chest. Locked, and none of your business.",
+ (Crate, _) => "Crates, stencilled with marks you can't read.",
+ (Barrel, _) => "A barrel. It sloshes.",
+ (Altar, Qaari) => "The sacred well. The water is so deep it's black.",
+ (Altar, Cant) => "The Engine: a vast machine that only turns. It hums in your teeth.",
+ (Altar, Houses) => "An altar of white stone, gold-leafed, crowded with candles.",
+ (Altar, Drift) => "A lantern-mast, a lantern hung on it for everyone lost in the void.",
+ (Pillar, _) => "A pillar, carved all the way up.",
+ (Statue, Qaari) => "A statue of a caravan-master, worn smooth by sandstorms.",
+ (Statue, Cant) => "A statue of a Fabricant, its face a smooth steel plate.",
+ (Statue, Houses) => "A marble statue of someone important, looking down on you.",
+ (Statue, Drift) => "A figurehead, salvaged from some old ship.",
+ (Plant, Qaari) => "A potted palm, carefully watered.",
+ (Plant, _) => "A potted plant, carefully watered.",
+ (Bath, _) => "Steaming water. It smells of minerals.",
+ (Anvil, _) => "An anvil, bright where the hammer falls.",
+ (Forge, _) => "A forge, roaring. The heat pushes you back.",
+ (Loom, _) => "A loom, strung with half a rug.",
+ (Workbench, _) => "A workbench, scattered with tools and offcuts.",
+ (Rack, _) => "A rack of pikes and stun-staves.",
+ (Lamp, Cant) => "A glow-tube on a stand, flickering.",
+ (Lamp, _) => "A lamp on a tall stand.",
+ (Engine, _) => "Half a ship's engine, opened up like a patient.",
+ }
+}
+
+/// What bumping into something in the street tells you, if anything, in
+/// a city of `culture` on `land`. For `Tile::Wild`, see `wild_about`.
+pub fn tile_about(tile: Tile, culture: Culture, land: Land) -> Option<&'static str> {
+ use Culture::*;
+ Some(match (tile, culture) {
+ (Tile::Centrepiece, Qaari) => "The cistern's water is cool, dark and very still.",
+ (Tile::Centrepiece, Cant) => "The furnace roars. Prayers are welded to its flue.",
+ (Tile::Centrepiece, Houses) => {
+ "Coins glint at the bottom of the fountain. Wishes, or bribes."
+ }
+ (Tile::Centrepiece, Drift) => {
+ "A mast of welded hull-plate, hung with a hundred little bells."
+ }
+ (Tile::Statue, _) => thing_about(Thing::Statue, culture),
+ (Tile::Well, Qaari) => "A covered well, padlocked. Water isn't free here.",
+ (Tile::Well, Cant) => "A coolant tap, sealed with a litany-tag.",
+ (Tile::Well, Houses) => "A little fountain, a stone fish spitting into a basin.",
+ (Tile::Well, Drift) => "A water tank on legs, with a cup on a chain.",
+ (Tile::Water, _) => pond_about(land),
+ (Tile::Sea, _) => "The sea, grey-green and restless, all the way to the sky.",
+ (Tile::Tree, _) => tree_about(culture, land.terrain),
+ (Tile::Lamp, Cant) => "A glow-tube on a post, buzzing.",
+ (Tile::Lamp, _) => "A lamp post.",
+ (Tile::Decor, Drift) => "A heap of salvage, sorted by someone, for some reason.",
+ (Tile::Decor, _) => "A tangle of pipes, warm to the touch.",
+ (Tile::Roof, _) => return None,
+ _ => return None,
+ })
+}
+
+fn pond_about(land: Land) -> &'static str {
+ match (land.pond(), land.terrain) {
+ (Pond::Water, Terrain::Desert) => "A cistern pool, shaded and very still.",
+ (Pond::Water, _) => "A pond. Fish, or something like them, move under the surface.",
+ (Pond::Ice, Terrain::Ice) => "A pond frozen to the bottom, clear as glass.",
+ (Pond::Ice, _) => "A frozen pond. Something dark moves slowly under the ice.",
+ (Pond::Dry, _) => "A pond, dried to cracked mud. Someone's raked the cracks.",
+ (Pond::Lava, _) => "A pool of molten rock behind an iron rail. The heat is ferocious.",
+ }
+}
+
+fn tree_about(culture: Culture, terrain: Terrain) -> &'static str {
+ match (terrain, culture) {
+ (Terrain::Desert, Culture::Qaari) => "A date palm, rattling in the hot wind.",
+ (Terrain::Desert, _) => "A date palm, its fronds grey with dust.",
+ (Terrain::Ice, _) => "A glass-pine. Its needles chime when the wind moves them.",
+ (Terrain::Ocean | Terrain::Jungle, Culture::Houses) => {
+ "A clipped yew, shaped like a bird. Something keeps trying to unclip it."
+ }
+ (Terrain::Ocean, _) => "A salt-pine, leaning away from the sea wind.",
+ (Terrain::Jungle, _) => "A strangler fig, wound round what used to be another tree.",
+ (Terrain::Barren | Terrain::Clouds, _) => {
+ "A spire of violet crystal, taller than you. It's growing, very slowly."
+ }
+ (Terrain::Forge, _) => "A fungus stalk as tall as a lamp post, warm and faintly pulsing.",
+ }
+}
+
+/// What bumping into the `sort`th sort of thing outside the walls tells
+/// you (see `Town::wild_sort`).
+pub fn wild_about(terrain: Terrain, sort: usize) -> &'static str {
+ let [first, second] = match terrain {
+ Terrain::Desert => [
+ "A cactus, fat with stored water and bristling with spines.",
+ "A boulder, scoured smooth by the sand.",
+ ],
+ Terrain::Ice => [
+ "A spire of blue ice, taller than you and slowly leaning.",
+ "A boulder, glazed with ice.",
+ ],
+ Terrain::Ocean => {
+ ["Black rock, slick with spray.", "A heap of kelp, washed up and drying. It smells."]
+ }
+ Terrain::Jungle => [
+ "Trees, crowded so close you can't push between them.",
+ "A wall of ferns and creepers, alive with clicking.",
+ ],
+ Terrain::Barren | Terrain::Clouds => [
+ "A crag of grey rock, crusted with rust-red lichen.",
+ "A cluster of violet crystals, knee-high.",
+ ],
+ Terrain::Forge => {
+ ["A heap of black basalt, still warm.", "A vent, breathing hot, sulphurous air."]
+ }
+ };
+ if sort == 0 { first } else { second }
+}
+
+/// What the road out of a gate leads to.
+pub fn beyond(terrain: Terrain) -> &'static str {
+ match terrain {
+ Terrain::Desert => "The road runs on into the dunes. There's nothing out there for you.",
+ Terrain::Ice => {
+ "The road runs on across the snowfields. There's nothing out there for you."
+ }
+ Terrain::Ocean => {
+ "The causeway runs on out to sea, to the fishing piers. There's nothing out there \
+ for you."
+ }
+ Terrain::Jungle => "The road runs on under the canopy. There's nothing out there for you.",
+ Terrain::Barren | Terrain::Clouds => {
+ "The road runs on across the grey dust. There's nothing out there for you."
+ }
+ Terrain::Forge => {
+ "The road runs on between smoking lava fields. There's nothing out there for you."
+ }
+ }
+}