summaryrefslogtreecommitdiff
path: root/src/scenes/keys.rs
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-11 15:24:37 -0800
committerskylar <me@casper>2026-11-11 15:24:37 -0800
commit9ccf951473e21a8b90c25161156fa09a9b227a7b (patch)
treee7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/scenes/keys.rs
parentd36c54d996f3c14ae811312dbbe621e12a39edde (diff)
Orrery 0.1.0: the galaxy, flight, cities, inventory and saves
Grows the derelict crawler into the whole game, and gets it ready to publish. - A galaxy of star systems joined by lanes, split between four peoples' territories, with a star map and a system map. Systems are modelled with Kepler orbits, moons inside their Hill spheres, belts past the frost line and stations at Lagrange points. - Flight around one place at a time: arcade flight with a throttle fallback where held keys can't be detected, mining rocks for ore, landing, docking at stations, and boarding wrecks that come and go by the calendar. - Your ship to walk about, and capital cities to land at: generated plans with walls, quarters and streets, building interiors, shops, taverns, scholars and armourers. - Fighting with a weapon and a gun ready, six damage types and how each monster takes them, and armour that guards, wears and can be mended. - Everything you own has a weight: a vim-keyed inventory, a pack and the ship's lockers, and trade in ore, salvage, arms and fuel. - Characters with a people and a ship of their own, saved as versioned JSON on every scene change and on quit, with a character sheet, a saves list and a pause menu wherever you are. - `orrery weapons`, `armour`, `galaxy`, `system` and `town` print the generators' output without the TUI. - Release: README install steps for macOS and Linux, a Makefile whose install also works with macOS's install, an MIT licence, and Cargo metadata with rust-version 1.88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/scenes/keys.rs')
-rw-r--r--src/scenes/keys.rs97
1 files changed, 97 insertions, 0 deletions
diff --git a/src/scenes/keys.rs b/src/scenes/keys.rs
new file mode 100644
index 0000000..8c07fed
--- /dev/null
+++ b/src/scenes/keys.rs
@@ -0,0 +1,97 @@
+//! Keys every screen shares: which way the movement keys go, and how key
+//! hints are drawn. A hint is the key in the accent colour, bold, then what
+//! it does, dimmed; hints sit two spaces apart, and side panels pack as
+//! many to a line as fit.
+
+use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+use ratatui::style::{Color, Stylize};
+use ratatui::text::{Line, Span};
+
+pub const ACCENT: Color = Color::Rgb(240, 200, 110);
+pub const DIM: Color = Color::Rgb(120, 124, 142);
+
+/// Vi keys, arrows, and the number pad with or without num lock.
+pub fn direction(code: KeyCode) -> Option<(i32, i32)> {
+ Some(match code {
+ KeyCode::Left | KeyCode::Char('h' | '4') => (-1, 0),
+ KeyCode::Right | KeyCode::Char('l' | '6') => (1, 0),
+ KeyCode::Up | KeyCode::Char('k' | '8') => (0, -1),
+ KeyCode::Down | KeyCode::Char('j' | '2') => (0, 1),
+ KeyCode::Home | KeyCode::Char('y' | '7') => (-1, -1),
+ KeyCode::PageUp | KeyCode::Char('u' | '9') => (1, -1),
+ KeyCode::End | KeyCode::Char('b' | '1') => (-1, 1),
+ KeyCode::PageDown | KeyCode::Char('n' | '3') => (1, 1),
+ _ => return None,
+ })
+}
+
+/// A step, or with shift a run: which way, and whether to run. Shifted vi
+/// keys come as capitals.
+pub fn movement(key: KeyEvent) -> Option<(i32, i32, bool)> {
+ let (code, run) = match key.code {
+ KeyCode::Char(c) if c.is_ascii_uppercase() => (KeyCode::Char(c.to_ascii_lowercase()), true),
+ code => (code, key.modifiers.contains(KeyModifiers::SHIFT)),
+ };
+ direction(code).map(|(dx, dy)| (dx, dy, run))
+}
+
+/// One hint: "esc back".
+fn hint(key: &str, what: &str) -> [Span<'static>; 2] {
+ [Span::raw(key.to_string()).fg(ACCENT).bold(), Span::raw(format!(" {what}")).fg(DIM)]
+}
+
+/// Hints in a row.
+pub fn line(hints: &[(&str, &str)]) -> Line<'static> {
+ let mut spans = Vec::new();
+ for (i, &(key, what)) in hints.iter().enumerate() {
+ if i > 0 {
+ spans.push(Span::raw(" "));
+ }
+ spans.extend(hint(key, what));
+ }
+ Line::from(spans)
+}
+
+/// Hints packed into lines at most `width` wide, as many to a line as fit.
+/// A hint is never split; one too wide for any line gets a line to itself.
+pub fn packed(hints: &[(&str, &str)], width: u16) -> Vec<Line<'static>> {
+ let width = usize::from(width);
+ let mut lines: Vec<Line<'static>> = Vec::new();
+ for &(key, what) in hints {
+ let [k, w] = hint(key, what);
+ let needs = k.width() + w.width();
+ match lines.last_mut() {
+ Some(last) if last.width() + 2 + needs <= width => {
+ last.spans.extend([Span::raw(" "), k, w]);
+ }
+ _ => lines.push(Line::from(vec![k, w])),
+ }
+ }
+ lines
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn text(line: &Line) -> String {
+ line.spans.iter().map(|s| s.content.as_ref()).collect()
+ }
+
+ #[test]
+ fn hints_pack_without_splitting() {
+ let hints = [("f", "fire"), ("r", "reload"), ("esc", "menu: save and quit")];
+ let lines: Vec<String> = packed(&hints, 16).iter().map(text).collect();
+ assert_eq!(lines, ["f fire r reload", "esc menu: save and quit"]);
+ assert_eq!(text(&line(&hints[..2])), "f fire r reload");
+ }
+
+ #[test]
+ fn shift_or_a_capital_runs() {
+ let key = |code, modifiers| movement(KeyEvent::new(code, modifiers));
+ assert_eq!(key(KeyCode::Char('h'), KeyModifiers::NONE), Some((-1, 0, false)));
+ assert_eq!(key(KeyCode::Char('L'), KeyModifiers::SHIFT), Some((1, 0, true)));
+ assert_eq!(key(KeyCode::Down, KeyModifiers::SHIFT), Some((0, 1, true)));
+ assert_eq!(key(KeyCode::Char('G'), KeyModifiers::SHIFT), None);
+ }
+}