summaryrefslogtreecommitdiff
path: root/src/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.rs')
-rw-r--r--src/app.rs110
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(),
+ }
+}