diff options
Diffstat (limited to 'src/app.rs')
| -rw-r--r-- | src/app.rs | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e3fe72a --- /dev/null +++ b/src/app.rs @@ -0,0 +1,110 @@ +//! 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<Artifact>, + /// 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<Box<dyn Scene>> = 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<Box<dyn Scene>>, transition: Transition) { + match transition { + Transition::None => {} + Transition::Push(scene) => stack.push(scene), + Transition::Pop => { + stack.pop(); + } + Transition::Quit => stack.clear(), + } +} |