use std::{ collections::HashMap, fs::{read_dir, read_to_string}, }; use raylib::prelude::*; mod editor; mod level; mod marble_engine; mod solution; mod util; use editor::{Editor, ExitState}; use level::Level; use solution::Solution; use util::*; struct Game { levels: Vec, 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("good window title") .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); Self { levels: get_levels(), 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.input(&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.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.save(); } ExitState::ExitNoSave => self.open_editor = None, } } else { self.draw(&mut d); } } } fn draw(&mut self, d: &mut RaylibDrawHandle) { d.clear_background(Color::new(64, 64, 64, 255)); // let level_list_width = 240; let level_list_width = d.get_screen_width() / 4; // woah! Reactive UI! so fancy let screen_height = d.get_screen_height(); d.draw_rectangle(0, 0, level_list_width, screen_height, Color::GRAY); // let (a, b, c) = d.gui_scroll_panel( // Rectangle { // x: 10., // y: 10., // width: level_list_width as f32 - 20., // height: screen_height as f32 - 40., // }, // Some(rstr!("text")), // Rectangle{}, // scroll, // view, // ); let clicked = d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT); let mouse_pos = d.get_mouse_position(); for (i, level) in self.levels.iter().enumerate() { let level_entry_height = 48; 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 != i { self.selected_solution = 0; self.editing_solution_name = false; self.selected_level = i; } if self.selected_level == i { d.draw_rectangle_rec(bounds, Color::DARKCYAN); } d.draw_text(level.name(), 10, y, 20, Color::WHITE); 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 + 20, 10, subtext_color); } if let Some(level) = self.levels.get(self.selected_level) { d.draw_text(level.name(), level_list_width + 10, 10, 30, Color::CYAN); d.draw_text(level.id(), level_list_width + 10, 40, 10, Color::GRAY); let mut y = 60; if let Some(solutions) = self.solutions.get_mut(level.id()) { let solution_entry_height = 40; let solution_entry_width = 200; for (solution_index, solution) in solutions.iter().enumerate() { if simple_option_button( d, level_list_width + 10, y, solution_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, y + 5, 20, name_color); d.draw_text( &solution.score_text(), level_list_width + 15, y + 25, 10, Color::WHITE, ); y += solution_entry_height + 10; } if simple_button(d, level_list_width + 10, y, solution_entry_width, 30) { let n = solutions.len(); solutions.push(Solution::new(&level, n)); } d.draw_text( "new solution", level_list_width + 15, y + 5, 20, Color::WHITE, ); if let Some(solution) = solutions.get_mut(self.selected_solution) { let bounds = Rectangle { x: (level_list_width + 10 + solution_entry_width + 10) as f32, y: 60., width: 220., height: 30., }; text_input( d, bounds, &mut solution.name, &mut self.editing_solution_name, ); let button_x = level_list_width + solution_entry_width + 20; if simple_button(d, button_x, 100, 220, 30) { self.open_editor = Some(Editor::new(solution.clone(), level.clone())); } d.draw_text("edit", button_x + 5, 105, 20, Color::WHITE); } } else { self.solutions.insert(level.id().to_owned(), Vec::new()); } } } } fn get_levels() -> Vec { let mut levels = Vec::::new(); for d in read_dir("levels").unwrap().flatten() { let l = read_to_string(d.path()) .ok() .as_deref() .map(|s| serde_json::from_str(s).ok()) .flatten(); if let Some(level) = l { levels.push(level); } } levels.sort_by(|a, b| a.id().cmp(b.id())); levels } fn get_solutions() -> HashMap> { let mut levels = 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() .map(|s| serde_json::from_str(s).ok()) .flatten(); if let Some(solution) = s { solutions.push(solution) } } levels.insert(level_name, solutions); } } } } levels }