//! The game loop and the scene stack. use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::io; use std::path::PathBuf; use std::time::{Duration, Instant}; use ratatui::DefaultTerminal; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crate::dungeon::Pos; use crate::dungeon::arms::Dmg; use crate::dungeon::crawl::SavedCrawl; use crate::items::{self, Gear, Item, Stack}; use crate::save::{self, Character, Record}; use crate::scenes::inventory::Sort; use crate::scenes::{Scene, Transition, title::Title}; use crate::seed; use crate::space::galaxy::{self, Galaxy}; use crate::space::system::{System, Wreck}; use crate::term::{self, KeyboardCaps}; use crate::trade::Rack; /// Simulation rate. Real-time scenes (flight) move by this many steps a /// second; turn-based scenes (dungeons) can ignore ticks and react to keys. pub const TICKS_PER_SECOND: u32 = 30; const TICK: Duration = Duration::from_nanos(1_000_000_000 / TICKS_PER_SECOND as u64); /// After a stall (suspended with ^Z, a slow terminal), run at most this many /// missed ticks, then carry on from now instead of fast-forwarding. const MAX_CATCH_UP: u32 = 5; /// Without key releases, a held key's auto-repeats look like presses. After /// the scene changes, presses are dropped for as long as they keep coming /// closer together than this, as auto-repeats do. const REPEAT_GAP: Duration = Duration::from_millis(150); /// State every scene can see. pub struct Ctx { /// The universe seed. Everything procedural derives from it (see `seed`). pub seed: u64, /// Who you're playing. pub character: Character, /// What they've done. pub record: Record, /// Ticks since the game started. pub ticks: u64, pub keyboard: KeyboardCaps, /// How many derelicts you've boarded. Each boarding is a different ship, /// `derive(seed, "derelict", n)`. pub boarded: u64, pub hold: Hold, /// Wrecks you've boarded, by seed. There's nothing left aboard them. pub stripped: HashSet, /// Every star system, and the lanes between them. pub galaxy: Galaxy, /// Which of the galaxy's systems you're in: an index into `galaxy.sites`. pub star: usize, /// The systems you've been to. They, and the systems a lane joins to /// them, are charted: you can plot a route through them. pub visited: BTreeSet, /// The star system you're in, generated whole. pub system: System, /// What to tell you when you next take the controls, after a journey /// the map can describe better than the flight can. pub arrival_note: Option, /// Days since the game began. It moves on only when you travel, and /// everything in the system moves with it. pub day: f64, /// The day you set out for where you are now, while you're still /// there. Wrecks that were calling then are still here, even if their /// beacons have failed on the way: you came by their last fix. `None` /// after a jump or a tow, which bring you somewhere you didn't choose. pub departed: Option, /// Where in the system you are: an index into `system.places`. pub location: usize, /// Whether your ship is set down at `location`, rather than flying. pub landed: bool, /// Better lenses fitted to the mining laser. pub lenses: u32, /// How each kind of monster takes each kind of damage, as far as you've /// found out. Unlike what you carry, you keep this whatever happens. pub bestiary: BTreeSet<(&'static str, Dmg)>, /// The wreck you're aboard, as it stood after your last move, so a save /// can bring you back to it. `None` when you're not in one. pub crawl: Option, /// Armourers' racks you've traded at this fortnight, as you left them. pub racks: Vec, /// How your inventory's lists are sorted. pub inventory_sort: Sort, /// The folder saves go in. Without one, nothing is saved. pub saves: Option, /// This character's save file. There's none until you've made or /// loaded a character, and nothing is saved until there is. pub slot: Option, /// Why the last save failed, if it did. pub save_error: Option, /// Keys held down right now. Only kept on terminals that report key /// releases (`keyboard.key_release`); elsewhere there's no telling when /// a key is let go, and this stays empty. held: HashSet, /// Keys that were down when the scene last changed. They don't count as /// held, and their auto-repeats are dropped, until they're let go: the /// step that takes you out through an airlock shouldn't carry on /// walking you about on the other side. carried: HashSet, /// The same for terminals that don't report releases: while the scene /// has only just changed, when the last key came. settling: Option, } impl Ctx { pub fn new(seed: u64, keyboard: KeyboardCaps) -> Ctx { let galaxy = galaxy::generate(seed); let star = galaxy.home; let system = galaxy.system(star); // Someone to be until you make a character of your own. let culture = system.culture; let character = Character { name: culture.person(&mut seed::rng(seed::derive(seed, "you", 0))), origin: culture, ship: culture.word(&mut seed::rng(seed::derive(seed, "your ship", 0))), }; Ctx { character, record: Record::default(), landed: false, crawl: None, racks: Vec::new(), inventory_sort: Sort::default(), saves: None, slot: None, save_error: None, location: system.home_place(), system, galaxy, star, visited: BTreeSet::from([star]), arrival_note: None, day: 0.0, departed: None, lenses: 0, bestiary: BTreeSet::new(), seed, ticks: 0, keyboard, boarded: 0, hold: Hold::default(), stripped: HashSet::new(), held: HashSet::new(), carried: HashSet::new(), settling: None, } } /// Starts a new game for `character` in universe `seed`, keeping only /// what belongs to the terminal: the clock, the keyboard, where saves go. /// The slot is cleared; whoever calls this decides where the new game /// saves. /// You start at the seat of your own people, on their homeworld. pub fn begin(&mut self, seed: u64, character: Character) { let mut fresh = Ctx::new(seed, self.keyboard); let seat = fresh.galaxy.seat(character.origin); if seat != fresh.star { fresh.visited.clear(); fresh.enter_system(seat); fresh.location = fresh.system.home_place(); } fresh.character = character; fresh.ticks = self.ticks; fresh.saves = self.saves.take(); fresh.held = std::mem::take(&mut self.held); fresh.carried = std::mem::take(&mut self.carried); fresh.settling = self.settling; *self = fresh; } /// Arrives in another system, at its edge, in flight. pub fn enter_system(&mut self, star: usize) { self.star = star; self.system = self.galaxy.system(star); self.visited.insert(star); self.location = self.system.edge_place(); self.landed = false; self.departed = None; } /// Flies to `place` in this system, `days` away. pub fn travel(&mut self, place: usize, days: f64) { self.departed = Some(self.day); self.day += days; self.location = place; } /// The wrecks you know of in this system that you haven't stripped: /// those whose beacons are calling, and those here you came by a /// failed beacon's last fix. Oldest first. pub fn wrecks(&self) -> Vec { let s = &self.system; let mut wrecks = s.beacons(self.day, &self.stripped); wrecks.retain(|w| w.place != self.location); wrecks.extend(s.wrecks_at(self.location, self.day, self.departed, &self.stripped)); wrecks.sort_by(|a, b| a.arrived.total_cmp(&b.arrived)); wrecks } /// Whether you're out of fuel somewhere no one sells any. pub fn stranded(&self) -> bool { self.hold.fuel == 0 && !self.system.sells_fuel() } /// Saves the game, if there's a character to save. pub fn autosave(&mut self) { self.save_error = save::write(self).err().map(|e| format!("couldn't save: {e}")); } pub fn is_held(&self, code: KeyCode) -> bool { let code = normalise(code); self.held.contains(&code) && !self.carried.contains(&code) } /// Whether the top scene should get this key event. Keeps track of /// which keys are held, too. fn admit(&mut self, key: &KeyEvent, now: Instant) -> bool { let code = normalise(key.code); if self.keyboard.key_release { // Scenes get presses and auto-repeats; releases only update // which keys are held. return match key.kind { KeyEventKind::Release => { self.held.remove(&code); self.carried.remove(&code); false } KeyEventKind::Repeat if self.carried.contains(&code) => false, _ => { self.held.insert(code); self.carried.remove(&code); true } }; } match self.settling { _ if key.kind == KeyEventKind::Release => false, Some(last) if now.saturating_duration_since(last) < REPEAT_GAP => { self.settling = Some(now); false } _ => { self.settling = None; true } } } /// What's on screen changed under keys that are still down, without /// the scene changing (a summary popped up over it): treat them as the /// scene changing would, so a held key doesn't dismiss it unread. pub fn settle(&mut self) { self.scene_changed(Instant::now()); } /// The top scene changed: keys still down were for the old one. fn scene_changed(&mut self, now: Instant) { self.carried.clone_from(&self.held); if !self.keyboard.key_release { self.settling = Some(now); } } } /// Letters count as the same key whatever the shift state, so releasing /// shift mid-hold doesn't leave a key stuck down. fn normalise(code: KeyCode) -> KeyCode { match code { KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()), other => other, } } /// Everything you own: money, ore and fuel, what you carry, and what's /// stowed aboard your ship. pub struct Hold { /// Money, in ¤. pub scrip: u32, /// Tons of each kind of ore. pub ore: BTreeMap<&'static str, u32>, /// Tons of ore the hold can carry; cargo racks add more. pub ore_capacity: u32, /// What's on you: worn, ready and in your pack. It goes everywhere you /// go, wrecks too, and if you don't get out of one, all but your own /// torch and rivet gun stays there. pub gear: Gear, /// The ship's lockers, safe aboard whatever happens to you. Shops can /// reach them, and send what you buy to them. pub lockers: Vec, /// What you've put down on your ship's deck, and where. pub deck: Vec<(Pos, Stack)>, /// Jumps' worth of fuel in the tank. Each jump burns one, however long. pub fuel: u32, /// How many jumps' worth the tank holds; extra tanks add more. pub fuel_capacity: u32, /// What you owe, in ¤: a tow you couldn't pay for. What you earn pays it /// off first. pub debt: u32, } /// Where something you own is kept, for a shop. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kept { Pack(usize), Lockers(usize), } impl Kept { /// "", or " (in your ship)". pub fn note(self) -> &'static str { match self { Kept::Pack(_) => "", Kept::Lockers(_) => " (in your ship)", } } } impl Default for Hold { fn default() -> Hold { Hold { scrip: 0, ore: BTreeMap::new(), ore_capacity: Hold::BASE_ORE_CAPACITY, gear: Gear::default(), lockers: Vec::new(), deck: Vec::new(), fuel: Hold::BASE_FUEL, fuel_capacity: Hold::BASE_FUEL, debt: 0, } } } impl Hold { /// Tons of ore the hold carries before any racks are fitted. pub const BASE_ORE_CAPACITY: u32 = 40; /// Jumps the tank holds before any extra tanks are fitted. pub const BASE_FUEL: u32 = 4; /// Takes in money, paying off any debt first. Returns how much went to /// the debt. pub fn earn(&mut self, amount: u32) -> u32 { let paid = amount.min(self.debt); self.debt -= paid; self.scrip += amount - paid; paid } /// Pays a fee from what you have, and owes the rest. Returns how much /// is owed. pub fn charge(&mut self, fee: u32) -> u32 { let paid = fee.min(self.scrip); self.scrip -= paid; self.debt += fee - paid; fee - paid } pub fn ore_tons(&self) -> u32 { self.ore.values().sum() } /// Everything a shop can reach: what's in your pack, then what's in /// your lockers. pub fn kept(&self) -> impl Iterator { let pack = self.gear.pack.iter().enumerate().map(|(i, s)| (Kept::Pack(i), s)); pack.chain(self.lockers.iter().enumerate().map(|(i, s)| (Kept::Lockers(i), s))) } pub fn kept_mut(&mut self, at: Kept) -> Option<&mut Stack> { match at { Kept::Pack(i) => self.gear.pack.get_mut(i), Kept::Lockers(i) => self.lockers.get_mut(i), } } /// Takes up to `count` of what's kept `at`. pub fn take_kept(&mut self, at: Kept, count: u32) -> Stack { match at { Kept::Pack(i) => items::take(&mut self.gear.pack, i, count), Kept::Lockers(i) => items::take(&mut self.lockers, i, count), } } /// You lose everything you were `carrying` but your own torch and rivet /// gun. If you'd left one in the lockers, you take it out again. pub fn lose_carried(&mut self, carrying: &Gear) { let lockers = &mut self.lockers; self.gear = carrying.stripped(|gun| { let own = |s: &Stack| matches!(&s.item, Item::Weapon(w) if w.kit && w.is_gun() == gun); let i = lockers.iter().position(own)?; match items::take(lockers, i, 1).item { Item::Weapon(w) => Some(w), _ => None, } }); } /// How many artifacts you have, on you and aboard. pub fn artifacts(&self) -> u32 { self.kept().filter(|(_, s)| matches!(s.item, Item::Artifact(_))).map(|(_, s)| s.count).sum() } /// How many medpatches you have, on you and aboard. pub fn medpatches(&self) -> u32 { self.kept().filter(|(_, s)| s.item == Item::Medpatch).map(|(_, s)| s.count).sum() } /// Puts something you've bought in your pack if there's room, and in /// your lockers if not. Says whether it's on you. pub fn deliver(&mut self, stack: Stack) -> bool { if self.gear.fits(&stack.item) >= stack.count { self.gear.add(stack); true } else { items::stow(&mut self.lockers, stack); false } } pub fn locker_mass(&self) -> u32 { items::pile_mass(&self.lockers) } pub fn ore_room(&self) -> u32 { self.ore_capacity.saturating_sub(self.ore_tons()) } } /// Plays. Given a universe, starts a new character in it; otherwise picks up /// the character you last played, if there is one. pub fn run(universe: Option) -> io::Result<()> { let (mut terminal, keyboard) = term::init()?; let mut ctx = Ctx::new(universe.unwrap_or_else(seed::from_clock), keyboard); ctx.saves = save::default_dir(); if universe.is_none() && let Some(dir) = &ctx.saves { let last = save::list(dir).into_iter().find(|s| s.summary.is_ok()); if let Some(slot) = last { let _ = save::load(&slot.path, &mut ctx); } } let result = run_loop(&mut terminal, &mut ctx); ctx.autosave(); term::restore(keyboard); result } /// After the stack changes: the new top scene catches up, and the game is /// saved. fn changed(stack: &mut [Box], ctx: &mut Ctx) { ctx.scene_changed(Instant::now()); if let Some(top) = stack.last_mut() { top.on_top(ctx); } ctx.autosave(); } fn run_loop(terminal: &mut DefaultTerminal, ctx: &mut Ctx) -> io::Result<()> { let mut stack: Vec> = vec![Box::new(Title::new(ctx))]; let mut next_tick = Instant::now() + TICK; while let Some(top) = stack.last() { terminal.draw(|frame| top.render(frame, ctx))?; // Wait for input until the next tick is due, then take whatever else // is already queued without waiting again. let mut timeout = next_tick.saturating_duration_since(Instant::now()); while event::poll(timeout)? { timeout = Duration::ZERO; let Event::Key(key) = event::read()? else { continue; }; if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { return Ok(()); } if !ctx.admit(&key, Instant::now()) { continue; } let Some(top) = stack.last_mut() else { break }; let transition = top.key(key, ctx); if apply(&mut stack, transition) { changed(&mut stack, ctx); } } let mut caught_up = 0; while Instant::now() >= next_tick { let Some(top) = stack.last_mut() else { break }; ctx.ticks += 1; let transition = top.tick(ctx); if apply(&mut stack, transition) { changed(&mut stack, ctx); } next_tick += TICK; caught_up += 1; if caught_up == MAX_CATCH_UP { next_tick = Instant::now() + TICK; } } } Ok(()) } /// Changes the scene stack, and says whether it did. fn apply(stack: &mut Vec>, transition: Transition) -> bool { match transition { Transition::None => return false, Transition::Push(scene) => stack.push(scene), Transition::Open(scenes) => stack.extend(scenes), Transition::Pop => { stack.pop(); } Transition::Replace(scenes) => { stack.pop(); stack.extend(scenes); } Transition::Title => stack.truncate(1), Transition::Quit => stack.clear(), } true } #[cfg(test)] mod tests { use super::*; fn event(code: KeyCode, kind: KeyEventKind) -> KeyEvent { KeyEvent::new_with_kind(code, KeyModifiers::NONE, kind) } #[test] fn going_to_the_title_closes_everything_above_it() { let ctx = Ctx::new(1, KeyboardCaps { key_release: false }); let mut stack: Vec> = vec![Box::new(Title::new(&ctx))]; assert!(apply(&mut stack, Transition::Open(crate::scenes::flight::resume(&ctx)))); assert!(stack.len() > 1); assert!(apply(&mut stack, Transition::Title)); assert_eq!(stack.len(), 1); } #[test] fn keys_held_through_a_change_of_scene_stay_behind() { let mut ctx = Ctx::new(1, KeyboardCaps { key_release: true }); let now = Instant::now(); assert!(ctx.admit(&event(KeyCode::Left, KeyEventKind::Press), now)); assert!(ctx.is_held(KeyCode::Left)); ctx.scene_changed(now); assert!(!ctx.is_held(KeyCode::Left)); assert!(!ctx.admit(&event(KeyCode::Left, KeyEventKind::Repeat), now)); assert!(ctx.admit(&event(KeyCode::Right, KeyEventKind::Press), now), "others get through"); assert!(!ctx.admit(&event(KeyCode::Left, KeyEventKind::Release), now)); assert!(ctx.admit(&event(KeyCode::Left, KeyEventKind::Press), now)); assert!(ctx.is_held(KeyCode::Left)); } #[test] fn without_key_releases_auto_repeats_stay_behind() { let mut ctx = Ctx::new(1, KeyboardCaps { key_release: false }); let start = Instant::now(); let at = |ms| start + Duration::from_millis(ms); let left = event(KeyCode::Left, KeyEventKind::Press); ctx.scene_changed(start); assert!(!ctx.admit(&left, at(30))); assert!(!ctx.admit(&left, at(60))); assert!(!ctx.admit(&left, at(150))); assert!(ctx.admit(&left, at(400)), "a press after a pause is a new one"); assert!(ctx.admit(&left, at(420)), "and from then on they all count"); } }