use std::{ collections::HashMap, fs::{read_dir, read_to_string}, }; use raylib::prelude::*; mod blueprint; mod editor; mod level; mod marble_engine; mod solution; mod theme; mod util; use editor::{Editor, ExitState}; use level::Level; use solution::Solution; use theme::*; use util::*; const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION")); pub const TILE_TEXTURE_SIZE: f32 = 16.0; struct Game { levels: Vec, level_scroll: usize, solutions: HashMap>, open_editor: Option, textures: Textures, selected_level: usize, selected_solution: usize, editing_solution_name: bool, } 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); Self { levels: get_levels(), level_scroll: 0, solutions: get_solutions(), open_editor: None, textures, selected_level: 0, selected_solution: 0, editing_solution_name: false, } } 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().to_string(); 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().to_string(); solution.score = editor.score(); solution.save(); } ExitState::ExitNoSave => self.open_editor = None, } } else { self.draw(&mut d); } } } fn draw(&mut self, d: &mut RaylibDrawHandle) { d.clear_background(BG_DARK); 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 clicked = d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT); let mouse_pos = d.get_mouse_position(); let scroll_delta = d.get_mouse_wheel_move(); if mouse_pos.x < level_list_width as f32 { if scroll_delta < 0. && self.level_scroll < self.levels.len().saturating_sub(5) { self.level_scroll += 1; } else if scroll_delta > 0. && self.level_scroll > 0 { self.level_scroll -= 1; } } for (i, level) in self.levels[self.level_scroll..].iter().enumerate() { let level_entry_height = 65; let index = i + self.level_scroll; let y = 10 + i as i32 * level_entry_height; let bounds = Rectangle { x: 5., y: y as f32 - 5., width: level_list_width as f32 - 10., height: level_entry_height as f32 - 5., }; if clicked && bounds.check_collision_point_rec(mouse_pos) && self.selected_level != index { self.selected_solution = 0; self.editing_solution_name = false; self.selected_level = index; } d.draw_rectangle_rec(bounds, widget_bg(self.selected_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(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; for line in level.description().lines() { d.draw_text(line, level_list_width + 10, y, 20, Color::WHITE); y += 30; } 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, 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, ); solution_y += solution_entry_height + 10; } let next_id = get_free_id(solutions, Solution::id); if simple_button(d, level_list_width + 10, solution_y, entry_width, 30) { self.selected_solution = solutions.len(); solutions.push(Solution::new(level, next_id)); } d.draw_text( "new solution", level_list_width + 15, solution_y + 5, 20, Color::WHITE, ); if let Some(solution) = solutions.get_mut(self.selected_solution) { let column_x = level_list_width + entry_width + 20; let bounds = Rectangle::new(column_x as f32, y as f32, 220., 30.); if text_input( d, 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 simple_button(d, column_x, y + 50, 220, 30) { let cloned = solution.new_copy(next_id); self.selected_solution = solutions.len(); solutions.push(cloned); return; } d.draw_text("clone", column_x + 5, y + 55, 20, Color::WHITE); if simple_button(d, column_x, y + 85, 220, 30) { let mut editor = Editor::new(solution.clone(), level.clone()); editor.center_view(d); self.open_editor = Some(editor); } d.draw_text("edit", column_x + 5, y + 90, 20, Color::WHITE); } } else { self.solutions.insert(level.id().to_owned(), Vec::new()); } } } } fn get_levels() -> Vec { let mut levels = Vec::::new(); let mut add_level = |path| { let l = read_to_string(path) .ok() .as_deref() .and_then(|s| serde_json::from_str(s).ok()); if let Some(level) = l { levels.push(level); } }; for d in read_dir("levels").unwrap().flatten() { if d.path().is_dir() { for d in read_dir(d.path()).unwrap().flatten() { add_level(d.path()); } } else { add_level(d.path()); } } levels.sort_unstable_by_key(Level::sort_order); levels } 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 }