thedes_app/
session.rs

1use std::fmt;
2
3use num::rational::Ratio;
4use thedes_domain::game::Game;
5use thedes_geometry::orientation::Direction;
6use thedes_session::Session;
7use thedes_tui::{
8    core::{
9        App,
10        event::{Event, Key, KeyEvent},
11        input,
12        screen::FlushError,
13    },
14    menu::{self, Menu},
15};
16use thiserror::Error;
17
18pub fn default_key_bindings() -> KeyBindingMap {
19    let mut map = KeyBindingMap::new()
20        .with(Key::Esc, Command::Pause)
21        .with(Key::Char('q'), Command::Pause)
22        .with(Key::Char('Q'), Command::Pause);
23
24    let arrow_key_table = [
25        (Key::Up, Direction::Up),
26        (Key::Left, Direction::Left),
27        (Key::Down, Direction::Down),
28        (Key::Right, Direction::Right),
29    ];
30    for (key, direction) in arrow_key_table {
31        let pointer_key = key;
32        let head_key =
33            KeyEvent { main_key: key, ctrl: true, alt: false, shift: false };
34        map = map
35            .with(pointer_key, ControlCommand::MovePlayerPointer(direction))
36            .with(head_key, ControlCommand::MovePlayerHead(direction));
37    }
38
39    map
40}
41
42#[derive(Debug, Error)]
43pub enum InitError {
44    #[error("Failed to build pause menu")]
45    PauseMenu(
46        #[source]
47        #[from]
48        menu::Error,
49    ),
50    #[error("Pause menu is inconsistent, quit not found")]
51    MissingPauseQuit,
52}
53
54#[derive(Debug, Error)]
55pub enum Error {
56    #[error("TUI cancelled")]
57    Cancelled,
58    #[error("Input driver was cancelled")]
59    InputCancelled(
60        #[source]
61        #[from]
62        input::ReadError,
63    ),
64    #[error("Failed to render session")]
65    Render(
66        #[source]
67        #[from]
68        thedes_session::RenderError,
69    ),
70    #[error("Failed to move player around")]
71    MoveAround(
72        #[source]
73        #[from]
74        thedes_session::MoveAroundError,
75    ),
76    #[error("Failed to move player with quick step")]
77    QuickStep(
78        #[source]
79        #[from]
80        thedes_session::QuickStepError,
81    ),
82    #[error("Pause menu failed to run")]
83    PauseMenu(
84        #[source]
85        #[from]
86        menu::Error,
87    ),
88    #[error("Failed to flush commands to screen")]
89    Flush(
90        #[from]
91        #[source]
92        FlushError,
93    ),
94}
95
96pub type KeyBindingMap = thedes_tui::key_bindings::KeyBindingMap<Command>;
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum Command {
100    Pause,
101    Control(ControlCommand),
102}
103
104impl From<ControlCommand> for Command {
105    fn from(command: ControlCommand) -> Self {
106        Command::Control(command)
107    }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
111pub enum ControlCommand {
112    MovePlayerHead(Direction),
113    MovePlayerPointer(Direction),
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117enum PauseMenuItem {
118    Continue,
119    Quit,
120}
121
122impl fmt::Display for PauseMenuItem {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str(match self {
125            Self::Continue => "Continue Game",
126            Self::Quit => "Quit Game",
127        })
128    }
129}
130
131#[derive(Debug, Clone)]
132pub struct Config {
133    control_events_per_tick: Ratio<u32>,
134    inner: thedes_session::Config,
135    key_bindings: KeyBindingMap,
136}
137
138impl Default for Config {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl Config {
145    pub fn new() -> Self {
146        Self {
147            control_events_per_tick: Ratio::new(1, 8),
148            inner: thedes_session::Config::new(),
149            key_bindings: default_key_bindings(),
150        }
151    }
152
153    pub fn with_control_events_per_tick(
154        self,
155        events: impl Into<Ratio<u32>>,
156    ) -> Self {
157        Self { control_events_per_tick: events.into(), ..self }
158    }
159
160    pub fn with_key_bindings(self, key_bindings: KeyBindingMap) -> Self {
161        Self { key_bindings, ..self }
162    }
163
164    pub fn with_inner(self, config: thedes_session::Config) -> Self {
165        Self { inner: config, ..self }
166    }
167
168    pub fn finish(self, game: Game) -> Result<Component, InitError> {
169        let pause_menu_items = [PauseMenuItem::Continue, PauseMenuItem::Quit];
170
171        let quit_position = pause_menu_items
172            .iter()
173            .position(|item| *item == PauseMenuItem::Quit)
174            .ok_or(InitError::MissingPauseQuit)?;
175
176        let pause_menu_bindings = menu::default_key_bindings()
177            .with(Key::Char('q'), menu::Command::SelectConfirm(quit_position));
178
179        let pause_menu = Menu::new("!! Game is paused !!", pause_menu_items)?
180            .with_keybindings(pause_menu_bindings);
181
182        Ok(Component {
183            inner: self.inner.finish(game),
184            control_events_per_tick: self.control_events_per_tick,
185            controls_left: Ratio::new(0, 1),
186            key_bindings: self.key_bindings,
187            pause_menu,
188        })
189    }
190}
191
192#[derive(Debug, Clone)]
193pub struct Component {
194    inner: Session,
195    control_events_per_tick: Ratio<u32>,
196    controls_left: Ratio<u32>,
197    key_bindings: KeyBindingMap,
198    pause_menu: Menu<PauseMenuItem>,
199}
200
201impl Component {
202    pub async fn run(&mut self, app: &mut App) -> Result<(), Error> {
203        while self.handle_input(app).await? {
204            let more_controls_left =
205                self.controls_left + self.control_events_per_tick;
206            if more_controls_left < self.control_events_per_tick.ceil() * 2 {
207                self.controls_left = more_controls_left;
208            }
209            self.inner.render(app)?;
210            app.canvas.flush()?;
211
212            tokio::select! {
213                _ = app.tick_session.tick() => (),
214                _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
215            }
216        }
217        Ok(())
218    }
219
220    async fn handle_input(&mut self, app: &mut App) -> Result<bool, Error> {
221        let events: Vec<_> = app.events.read_until_now()?.collect();
222
223        for event in events {
224            match event {
225                Event::Key(key) => {
226                    if !self.handle_key(app, key).await? {
227                        return Ok(false);
228                    }
229                },
230                Event::Paste(_) => (),
231            }
232        }
233
234        Ok(true)
235    }
236
237    async fn handle_key(
238        &mut self,
239        app: &mut App,
240        key: KeyEvent,
241    ) -> Result<bool, Error> {
242        if let Some(command) = self.key_bindings.command_for(key) {
243            match command {
244                Command::Pause => {
245                    self.pause_menu.run(app).await?;
246                    match self.pause_menu.output() {
247                        PauseMenuItem::Continue => (),
248                        PauseMenuItem::Quit => return Ok(false),
249                    }
250                },
251                Command::Control(command) => {
252                    if self.controls_left >= Ratio::ONE {
253                        self.controls_left -= Ratio::ONE;
254                        self.handle_control(app, *command)?;
255                    }
256                },
257            }
258        }
259
260        Ok(true)
261    }
262
263    fn handle_control(
264        &mut self,
265        app: &mut App,
266        command: ControlCommand,
267    ) -> Result<(), Error> {
268        match command {
269            ControlCommand::MovePlayerHead(direction) => {
270                self.inner.quick_step(app, direction)?;
271            },
272            ControlCommand::MovePlayerPointer(direction) => {
273                self.inner.move_around(app, direction)?;
274            },
275        }
276        Ok(())
277    }
278}