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}