diff options
Diffstat (limited to 'src/town')
| -rw-r--r-- | src/town/interior.rs | 1112 | ||||
| -rw-r--r-- | src/town/mod.rs | 850 | ||||
| -rw-r--r-- | src/town/plan.rs | 1230 | ||||
| -rw-r--r-- | src/town/words.rs | 633 |
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." + } + } +} |