initial level selection gui

This commit is contained in:
Crispy 2024-10-06 12:39:36 +02:00
parent 66c9b10264
commit ed5084d0fd
11 changed files with 413 additions and 29 deletions

46
Cargo.lock generated
View file

@ -192,6 +192,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -247,6 +253,8 @@ name = "marble2"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"raylib", "raylib",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@ -433,12 +441,50 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"

View file

@ -5,3 +5,5 @@ edition = "2021"
[dependencies] [dependencies]
raylib = "5.0.2" raylib = "5.0.2"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"

View file

@ -3,7 +3,20 @@
logic mostly like https://git.crispypin.cc/CrispyPin/marble logic mostly like https://git.crispypin.cc/CrispyPin/marble
file hierarchy ## todo
level selection
solution saving & loading
input/output display
grow grid automatically while editing
sim/speed control gui
make marble movement not order-dependent (`>ooo <` does not behave symmetrically)
blueprints
decide on marble data size (u32 or byte?)
blueprint rotation
## file hierarchy
``` ```
- assets/ - assets/
- storage/ - storage/
@ -28,27 +41,33 @@ file hierarchy
`00_zeroes.json` `00_zeroes.json`
```json ```json
{ {
"id": "00_zeroes", "id": "00_zeroes",
"name": "Zeroes", "name": "Zeroes",
"description": "learn how to output data", "description": "learn how to output data",
"init_board": null, "init_board": null,
"inputs": [], "inputs": [],
"outputs": [0, 0, 0, 0, 0, 0, 0, 0] "outputs": [0, 0, 0, 0, 0, 0, 0, 0]
} }
``` ```
`00_zeroes/solution_0.json` `00_zeroes/solution_0.json`
```json ```json
{ {
"level_id": "00_zeroes", //redundant, useful if sharing solution files? "level_id": "00_zeroes", //redundant, useful if sharing solution files?
"name": "unnamed 1", "solution_id": "solution_0",
"board": "oo\nP*\n|-" "name": "unnamed 1",
"board": "oo\nP*\n|-",
"score": {
"cycles": 8,
"tiles": 6,
"area": 6,
}
} }
``` ```
`blueprints/blueprint_0.json` `blueprints/blueprint_0.json`
```json ```json
{ {
"name": "fast printer", "name": "fast printer",
"board": "oo\nP*\n|-" "board": "oo\nP*\n|-"
} }
``` ```

8
levels/00_zeroes.json Normal file
View file

@ -0,0 +1,8 @@
{
"id": "00_zeroes",
"name": "Zeroes",
"description": "learn how to output data",
"init_board": null,
"inputs": [],
"outputs": [0, 0, 0, 0, 0, 0, 0, 0]
}

8
levels/01_cat.json Normal file
View file

@ -0,0 +1,8 @@
{
"id": "01_cat",
"name": "Copy Cat",
"description": "learn how to meow",
"init_board": null,
"inputs": [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 103, 33],
"outputs": [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 103, 33]
}

View file

@ -3,17 +3,19 @@ use std::ops::Rem;
use raylib::prelude::*; use raylib::prelude::*;
use crate::{ use crate::{
level::Level,
marble_engine::{ marble_engine::{
board::{Board, Pos}, board::{Board, Pos},
tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType}, tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType},
Machine, Machine,
}, },
text_input, texture_button, Textures, text_input, texture_option_button, Textures,
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct Editor { pub struct Editor {
source_board: Board, source_board: Board,
level: Level,
machine: Machine, machine: Machine,
sim_state: SimState, sim_state: SimState,
view_offset: Vector2, view_offset: Vector2,
@ -70,6 +72,7 @@ impl Editor {
tool_menu_arrow: Direction::Right, tool_menu_arrow: Direction::Right,
tool_menu_mirror: MirrorType::Forward, tool_menu_mirror: MirrorType::Forward,
tool_menu_wire: WireType::Vertical, tool_menu_wire: WireType::Vertical,
level: Level::new_sandbox(),
} }
} }
@ -188,6 +191,7 @@ impl Editor {
} }
pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) {
d.clear_background(Color::new(64, 64, 64, 255));
self.draw_board(d, textures); self.draw_board(d, textures);
let height = d.get_screen_height(); let height = d.get_screen_height();
@ -242,7 +246,7 @@ impl Editor {
let border = 4.; let border = 4.;
let gap = 2.; let gap = 2.;
let bound_offset = 32. + gap * 2. + border * 2.; let bound_offset = 32. + gap * 2. + border * 2.;
texture_button( texture_option_button(
d, d,
Vector2 { Vector2 {
x: 320. + col as f32 * bound_offset - if col < 0 { 15. } else { 0. }, x: 320. + col as f32 * bound_offset - if col < 0 { 15. } else { 0. },

50
src/level.rs Normal file
View file

@ -0,0 +1,50 @@
use serde::Deserialize;
use crate::marble_engine::board::Board;
#[derive(Debug, Deserialize)]
pub struct Level {
id: String,
name: String,
description: String,
init_board: Option<String>,
inputs: Vec<u8>,
outputs: Vec<u8>,
}
impl Level {
pub fn new_sandbox() -> Self {
Self {
id: "sandbox".into(),
name: "Sandbox".into(),
description: String::new(),
init_board: None,
inputs: Vec::new(),
outputs: Vec::new(),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn init_board(&self) -> Option<Board> {
self.init_board.as_deref().map(Board::parse)
}
pub fn inputs(&self) -> &[u8] {
&self.inputs
}
pub fn outputs(&self) -> &[u8] {
&self.outputs
}
}

View file

@ -1,14 +1,31 @@
use std::fs::read_to_string; use std::{
collections::HashMap,
fs::{read_dir, read_to_string},
};
use editor::Editor;
use marble_engine::board::Board;
use raylib::prelude::*; use raylib::prelude::*;
mod editor; mod editor;
mod level;
mod marble_engine; mod marble_engine;
mod solution;
mod util; mod util;
use editor::Editor;
use level::Level;
use marble_engine::board::Board;
use solution::Solution;
use util::*; use util::*;
struct Game {
levels: Vec<Level>,
solutions: HashMap<String, Vec<Solution>>,
open_editor: Option<Editor>,
textures: Textures,
selected_level: usize,
selected_solution: usize,
}
fn main() { fn main() {
let (mut rl, thread) = raylib::init() let (mut rl, thread) = raylib::init()
.resizable() .resizable()
@ -20,18 +37,156 @@ fn main() {
rl.set_exit_key(None); rl.set_exit_key(None);
rl.set_trace_log(TraceLogLevel::LOG_WARNING); rl.set_trace_log(TraceLogLevel::LOG_WARNING);
let mut textures = Textures::default(); let mut game = Game::new(&mut rl, &thread);
textures.load_dir("assets", &mut rl, &thread); game.run(&mut rl, &thread);
textures.load_dir("assets/tiles", &mut rl, &thread); // let board = Board::parse(&read_to_string("boards/adder.mbl").unwrap());
// game.load_board(board);
}
let mut game = Editor::new_sandbox(); impl Game {
let board = Board::parse(&read_to_string("boards/adder.mbl").unwrap()); fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self {
game.load_board(board); let mut textures = Textures::default();
textures.load_dir("assets", rl, &thread);
textures.load_dir("assets/tiles", rl, &thread);
while !rl.window_should_close() { Self {
game.input(&rl); levels: get_levels(),
let mut d = rl.begin_drawing(&thread); solutions: HashMap::new(),
open_editor: None,
textures,
selected_level: 0,
selected_solution: 0,
}
}
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);
} else {
self.draw(&mut d);
}
}
}
fn draw(&mut self, d: &mut RaylibDrawHandle) {
d.clear_background(Color::new(64, 64, 64, 255)); d.clear_background(Color::new(64, 64, 64, 255));
game.draw(&mut d, &textures); let level_list_width = 320;
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.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;
for (solution_index, solution) in solutions.iter().enumerate() {
simple_option_button(
d,
level_list_width + 10,
y,
200,
solution_entry_height,
solution_index,
&mut self.selected_solution,
);
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;
}
// d.gui_button(bounds, text)
if simple_button(d, level_list_width + 10, y, 200, 30) {
let n = solutions.len();
solutions.push(Solution::new(level.id().to_owned(), n));
}
d.draw_text(
"new solution",
level_list_width + 15,
y + 5,
20,
Color::WHITE,
);
} 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()
.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
}

View file

@ -10,7 +10,7 @@ pub struct Pos {
} }
impl Pos { impl Pos {
pub const fn to_vec(&self) -> Vector2 { pub const fn to_vec(self) -> Vector2 {
Vector2 { Vector2 {
x: self.x as f32, x: self.x as f32,
y: self.y as f32, y: self.y as f32,

40
src/solution.rs Normal file
View file

@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Solution {
solution_id: String,
level_id: String, // redundant?
pub name: String,
pub board: String,
pub score: Option<Score>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Score {
pub cycles: u32,
pub tiles: u32,
pub area: u32,
}
impl Solution {
pub fn new(level_id: String, number: usize) -> Self {
Self {
solution_id: format!("solution_{number}"),
level_id,
name: format!("Unnamed {number}"),
board: " \n".repeat(20), // todo remove when auto resizing is implemented
// score: Some(Score { cycles: 5, tiles: 88, area: 987 }),
score: None,
}
}
pub fn score_text(&self) -> String {
if let Some(score) = &self.score {
format!(
"C: {} T: {} A: {}",
score.cycles, score.tiles, score.area
)
} else {
"unsolved".into()
}
}
}

View file

@ -28,6 +28,58 @@ impl Textures {
} }
} }
pub fn simple_button(d: &mut RaylibDrawHandle, x: i32, y: i32, width: i32, height: i32) -> bool {
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let mut pressed = false;
let color = if bounds.check_collision_point_rec(mouse_pos) {
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) {
pressed = true;
}
Color::DARKCYAN
} else {
Color::GRAY
};
d.draw_rectangle(x, y, width, height, color);
pressed
}
pub fn simple_option_button<T>(
d: &mut RaylibDrawHandle,
x: i32,
y: i32,
width: i32,
height: i32,
option: T,
current: &mut T,
) where
T: PartialEq,
{
let color = if &option == current {
Color::DARKCYAN
} else {
Color::GRAY
};
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let mouse_pos = d.get_mouse_position();
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& bounds.check_collision_point_rec(mouse_pos)
{
*current = option;
}
d.draw_rectangle_rec(bounds, color);
}
pub fn text_input( pub fn text_input(
d: &mut RaylibDrawHandle, d: &mut RaylibDrawHandle,
bounds: Rectangle, bounds: Rectangle,
@ -78,7 +130,7 @@ pub fn text_input(
changed changed
} }
pub fn texture_button<T>( pub fn texture_option_button<T>(
d: &mut RaylibDrawHandle, d: &mut RaylibDrawHandle,
pos: Vector2, pos: Vector2,
texture: &Texture2D, texture: &Texture2D,