diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f243f2..75eb77f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Game store page: https://crispypin.itch.io/marble-machinations ## [unreleased] +### added +- click to collapse chapters in level list ### fixed - When two input bindings had the same trigger but one has a strict subset of the others modifiers, both would activate when the one with more modifiers was pressed. For example (Ctrl+S -> Save) would also trigger (S -> Wire Tool). Now, Shift+S will still trigger Wire Tool, unless Shift+S (or eg. Shift+Ctrl+S) is bound to something else. diff --git a/src/level.rs b/src/level.rs index 0ef9b12..0a60a84 100644 --- a/src/level.rs +++ b/src/level.rs @@ -6,6 +6,8 @@ use crate::board::Board; pub struct Chapter { pub title: String, pub levels: Vec, + #[serde(default = "default_true")] + pub visible: bool, } #[derive(Debug, Clone, Deserialize)] @@ -81,3 +83,7 @@ impl Stage { &self.output } } + +fn default_true() -> bool { + true +} diff --git a/src/main.rs b/src/main.rs index 872e913..86e4830 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use marble_machinations::*; use config::Config; use editor::{Editor, ExitState}; -use level::{Chapter, Level}; +use level::Chapter; use solution::Solution; use theme::*; use ui::{simple_option_button, tex32_button, text_button, text_input, ShapedText, Tooltip}; @@ -19,11 +19,11 @@ use util::*; const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION")); struct Game { - levels: Vec, - level_scroll: usize, + chapters: Vec, + level_scroll: i32, solutions: HashMap>, open_editor: Option, - selected_level: usize, + selected_level: (usize, usize), selected_solution: usize, delete_solution: Option, editing_solution_name: bool, @@ -32,12 +32,6 @@ struct Game { edit_settings: Option, } -#[derive(Debug)] -enum LevelListEntry { - Level(Level), - Chapter(String, usize), -} - fn main() { let (mut rl, thread) = raylib::init().resizable().title(TITLE_TEXT).build(); rl.set_target_fps(60); @@ -52,15 +46,15 @@ fn main() { impl Game { fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self { - let levels = get_levels(); + let chapters = get_chapters(); let solutions = get_solutions(); Self { - levels, + chapters, level_scroll: 0, solutions, open_editor: None, - selected_level: 0, + selected_level: (0, 0), selected_solution: 0, delete_solution: None, editing_solution_name: false, @@ -147,71 +141,89 @@ impl Game { const ENTRY_SPACING: i32 = 65; let fit_on_screen = (d.get_screen_height() / ENTRY_SPACING) as usize; - let max_scroll = self.levels.len().saturating_sub(fit_on_screen); + let max_scroll = self + .chapters + .iter() + .map(|c| 1 + if c.visible { c.levels.len() } else { 0 }) + .sum::() + .saturating_sub(fit_on_screen) as i32 + * ENTRY_SPACING; if self.globals.mouse.pos().x < level_list_width as f32 { if self.globals.mouse.scroll() == Some(Scroll::Down) && self.level_scroll < max_scroll { - self.level_scroll += 1; + self.level_scroll += ENTRY_SPACING; } if self.globals.mouse.scroll() == Some(Scroll::Up) && self.level_scroll > 0 { - self.level_scroll -= 1; + self.level_scroll -= ENTRY_SPACING; } } - for (row_index, level_index) in (self.level_scroll..self.levels.len()).enumerate() { - let level = &mut self.levels[level_index]; - let y = 10 + row_index as i32 * ENTRY_SPACING; - let bounds = Rectangle { - x: 5., - y: y as f32 - 5., - width: level_list_width as f32 - 10., - height: ENTRY_SPACING as f32 - 5., - }; + let mut y = 10 - self.level_scroll; + for (chapter_i, chapter) in self.chapters.iter_mut().enumerate() { + let bounds = rect(5, y - 5, level_list_width - 10, ENTRY_SPACING - 5); + d.draw_rectangle_rec(bounds, BG_DARK); + d.draw_text(&chapter.title, 10, y, 30, FG_CHAPTER_TITLE); + let subtitle = format!("{} levels", chapter.levels.len()); + d.draw_text(&subtitle, 10, y + 30, 20, Color::WHITE); + y += ENTRY_SPACING; let clicked_this = self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds); - match level { - LevelListEntry::Chapter(title, level_count) => { - d.draw_rectangle_rec(bounds, BG_DARK); - d.draw_text(title, 10, y, 30, FG_CHAPTER_TITLE); - let subtitle = format!("{level_count} levels"); - d.draw_text(&subtitle, 10, y + 30, 20, Color::WHITE); - } - LevelListEntry::Level(level) => { - if clicked_this && self.selected_level != level_index { - self.editing_solution_name = false; - self.selected_level = level_index; - self.selected_solution = 0; - self.delete_solution = None; - // select the last solution of the level, if there is one - if let Some(solutions) = self.solutions.get(level.id()) { - self.selected_solution = solutions.len().saturating_sub(1); - } - } - d.draw_rectangle_rec(bounds, widget_bg(self.selected_level == level_index)); - let mut title_color = Color::WHITE; + if clicked_this { + chapter.visible = !chapter.visible; + } + + if !chapter.visible { + continue; + } + + for (level_index, level) in chapter.levels.iter().enumerate() { + let bounds = rect(5, y - 5, level_list_width - 10, ENTRY_SPACING - 5); + let clicked_this = + self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds); + + if clicked_this && self.selected_level != (chapter_i, level_index) { + self.editing_solution_name = false; + self.selected_level = (chapter_i, level_index); + self.selected_solution = 0; + self.delete_solution = None; + // select the last solution of the level, if there is one if let Some(solutions) = self.solutions.get(level.id()) { - if solutions.iter().any(|s| s.score.is_some()) { - title_color = Color::LIGHTGREEN; - } + self.selected_solution = solutions.len().saturating_sub(1); } - d.draw_text(level.name(), 10, y, 30, title_color); - let solution_count = self - .solutions - .get(level.id()) - .map(Vec::len) - .unwrap_or_default(); - let subtext = format!("solutions: {solution_count}"); - let subtext_color = if solution_count > 0 { - Color::GOLD - } else { - Color::LIGHTGRAY - }; - d.draw_text(&subtext, 10, y + 30, 20, subtext_color); } + d.draw_rectangle_rec( + bounds, + widget_bg(self.selected_level == (chapter_i, level_index)), + ); + + let mut title_color = Color::WHITE; + if let Some(solutions) = self.solutions.get(level.id()) { + if solutions.iter().any(|s| s.score.is_some()) { + title_color = Color::LIGHTGREEN; + } + } + d.draw_text(level.name(), 10, y, 30, title_color); + let solution_count = self + .solutions + .get(level.id()) + .map(Vec::len) + .unwrap_or_default(); + let subtext = format!("solutions: {solution_count}"); + let subtext_color = if solution_count > 0 { + Color::GOLD + } else { + Color::LIGHTGRAY + }; + d.draw_text(&subtext, 10, y + 30, 20, subtext_color); + y += ENTRY_SPACING; } } - if let Some(LevelListEntry::Level(level)) = self.levels.get(self.selected_level) { + if let Some(level) = self + .chapters + .get(self.selected_level.0) + .and_then(|c| c.levels.get(self.selected_level.1)) + { d.draw_text(level.name(), level_list_width + 10, 10, 40, Color::CYAN); d.draw_text(level.id(), level_list_width + 10, 50, 10, Color::GRAY); @@ -343,7 +355,7 @@ impl Game { } } -fn get_levels() -> Vec { +fn get_chapters() -> Vec { let mut chapters = Vec::::new(); for d in read_dir("levels").unwrap().flatten() { if let Ok(text) = read_to_string(d.path()) { @@ -354,12 +366,6 @@ fn get_levels() -> Vec { } chapters.sort_unstable_by_key(|c| c.title.clone()); - let mut level_list = Vec::new(); - for c in chapters { - level_list.push(LevelListEntry::Chapter(c.title, c.levels.len())); - level_list.extend(c.levels.into_iter().map(LevelListEntry::Level)); - } - // user levels let mut custom_levels = Vec::new(); let custom_level_dir = userdata_dir().join("levels"); @@ -372,15 +378,13 @@ fn get_levels() -> Vec { } } } - level_list.push(LevelListEntry::Chapter( - "Custom levels".into(), - custom_levels.len(), - )); - for l in custom_levels { - level_list.push(LevelListEntry::Level(l)); - } + chapters.push(Chapter { + title: "Custom levels".into(), + levels: custom_levels, + visible: true, + }); - level_list + chapters } fn get_solutions() -> HashMap> {