diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 358 |
1 files changed, 350 insertions, 8 deletions
@@ -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")) |