2024-10-06 12:39:36 +02:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
fs::{read_dir, read_to_string},
|
|
|
|
};
|
2024-10-03 22:59:49 +02:00
|
|
|
|
|
|
|
use raylib::prelude::*;
|
|
|
|
|
2024-10-06 00:24:11 +02:00
|
|
|
mod editor;
|
2024-10-06 12:39:36 +02:00
|
|
|
mod level;
|
2024-10-03 22:59:49 +02:00
|
|
|
mod marble_engine;
|
2024-10-06 12:39:36 +02:00
|
|
|
mod solution;
|
2024-10-04 21:20:53 +02:00
|
|
|
mod util;
|
2024-10-06 12:39:36 +02:00
|
|
|
|
2024-10-06 16:00:12 +02:00
|
|
|
use editor::{Editor, ExitState};
|
2024-10-06 12:39:36 +02:00
|
|
|
use level::Level;
|
|
|
|
use solution::Solution;
|
2024-10-04 21:20:53 +02:00
|
|
|
use util::*;
|
|
|
|
|
2024-10-06 12:39:36 +02:00
|
|
|
struct Game {
|
|
|
|
levels: Vec<Level>,
|
|
|
|
solutions: HashMap<String, Vec<Solution>>,
|
|
|
|
open_editor: Option<Editor>,
|
|
|
|
textures: Textures,
|
|
|
|
selected_level: usize,
|
|
|
|
selected_solution: usize,
|
2024-10-06 13:12:05 +02:00
|
|
|
editing_solution_name: bool,
|
2024-10-06 12:39:36 +02:00
|
|
|
}
|
|
|
|
|
2024-10-03 22:59:49 +02:00
|
|
|
fn main() {
|
2024-10-04 01:21:52 +02:00
|
|
|
let (mut rl, thread) = raylib::init()
|
|
|
|
.resizable()
|
|
|
|
.title("good window title")
|
|
|
|
.build();
|
2024-10-03 22:59:49 +02:00
|
|
|
rl.set_target_fps(60);
|
2024-10-05 17:46:45 +02:00
|
|
|
rl.set_window_min_size(640, 480);
|
|
|
|
rl.set_mouse_cursor(MouseCursor::MOUSE_CURSOR_CROSSHAIR);
|
2024-10-04 22:10:00 +02:00
|
|
|
rl.set_exit_key(None);
|
2024-10-05 23:16:24 +02:00
|
|
|
rl.set_trace_log(TraceLogLevel::LOG_WARNING);
|
2024-10-03 22:59:49 +02:00
|
|
|
|
2024-10-06 12:39:36 +02:00
|
|
|
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();
|
2024-10-06 23:37:21 +02:00
|
|
|
textures.load_dir("assets", rl, thread);
|
|
|
|
textures.load_dir("assets/tiles", rl, thread);
|
2024-10-06 12:39:36 +02:00
|
|
|
|
|
|
|
Self {
|
|
|
|
levels: get_levels(),
|
2024-10-06 16:29:45 +02:00
|
|
|
solutions: get_solutions(),
|
2024-10-06 12:39:36 +02:00
|
|
|
open_editor: None,
|
|
|
|
textures,
|
|
|
|
selected_level: 0,
|
|
|
|
selected_solution: 0,
|
2024-10-06 13:12:05 +02:00
|
|
|
editing_solution_name: false,
|
2024-10-06 12:39:36 +02:00
|
|
|
}
|
|
|
|
}
|
2024-10-05 15:19:27 +02:00
|
|
|
|
2024-10-06 12:39:36 +02:00
|
|
|
fn run(&mut self, rl: &mut RaylibHandle, thread: &RaylibThread) {
|
|
|
|
while !rl.window_should_close() {
|
2024-10-06 23:37:21 +02:00
|
|
|
let mut d = rl.begin_drawing(thread);
|
2024-10-06 12:39:36 +02:00
|
|
|
if let Some(editor) = &mut self.open_editor {
|
2024-10-06 22:24:37 +02:00
|
|
|
editor.update(&d);
|
2024-10-06 12:39:36 +02:00
|
|
|
editor.draw(&mut d, &self.textures);
|
2024-10-06 16:00:12 +02:00
|
|
|
match editor.get_exit_state() {
|
|
|
|
ExitState::Dont => (),
|
|
|
|
ExitState::ExitAndSave => {
|
2024-10-06 16:29:45 +02:00
|
|
|
let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap()
|
|
|
|
[self.selected_solution];
|
|
|
|
solution.board = editor.source_board().to_string();
|
2024-10-06 22:24:37 +02:00
|
|
|
solution.score = editor.score();
|
2024-10-06 16:29:45 +02:00
|
|
|
solution.save();
|
2024-10-06 16:00:12 +02:00
|
|
|
self.open_editor = None;
|
|
|
|
}
|
2024-10-06 16:29:45 +02:00
|
|
|
ExitState::Save => {
|
|
|
|
let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap()
|
|
|
|
[self.selected_solution];
|
|
|
|
solution.board = editor.source_board().to_string();
|
2024-10-06 22:24:37 +02:00
|
|
|
solution.score = editor.score();
|
2024-10-06 16:29:45 +02:00
|
|
|
solution.save();
|
|
|
|
}
|
|
|
|
ExitState::ExitNoSave => self.open_editor = None,
|
2024-10-06 16:00:12 +02:00
|
|
|
}
|
2024-10-06 12:39:36 +02:00
|
|
|
} else {
|
|
|
|
self.draw(&mut d);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-10-04 01:21:52 +02:00
|
|
|
|
2024-10-06 12:39:36 +02:00
|
|
|
fn draw(&mut self, d: &mut RaylibDrawHandle) {
|
2024-10-04 22:10:00 +02:00
|
|
|
d.clear_background(Color::new(64, 64, 64, 255));
|
2024-10-06 20:02:43 +02:00
|
|
|
|
2024-10-06 13:12:05 +02:00
|
|
|
let level_list_width = d.get_screen_width() / 4; // woah! Reactive UI! so fancy
|
2024-10-06 12:39:36 +02:00
|
|
|
let screen_height = d.get_screen_height();
|
|
|
|
d.draw_rectangle(0, 0, level_list_width, screen_height, Color::GRAY);
|
|
|
|
|
|
|
|
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() {
|
2024-10-06 20:12:02 +02:00
|
|
|
let level_entry_height = 65;
|
2024-10-06 12:39:36 +02:00
|
|
|
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;
|
2024-10-06 13:12:05 +02:00
|
|
|
self.editing_solution_name = false;
|
2024-10-06 12:39:36 +02:00
|
|
|
self.selected_level = i;
|
|
|
|
}
|
|
|
|
if self.selected_level == i {
|
|
|
|
d.draw_rectangle_rec(bounds, Color::DARKCYAN);
|
|
|
|
}
|
2024-10-06 23:44:29 +02:00
|
|
|
|
|
|
|
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);
|
2024-10-06 12:39:36 +02:00
|
|
|
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
|
|
|
|
};
|
2024-10-06 20:12:02 +02:00
|
|
|
d.draw_text(&subtext, 10, y + 30, 20, subtext_color);
|
2024-10-06 12:39:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(level) = self.levels.get(self.selected_level) {
|
2024-10-06 20:12:02 +02:00
|
|
|
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;
|
|
|
|
}
|
2024-10-06 12:39:36 +02:00
|
|
|
|
|
|
|
if let Some(solutions) = self.solutions.get_mut(level.id()) {
|
|
|
|
let solution_entry_height = 40;
|
2024-10-06 20:12:02 +02:00
|
|
|
let entry_width = 200;
|
|
|
|
let mut solution_y = y;
|
2024-10-06 12:39:36 +02:00
|
|
|
for (solution_index, solution) in solutions.iter().enumerate() {
|
2024-10-06 13:12:05 +02:00
|
|
|
if simple_option_button(
|
2024-10-06 12:39:36 +02:00
|
|
|
d,
|
|
|
|
level_list_width + 10,
|
2024-10-06 20:12:02 +02:00
|
|
|
solution_y,
|
|
|
|
entry_width,
|
2024-10-06 12:39:36 +02:00
|
|
|
solution_entry_height,
|
|
|
|
solution_index,
|
|
|
|
&mut self.selected_solution,
|
2024-10-06 13:12:05 +02:00
|
|
|
) {
|
|
|
|
self.editing_solution_name = false;
|
|
|
|
}
|
2024-10-06 12:39:36 +02:00
|
|
|
let name_color = if solution.score.is_some() {
|
|
|
|
Color::LIME
|
|
|
|
} else {
|
|
|
|
Color::ORANGE
|
|
|
|
};
|
2024-10-06 20:12:02 +02:00
|
|
|
d.draw_text(
|
|
|
|
&solution.name,
|
|
|
|
level_list_width + 15,
|
|
|
|
solution_y + 5,
|
|
|
|
20,
|
|
|
|
name_color,
|
|
|
|
);
|
2024-10-06 12:39:36 +02:00
|
|
|
d.draw_text(
|
|
|
|
&solution.score_text(),
|
|
|
|
level_list_width + 15,
|
2024-10-06 20:12:02 +02:00
|
|
|
solution_y + 25,
|
2024-10-06 12:39:36 +02:00
|
|
|
10,
|
|
|
|
Color::WHITE,
|
|
|
|
);
|
2024-10-06 20:12:02 +02:00
|
|
|
solution_y += solution_entry_height + 10;
|
2024-10-06 12:39:36 +02:00
|
|
|
}
|
|
|
|
|
2024-10-06 20:12:02 +02:00
|
|
|
if simple_button(d, level_list_width + 10, solution_y, entry_width, 30) {
|
2024-10-06 12:39:36 +02:00
|
|
|
let n = solutions.len();
|
2024-10-06 23:37:21 +02:00
|
|
|
solutions.push(Solution::new(level, n));
|
2024-10-06 12:39:36 +02:00
|
|
|
}
|
|
|
|
d.draw_text(
|
|
|
|
"new solution",
|
|
|
|
level_list_width + 15,
|
2024-10-06 20:12:02 +02:00
|
|
|
solution_y + 5,
|
2024-10-06 12:39:36 +02:00
|
|
|
20,
|
|
|
|
Color::WHITE,
|
|
|
|
);
|
2024-10-06 13:12:05 +02:00
|
|
|
|
|
|
|
if let Some(solution) = solutions.get_mut(self.selected_solution) {
|
|
|
|
let bounds = Rectangle {
|
2024-10-06 20:12:02 +02:00
|
|
|
x: (level_list_width + 10 + entry_width + 10) as f32,
|
|
|
|
y: y as f32,
|
2024-10-06 16:00:12 +02:00
|
|
|
width: 220.,
|
2024-10-06 13:12:05 +02:00
|
|
|
height: 30.,
|
|
|
|
};
|
|
|
|
text_input(
|
|
|
|
d,
|
|
|
|
bounds,
|
|
|
|
&mut solution.name,
|
|
|
|
&mut self.editing_solution_name,
|
2024-10-07 12:21:57 +02:00
|
|
|
true,
|
2024-10-06 13:12:05 +02:00
|
|
|
);
|
2024-10-06 16:00:12 +02:00
|
|
|
|
2024-10-06 20:12:02 +02:00
|
|
|
let button_x = level_list_width + entry_width + 20;
|
|
|
|
if simple_button(d, button_x, y + 40, 220, 30) {
|
2024-10-06 16:00:12 +02:00
|
|
|
self.open_editor = Some(Editor::new(solution.clone(), level.clone()));
|
|
|
|
}
|
2024-10-06 20:12:02 +02:00
|
|
|
d.draw_text("edit", button_x + 5, y + 45, 20, Color::WHITE);
|
2024-10-06 13:12:05 +02:00
|
|
|
}
|
2024-10-06 12:39:36 +02:00
|
|
|
} else {
|
|
|
|
self.solutions.insert(level.id().to_owned(), Vec::new());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_levels() -> Vec<Level> {
|
|
|
|
let mut levels = Vec::<Level>::new();
|
|
|
|
for d in read_dir("levels").unwrap().flatten() {
|
|
|
|
let l = read_to_string(d.path())
|
|
|
|
.ok()
|
|
|
|
.as_deref()
|
2024-10-06 23:37:21 +02:00
|
|
|
.and_then(|s| serde_json::from_str(s).ok());
|
2024-10-06 12:39:36 +02:00
|
|
|
if let Some(level) = l {
|
|
|
|
levels.push(level);
|
|
|
|
}
|
2024-10-05 20:22:18 +02:00
|
|
|
}
|
2024-10-07 12:04:55 +02:00
|
|
|
levels.sort_by(|a, b| a.sort_order().cmp(&b.sort_order()));
|
2024-10-06 12:39:36 +02:00
|
|
|
levels
|
2024-10-05 20:22:18 +02:00
|
|
|
}
|
2024-10-06 16:29:45 +02:00
|
|
|
|
|
|
|
fn get_solutions() -> HashMap<String, Vec<Solution>> {
|
|
|
|
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()
|
2024-10-06 23:37:21 +02:00
|
|
|
.and_then(|s| serde_json::from_str(s).ok());
|
2024-10-06 16:29:45 +02:00
|
|
|
if let Some(solution) = s {
|
|
|
|
solutions.push(solution)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
levels.insert(level_name, solutions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
levels
|
|
|
|
}
|