//! Scenes: the title screen, characters and their saves, the name forge, //! derelicts to board, flight (around a place, across the system map, and //! between systems on the star map), and what's on the ground: cities to //! walk, and ports. pub mod aboard; pub mod character; pub mod derelict; pub mod flight; pub mod forge; pub mod galaxy_map; pub mod inventory; pub mod keys; pub mod pause; pub mod port; pub mod saves; pub mod sky; pub mod system_map; pub mod title; pub mod town; #[cfg(test)] mod zz_dump; use ratatui::Frame; use ratatui::crossterm::event::KeyEvent; use crate::app::Ctx; /// What a scene wants to happen after it handles a key or a tick. pub enum Transition { None, /// Open a scene on top of this one. Popping it comes back here. Push(Box), /// Open several scenes at once, each on top of the one before. Open(Vec>), /// Close this scene and return to the one beneath it. Pop, /// Close this scene and open these in its place. Replace(Vec>), /// Close every scene but the title screen, at the bottom of the stack. Title, Quit, } /// One screen of the game. The game keeps a stack of them; only the top one /// gets keys and ticks and is drawn. pub trait Scene { /// A key was pressed, or auto-repeated while held. fn key(&mut self, key: KeyEvent, ctx: &mut Ctx) -> Transition; /// One fixed step of game time; there are `app::TICKS_PER_SECOND` of /// these a second, whatever the frame rate. Turn-based scenes can ignore it. fn tick(&mut self, _ctx: &mut Ctx) -> Transition { Transition::None } fn render(&self, frame: &mut Frame, ctx: &Ctx); /// This scene has come to the top of the stack: just opened, or /// uncovered when the ones above it closed. What it shows may need /// bringing up to date with what happened up there. fn on_top(&mut self, _ctx: &Ctx) {} } #[cfg(test)] pub(crate) mod tests { use ratatui::Terminal; use ratatui::backend::TestBackend; use ratatui::crossterm::event::{KeyCode, KeyModifiers}; use super::*; use crate::term::KeyboardCaps; pub fn test_ctx() -> Ctx { Ctx::new(1, KeyboardCaps { key_release: false }) } /// Draws every scene at several sizes, down to absurdly small ones, to /// catch layout arithmetic that panics. #[test] fn scenes_render_at_any_size() { let mut ctx = test_ctx(); let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE); let mut pausing = derelict::Derelict::new(2); pausing.key(esc, &mut ctx); let mut abandoning = derelict::Derelict::new(2); abandoning.key(esc, &mut ctx); abandoning.key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE), &mut ctx); let mut aiming = derelict::Derelict::new(2); aiming.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE), &mut ctx); aiming.key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), &mut ctx); let mut packing = derelict::Derelict::new(2); packing.key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE), &mut ctx); // Something of every kind, on you and in the lockers. { use crate::dungeon::armour; use crate::dungeon::arms::Source; use crate::items::{Food, Item, Salvage, Slot, Stack}; let mut rng = crate::seed::rng(8); let mut armor = |slot| { let mut a = armour::find(&mut rng, Source::Armoury, false); a.slot = slot; a.durability = 1; a }; let food = Food { title: "ration brick".into(), description: "dense and grey".into(), nourishment: 10, mass: 400, }; let scrap = Salvage { title: "coil of superconductor".into(), value: 30, mass: 6000 }; let g = &mut ctx.hold.gear; g.worn.insert(Slot::Body, armor(Slot::Body)); g.add(Stack::one(Item::Armor(armor(Slot::Head)))); g.add(Stack { item: Item::Food(food), count: 3 }); g.add(Stack { item: Item::Medpatch, count: 2 }); ctx.hold.lockers.push(Stack { item: Item::Salvage(scrap.clone()), count: 4 }); ctx.hold.deck.push((crate::dungeon::Pos::new(0, 0), Stack::one(Item::Salvage(scrap)))); } let mut rummaging = derelict::Derelict::board(2, &mut ctx); for code in [KeyCode::Char('i'), KeyCode::Char('j'), KeyCode::Enter] { rummaging.key(KeyEvent::new(code, KeyModifiers::NONE), &mut ctx); } let home = ctx.system.home_place(); let mut stowing = aboard::Aboard::landed(&ctx, home); for code in [KeyCode::Char('i'), KeyCode::Char('l'), KeyCode::Char('/')] { stowing.key(KeyEvent::new(code, KeyModifiers::NONE), &mut ctx); } let mut strolling = town::TownScene::new(&ctx, home); strolling.key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE), &mut ctx); let mut leaving = town::TownScene::new(&ctx, home); leaving.key(esc, &mut ctx); let mut resting = aboard::Aboard::landed(&ctx, home); resting.key(esc, &mut ctx); let mut flying = flight::FlightScene::new(&ctx); flying.key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), &mut ctx); let mut holding = flight::FlightScene::new(&ctx); holding.key(esc, &mut ctx); let mut docked = port::Port::new(&ctx, 0); docked.key(esc, &mut ctx); let belt = ctx.system.places.len() - 1; let mut trading = town::TownScene::new(&ctx, home); for _ in 0..12 { trading.key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), &mut ctx); } let wreck = ctx.system.wrecks_on(ctx.day).pop(); let mut renaming = character::Sheet::new(); renaming.key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), &mut ctx); let mut creating = character::Create::new(&ctx); creating.key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), &mut ctx); let dir = crate::save::scratch("render"); ctx.saves = Some(dir.clone()); ctx.slot = Some(crate::save::new_slot(&dir, &ctx.character, ctx.seed)); ctx.record.slain.insert("rust-mite".into(), 2); ctx.bestiary.insert(("rust-mite", crate::dungeon::arms::Dmg::Blunt)); ctx.autosave(); std::fs::write(dir.join("broken.json"), "{").unwrap(); let playing = title::Title::new(&ctx); let mut plotting = galaxy_map::GalaxyMap::new(&ctx); plotting.key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), &mut ctx); let mut far = galaxy_map::GalaxyMap::new(&ctx); for _ in 0..6 { far.key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), &mut ctx); } let mut stranded = test_ctx(); let dead = (0..stranded.galaxy.sites.len()) .find(|&i| !stranded.galaxy.sites[i].kind.sells_fuel()) .unwrap(); stranded.enter_system(dead); stranded.hold.fuel = 0; stranded.hold.debt = 120; let scenes: [Box; 33] = [ Box::new(title::Title::new(&test_ctx())), Box::new(playing), Box::new(character::Create::new(&ctx)), Box::new(creating), Box::new(character::Sheet::new()), Box::new(renaming), Box::new(saves::Saves::new(&ctx)), Box::new(forge::Forge::new(&ctx)), Box::new(derelict::Derelict::new(1)), Box::new(pausing), Box::new(abandoning), Box::new(holding), Box::new(docked), Box::new(leaving), Box::new(resting), Box::new(aiming), Box::new(packing), Box::new(rummaging), Box::new(stowing), Box::new(strolling), Box::new(flying), Box::new(system_map::SystemMap::new(&ctx)), Box::new(galaxy_map::GalaxyMap::new(&ctx)), Box::new(plotting), Box::new(far), Box::new(port::Port::new(&ctx, ctx.system.home_place())), Box::new(port::Port::new(&ctx, belt)), Box::new(port::Port::new(&ctx, 0)), Box::new(town::TownScene::new(&ctx, home)), Box::new(trading), Box::new(aboard::Aboard::landed(&ctx, home)), Box::new(aboard::Aboard::resumed(&ctx, home)), Box::new(aboard::Aboard::adrift("in orbit around nowhere", wreck)), ]; for (width, height) in [(160, 50), (80, 24), (40, 12), (5, 3), (1, 1)] { let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); for scene in &scenes { for ticks in [0, 12_345] { ctx.ticks = ticks; terminal.draw(|frame| scene.render(frame, &ctx)).unwrap(); } } let map = galaxy_map::GalaxyMap::new(&stranded); terminal.draw(|frame| map.render(frame, &stranded)).unwrap(); } let _ = std::fs::remove_dir_all(dir); } }