summaryrefslogtreecommitdiff
path: root/src/app.rs
blob: e3fe72a1473b32f1e53c97c50bd3688ed78d3ebf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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(),
    }
}