378 lines
11 KiB
Rust
378 lines
11 KiB
Rust
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<LevelListEntry>,
|
|
level_scroll: usize,
|
|
solutions: HashMap<String, Vec<Solution>>,
|
|
open_editor: Option<Editor>,
|
|
textures: Textures,
|
|
selected_level: usize,
|
|
selected_solution: usize,
|
|
delete_solution: Option<usize>,
|
|
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<LevelListEntry> {
|
|
let mut chapters = Vec::<Chapter>::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<String, Vec<Solution>> {
|
|
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
|
|
}
|