summaryrefslogtreecommitdiff
path: root/src/town/interior.rs
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-11 15:24:37 -0800
committerskylar <me@casper>2026-11-11 15:24:37 -0800
commit9ccf951473e21a8b90c25161156fa09a9b227a7b (patch)
treee7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/town/interior.rs
parentd36c54d996f3c14ae811312dbbe621e12a39edde (diff)
Orrery 0.1.0: the galaxy, flight, cities, inventory and saves
Grows the derelict crawler into the whole game, and gets it ready to publish. - A galaxy of star systems joined by lanes, split between four peoples' territories, with a star map and a system map. Systems are modelled with Kepler orbits, moons inside their Hill spheres, belts past the frost line and stations at Lagrange points. - Flight around one place at a time: arcade flight with a throttle fallback where held keys can't be detected, mining rocks for ore, landing, docking at stations, and boarding wrecks that come and go by the calendar. - Your ship to walk about, and capital cities to land at: generated plans with walls, quarters and streets, building interiors, shops, taverns, scholars and armourers. - Fighting with a weapon and a gun ready, six damage types and how each monster takes them, and armour that guards, wears and can be mended. - Everything you own has a weight: a vim-keyed inventory, a pack and the ship's lockers, and trade in ore, salvage, arms and fuel. - Characters with a people and a ship of their own, saved as versioned JSON on every scene change and on quit, with a character sheet, a saves list and a pause menu wherever you are. - `orrery weapons`, `armour`, `galaxy`, `system` and `town` print the generators' output without the TUI. - Release: README install steps for macOS and Linux, a Makefile whose install also works with macOS's install, an MIT licence, and Cargo metadata with rust-version 1.88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/town/interior.rs')
-rw-r--r--src/town/interior.rs1112
1 files changed, 1112 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);
+ }
+ }
+ }
+}