tmaze/app/
game.rs

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                &registries.region_generator,
277                &registries.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); // dummy value
294                            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); // dummy value
311                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    // spacing
474    margins: Dims,
475    viewport_rect: Rect,
476    dpad_rect: Option<Rect>,
477
478    // smooth
479    sm_camera_pos: Dims3D,
480    sm_player_pos: Dims3D,
481
482    // touch
483    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    /// Returns the size of the viewport and whether the floor fits in the viewport
518    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        // texts
539        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"]); // FIXME: move out of the
593                                                                  // loop
594            }
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] // enable all
731            });
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        // TODO: reuse the viewport between frames and resize it when needed
807        let mut viewport = Frame::new(vp_size);
808
809        // maze
810        viewport.draw(maze_pos, maze_frame, ());
811        self.render_visited_places(&mut viewport, maze_pos, theme);
812
813        // player
814        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        // show viewport box
819        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        // touch controls
834        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        // stairs
988        .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        // game
994        .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        // special
1000        .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}