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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
//! 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<dyn Scene>),
/// Open several scenes at once, each on top of the one before.
Open(Vec<Box<dyn Scene>>),
/// Close this scene and return to the one beneath it.
Pop,
/// Close this scene and open these in its place.
Replace(Vec<Box<dyn Scene>>),
/// 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<dyn Scene>; 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);
}
}
|