summaryrefslogtreecommitdiff
path: root/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs358
1 files changed, 350 insertions, 8 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 70e9156..88d775c 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,28 +1,62 @@
-//! Command-line arguments, `orrery names` and `orrery ship`.
+//! Command-line arguments, `orrery names`, `ship`, `weapons`, `armour`,
+//! `galaxy`, `system` and `town`.
+
+use std::collections::HashMap;
use crate::dungeon::Pos;
use crate::dungeon::crawl::Crawl;
+use crate::dungeon::{armour, arms};
+use crate::items::Item;
use crate::names::{self, Culture};
-use crate::scenes::derelict::{item_look, tile_look};
+use crate::scenes::derelict::tile_look;
+use crate::scenes::inventory::look;
use crate::seed;
+use crate::space::Vec2;
+use crate::space::galaxy::{self, HALF_HEIGHT, HALF_WIDTH};
+use crate::space::system::{Landing, PlaceKind, SystemKind};
+use crate::space::{orbit, survey};
+use crate::{scenes, town};
pub const USAGE: &str = "\
usage: orrery [--seed N]
orrery names [--culture NAME] [-n COUNT] [--seed N]
orrery ship [-n WHICH] [--seed N]
+ orrery weapons [-n COUNT] [--seed N]
+ orrery armour [-n COUNT] [--seed N]
+ orrery galaxy [--seed N]
+ orrery system [-n WHICH] [--seed N]
+ orrery town [-n WHICH] [--inside] [--seed N]
- --seed N universe seed; the same seed always makes the same universe
+ --seed N universe seed; the same seed always makes the same universe.
+ To play, without it you carry on as whoever you last played;
+ with it, you make a new character in that universe
names print generated names instead of playing
--culture NAME qaari, cant, houses or drift (default: all four)
-n COUNT how many of each kind to print (default: 8)
ship print a whole derelict, as you'd find it on boarding
-n WHICH which derelict: the first you'd board, the second... (default: 1)
+ weapons print weapons as you might find them aboard wrecks
+ -n COUNT how many (default: 12)
+ armour print armour as you might find it aboard wrecks
+ -n COUNT how many (default: 12)
+ galaxy print the star map: every system, its lanes and who holds it
+ system print a star system: orbits, moons, places
+ -n WHICH which system, numbered as `galaxy` lists them (default: the
+ one the universe started in)
+ town print a capital city in the system you start in
+ -n WHICH which capital: the homeworld's is 1, then outwards (default: 1)
+ --inside print the inside of one building of each kind, too
";
pub enum Command {
Play { seed: Option<u64> },
Names(NamesOpts),
Ship { seed: u64, which: u64 },
+ Weapons { seed: u64, count: usize },
+ Armour { seed: u64, count: usize },
+ Galaxy { seed: u64 },
+ System { seed: u64, which: Option<u64> },
+ Town { seed: u64, which: u64, inside: bool },
Help,
}
@@ -34,13 +68,22 @@ pub struct NamesOpts {
pub fn parse(args: &[String]) -> Result<Command, String> {
let mut args = args.iter().map(String::as_str).peekable();
- let sub = args.next_if(|a| matches!(*a, "names" | "ship"));
- let (mut seed, mut culture, mut count) = (None, None, None);
+ let sub = args.next_if(|a| {
+ matches!(*a, "names" | "ship" | "weapons" | "armour" | "galaxy" | "system" | "town")
+ });
+ let (mut seed, mut culture, mut count, mut inside) = (None, None, None, false);
while let Some(arg) = args.next() {
match arg {
"-h" | "--help" => return Ok(Command::Help),
"--seed" => seed = Some(number(args.next(), "--seed")?),
- "-n" if sub.is_some() => count = Some(number(args.next(), "-n")?),
+ "--inside" if sub == Some("town") => inside = true,
+ "-n" if matches!(
+ sub,
+ Some("names" | "ship" | "weapons" | "armour" | "system" | "town")
+ ) =>
+ {
+ count = Some(number(args.next(), "-n")?)
+ }
"--culture" if sub == Some("names") => {
let name = args.next().ok_or("--culture needs a value")?;
culture = Some(Culture::parse(name).ok_or(format!("no culture called {name:?}"))?);
@@ -54,6 +97,23 @@ pub fn parse(args: &[String]) -> Result<Command, String> {
seed: seed.unwrap_or_else(seed::from_clock),
which: count.unwrap_or(1),
}),
+ Some("weapons") => Ok(Command::Weapons {
+ seed: seed.unwrap_or_else(seed::from_clock),
+ count: count.unwrap_or(12) as usize,
+ }),
+ Some("armour") => Ok(Command::Armour {
+ seed: seed.unwrap_or_else(seed::from_clock),
+ count: count.unwrap_or(12) as usize,
+ }),
+ Some("galaxy") => Ok(Command::Galaxy { seed: seed.unwrap_or_else(seed::from_clock) }),
+ Some("system") => {
+ Ok(Command::System { seed: seed.unwrap_or_else(seed::from_clock), which: count })
+ }
+ Some("town") => Ok(Command::Town {
+ seed: seed.unwrap_or_else(seed::from_clock),
+ which: count.unwrap_or(1),
+ inside,
+ }),
_ => Ok(Command::Names(NamesOpts {
cultures: culture.map_or(Culture::ALL.to_vec(), |c| vec![c]),
count: count.unwrap_or(8) as usize,
@@ -80,7 +140,7 @@ pub fn print_ship(seed: u64, which: u64) {
} else if let Some(m) = crawl.monster_at(p) {
m.kind.glyph
} else if let Some(l) = crawl.loot_at(p) {
- item_look(&l.item).0
+ look(&l.stack.item).0
} else {
tile_look(map.tile(p)).0
}
@@ -91,14 +151,296 @@ pub fn print_ship(seed: u64, which: u64) {
let mut rooms: Vec<&str> = map.rooms.iter().map(|r| r.name).collect();
rooms.sort_unstable();
rooms.dedup();
- println!("\n@ you Θ airlock + door * artifact % salvage ! medpatch");
+ println!(
+ "\n@ you Θ airlock + door * artifact ( salvage ! medpatch ) weapon }} gun [ armor % food"
+ );
println!(
"monsters: {}",
crawl.monsters.iter().map(|m| m.kind.name).collect::<Vec<_>>().join(", ")
);
+ let weapons: Vec<String> = crawl
+ .loot
+ .iter()
+ .filter_map(|l| match &l.stack.item {
+ Item::Weapon(w) => Some(format!("{} ({})", w.name(), w.damage_text())),
+ _ => None,
+ })
+ .collect();
+ println!("weapons: {}", weapons.join(", "));
+ let armour: Vec<String> = crawl
+ .loot
+ .iter()
+ .filter_map(|l| match &l.stack.item {
+ Item::Armor(a) => Some(format!("{} ({})", a.name(), a.guards_text())),
+ _ => None,
+ })
+ .collect();
+ println!("armour: {}", armour.join(", "));
println!("rooms: {}", rooms.join(", "));
}
+/// Prints `count` weapons as you might find them aboard wrecks in universe
+/// `seed`, for tuning the tables in `dungeon::arms`.
+pub fn print_weapons(seed: u64, count: usize) {
+ print!("{}", arms::catalogue(&mut seed::rng(seed::derive(seed, "catalogue", 0)), count));
+}
+
+/// Prints `count` pieces of armour as you might find them aboard wrecks in
+/// universe `seed`, for tuning the tables in `dungeon::armour`.
+pub fn print_armour(seed: u64, count: usize) {
+ let mut rng = seed::rng(seed::derive(seed, "armour catalogue", 0));
+ print!("{}", armour::catalogue(&mut rng, count));
+}
+
+/// Prints the galaxy of universe `seed`: a map, then every system by who
+/// holds it, numbered for `orrery system -n`.
+pub fn print_galaxy(seed: u64) {
+ let g = galaxy::generate(seed);
+ // Two light-years to a column, four to a row, so it fits a terminal.
+ let (cols, rows) = ((HALF_WIDTH * 1.2) as usize, (HALF_HEIGHT * 0.6) as usize);
+ let cell = |p: Vec2| {
+ let col = ((p.x / HALF_WIDTH * 1.1 + 1.1) / 2.2 * cols as f64) as usize;
+ let row = ((1.1 - p.y / HALF_HEIGHT * 1.1) / 2.2 * rows as f64) as usize;
+ (col.min(cols - 1), row.min(rows - 1))
+ };
+ let mut grid = vec![vec![' '; cols]; rows];
+ for (i, s) in g.sites.iter().enumerate() {
+ for &j in s.lanes.iter().filter(|&&j| j > i) {
+ let to = g.sites[j].pos;
+ let steps = (s.pos.distance(to) * 2.0) as usize;
+ for k in 1..steps {
+ let (c, r) = cell(s.pos + (to - s.pos) * (k as f64 / steps as f64));
+ grid[r][c] = '·';
+ }
+ }
+ }
+ for (i, s) in g.sites.iter().enumerate() {
+ let (c, r) = cell(s.pos);
+ let letter = s.culture.adjective().chars().next().unwrap();
+ grid[r][c] = if g.is_seat(i) { letter } else { letter.to_ascii_lowercase() };
+ }
+ println!("A galaxy of {} systems (universe {seed})\n", g.sites.len());
+ for row in grid {
+ println!(" {}", row.into_iter().collect::<String>().trim_end());
+ }
+ println!("\n Q/q Qaari C/c Cant H/h High Houses D/d Drift (capitals: seats)");
+ for c in Culture::ALL {
+ let held: Vec<usize> = (0..g.sites.len()).filter(|&i| g.sites[i].culture == c).collect();
+ let seat = &g.sites[g.seat(c)].name;
+ println!("\n{}: {} systems, seat {seat}", c.name().to_uppercase(), held.len());
+ for i in held {
+ let s = &g.sites[i];
+ let start = if i == g.home { " (the universe began here)" } else { "" };
+ let border = s.neighbour.map_or(String::new(), |n| format!(", borders {}", n.name()));
+ let lanes = match s.lanes.len() {
+ 1 => "1 lane".to_string(),
+ n => format!("{n} lanes"),
+ };
+ println!(
+ " {:>3} {:<16}{:<22}{:<16}{lanes}{border}{start}",
+ i + 1,
+ s.name,
+ s.kind.name(),
+ s.star.name
+ );
+ }
+ }
+ let count = |k: SystemKind| g.sites.iter().filter(|s| s.kind == k).count();
+ let kinds: Vec<String> =
+ SystemKind::ALL.into_iter().map(|k| format!("{} {}", count(k), k.name())).collect();
+ println!("\n{}", kinds.join(", "));
+}
+
+/// Prints a star system in universe `seed`: the `which`th as `galaxy`
+/// numbers them, or the one the universe started in.
+pub fn print_system(seed: u64, which: Option<u64>) {
+ let g = galaxy::generate(seed);
+ let site = match which {
+ Some(n) if (1..=g.sites.len() as u64).contains(&n) => n as usize - 1,
+ Some(n) => {
+ println!("There's no system {n}: they're numbered 1 to {}.", g.sites.len());
+ return;
+ }
+ None => g.home,
+ };
+ let s = g.system(site);
+ let kind = match s.kind {
+ SystemKind::Seat => format!("the seat of {}", s.culture.name()),
+ k => format!("{} held by {}", k.name(), s.culture.name()),
+ };
+ println!("The {} system, a {}: {kind} (universe {seed})", s.name, s.star.name);
+ println!("\n {:<26}{:>7}{:>6}{:>9} what", "planet", "AU", "e", "year");
+ for (i, p) in s.planets.iter().enumerate() {
+ let people = p.people.map_or(String::new(), |c| format!(", {}", c.adjective()));
+ let home = if i == s.home { " (home)" } else { "" };
+ println!(
+ " {:<26}{:>7.2}{:>6.2}{:>8.0}d {}{people}{home}",
+ p.name, p.orbit.a, p.orbit.e, p.orbit.period, p.kind.name
+ );
+ for m in &p.moons {
+ let people = m.people.map_or(String::new(), |c| format!(", {}", c.adjective()));
+ let km = m.orbit.a * 149.6e6;
+ println!(
+ " moon {:<21}{:>6.0}k km{:>7.1}d {}{people}",
+ m.name,
+ km / 1000.0,
+ m.orbit.period,
+ m.kind.name
+ );
+ }
+ }
+ println!("\n {} lies from {:.2} to {:.2} AU", s.belt.name, s.belt.inner, s.belt.outer);
+ for st in &s.stations {
+ let point = if st.leading { "L4" } else { "L5" };
+ println!(" {} sits at {}'s {point} point", st.name, s.planets[st.planet].name);
+ }
+
+ let home = s.home_place();
+ let from = s.position(home, 0.0);
+ println!(
+ "\n places (⌂ capital, ¤ trade only, · neither), and the trip from {} on day 0:",
+ s.places[home].name
+ );
+ // The wrecks calling on day 0, when a new game starts.
+ let calling = s.wrecks_on(0.0);
+ for (i, place) in s.places.iter().enumerate() {
+ let indent = if matches!(place.kind, PlaceKind::Moon(..) | PlaceKind::Station(_)) {
+ " "
+ } else {
+ " "
+ };
+ let trip = if i == home {
+ "you are here".to_string()
+ } else {
+ format!("{:.1} days", orbit::plan_trip(from, 0.0, |d| s.position(i, d)))
+ };
+ let wrecks: Vec<String> = calling
+ .iter()
+ .filter(|w| w.place == i)
+ .map(|w| format!("wreck: {}, {} (until day {:.0})", w.name, w.description, w.leaves))
+ .collect();
+ let mark = match s.landing(i) {
+ Landing::Capital(_) => "⌂",
+ Landing::Market(_) => "¤",
+ _ => "·",
+ };
+ let name = format!("{indent}{}", place.name);
+ println!("{mark} {name:<30}{trip:<14}{}", wrecks.join("; "));
+ }
+}
+
+/// Prints the `which`th capital in the system you start in, in universe
+/// `seed`: the homeworld's first, then the others from the star outwards.
+/// With `inside`, prints the inside of one building of each kind too.
+pub fn print_town(seed: u64, which: u64, inside: bool) {
+ let s = crate::space::home_system(seed);
+ let home = s.home_place();
+ let mut capitals: Vec<(usize, Culture)> = (0..s.places.len())
+ .filter_map(|i| match s.landing(i) {
+ Landing::Capital(c) => Some((i, c)),
+ _ => None,
+ })
+ .collect();
+ capitals.sort_by_key(|&(i, _)| i != home);
+ let Some(&(place, culture)) = capitals.get(which.saturating_sub(1) as usize) else {
+ println!("There are only {} capitals in this system.", capitals.len());
+ return;
+ };
+ let people = survey::survey(&s, place, 0.0).population;
+ let land = town::Land::of(&s, place, 0.0).expect("a capital is on a planet or moon");
+ let t = town::generate(town::seed_for(s.seed, place), culture, town::size_for(people), land);
+ let theme = scenes::town::theme(&t);
+ println!(
+ "{}, the capital of {}: a city of {} on {:?} ground, {} by {} \
+ (universe {seed}, capital {which} of {})\n",
+ t.name,
+ s.places[place].name,
+ culture.name(),
+ t.land.terrain,
+ t.width(),
+ t.height(),
+ capitals.len()
+ );
+ let folk: HashMap<Pos, ()> = t.site.people.iter().map(|p| (p.pos, ())).collect();
+ for y in 0..t.height() {
+ let row: String = (0..t.width())
+ .map(|x| {
+ let p = Pos::new(x, y);
+ if folk.contains_key(&p) {
+ '&'
+ } else {
+ scenes::town::street_look(&t, p, &theme, 0).0
+ }
+ })
+ .collect();
+ println!("{}", row.trim_end());
+ }
+ println!("\nquarters:");
+ for q in &t.quarters {
+ println!(" {:<28}{:?}", q.name, q.kind);
+ }
+ println!("\nshops:");
+ let wide = t.shops.iter().map(|s| s.name.chars().count()).max().unwrap_or(0) + 2;
+ for shop in &t.shops {
+ let b = &t.buildings[shop.building];
+ let quarter = t.quarter_at(b.door).map_or("", |q| q.name.as_str());
+ println!(
+ " {:<wide$}{:<10}kept by {}, in {quarter}",
+ shop.name,
+ shop.kind.sign(),
+ shop.keeper
+ );
+ }
+ let mut kinds: Vec<(String, usize)> = Vec::new();
+ for b in &t.buildings {
+ let kind = format!("{:?}", b.kind);
+ match kinds.iter_mut().find(|(k, _)| *k == kind) {
+ Some((_, n)) => *n += 1,
+ None => kinds.push((kind, 1)),
+ }
+ }
+ let kinds: Vec<String> = kinds.iter().map(|(k, n)| format!("{n} {k}")).collect();
+ println!(
+ "\n{} buildings: {}\n{} people in the streets",
+ t.buildings.len(),
+ kinds.join(", "),
+ t.site.people.len()
+ );
+ if !inside {
+ return;
+ }
+ let mut shown: Vec<String> = Vec::new();
+ for (b, building) in t.buildings.iter().enumerate() {
+ let kind = format!("{:?}", building.kind);
+ if building.barred || shown.contains(&kind) {
+ continue;
+ }
+ shown.push(kind);
+ let inside = t.enter(b);
+ println!(
+ "\n{} ({} rooms, {} people)",
+ building.name,
+ inside.room_count(),
+ inside.site.people.len()
+ );
+ for y in 0..inside.site.height {
+ let row: String = (0..inside.site.width)
+ .map(|x| {
+ let p = Pos::new(x, y);
+ if p == inside.start {
+ '@'
+ } else if inside.site.person_at(p).is_some() {
+ '&'
+ } else {
+ scenes::town::inside_look(&t, &inside, p, &theme, 0).0
+ }
+ })
+ .collect();
+ println!(" {}", row.trim_end());
+ }
+ }
+}
+
fn number(value: Option<&str>, flag: &str) -> Result<u64, String> {
let value = value.ok_or(format!("{flag} needs a value"))?;
value.parse().map_err(|_| format!("{flag}: {value:?} isn't a whole number"))