summaryrefslogtreecommitdiff
path: root/src/scenes/title.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/scenes/title.rs')
-rw-r--r--src/scenes/title.rs178
1 files changed, 139 insertions, 39 deletions
diff --git a/src/scenes/title.rs b/src/scenes/title.rs
index 7c75888..898452e 100644
--- a/src/scenes/title.rs
+++ b/src/scenes/title.rs
@@ -1,4 +1,6 @@
-//! Title screen: a turning orrery over a drifting starfield.
+//! Title screen: a turning orrery over a drifting starfield, and the way in:
+//! carry on as the character you last played, make a new one, or load
+//! another.
//!
//! Every universe seed gets its own orrery, and everything here moves as a
//! function of `ctx.ticks`, so it doubles as a check that the tick loop runs.
@@ -16,10 +18,15 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::widgets::canvas::{Canvas, Points};
+use super::character::{Create, Sheet};
use super::derelict::Derelict;
+use super::flight;
use super::forge::Forge;
+use super::keys::ACCENT;
+use super::saves::Saves;
use super::{Scene, Transition};
use crate::app::{Ctx, TICKS_PER_SECOND};
+use crate::save;
use crate::seed;
const LOGO: [&str; 4] = [
@@ -64,8 +71,13 @@ struct Planet {
}
pub struct Title {
+ /// Whose orrery this is. Loading or making a character can change
+ /// universe under it.
+ universe: u64,
stars: Vec<Star>,
planets: Vec<Planet>,
+ /// Whether there are saved characters to load.
+ saved: bool,
}
impl Title {
@@ -92,7 +104,8 @@ impl Title {
}
})
.collect();
- Title { stars, planets }
+ let saved = ctx.saves.as_deref().is_some_and(|dir| !save::list(dir).is_empty());
+ Title { universe: ctx.seed, stars, planets, saved }
}
fn draw_stars(&self, buf: &mut Buffer, area: Rect, seconds: f64) {
@@ -143,16 +156,43 @@ impl Title {
}
}
+/// The menu: key, what it does.
+fn menu(ctx: &Ctx, saved: bool) -> Vec<(&'static str, String)> {
+ let mut items = Vec::new();
+ if ctx.slot.is_some() {
+ let c = &ctx.character;
+ items.push(("enter", format!("carry on as {}, of {}", c.name, c.ship_name())));
+ items.push(("c", "your character".into()));
+ }
+ items.push(("n", "a new character".into()));
+ if saved {
+ items.push(("l", "load a character".into()));
+ }
+ if ctx.slot.is_some() {
+ items.push(("d", "board a derelict, straight away".into()));
+ }
+ items.push(("g", "the name forge".into()));
+ items.push(("q", "leave".into()));
+ items
+}
+
impl Scene for Title {
fn key(&mut self, key: KeyEvent, ctx: &mut Ctx) -> Transition {
+ let playing = ctx.slot.is_some();
match key.code {
+ KeyCode::Enter if playing => Transition::Open(flight::resume(ctx)),
+ KeyCode::Enter => Transition::Push(Box::new(Create::new(ctx))),
KeyCode::Char(c) => match c.to_ascii_lowercase() {
- 'd' => {
+ 'f' if playing => Transition::Open(flight::resume(ctx)),
+ 'c' if playing => Transition::Push(Box::new(Sheet::new())),
+ 'n' => Transition::Push(Box::new(Create::new(ctx))),
+ 'l' if self.saved => Transition::Push(Box::new(Saves::new(ctx))),
+ 'd' if playing => {
ctx.boarded += 1;
let ship = seed::derive(ctx.seed, "derelict", ctx.boarded);
- Transition::Push(Box::new(Derelict::new(ship)))
+ Transition::Push(Box::new(Derelict::board(ship, ctx)))
}
- 'n' => Transition::Push(Box::new(Forge::new(ctx))),
+ 'g' => Transition::Push(Box::new(Forge::new(ctx))),
'q' => Transition::Quit,
_ => Transition::None,
},
@@ -161,17 +201,26 @@ impl Scene for Title {
}
}
+ fn on_top(&mut self, ctx: &Ctx) {
+ if ctx.seed != self.universe {
+ *self = Title::new(ctx);
+ } else {
+ self.saved = ctx.saves.as_deref().is_some_and(|dir| !save::list(dir).is_empty());
+ }
+ }
+
fn render(&self, frame: &mut Frame, ctx: &Ctx) {
let seconds = ctx.ticks as f64 / f64::from(TICKS_PER_SECOND);
let area = frame.area();
self.draw_stars(frame.buffer_mut(), area, seconds);
+ let items = menu(ctx, self.saved);
let [sky, logo, _, menu, hold, footer] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(LOGO.len() as u16),
Constraint::Length(1),
- Constraint::Length(4),
- Constraint::Length(1),
+ Constraint::Length(items.len() as u16),
+ Constraint::Length(2),
Constraint::Length(1),
])
.areas(area);
@@ -181,44 +230,95 @@ impl Scene for Title {
let logo_lines: Vec<Line> = LOGO.iter().map(|l| Line::from(*l).bold()).collect();
frame.render_widget(Paragraph::new(logo_lines).centered().fg(Color::Yellow), logo);
- let item = |key: &'static str, label: &'static str, built: bool| {
- let style = if built { Style::new() } else { Style::new().fg(Color::DarkGray) };
- let mut spans = vec![Span::styled(key, style.bold()), Span::styled(label, style)];
- if !built {
- spans.push(Span::styled(" (not yet built)", style.italic()));
- }
- Line::from(spans)
- };
- let [menu] = Layout::horizontal([Constraint::Length(46)]).flex(Flex::Center).areas(menu);
- frame.render_widget(
- Paragraph::new(vec![
- item("[d] ", "board a derelict", true),
- item("[n] ", "the name forge", true),
- item("[f] ", "fly a star system", false),
- item("[q] ", "leave", true),
- ]),
- menu,
- );
-
- let h = &ctx.hold;
- if !h.artifacts.is_empty() || h.scrip > 0 {
- let plural = if h.artifacts.len() == 1 { "" } else { "s" };
+ // Keys in a column, in the colour key hints are everywhere.
+ let keys = items.iter().map(|(k, _)| k.chars().count()).max().unwrap_or(0) + 2;
+ let width = items.iter().map(|(_, l)| keys + l.chars().count()).max();
+ let [menu] = Layout::horizontal([Constraint::Length(width.unwrap_or(0) as u16)])
+ .flex(Flex::Center)
+ .areas(menu);
+ let lines: Vec<Line> = items
+ .into_iter()
+ .map(|(key, label)| {
+ Line::from(vec![
+ Span::raw(format!("{key:<keys$}")).fg(ACCENT).bold(),
+ Span::raw(label),
+ ])
+ })
+ .collect();
+ frame.render_widget(Paragraph::new(lines), menu);
+
+ if ctx.slot.is_some() {
+ let h = &ctx.hold;
+ let plural = if h.artifacts() == 1 { "" } else { "s" };
+ let place = match &ctx.crawl {
+ Some(c) => format!("aboard {}", c.name),
+ None => format!("at {}", ctx.system.places[ctx.location].name),
+ };
let text = format!(
- "your hold: {} artifact{plural}, {} ¤ of salvage",
- h.artifacts.len(),
+ "day {:.1} {place} in {} · {} artifact{plural}, {} t of ore, {} ¤",
+ ctx.day,
+ ctx.system.name,
+ h.artifacts(),
+ h.ore_tons(),
h.scrip
);
+ let [_, hold] = Layout::vertical([Constraint::Length(1); 2]).areas(hold);
frame.render_widget(Paragraph::new(text).centered().fg(Color::Yellow), hold);
}
- let keys = if ctx.keyboard.key_release {
- "held keys detectable (kitty keyboard protocol)"
- } else {
- "held keys not detectable: flight will need a throttle"
+ let footer_text = match &ctx.save_error {
+ Some(problem) => Paragraph::new(problem.as_str()).centered().light_red(),
+ None => {
+ let keys = if ctx.keyboard.key_release {
+ "held keys detectable (kitty keyboard protocol)"
+ } else {
+ "held keys not detectable: flight will need a throttle"
+ };
+ Paragraph::new(format!("universe {} · {keys}", ctx.seed)).centered().dark_gray()
+ }
+ };
+ frame.render_widget(footer_text, footer);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use ratatui::crossterm::event::KeyModifiers;
+
+ use super::*;
+ use crate::scenes::tests::test_ctx;
+
+ fn press(title: &mut Title, ctx: &mut Ctx, code: KeyCode) -> Transition {
+ title.key(KeyEvent::new(code, KeyModifiers::NONE), ctx)
+ }
+
+ #[test]
+ fn without_a_character_you_make_one() {
+ let mut ctx = test_ctx();
+ let mut title = Title::new(&ctx);
+ let keys: Vec<&str> = menu(&ctx, false).iter().map(|&(key, _)| key).collect();
+ assert_eq!(keys, ["n", "g", "q"]);
+ assert!(matches!(press(&mut title, &mut ctx, KeyCode::Enter), Transition::Push(_)));
+ assert!(matches!(press(&mut title, &mut ctx, KeyCode::Char('d')), Transition::None));
+ assert!(matches!(press(&mut title, &mut ctx, KeyCode::Char('l')), Transition::None));
+ }
+
+ #[test]
+ fn with_one_you_carry_on_where_they_were() {
+ let mut ctx = test_ctx();
+ ctx.slot = Some("someone.json".into());
+ let mut title = Title::new(&ctx);
+ let c = &ctx.character;
+ let carry_on = format!("carry on as {}, of {}", c.name, c.ship_name());
+ assert_eq!(menu(&ctx, true)[0], ("enter", carry_on));
+ let Transition::Open(scenes) = press(&mut title, &mut ctx, KeyCode::Enter) else {
+ panic!("enter carries on");
};
- frame.render_widget(
- Paragraph::new(format!("universe {} · {keys}", ctx.seed)).centered().dark_gray(),
- footer,
- );
+ assert_eq!(scenes.len(), 1, "flying, as they weren't landed");
+
+ // Whoever comes back to the title may be from another universe.
+ ctx.begin(99, ctx.character.clone());
+ title.on_top(&ctx);
+ assert_eq!(title.universe, 99);
}
}