1use std::{fmt, path::PathBuf};
2
3use num::rational::Ratio;
4use thedes_asset::Assets;
5use thedes_domain::game::{Game, LoadError, SaveError};
6use thedes_geometry::orientation::Direction;
7use thedes_session::{EventError, Session};
8use thedes_settings::AudioSinkType;
9use thedes_tui::{
10 core::{
11 App,
12 audio,
13 event::{Event, Key, KeyEvent},
14 input,
15 screen::FlushError,
16 },
17 info::{self, Info},
18 menu::{self, Menu},
19};
20use thiserror::Error;
21
22use crate::settings;
23
24pub mod dev;
25
26pub fn default_key_bindings() -> KeyBindingMap {
27 let mut map = KeyBindingMap::new()
28 .with(Key::Esc, Command::Pause)
29 .with(Key::Char('q'), Command::Pause)
30 .with(Key::Char('Q'), Command::Pause);
31
32 let arrow_key_table = [
33 (Key::Up, Direction::Up),
34 (Key::Left, Direction::Left),
35 (Key::Down, Direction::Down),
36 (Key::Right, Direction::Right),
37 ];
38 for (key, direction) in arrow_key_table {
39 let pointer_key = key;
40 let head_key =
41 KeyEvent { main_key: key, ctrl: true, alt: false, shift: false };
42 map = map
43 .with(pointer_key, ControlCommand::MovePlayerPointer(direction))
44 .with(head_key, ControlCommand::MovePlayerHead(direction));
45 }
46
47 map = map.with(Key::Char('o'), Command::Script);
48
49 map
50}
51
52#[derive(Debug, Error)]
53pub enum InitError {
54 #[error("Failed to build pause menu")]
55 PauseMenu(
56 #[source]
57 #[from]
58 menu::Error,
59 ),
60 #[error("Pause menu is inconsistent, quit not found")]
61 MissingPauseQuit,
62 #[error("Failed to load game")]
63 Load(#[from] LoadError),
64}
65
66#[derive(Debug, Error)]
67pub enum Error {
68 #[error("TUI cancelled")]
69 Cancelled,
70 #[error("Input driver was cancelled")]
71 InputCancelled(
72 #[source]
73 #[from]
74 input::ReadError,
75 ),
76 #[error("Failed to render session")]
77 Render(
78 #[source]
79 #[from]
80 thedes_session::RenderError,
81 ),
82 #[error("Failed to move player around")]
83 MoveAround(
84 #[source]
85 #[from]
86 thedes_session::MoveAroundError,
87 ),
88 #[error("Failed to move player with quick step")]
89 QuickStep(
90 #[source]
91 #[from]
92 thedes_session::QuickStepError,
93 ),
94 #[error("Pause menu failed to run")]
95 PauseMenu(
96 #[source]
97 #[from]
98 menu::Error,
99 ),
100 #[error("Failed to flush commands to screen")]
101 Flush(
102 #[from]
103 #[source]
104 FlushError,
105 ),
106 #[error("Failed to simulate events")]
107 Event(
108 #[from]
109 #[source]
110 EventError,
111 ),
112 #[error("Failed to run development mode")]
113 Dev(
114 #[from]
115 #[source]
116 dev::Error,
117 ),
118 #[error("Failed to save game")]
119 Save(#[from] SaveError),
120 #[error("Failed to show death info")]
121 DeathInfo(#[source] info::Error),
122 #[error("Failed to run settings")]
123 Settings(#[from] settings::Error),
124 #[error("Failed to flush audio commands")]
125 AudioFlush(#[from] audio::FlushError),
126 #[error("Failed to load assets")]
127 AssetsLoad(#[from] thedes_asset::LoadError),
128 #[error("Failed to consume meta events")]
129 ConsumeMetaEvent(#[from] thedes_session::MetaEventError),
130}
131
132pub type KeyBindingMap = thedes_tui::key_bindings::KeyBindingMap<Command>;
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
135pub enum Command {
136 Pause,
137 Script,
138 Control(ControlCommand),
139}
140
141impl From<ControlCommand> for Command {
142 fn from(command: ControlCommand) -> Self {
143 Command::Control(command)
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
148pub enum ControlCommand {
149 MovePlayerHead(Direction),
150 MovePlayerPointer(Direction),
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154enum PauseMenuItem {
155 Continue,
156 Save,
157 Settings,
158 Quit,
159}
160
161impl fmt::Display for PauseMenuItem {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_str(match self {
164 Self::Continue => "Continue Game",
165 Self::Save => "Save Game",
166 Self::Settings => "Settings",
167 Self::Quit => "Quit Game",
168 })
169 }
170}
171
172#[derive(Debug, Clone)]
173pub struct Config {
174 control_events_per_tick: Ratio<u32>,
175 inner: thedes_session::Config,
176 key_bindings: KeyBindingMap,
177}
178
179impl Default for Config {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185impl Config {
186 pub fn new() -> Self {
187 Self {
188 control_events_per_tick: Ratio::new(1, 8),
189 inner: thedes_session::Config::new(),
190 key_bindings: default_key_bindings(),
191 }
192 }
193
194 pub fn with_control_events_per_tick(
195 self,
196 events: impl Into<Ratio<u32>>,
197 ) -> Self {
198 Self { control_events_per_tick: events.into(), ..self }
199 }
200
201 pub fn with_key_bindings(self, key_bindings: KeyBindingMap) -> Self {
202 Self { key_bindings, ..self }
203 }
204
205 pub fn with_inner(self, config: thedes_session::Config) -> Self {
206 Self { inner: config, ..self }
207 }
208
209 pub fn finish(
210 self,
211 save_path: impl Into<PathBuf>,
212 game: Game,
213 ) -> Result<Component, InitError> {
214 let pause_menu_items = [
215 PauseMenuItem::Continue,
216 PauseMenuItem::Save,
217 PauseMenuItem::Settings,
218 PauseMenuItem::Quit,
219 ];
220
221 let quit_position = pause_menu_items
222 .iter()
223 .position(|item| *item == PauseMenuItem::Quit)
224 .ok_or(InitError::MissingPauseQuit)?;
225
226 let pause_menu_bindings = menu::default_key_bindings()
227 .with(Key::Char('q'), menu::Command::SelectConfirm(quit_position));
228
229 let pause_menu = Menu::new("!! Game is paused !!", pause_menu_items)?
230 .with_keybindings(pause_menu_bindings);
231
232 let death_info =
233 Info::new("You died!", "You cannot continue to this game.");
234
235 Ok(Component {
236 inner: self.inner.finish(game),
237 save_path: save_path.into(),
238 control_events_per_tick: self.control_events_per_tick,
239 controls_left: Ratio::new(0, 1),
240 key_bindings: self.key_bindings,
241 pause_menu,
242 dev_mode: dev::Component::new(),
243 death_info,
244 })
245 }
246
247 pub async fn finish_loading(
248 self,
249 save_path: impl Into<PathBuf>,
250 ) -> Result<Component, InitError> {
251 let save_path = save_path.into();
252 let game = Game::load(&save_path).await?;
253 self.finish(save_path, game)
254 }
255}
256
257#[derive(Debug, Clone)]
258pub struct Component {
259 inner: Session,
260 save_path: PathBuf,
261 control_events_per_tick: Ratio<u32>,
262 controls_left: Ratio<u32>,
263 key_bindings: KeyBindingMap,
264 pause_menu: Menu<PauseMenuItem>,
265 dev_mode: dev::Component,
266 death_info: Info,
267}
268
269impl Component {
270 pub async fn run(
271 &mut self,
272 settings: &mut settings::Component,
273 app: &mut App,
274 ) -> Result<(), Error> {
275 let assets = Assets::get().await?;
276
277 app.audio_controller.queue([audio::Command::new_enter_repeated(
278 AudioSinkType::Music,
279 &assets.sound.calm_song[..],
280 )]);
281 app.audio_controller.flush()?;
282
283 while self.handle_input(settings, app).await? {
284 let more_controls_left =
285 self.controls_left + self.control_events_per_tick;
286 if more_controls_left < self.control_events_per_tick.ceil() * 2 {
287 self.controls_left = more_controls_left;
288 }
289 if self.inner.game().player().hp().value() == 0 {
290 self.death_info.run(app).await.map_err(Error::DeathInfo)?;
291 break;
292 }
293 self.inner.tick_event()?;
294 self.inner.render(app)?;
295 self.inner.consume_meta_events(app, &assets)?;
296 app.canvas.flush()?;
297
298 tokio::select! {
299 _ = app.tick_session.tick() => (),
300 _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
301 }
302 }
303
304 app.audio_controller
305 .queue([audio::Command::new_leave_repeated(AudioSinkType::Music)]);
306 app.audio_controller.flush()?;
307
308 Ok(())
309 }
310
311 async fn handle_input(
312 &mut self,
313 settings: &mut settings::Component,
314 app: &mut App,
315 ) -> Result<bool, Error> {
316 let events: Vec<_> = app.events.read_until_now()?.collect();
317
318 for event in events {
319 match event {
320 Event::Key(key) => {
321 if !self.handle_key(settings, app, key).await? {
322 return Ok(false);
323 }
324 },
325 Event::Paste(_) => (),
326 }
327 }
328
329 Ok(true)
330 }
331
332 async fn handle_key(
333 &mut self,
334 settings: &mut settings::Component,
335 app: &mut App,
336 key: KeyEvent,
337 ) -> Result<bool, Error> {
338 if let Some(command) = self.key_bindings.command_for(key) {
339 match command {
340 Command::Pause => {
341 self.pause_menu.run(app).await?;
342 match self.pause_menu.output() {
343 PauseMenuItem::Continue => (),
344 PauseMenuItem::Save => {
345 self.inner.game().save(&self.save_path).await?
346 },
347 PauseMenuItem::Settings => settings.run(app).await?,
348 PauseMenuItem::Quit => return Ok(false),
349 }
350 },
351 Command::Script => {
352 self.dev_mode
353 .run(app, &mut self.inner.dev_command_context())
354 .await?;
355 },
356 Command::Control(command) => {
357 if self.controls_left >= Ratio::ONE {
358 self.controls_left -= Ratio::ONE;
359 self.handle_control(*command)?;
360 }
361 },
362 }
363 }
364
365 Ok(true)
366 }
367
368 fn handle_control(&mut self, command: ControlCommand) -> Result<(), Error> {
369 match command {
370 ControlCommand::MovePlayerHead(direction) => {
371 self.inner.quick_step(direction)?;
372 },
373 ControlCommand::MovePlayerPointer(direction) => {
374 self.inner.move_around(direction)?;
375 },
376 }
377 Ok(())
378 }
379}