Skip to main content

thedes_app/
session.rs

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}