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(),
}
}
|