//! The game loop and the scene stack. use std::io; use std::time::{Duration, Instant}; use ratatui::DefaultTerminal; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crate::names::Artifact; use crate::scenes::{Scene, Transition, title::Title}; use crate::term::{self, KeyboardCaps}; /// 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; /// State every scene can see. pub struct Ctx { /// The universe seed. Everything procedural derives from it (see `seed`). pub seed: u64, /// 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, } impl Ctx { pub fn new(seed: u64, keyboard: KeyboardCaps) -> Ctx { Ctx { seed, ticks: 0, keyboard, boarded: 0, hold: Hold::default() } } } /// What you've brought back from expeditions. #[derive(Default)] pub struct Hold { pub artifacts: Vec, /// Salvage, counted by what it'll sell for. pub scrip: u32, } pub fn run(seed: u64) -> io::Result<()> { let (mut terminal, keyboard) = term::init()?; let ctx = Ctx::new(seed, keyboard); let result = run_loop(&mut terminal, ctx); term::restore(keyboard); result } fn run_loop(terminal: &mut DefaultTerminal, mut ctx: 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(()); } // Scenes get presses and auto-repeats. Releases only arrive on // terminals with `ctx.keyboard.key_release`; when flight needs // held keys, track them here before this filter. if key.kind == KeyEventKind::Release { continue; } let Some(top) = stack.last_mut() else { break }; let transition = top.key(key, &mut ctx); apply(&mut stack, transition); } 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(&mut ctx); apply(&mut stack, transition); next_tick += TICK; caught_up += 1; if caught_up == MAX_CATCH_UP { next_tick = Instant::now() + TICK; } } } Ok(()) } fn apply(stack: &mut Vec>, transition: Transition) { match transition { Transition::None => {} Transition::Push(scene) => stack.push(scene), Transition::Pop => { stack.pop(); } Transition::Quit => stack.clear(), } }