allow collapsing chapters in level list
This commit is contained in:
parent
69fe8546f5
commit
522a027f7a
3 changed files with 89 additions and 77 deletions
|
@ -2,6 +2,8 @@
|
||||||
Game store page: https://crispypin.itch.io/marble-machinations
|
Game store page: https://crispypin.itch.io/marble-machinations
|
||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
### added
|
||||||
|
- click to collapse chapters in level list
|
||||||
### fixed
|
### 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ use crate::board::Board;
|
||||||
pub struct Chapter {
|
pub struct Chapter {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub levels: Vec<Level>,
|
pub levels: Vec<Level>,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -81,3 +83,7 @@ impl Stage {
|
||||||
&self.output
|
&self.output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
108
src/main.rs
108
src/main.rs
|
@ -10,7 +10,7 @@ use marble_machinations::*;
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use editor::{Editor, ExitState};
|
use editor::{Editor, ExitState};
|
||||||
use level::{Chapter, Level};
|
use level::Chapter;
|
||||||
use solution::Solution;
|
use solution::Solution;
|
||||||
use theme::*;
|
use theme::*;
|
||||||
use ui::{simple_option_button, tex32_button, text_button, text_input, ShapedText, Tooltip};
|
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"));
|
const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
struct Game {
|
struct Game {
|
||||||
levels: Vec<LevelListEntry>,
|
chapters: Vec<Chapter>,
|
||||||
level_scroll: usize,
|
level_scroll: i32,
|
||||||
solutions: HashMap<String, Vec<Solution>>,
|
solutions: HashMap<String, Vec<Solution>>,
|
||||||
open_editor: Option<Editor>,
|
open_editor: Option<Editor>,
|
||||||
selected_level: usize,
|
selected_level: (usize, usize),
|
||||||
selected_solution: usize,
|
selected_solution: usize,
|
||||||
delete_solution: Option<usize>,
|
delete_solution: Option<usize>,
|
||||||
editing_solution_name: bool,
|
editing_solution_name: bool,
|
||||||
|
@ -32,12 +32,6 @@ struct Game {
|
||||||
edit_settings: Option<Config>,
|
edit_settings: Option<Config>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum LevelListEntry {
|
|
||||||
Level(Level),
|
|
||||||
Chapter(String, usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let (mut rl, thread) = raylib::init().resizable().title(TITLE_TEXT).build();
|
let (mut rl, thread) = raylib::init().resizable().title(TITLE_TEXT).build();
|
||||||
rl.set_target_fps(60);
|
rl.set_target_fps(60);
|
||||||
|
@ -52,15 +46,15 @@ fn main() {
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self {
|
fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self {
|
||||||
let levels = get_levels();
|
let chapters = get_chapters();
|
||||||
let solutions = get_solutions();
|
let solutions = get_solutions();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
levels,
|
chapters,
|
||||||
level_scroll: 0,
|
level_scroll: 0,
|
||||||
solutions,
|
solutions,
|
||||||
open_editor: None,
|
open_editor: None,
|
||||||
selected_level: 0,
|
selected_level: (0, 0),
|
||||||
selected_solution: 0,
|
selected_solution: 0,
|
||||||
delete_solution: None,
|
delete_solution: None,
|
||||||
editing_solution_name: false,
|
editing_solution_name: false,
|
||||||
|
@ -147,38 +141,49 @@ impl Game {
|
||||||
|
|
||||||
const ENTRY_SPACING: i32 = 65;
|
const ENTRY_SPACING: i32 = 65;
|
||||||
let fit_on_screen = (d.get_screen_height() / ENTRY_SPACING) as usize;
|
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::<usize>()
|
||||||
|
.saturating_sub(fit_on_screen) as i32
|
||||||
|
* ENTRY_SPACING;
|
||||||
if self.globals.mouse.pos().x < level_list_width as f32 {
|
if self.globals.mouse.pos().x < level_list_width as f32 {
|
||||||
if self.globals.mouse.scroll() == Some(Scroll::Down) && self.level_scroll < max_scroll {
|
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 {
|
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 mut y = 10 - self.level_scroll;
|
||||||
let level = &mut self.levels[level_index];
|
for (chapter_i, chapter) in self.chapters.iter_mut().enumerate() {
|
||||||
let y = 10 + row_index as i32 * ENTRY_SPACING;
|
let bounds = rect(5, y - 5, level_list_width - 10, ENTRY_SPACING - 5);
|
||||||
let bounds = Rectangle {
|
d.draw_rectangle_rec(bounds, BG_DARK);
|
||||||
x: 5.,
|
d.draw_text(&chapter.title, 10, y, 30, FG_CHAPTER_TITLE);
|
||||||
y: y as f32 - 5.,
|
let subtitle = format!("{} levels", chapter.levels.len());
|
||||||
width: level_list_width as f32 - 10.,
|
d.draw_text(&subtitle, 10, y + 30, 20, Color::WHITE);
|
||||||
height: ENTRY_SPACING as f32 - 5.,
|
y += ENTRY_SPACING;
|
||||||
};
|
|
||||||
let clicked_this =
|
let clicked_this =
|
||||||
self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds);
|
self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds);
|
||||||
match level {
|
|
||||||
LevelListEntry::Chapter(title, level_count) => {
|
if clicked_this {
|
||||||
d.draw_rectangle_rec(bounds, BG_DARK);
|
chapter.visible = !chapter.visible;
|
||||||
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 {
|
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.editing_solution_name = false;
|
||||||
self.selected_level = level_index;
|
self.selected_level = (chapter_i, level_index);
|
||||||
self.selected_solution = 0;
|
self.selected_solution = 0;
|
||||||
self.delete_solution = None;
|
self.delete_solution = None;
|
||||||
// select the last solution of the level, if there is one
|
// select the last solution of the level, if there is one
|
||||||
|
@ -186,7 +191,10 @@ impl Game {
|
||||||
self.selected_solution = solutions.len().saturating_sub(1);
|
self.selected_solution = solutions.len().saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.draw_rectangle_rec(bounds, widget_bg(self.selected_level == level_index));
|
d.draw_rectangle_rec(
|
||||||
|
bounds,
|
||||||
|
widget_bg(self.selected_level == (chapter_i, level_index)),
|
||||||
|
);
|
||||||
|
|
||||||
let mut title_color = Color::WHITE;
|
let mut title_color = Color::WHITE;
|
||||||
if let Some(solutions) = self.solutions.get(level.id()) {
|
if let Some(solutions) = self.solutions.get(level.id()) {
|
||||||
|
@ -207,11 +215,15 @@ impl Game {
|
||||||
Color::LIGHTGRAY
|
Color::LIGHTGRAY
|
||||||
};
|
};
|
||||||
d.draw_text(&subtext, 10, y + 30, 20, subtext_color);
|
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.name(), level_list_width + 10, 10, 40, Color::CYAN);
|
||||||
d.draw_text(level.id(), level_list_width + 10, 50, 10, Color::GRAY);
|
d.draw_text(level.id(), level_list_width + 10, 50, 10, Color::GRAY);
|
||||||
|
|
||||||
|
@ -343,7 +355,7 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_levels() -> Vec<LevelListEntry> {
|
fn get_chapters() -> Vec<Chapter> {
|
||||||
let mut chapters = Vec::<Chapter>::new();
|
let mut chapters = Vec::<Chapter>::new();
|
||||||
for d in read_dir("levels").unwrap().flatten() {
|
for d in read_dir("levels").unwrap().flatten() {
|
||||||
if let Ok(text) = read_to_string(d.path()) {
|
if let Ok(text) = read_to_string(d.path()) {
|
||||||
|
@ -354,12 +366,6 @@ fn get_levels() -> Vec<LevelListEntry> {
|
||||||
}
|
}
|
||||||
chapters.sort_unstable_by_key(|c| c.title.clone());
|
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
|
// user levels
|
||||||
let mut custom_levels = Vec::new();
|
let mut custom_levels = Vec::new();
|
||||||
let custom_level_dir = userdata_dir().join("levels");
|
let custom_level_dir = userdata_dir().join("levels");
|
||||||
|
@ -372,15 +378,13 @@ fn get_levels() -> Vec<LevelListEntry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
level_list.push(LevelListEntry::Chapter(
|
chapters.push(Chapter {
|
||||||
"Custom levels".into(),
|
title: "Custom levels".into(),
|
||||||
custom_levels.len(),
|
levels: custom_levels,
|
||||||
));
|
visible: true,
|
||||||
for l in custom_levels {
|
});
|
||||||
level_list.push(LevelListEntry::Level(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
level_list
|
chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_solutions() -> HashMap<String, Vec<Solution>> {
|
fn get_solutions() -> HashMap<String, Vec<Solution>> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue