marble-machinations/src/main.rs

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
}