From 9ccf951473e21a8b90c25161156fa09a9b227a7b Mon Sep 17 00:00:00 2001 From: skylar Date: Wed, 11 Nov 2026 15:24:37 -0800 Subject: 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 --- src/scenes/keys.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/scenes/keys.rs (limited to 'src/scenes/keys.rs') 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> { + let width = usize::from(width); + let mut lines: Vec> = 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 = 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); + } +} -- cgit v1.2.3