use std::{ collections::HashMap, fs::{read_dir, read_to_string}, }; use raylib::prelude::*; use marble_machinations::*; use editor::{Editor, ExitState}; use level::{Chapter, Level}; use solution::Solution; use theme::*; use ui::{simple_option_button, tex32_button, text_button, text_input, ShapedText, Tooltip}; use util::*; const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION")); struct Game { levels: Vec, level_scroll: usize, solutions: HashMap>, open_editor: Option, textures: Textures, selected_level: usize, selected_solution: usize, delete_solution: Option, editing_solution_name: bool, level_desc_text: ShapedText, } #[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); rl.set_window_min_size(640, 480); rl.set_mouse_cursor(MouseCursor::MOUSE_CURSOR_CROSSHAIR); rl.set_exit_key(None); rl.set_trace_log(TraceLogLevel::LOG_WARNING); let mut game = Game::new(&mut rl, &thread); game.run(&mut rl, &thread); } impl Game { fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self { let mut textures = Textures::default(); textures.load_dir("assets", rl, thread); textures.load_dir("assets/tiles", rl, thread); textures.load_dir("assets/digits", rl, thread); let levels = get_levels(); let solutions = get_solutions(); Self { levels, level_scroll: 0, solutions, open_editor: None, textures, selected_level: 0, selected_solution: 0, delete_solution: None, editing_solution_name: false, level_desc_text: ShapedText::new(20), } } fn run(&mut self, rl: &mut RaylibHandle, thread: &RaylibThread) { while !rl.window_should_close() { let mut d = rl.begin_drawing(thread); if let Some(editor) = &mut self.open_editor { editor.update(&d); editor.draw(&mut d, &self.textures); match editor.get_exit_state() { ExitState::Dont => (), ExitState::ExitAndSave => { let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap() [self.selected_solution]; solution.board = editor.source_board().clone(); solution.score = editor.score(); solution.save(); self.open_editor = None; } ExitState::Save => { let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap() [self.selected_solution]; solution.board = editor.source_board().clone(); solution.score = editor.score(); solution.save(); } } } else { self.draw(&mut d); } } } fn draw(&mut self, d: &mut RaylibDrawHandle) { d.clear_background(BG_DARK); let mut tooltip = Tooltip::default(); tooltip.init_frame(d); d.draw_text( TITLE_TEXT, d.get_screen_width() - 275, d.get_screen_height() - 20, 20, Color::GRAY, ); let level_list_width = (d.get_screen_width() / 3).min(400); let screen_height = d.get_screen_height(); d.draw_rectangle(0, 0, level_list_width, screen_height, BG_MEDIUM); let mouse = MouseInput::get(d); 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); if mouse.pos().x < level_list_width as f32 { if mouse.scroll() == Some(Scroll::Down) && self.level_scroll < max_scroll { self.level_scroll += 1; } if mouse.scroll() == Some(Scroll::Up) && self.level_scroll > 0 { self.level_scroll -= 1; } } 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 clicked_this = mouse.left_click() && 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 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); } } } if let Some(LevelListEntry::Level(level)) = self.levels.get(self.selected_level) { 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); let mut y = 70; self.level_desc_text.set_text(level.description()); self.level_desc_text .update_width(d, d.get_render_width() - level_list_width - 30); self.level_desc_text.draw(d, level_list_width + 10, y); y += self.level_desc_text.height() + 10; if let Some(solutions) = self.solutions.get_mut(level.id()) { let solution_entry_height = 40; let entry_width = 200; let mut solution_y = y; for (solution_index, solution) in solutions.iter().enumerate() { if simple_option_button( (d, &mouse), rect( level_list_width + 10, solution_y, entry_width, solution_entry_height, ), solution_index, &mut self.selected_solution, ) { self.editing_solution_name = false; } let name_color = if solution.score.is_some() { Color::LIME } else { Color::ORANGE }; d.draw_text( &solution.name, level_list_width + 15, solution_y + 5, 20, name_color, ); d.draw_text( &solution.score_text(), level_list_width + 15, solution_y + 25, 10, Color::WHITE, ); if tex32_button( (d, &mouse), (level_list_width + entry_width + 15, solution_y + 4), self.textures.get("cancel"), (&mut tooltip, "delete"), ) { self.delete_solution = Some(solution_index); } solution_y += solution_entry_height + 10; } let next_id = get_free_id(solutions, Solution::id); if text_button( d, &mouse, level_list_width + 10, solution_y, entry_width, "new solution", ) { self.selected_solution = solutions.len(); solutions.push(Solution::new(level, next_id)); } if let Some(i) = self.delete_solution { let text = format!("really delete solution '{}'?", &solutions[i].name); let y = (solution_y + 40).max(240); let x = level_list_width + 10; d.draw_text(&text, x, y, 20, Color::ORANGE); if text_button(d, &mouse, x, y + 30, 100, "yes") { solutions[i].remove_file(); solutions.remove(i); self.delete_solution = None; } if text_button(d, &mouse, x + 110, y + 30, 100, "no") { self.delete_solution = None; } } if let Some(solution) = solutions.get_mut(self.selected_solution) { let column_x = level_list_width + entry_width + 56; let bounds = Rectangle::new(column_x as f32, y as f32, 220., 30.); if text_input( d, &mouse, bounds, &mut solution.name, &mut self.editing_solution_name, 24, true, ) { solution.save(); } let id_text = format!("{}", solution.id()); d.draw_text(&id_text, column_x, y + 35, 10, Color::GRAY); if text_button(d, &mouse, column_x, y + 50, 220, "clone") { let cloned = solution.new_copy(next_id); self.selected_solution = solutions.len(); solutions.push(cloned); return; } if text_button(d, &mouse, column_x, y + 85, 220, "edit") { let mut editor = Editor::new(solution.clone(), level.clone()); editor.center_view(d); self.open_editor = Some(editor); } } } else { self.solutions.insert(level.id().to_owned(), Vec::new()); } } tooltip.draw(d); } } fn get_levels() -> Vec { let mut chapters = Vec::::new(); for d in read_dir("levels").unwrap().flatten() { if let Ok(text) = read_to_string(d.path()) { if let Ok(chapter) = serde_json::from_str(&text) { chapters.push(chapter); } } } 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"); if custom_level_dir.is_dir() { for d in read_dir(custom_level_dir).unwrap().flatten() { if let Ok(text) = read_to_string(d.path()) { if let Ok(level) = serde_json::from_str(&text) { custom_levels.push(level); } } } } level_list.push(LevelListEntry::Chapter( "Custom levels".into(), custom_levels.len(), )); for l in custom_levels { level_list.push(LevelListEntry::Level(l)); } level_list } fn get_solutions() -> HashMap> { let mut by_level = HashMap::new(); let solution_dir = userdata_dir().join("solutions"); if let Ok(dir_contents) = read_dir(solution_dir) { for dir in dir_contents.flatten() { if dir.path().is_dir() { let level_name = dir.file_name().to_string_lossy().to_string(); let mut solutions = Vec::new(); if let Ok(files) = read_dir(dir.path()) { for file in files.flatten() { let s = read_to_string(file.path()) .ok() .as_deref() .and_then(|s| serde_json::from_str(s).ok()); if let Some(solution) = s { solutions.push(solution); } } solutions.sort_unstable_by_key(Solution::id); } by_level.insert(level_name, solutions); } } } by_level }