1use std::mem;
2
3use cmaze::{
4 algorithms::GeneratorError,
5 array::Array2DView,
6 dims::*,
7 game::{GameProperities, RunningGame, RunningGameState, RunningJob},
8 gameboard::{Cell, CellWall},
9 progress::Progress,
10};
11
12use crate::{
13 app::{game_state::GameData, GameViewMode},
14 helpers::{
15 constants, is_release, maze2screen, maze2screen_3d, maze_render_size, strings, LineDir,
16 },
17 lerp, menu_actions,
18 renderer::{self, Frame},
19 settings::{
20 self,
21 theme::{Theme, ThemeResolver},
22 CameraMode, MazePreset, Settings, SettingsActivity,
23 },
24 ui::{
25 self,
26 helpers::format_duration,
27 multisize_duration_format, split_menu_actions,
28 usecase::dpad::{DPad, DPadType},
29 Menu, MenuAction, MenuConfig, Popup, ProgressBar, Rect, Screen,
30 },
31};
32
33#[cfg(feature = "sound")]
34#[allow(unused_imports)]
35use crate::sound::{track::MusicTrack, SoundPlayer};
36
37use crossterm::event::{Event as TermEvent, KeyCode, KeyEvent};
38
39#[cfg(feature = "sound")]
40#[allow(unused_imports)]
41use rodio::Source;
42
43use super::{
44 app::{AppData, AppStateData, Registries},
45 Activity, ActivityHandler, Change, Event,
46};
47
48pub fn create_controls_popup() -> Activity {
49 let popup = Popup::new(
50 "Controls".to_string(),
51 [
52 "~ In game",
53 " WASD and arrows: move",
54 " Space: switch adventure/spectaror mode",
55 " Q, F or L: move down",
56 " E, R or P: move up",
57 " With SHIFT move at the end in single dir",
58 " Escape: pause menu",
59 "",
60 "~ In end game popup",
61 " Enter or space: main menu",
62 " Q: quit TMaze",
63 " R: restart game",
64 ]
65 .into_iter()
66 .map(String::from)
67 .collect::<Vec<_>>(),
68 );
69
70 Activity::new_base_boxed("controls".to_string(), popup)
71}
72
73pub struct MainMenu {
74 menu: Menu,
75 actions: Vec<MenuAction<Change>>,
76}
77
78#[allow(clippy::new_without_default)]
79impl MainMenu {
80 pub fn new() -> Self {
81 let options = menu_actions!(
82 "New Game" -> data => Self::start_new_game(&data.settings, &data.use_data),
83 "Settings" -> _ => Self::show_settings_screen(),
84 "Controls" -> _ => Self::show_controls_popup(),
85 "About" -> _ => Self::show_about_popup(),
86 "Quit" -> _ => Change::pop_top(),
87 );
88
89 let (options, actions) = split_menu_actions(options);
90
91 Self {
92 menu: Menu::new(MenuConfig::new("TMaze", options).counted()),
93 actions,
94 }
95 }
96
97 fn show_settings_screen() -> Change {
98 Change::push(Activity::new_base_boxed(
99 "settings".to_string(),
100 settings::SettingsActivity::new(),
101 ))
102 }
103
104 fn show_controls_popup() -> Change {
105 Change::push(create_controls_popup())
106 }
107
108 fn show_about_popup() -> Change {
109 const FEATURE_LIST: [(&str, bool); 2] = [
110 ("updates", cfg!(feature = "updates")),
111 ("sound", cfg!(feature = "sound")),
112 ];
113
114 let mut lines = vec![
115 "This is simple maze solving game".to_string(),
116 "Supported algorithms:".to_string(),
117 " - Depth-first search".to_string(),
118 " - Kruskal's algorithm".to_string(),
119 "Supports 3D mazes".to_string(),
120 "".to_string(),
121 "Created by:".to_string(),
122 format!(" - {}", env!("CARGO_PKG_AUTHORS")),
123 "".to_string(),
124 "Version:".to_string(),
125 format!(" {}", env!("CARGO_PKG_VERSION")),
126 ];
127
128 {
129 let (enabled, disabled) = FEATURE_LIST
130 .into_iter()
131 .partition::<Vec<_>, _>(|(_, enabled)| *enabled);
132
133 let enabled = enabled
134 .into_iter()
135 .map(|(name, _)| format!(" - {}", name))
136 .collect::<Vec<_>>();
137
138 let disabled = disabled
139 .into_iter()
140 .map(|(name, _)| format!(" - {}", name))
141 .collect::<Vec<_>>();
142
143 if !enabled.is_empty() {
144 lines.push("".to_string());
145 lines.push("Enabled features:".to_string());
146 lines.extend(enabled);
147 }
148
149 if !disabled.is_empty() {
150 lines.push("".to_string());
151 lines.push("Disabled features:".to_string());
152 lines.extend(disabled);
153 }
154 }
155
156 let popup = Popup::new("About".to_string(), lines);
157
158 Change::push(Activity::new_base_boxed("about".to_string(), popup))
159 }
160
161 fn start_new_game(settings: &Settings, use_data: &AppStateData) -> Change {
162 Change::push(Activity::new_base_boxed(
163 "maze size",
164 MazePresetMenu::new(settings, use_data),
165 ))
166 }
167
168 #[cfg(feature = "sound")]
169 fn play_menu_bgm(data: &mut AppData) {
170 data.play_bgm(MusicTrack::Menu);
171 }
172}
173
174impl ActivityHandler for MainMenu {
175 fn update(&mut self, events: Vec<super::Event>, data: &mut AppData) -> Option<Change> {
176 #[cfg(feature = "sound")]
177 Self::play_menu_bgm(data);
178
179 match self.menu.update(events, data)? {
180 Change::Pop {
181 res: Some(sub_activity),
182 ..
183 } => {
184 let index = *sub_activity
185 .downcast::<usize>()
186 .expect("menu should return index");
187 Some(self.actions[index](data))
188 }
189 res => Some(res),
190 }
191 }
192
193 fn screen(&self) -> &dyn ui::Screen {
194 &self.menu
195 }
196}
197
198pub struct MazePresetMenu {
199 menu: Menu,
200 presets: Vec<MazePreset>,
201}
202
203impl MazePresetMenu {
204 pub fn new(settings: &Settings, app_state_data: &AppStateData) -> Self {
205 let mut menu_config = MenuConfig::new_from_strings(
206 "Maze size".to_string(),
207 settings
208 .get_presets()
209 .iter()
210 .map(|maze| maze.title.clone())
211 .collect::<Vec<_>>(),
212 );
213
214 let default = app_state_data
215 .last_selected_preset
216 .or_else(|| settings.get_presets().iter().position(|maze| maze.default));
217
218 if let Some(i) = default {
219 menu_config = menu_config.default(i);
220 }
221
222 let menu = Menu::new(menu_config);
223
224 let presets = settings.get_presets().to_vec();
225
226 Self { menu, presets }
227 }
228}
229
230impl ActivityHandler for MazePresetMenu {
231 fn update(&mut self, events: Vec<super::Event>, data: &mut AppData) -> Option<Change> {
232 match self.menu.update(events, data) {
233 Some(change) => match change {
234 Change::Pop {
235 res: Some(size), ..
236 } => {
237 let index = *size.downcast::<usize>().expect("menu should return index");
238 data.use_data.last_selected_preset = Some(index);
239
240 let preset = self.presets[index].clone();
241
242 Some(Change::push(Activity::new_base_boxed(
243 "maze_gen".to_string(),
244 MazeGenerationActivity::new(preset, &data.registries),
245 )))
246 }
247 res => Some(res),
248 },
249 None => None,
250 }
251 }
252
253 fn screen(&self) -> &dyn ui::Screen {
254 &self.menu
255 }
256}
257
258pub struct MazeGenerationActivity {
259 comm: Result<RunningJob<Option<RunningGame>>, GeneratorError>,
260 preset: MazePreset,
261 progress_bar: ProgressBar,
262}
263
264impl MazeGenerationActivity {
265 pub fn new(preset: MazePreset, registries: &Registries) -> Self {
266 let desc = preset.short_desc().unwrap_or("Unknown".to_string());
267 let game_props = GameProperities {
268 maze_spec: preset.maze_spec.clone(),
269 };
270
271 let progress_bar = ProgressBar::new(format!("Generating maze: {}", desc));
272
273 Self {
274 comm: RunningGame::prepare(
275 game_props,
276 ®istries.region_generator,
277 ®istries.region_splitters,
278 ),
279 preset,
280 progress_bar,
281 }
282 }
283}
284
285impl ActivityHandler for MazeGenerationActivity {
286 fn update(&mut self, events: Vec<super::Event>, data: &mut AppData) -> Option<Change> {
287 for event in events {
288 #[allow(clippy::collapsible_match)]
289 match event {
290 Event::Term(TermEvent::Key(KeyEvent { code, kind, .. })) if !is_release(kind) => {
291 match code {
292 KeyCode::Esc | KeyCode::Char('q') => {
293 let mut comm = Err(GeneratorError::Unknown); mem::swap(&mut self.comm, &mut comm);
295 if let Ok(comm) = comm {
296 comm.progress.stop();
297 let _ = comm.handle.join().unwrap();
298 };
299 return Some(Change::pop(2));
300 }
301 _ => {}
302 }
303 }
304 _ => {}
305 }
306 }
307
308 match self.comm {
309 Ok(ref comm) if comm.handle.is_finished() => {
310 let mut comm = Err(GeneratorError::Unknown); mem::swap(&mut self.comm, &mut comm);
312 let res = comm
313 .ok()
314 .unwrap()
315 .handle
316 .join()
317 .expect("Could not join maze generation thread");
318
319 match res {
320 Some(game) => {
321 let game_data = GameData {
322 camera_pos: maze2screen_3d(game.get_player_pos()),
323 game,
324 view_mode: GameViewMode::Adventure,
325 player_char: constants::get_random_player_char(),
326 maze_preset: self.preset.clone(),
327 };
328 Some(Change::replace(Activity::new_base_boxed(
329 "game".to_string(),
330 GameActivity::new(game_data, data),
331 )))
332 }
333 None => {
334 const MSG: &str = "Maze generation was aborted";
335 log::info!("{}", MSG);
336
337 Some(Change::pop_top())
338 }
339 }
340 }
341
342 Ok(ref comm) => {
343 let progress @ Progress { done, from, .. } = comm.progress();
344 self.progress_bar.update_progress(progress.percent() as f64);
345 self.progress_bar.update_title(format!(
346 "Generating maze: {}/{} - {:.2} %",
347 done,
348 from,
349 done as f64 / from as f64 * 100.0
350 ));
351 None
352 }
353
354 Err(ref err) => {
355 const UNKNOWN_MSG: &str = "Unknown error while generating maze";
356 const VALIDATION_MSG: &str = "Invalid maze preset, please check it";
357 let msg = match err {
358 GeneratorError::Unknown => UNKNOWN_MSG,
359 GeneratorError::Validation => VALIDATION_MSG,
360 };
361 log::error!("{}: {:?}", msg, err);
362
363 Some(Change::replace(Activity::new_base_boxed(
364 "game gen error",
365 Popup::new("Error".to_string(), vec![msg.to_string()]),
366 )))
367 }
368 }
369 }
370
371 fn screen(&self) -> &dyn ui::Screen {
372 &self.progress_bar
373 }
374}
375
376pub struct PauseMenu {
377 menu: Menu,
378 actions: Vec<MenuAction<Change>>,
379}
380
381#[allow(clippy::new_without_default)]
382impl PauseMenu {
383 pub fn new() -> Self {
384 let options = menu_actions!(
385 "Resume" -> _ => Change::pop_top(),
386 "Main Menu" -> _ => Change::pop_until("main menu"),
387 "Controls" -> _ => Change::push(create_controls_popup()),
388 "Settings" -> _ => Change::push(SettingsActivity::new_activity()),
389 "Quit" -> _ => Change::pop_all(),
390 );
391
392 let (options, actions) = split_menu_actions(options);
393
394 let menu = Menu::new(MenuConfig::new("Paused", options));
395
396 Self { menu, actions }
397 }
398}
399
400impl ActivityHandler for PauseMenu {
401 fn update(&mut self, events: Vec<Event>, data: &mut AppData) -> Option<Change> {
402 match self.menu.update(events, data) {
403 Some(change) => match change {
404 Change::Pop { res: Some(res), .. } => {
405 let index = *res.downcast::<usize>().expect("menu should return index");
406
407 Some((self.actions[index])(data))
408 }
409 res => Some(res),
410 },
411 None => None,
412 }
413 }
414
415 fn screen(&self) -> &dyn Screen {
416 &self.menu
417 }
418}
419
420pub struct EndGamePopup {
421 popup: Popup,
422 preset: MazePreset,
423}
424
425impl EndGamePopup {
426 pub fn new(game: &RunningGame, preset: MazePreset) -> Self {
427 let maze_size = game.get_maze().size();
428 let texts = vec![
429 format!("Time: {}", format_duration(game.get_elapsed().unwrap())),
430 format!("Moves: {}", game.get_move_count()),
431 format!("Size: {}x{}x{}", maze_size.0, maze_size.1, maze_size.2,),
432 ];
433
434 let popup = Popup::new("You won".to_string(), texts);
435
436 Self { popup, preset }
437 }
438}
439
440impl ActivityHandler for EndGamePopup {
441 fn update(&mut self, events: Vec<Event>, data: &mut AppData) -> Option<Change> {
442 match self.popup.update(events, data) {
443 Some(Change::Pop {
444 n: 1,
445 res: Some(code),
446 }) => match code.downcast::<KeyCode>() {
447 Ok(b) => match *b {
448 KeyCode::Char('r') => Some(Change::replace(Activity::new_base_boxed(
449 "game",
450 MazeGenerationActivity::new(self.preset.clone(), &data.registries),
451 ))),
452 KeyCode::Char('q') => Some(Change::pop_all()),
453 KeyCode::Enter | KeyCode::Char(' ') => Some(Change::pop_top()),
454 _ => None,
455 },
456 _ => panic!("expected `KeyCode` from `Popup`"),
457 },
458 res => res,
459 }
460 }
461
462 fn screen(&self) -> &dyn Screen {
463 &self.popup
464 }
465}
466
467pub struct GameActivity {
468 camera_mode: CameraMode,
469 data: GameData,
470 maze_board: MazeBoard,
471 show_debug: bool,
472
473 margins: Dims,
475 viewport_rect: Rect,
476 dpad_rect: Option<Rect>,
477
478 sm_camera_pos: Dims3D,
480 sm_player_pos: Dims3D,
481
482 touch_controls: Option<Box<DPad>>,
484}
485
486impl GameActivity {
487 pub fn new(game: GameData, app_data: &mut AppData) -> Self {
488 let settings = &app_data.settings;
489
490 let camera_mode = settings.get_camera_mode();
491 let maze_board = MazeBoard::new(&game.game, &app_data.theme);
492 let margins = settings.get_viewport_margin();
493
494 #[cfg(feature = "sound")]
495 app_data.play_bgm(MusicTrack::choose_for_maze(game.game.get_maze()));
496
497 let sm_camera_pos = game.camera_pos;
498 let sm_player_pos = maze2screen_3d(game.game.get_player_pos());
499
500 Self {
501 camera_mode,
502 data: game,
503 maze_board,
504 show_debug: false,
505
506 margins,
507 viewport_rect: Rect::sized(app_data.screen_size),
508 dpad_rect: None,
509
510 sm_camera_pos,
511 sm_player_pos,
512
513 touch_controls: None,
514 }
515 }
516
517 pub fn viewport_size(&self, screen_size: Dims) -> (Dims, bool) {
519 let vp_size = screen_size - self.margins * 2;
520
521 let maze_frame = &self.maze_board.frames[self.data.game.get_player_pos().2 as usize];
522 let floor_size = maze_frame.size;
523
524 let does_fit = floor_size.0 <= vp_size.0 && floor_size.1 <= vp_size.1;
525
526 (if does_fit { floor_size } else { vp_size }, does_fit)
527 }
528
529 fn current_floor_frame(&self) -> &Frame {
530 &self.maze_board.frames[self.data.camera_pos.2 as usize]
531 }
532
533 fn render_meta_texts(&self, frame: &mut Frame, theme: &Theme, vp: Rect) {
534 let max_width = (vp.size().0 / 2 + 1) as usize;
535
536 let pl_pos = self.data.game.get_player_pos() + Dims3D(1, 1, 1);
537
538 let from_start =
540 multisize_duration_format(self.data.game.get_elapsed().unwrap(), max_width);
541 let move_count = strings::multisize_string(
542 [
543 format!("{} moves", self.data.game.get_move_count()),
544 format!("{}m", self.data.game.get_move_count()),
545 ],
546 max_width,
547 );
548
549 let pos_text = if self.data.game.get_maze().size().2 > 1 {
550 strings::multisize_string(
551 [
552 format!("x:{} y:{} floor:{}", pl_pos.0, pl_pos.1, pl_pos.2),
553 format!("x:{} y:{} f:{}", pl_pos.0, pl_pos.1, pl_pos.2),
554 format!("{}:{}:{}", pl_pos.0, pl_pos.1, pl_pos.2),
555 ],
556 max_width,
557 )
558 } else {
559 strings::multisize_string(
560 [
561 format!("x:{} y:{}", pl_pos.0, pl_pos.1),
562 format!("x:{} y:{}", pl_pos.0, pl_pos.1),
563 format!("{}:{}", pl_pos.0, pl_pos.1),
564 ],
565 max_width,
566 )
567 };
568
569 let view_mode = self.data.view_mode;
570 let view_mode = strings::multisize_string(view_mode.to_multisize_strings(), max_width);
571
572 let tl = vp.start - Dims(0, 1);
573 let br = vp.start + vp.size();
574
575 let style = theme["text"];
576 let mut draw = |text: &str, pos| frame.draw(pos, text, style);
577
578 draw(&pos_text, tl);
579 draw(view_mode, Dims(br.0 - view_mode.len() as i32, tl.1));
580 draw(&move_count, Dims(tl.0, br.1));
581 draw(&from_start, Dims(br.0 - from_start.len() as i32, br.1));
582 }
583
584 pub fn render_visited_places(&self, frame: &mut Frame, maze_pos: Dims, theme: &Theme) {
585 use CellWall::{Down, Up};
586
587 let game = &self.data.game;
588 for (move_pos, _) in game.get_moves() {
589 let cell = game.get_maze().board.get_cell(*move_pos).unwrap();
590 if move_pos.2 == game.get_player_pos().2 && cell.get_wall(Up) && cell.get_wall(Down) {
591 let real_pos = maze2screen(*move_pos) + maze_pos;
592 frame.draw(real_pos, '.', theme["game.visited"]); }
595 }
596 }
597
598 fn render_player(
599 &self,
600 maze_pos: Dims,
601 game: &RunningGame,
602 viewport: &mut Frame,
603 theme: &Theme,
604 ) {
605 let player = self.sm_player_pos;
606 let player_draw_pos = maze_pos + player.into();
607 let cell = game
608 .get_maze()
609 .board
610 .get_cell(self.data.game.get_player_pos())
611 .unwrap();
612 if !cell.get_wall(CellWall::Up) || !cell.get_wall(CellWall::Down) {
613 viewport[player_draw_pos]
614 .content_mut()
615 .unwrap()
616 .style
617 .foreground_color = theme["game.player"].to_cross().foreground_color;
618 } else {
619 viewport.draw(player_draw_pos, self.data.player_char, theme["game.player"]);
620 }
621 }
622
623 fn update_viewport(&mut self, data: &AppData) {
624 if self.is_dpad_enabled() {
625 let (viewport_rect, dpad_rect) = DPad::split_screen(data);
626 let mut dpad_rect = dpad_rect;
627 if data.settings.get_enable_margin_around_dpad() {
628 dpad_rect = dpad_rect.margin(self.margins);
629 }
630
631 self.viewport_rect = viewport_rect;
632 self.dpad_rect = Some(dpad_rect);
633 } else {
634 self.viewport_rect = Rect::sized(data.screen_size);
635 }
636 }
637}
638impl GameActivity {
639 fn is_dpad_enabled(&self) -> bool {
640 self.touch_controls.is_some()
641 }
642
643 fn init_dpad(&mut self, data: &AppData) {
644 let dpad_type = DPadType::from_maze(self.data.game.get_maze());
645 let swap_up_down = data.settings.get_dpad_swap_up_down();
646
647 let touch_controls = DPad::new(None, swap_up_down, dpad_type);
648 self.touch_controls = Some(Box::new(touch_controls));
649 }
650
651 fn update_dpad(&mut self, data: &AppData) {
652 if (data.settings.get_enable_dpad() && data.settings.get_enable_mouse())
653 != self.is_dpad_enabled()
654 {
655 if data.settings.get_enable_dpad() {
656 log::info!("Enabling dpad");
657 self.init_dpad(data);
658 } else {
659 log::info!("Disabling dpad");
660 self.deinit_dpad(data);
661 }
662 }
663
664 if self.is_dpad_enabled() {
665 let dpad = self.touch_controls.as_mut().expect("dpad not set");
666
667 dpad.swap_up_down = data.settings.get_dpad_swap_up_down();
668 dpad.disable_highlight(!data.settings.get_enable_dpad_highlight());
669 }
670 }
671
672 fn deinit_dpad(&mut self, data: &AppData) {
673 self.touch_controls = None;
674
675 self.viewport_rect = Rect::sized(data.screen_size);
676 self.dpad_rect = None;
677 }
678}
679
680impl ActivityHandler for GameActivity {
681 fn update(&mut self, events: Vec<Event>, data: &mut AppData) -> Option<Change> {
682 match self.data.game.get_state() {
683 RunningGameState::NotStarted => self.data.game.start().unwrap(),
684 RunningGameState::Paused => self.data.game.resume().unwrap(),
685 _ => {}
686 }
687
688 self.update_dpad(data);
689 self.update_viewport(data);
690
691 if let Some(ref mut tc) = self.touch_controls {
692 tc.update_space(self.dpad_rect.expect("dpad rect not set"));
693 }
694
695 for event in events {
696 #[allow(clippy::single_match)]
697 match event {
698 Event::Term(event) => match event {
699 TermEvent::Key(key_event) => {
700 match self.data.handle_event(&data.settings, key_event) {
701 Err(false) => {
702 self.data.game.pause().unwrap();
703
704 return Some(Change::push(Activity::new_base_boxed(
705 "pause".to_string(),
706 PauseMenu::new(),
707 )));
708 }
709 Err(true) => return Some(Change::pop_until("main menu")),
710 Ok(_) => {}
711 }
712 }
713 TermEvent::Mouse(event) => {
714 if let Some(ref mut touch_controls) = self.touch_controls {
715 if let Some(dir) = touch_controls.apply_mouse_event(event) {
716 self.data.apply_move(&data.settings, dir, false);
717 }
718 }
719 }
720 _ => {}
721 },
722 _ => (),
723 }
724 }
725
726 if let Some(ref mut tc) = self.touch_controls {
727 tc.update_available_moves(if self.data.view_mode == GameViewMode::Adventure {
728 self.data.game.get_available_moves()
729 } else {
730 [true; 6] });
732 }
733
734 if self.data.view_mode == GameViewMode::Adventure {
735 match self.camera_mode {
736 CameraMode::CloseFollow => {
737 self.data.camera_pos = maze2screen_3d(self.data.game.get_player_pos());
738 }
739 CameraMode::EdgeFollow { x: xoff, y: yoff } => 'b: {
740 self.data.camera_pos.2 = self.data.game.get_player_pos().2;
741
742 let (vp_size, does_fit) = self.viewport_size(data.screen_size);
743
744 if does_fit {
745 break 'b;
746 }
747
748 let xoff = xoff.to_abs(vp_size.0);
749 let yoff = yoff.to_abs(vp_size.1);
750
751 let player_pos = maze2screen(self.data.game.get_player_pos());
752 let player_pos_in_vp =
753 player_pos - self.data.camera_pos.into() + vp_size / 2 + Dims(1, 1);
754
755 if player_pos_in_vp.0 < xoff || player_pos_in_vp.0 > vp_size.0 - xoff {
756 self.data.camera_pos.0 = player_pos.0;
757 }
758
759 if player_pos_in_vp.1 < yoff || player_pos_in_vp.1 > vp_size.1 - yoff {
760 self.data.camera_pos.1 = player_pos.1;
761 }
762 }
763 }
764 }
765
766 self.sm_player_pos = lerp!((self.sm_player_pos) -> (maze2screen_3d(self.data.game.get_player_pos())) at data.settings.get_player_smoothing());
767 self.sm_camera_pos = lerp!((self.sm_camera_pos) -> (self.data.camera_pos) at data.settings.get_camera_smoothing());
768
769 self.show_debug = data.use_data.show_debug;
770
771 if self.data.game.get_state() == RunningGameState::Finished {
772 return Some(Change::replace_at(
773 1,
774 Activity::new_base_boxed(
775 "won".to_string(),
776 EndGamePopup::new(&self.data.game, self.data.maze_preset.clone()),
777 ),
778 ));
779 };
780
781 None
782 }
783
784 fn screen(&self) -> &dyn ui::Screen {
785 self
786 }
787}
788
789impl Screen for GameActivity {
790 fn draw(&self, frame: &mut Frame, theme: &Theme) -> std::io::Result<()> {
791 let maze_frame = self.current_floor_frame();
792 let game = &self.data.game;
793
794 let game_view_rect = self.viewport_rect;
795 let game_view_size = game_view_rect.size();
796
797 let (vp_size, does_fit) = self.viewport_size(game_view_size);
798 let maze_pos = match does_fit {
799 true => match self.data.view_mode {
800 GameViewMode::Adventure => Dims(0, 0),
801 GameViewMode::Spectator => maze2screen(Dims(0, 0)) - self.sm_camera_pos.into(),
802 },
803 false => vp_size / 2 - self.sm_camera_pos.into(),
804 };
805
806 let mut viewport = Frame::new(vp_size);
808
809 viewport.draw(maze_pos, maze_frame, ());
811 self.render_visited_places(&mut viewport, maze_pos, theme);
812
813 if (self.data.game.get_player_pos().2) == self.sm_camera_pos.2 {
815 self.render_player(maze_pos, game, &mut viewport, theme);
816 }
817
818 let vp_pos = (game_view_size - vp_size) / 2 + self.viewport_rect.start;
820 let vp_rect = Rect::sized_at(vp_pos, vp_size).margin(Dims(-1, -1));
821 vp_rect.render(frame, theme["game.viewport.border"]);
822
823 if let CameraMode::EdgeFollow { x: xoff, y: yoff } = self.camera_mode {
824 if !does_fit && self.show_debug {
825 render_edge_follow_rulers((xoff, yoff), frame, vp_rect, theme);
826 }
827 }
828
829 self.render_meta_texts(frame, theme, vp_rect);
830
831 frame.draw(vp_pos, &viewport, ());
832
833 if let Some(ref touch_controls) = self.touch_controls {
835 let mut dpad_frame = Frame::new(self.dpad_rect.unwrap().size());
836
837 touch_controls.render(&mut dpad_frame, theme);
838 frame.draw(self.dpad_rect.unwrap().start, &dpad_frame, ());
839 }
840
841 if self.show_debug {
842 if let Some(dpad_rect) = self.dpad_rect {
843 dpad_rect.render(frame, theme["debug.border"]);
844 }
845
846 self.viewport_rect.render(frame, theme["debug.border"]);
847 }
848
849 Ok(())
850 }
851}
852
853#[inline]
854fn render_edge_follow_rulers(rulers: (Offset, Offset), frame: &mut Frame, vp: Rect, theme: &Theme) {
855 let [s_start, s_end] = theme.extract(["debug.rulers.start", "debug.rulers.end"]);
856
857 let vps = vp.size();
858
859 let xo = rulers.0.to_abs(vps.0);
860 let yo = rulers.1.to_abs(vps.1);
861
862 let frame_pos = vp.start;
863
864 use LineDir::{Horizontal, Vertical};
865 const V: char = Vertical.round();
866 const H: char = Horizontal.round();
867
868 let mut draw = |pos, dir, end| {
869 let style = match end {
870 false => s_start,
871 true => s_end,
872 };
873 frame.draw(frame_pos + pos, dir, style)
874 };
875
876 #[rustfmt::skip]
877 {
878 draw(Dims(xo , 0 ), V, false);
879 draw(Dims(vps.0 - xo, 0 ), V, true);
880 draw(Dims(xo , vps.1 + 1), V, false);
881 draw(Dims(vps.0 - xo, vps.1 + 1), V, true);
882
883 draw(Dims(0 , yo ), H, false);
884 draw(Dims(0 , vps.1 - yo), H, true);
885 draw(Dims(vps.0 + 1 , yo ), H, false);
886 draw(Dims(vps.0 + 1 , vps.1 - yo), H, true);
887 };
888}
889
890pub struct MazeBoard {
891 frames: Vec<Frame>,
892}
893
894impl MazeBoard {
895 pub fn new(game: &RunningGame, theme: &Theme) -> Self {
896 let maze = game.get_maze();
897
898 let mut frames: Vec<_> = (0..maze.size().2)
899 .map(|floor| Self::render_floor(game, floor, theme))
900 .collect();
901
902 Self::render_special(&mut frames, game, theme);
903
904 Self { frames }
905 }
906
907 fn render_floor(game: &RunningGame, floor: i32, theme: &Theme) -> Frame {
908 let board = &game.get_maze().board;
909 let normals = theme["game.walls"];
910
911 let size = maze_render_size(board);
912
913 let mut frame = Frame::new(size);
914 frame.fill(renderer::Cell::styled(' ', theme["game.background"]));
915
916 let mut draw = |pos, l: LineDir| frame.draw(Dims::from(pos), l.double(), normals);
917
918 for y in -1..board.size().1 {
919 for x in -1..board.size().0 {
920 let cell_pos = Dims3D(x, y, floor);
921 let Dims(rx, ry) = maze2screen(cell_pos);
922
923 if board.get_wall(cell_pos, CellWall::Right) {
924 draw((rx + 1, ry), LineDir::Vertical);
925 }
926
927 if board.get_wall(cell_pos, CellWall::Bottom) {
928 draw((rx, ry + 1), LineDir::Horizontal);
929 }
930
931 let cp1 = cell_pos;
932 let cp2 = cell_pos + Dims3D(1, 1, 0);
933
934 let dir = LineDir::from_bools(
935 board.get_wall(cp1, CellWall::Bottom),
936 board.get_wall(cp1, CellWall::Right),
937 board.get_wall(cp2, CellWall::Top),
938 board.get_wall(cp2, CellWall::Left),
939 );
940
941 draw((rx + 1, ry + 1), dir);
942 }
943 }
944
945 let layer = board.get_cells().layer(floor as usize).unwrap();
946 Self::render_stairs(&mut frame, layer, game.get_maze().is_tower(), theme);
947
948 frame
949 }
950
951 fn render_stairs(frame: &mut Frame, floors: Array2DView<Cell>, tower: bool, theme: &Theme) {
952 let s_stairs_up = theme["game.stairs.up"];
953 let s_stairs_down = theme["game.stairs.down"];
954 let s_stairs_both = theme["game.stairs.both"];
955 let s_stairs_up_tower = theme["game.stairs.up.tower"];
956
957 for pos in floors.iter_pos() {
958 let cell = floors[pos];
959 let (up, down) = (!cell.get_wall(CellWall::Up), !cell.get_wall(CellWall::Down));
960 let (ch, st) = match (up, down) {
961 (true, true) => ('⥮', s_stairs_both),
962 (true, false) => ('↑', s_stairs_up),
963 (false, true) => ('↓', s_stairs_down),
964 _ => continue,
965 };
966
967 let style = if tower && up { s_stairs_up_tower } else { st };
968 let pos = maze2screen(pos);
969 frame.draw(pos, ch, style);
970 }
971 }
972
973 fn render_special(frames: &mut [Frame], game: &RunningGame, theme: &Theme) {
974 let goal_style = theme["game.goal"];
975 let goal_pos = game.get_goal_pos();
976
977 let frame = &mut frames[goal_pos.2 as usize];
978 frame.draw(maze2screen(goal_pos), '$', goal_style);
979 }
980}
981
982pub fn game_theme_resolver() -> ThemeResolver {
983 let mut resolver = ThemeResolver::new();
984
985 resolver
986 .link("game.walls", "border")
987 .link("game.stairs", "game.walls")
989 .link("game.stairs.up", "game.stairs")
990 .link("game.stairs.down", "game.stairs")
991 .link("game.stairs.both", "game.stairs")
992 .link("game.stairs.up.tower", "game.goal")
993 .link("game.goal", "")
995 .link("game.player", "highlight")
996 .link("game.player.on.stairs", "game.stairs")
997 .link("game.visited", "dim")
998 .link("game.background", "background")
999 .link("game.viewport.border", "border")
1001 .link("debug.border", "border")
1002 .link("debug.rulers", "debug.border")
1003 .link("debug.rulers.start", "debug.rulers")
1004 .link("debug.rulers.end", "debug.rulers");
1005
1006 resolver
1007}