split crate into game and marble engine library
This commit is contained in:
parent
d5bb0f7ba0
commit
8b1eaaa630
20 changed files with 241 additions and 456 deletions
10
marble_machinations/Cargo.toml
Normal file
10
marble_machinations/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "marble_machinations"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
marble_engine = { path = "../marble_engine" }
|
||||
raylib = "5.0.2"
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
9
marble_machinations/build.rs
Normal file
9
marble_machinations/build.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use std::{fs::File, io::Write};
|
||||
|
||||
fn main() {
|
||||
let version = concat!("v", env!("CARGO_PKG_VERSION"));
|
||||
File::create("../version.txt")
|
||||
.unwrap()
|
||||
.write_all(version.as_bytes())
|
||||
.unwrap();
|
||||
}
|
65
marble_machinations/src/blueprint.rs
Normal file
65
marble_machinations/src/blueprint.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::util::userdata_dir;
|
||||
use marble_engine::board::Board;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Blueprint {
|
||||
id: usize,
|
||||
pub name: String,
|
||||
pub board: String,
|
||||
#[serde(skip, default)]
|
||||
tile_board: Option<Board>,
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
pub fn new(content: &Board, id: usize) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: format!("Blueprint {id}"),
|
||||
board: content.serialize(),
|
||||
tile_board: Some(content.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn convert_board(&mut self) -> &Board {
|
||||
if self.tile_board.is_none() {
|
||||
self.tile_board = Some(Board::parse(&self.board));
|
||||
}
|
||||
self.tile_board.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_board(&self) -> Option<&Board> {
|
||||
self.tile_board.as_ref()
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
let dir = userdata_dir().join("blueprints");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir.join(format!("blueprint_{}.json", &self.id))
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let path = self.path();
|
||||
let json = serde_json::to_string_pretty(self).unwrap();
|
||||
let mut file = File::create(path).unwrap();
|
||||
file.write_all(json.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn remove_file(&self) {
|
||||
let path = self.path();
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
eprint!("Error removing blueprint file: {e}");
|
||||
}
|
||||
}
|
||||
}
|
26
marble_machinations/src/config.rs
Normal file
26
marble_machinations/src/config.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use raylib::ffi::KeyboardKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
hotkeys: Hotkeys,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Hotkeys {
|
||||
map: HashMap<String, Hotkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Hotkey {
|
||||
modifiers: Vec<u32>,
|
||||
trigger: Trigger,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Trigger {
|
||||
Mouse(u32),
|
||||
Key(u32),
|
||||
}
|
1468
marble_machinations/src/editor.rs
Normal file
1468
marble_machinations/src/editor.rs
Normal file
File diff suppressed because it is too large
Load diff
81
marble_machinations/src/level.rs
Normal file
81
marble_machinations/src/level.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Chapter {
|
||||
pub title: String,
|
||||
pub levels: Vec<Level>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Level {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
init_board: Option<String>,
|
||||
/// no stages means sandbox
|
||||
#[serde(default)]
|
||||
stages: Vec<Stage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Stage {
|
||||
input: IOData,
|
||||
output: IOData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum IOData {
|
||||
Bytes(Vec<u8>),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl IOData {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
IOData::Bytes(b) => b,
|
||||
IOData::Text(t) => t.as_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_text(&self) -> bool {
|
||||
matches!(self, IOData::Text(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn is_sandbox(&self) -> bool {
|
||||
self.stages.is_empty()
|
||||
}
|
||||
|
||||
pub fn init_board(&self) -> Option<String> {
|
||||
self.init_board.clone()
|
||||
}
|
||||
|
||||
pub fn stages(&self) -> &[Stage] {
|
||||
&self.stages
|
||||
}
|
||||
}
|
||||
|
||||
impl Stage {
|
||||
pub fn input(&self) -> &IOData {
|
||||
&self.input
|
||||
}
|
||||
|
||||
pub fn output(&self) -> &IOData {
|
||||
&self.output
|
||||
}
|
||||
}
|
9
marble_machinations/src/lib.rs
Normal file
9
marble_machinations/src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub mod blueprint;
|
||||
pub mod config;
|
||||
pub mod editor;
|
||||
pub mod level;
|
||||
pub mod marble_engine;
|
||||
pub mod solution;
|
||||
pub mod theme;
|
||||
pub mod ui;
|
||||
pub mod util;
|
379
marble_machinations/src/main.rs
Normal file
379
marble_machinations/src/main.rs
Normal file
|
@ -0,0 +1,379 @@
|
|||
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();
|
||||
let selected_solution = 0;
|
||||
|
||||
Self {
|
||||
levels,
|
||||
level_scroll: 0,
|
||||
solutions,
|
||||
open_editor: None,
|
||||
textures,
|
||||
selected_level: 0,
|
||||
selected_solution,
|
||||
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().serialize();
|
||||
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().serialize();
|
||||
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
|
||||
}
|
104
marble_machinations/src/marble_engine.rs
Normal file
104
marble_machinations/src/marble_engine.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use marble_engine::{
|
||||
board::Board,
|
||||
pos::{Pos, PosInt},
|
||||
tile::{Direction, Tile},
|
||||
Machine,
|
||||
};
|
||||
use raylib::{
|
||||
color::Color,
|
||||
drawing::{RaylibDraw, RaylibDrawHandle},
|
||||
math::Vector2,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
theme::TILE_TEXTURE_SIZE,
|
||||
ui::draw_usize_small,
|
||||
util::{draw_scaled_texture, Textures},
|
||||
};
|
||||
|
||||
pub trait PosRaylib {
|
||||
fn to_vec(self) -> Vector2;
|
||||
fn from_vec(vec: Vector2) -> Self;
|
||||
}
|
||||
|
||||
impl PosRaylib for Pos {
|
||||
fn from_vec(vec: Vector2) -> Self {
|
||||
Self {
|
||||
x: vec.x as PosInt,
|
||||
y: vec.y as PosInt,
|
||||
}
|
||||
}
|
||||
fn to_vec(self) -> Vector2 {
|
||||
Vector2 {
|
||||
x: self.x as f32,
|
||||
y: self.y as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_board(
|
||||
board: &Board,
|
||||
d: &mut RaylibDrawHandle,
|
||||
textures: &Textures,
|
||||
offset: Vector2,
|
||||
scale: f32,
|
||||
) {
|
||||
let tile_size = (TILE_TEXTURE_SIZE * scale) as i32;
|
||||
|
||||
let start_x = (-offset.x as i32) / tile_size - 1;
|
||||
let tile_width = d.get_screen_width() / tile_size + 2;
|
||||
let start_y = (-offset.y as i32) / tile_size - 1;
|
||||
let tile_height = d.get_screen_height() / tile_size + 2;
|
||||
|
||||
for x in start_x..(start_x + tile_width) {
|
||||
for y in start_y..(start_y + tile_height) {
|
||||
let px = x * tile_size + offset.x as i32;
|
||||
let py = y * tile_size + offset.y as i32;
|
||||
if let Some(tile) = board.get((x, y).into()) {
|
||||
let texname = tile.texture();
|
||||
if texname.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let texture = textures.get(texname);
|
||||
draw_scaled_texture(d, texture, px, py, scale);
|
||||
#[cfg(debug_assertions)]
|
||||
// TODO: some in-game option to show power direction
|
||||
if let Tile::Powerable(_, state) = &tile {
|
||||
for dir in Direction::ALL {
|
||||
if state.get_dir(dir) {
|
||||
let texture = textures.get(dir.debug_arrow_texture_name());
|
||||
draw_scaled_texture(d, texture, px, py, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d.draw_rectangle(px, py, tile_size, tile_size, Color::new(0, 0, 0, 80));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_marble_values(
|
||||
machine: &Machine,
|
||||
d: &mut RaylibDrawHandle,
|
||||
textures: &Textures,
|
||||
offset: Vector2,
|
||||
scale: f32,
|
||||
) {
|
||||
let tile_size = (TILE_TEXTURE_SIZE * scale) as i32;
|
||||
for marble in machine.marbles() {
|
||||
let x = marble.x;
|
||||
let y = marble.y;
|
||||
if let Some(tile) = machine.board().get(*marble) {
|
||||
let px = x as i32 * tile_size + offset.x as i32;
|
||||
let py = y as i32 * tile_size + offset.y as i32;
|
||||
if let Tile::Marble { value, dir } = tile {
|
||||
let texture = textures.get(dir.arrow_texture_name());
|
||||
let pos = Vector2::new(px as f32, py as f32);
|
||||
let faded_white = Color::new(255, 255, 255, 100);
|
||||
d.draw_texture_ex(texture, pos, 0., scale, faded_white);
|
||||
draw_usize_small(d, textures, value as usize, px, py, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
marble_machinations/src/solution.rs
Normal file
76
marble_machinations/src/solution.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{level::Level, util::userdata_dir};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Solution {
|
||||
solution_id: usize,
|
||||
level_id: String,
|
||||
pub name: String,
|
||||
pub board: String,
|
||||
#[serde(default)]
|
||||
pub score: Option<Score>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Score {
|
||||
pub cycles: usize,
|
||||
pub tiles: usize,
|
||||
}
|
||||
|
||||
impl Solution {
|
||||
pub fn new(level: &Level, id: usize) -> Self {
|
||||
Self {
|
||||
solution_id: id,
|
||||
level_id: level.id().to_owned(),
|
||||
name: format!("Unnamed {id}"),
|
||||
board: level.init_board().unwrap_or(String::from(" ")),
|
||||
score: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_copy(&self, id: usize) -> Self {
|
||||
let mut new = self.clone();
|
||||
new.solution_id = id;
|
||||
new.score = None;
|
||||
new
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
self.solution_id
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
let dir = userdata_dir().join("solutions").join(&self.level_id);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir.join(format!("solution_{}.json", &self.solution_id))
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let path = self.path();
|
||||
let json = serde_json::to_string_pretty(self).unwrap();
|
||||
let mut file = File::create(path).unwrap();
|
||||
file.write_all(json.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn remove_file(&self) {
|
||||
let path = self.path();
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
eprint!("Error removing solution file: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score_text(&self) -> String {
|
||||
if let Some(score) = &self.score {
|
||||
format!("C: {} T: {}", score.cycles, score.tiles)
|
||||
} else {
|
||||
"unsolved".into()
|
||||
}
|
||||
}
|
||||
}
|
30
marble_machinations/src/theme.rs
Normal file
30
marble_machinations/src/theme.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use raylib::prelude::*;
|
||||
|
||||
pub const TILE_TEXTURE_SIZE: f32 = 16.0;
|
||||
|
||||
pub const BG_WORLD: Color = gray(48);
|
||||
pub const FG_GRID: Color = gray(64);
|
||||
|
||||
pub const BG_DARK: Color = gray(32);
|
||||
pub const BG_MEDIUM: Color = gray(48);
|
||||
pub const BG_LIGHT: Color = gray(64);
|
||||
pub const BG_WIDGET: Color = gray(64);
|
||||
pub const BG_WIDGET_ACTIVE: Color = rgb(80, 120, 180);
|
||||
pub const FG_MARBLE_VALUE: Color = rgb(255, 80, 40);
|
||||
pub const FG_CHAPTER_TITLE: Color = rgb(255, 160, 40);
|
||||
|
||||
pub const fn widget_bg(highlight: bool) -> Color {
|
||||
if highlight {
|
||||
BG_WIDGET_ACTIVE
|
||||
} else {
|
||||
BG_WIDGET
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
|
||||
Color::new(r, g, b, 255)
|
||||
}
|
||||
|
||||
pub const fn gray(value: u8) -> Color {
|
||||
Color::new(value, value, value, 255)
|
||||
}
|
417
marble_machinations/src/ui.rs
Normal file
417
marble_machinations/src/ui.rs
Normal file
|
@ -0,0 +1,417 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crate::{theme::*, util::draw_scaled_texture, util::MouseInput, util::Scroll, util::Textures};
|
||||
use raylib::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ShapedText {
|
||||
text: String,
|
||||
max_width: i32,
|
||||
font_size: i32,
|
||||
lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl ShapedText {
|
||||
pub fn new(font_size: i32) -> Self {
|
||||
Self {
|
||||
text: String::new(),
|
||||
font_size,
|
||||
max_width: 500,
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if text != self.text {
|
||||
self.text = text.to_owned();
|
||||
self.lines.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i32 {
|
||||
self.font_size * self.lines.len() as i32
|
||||
}
|
||||
|
||||
pub fn update_width(&mut self, d: &RaylibHandle, width: i32) {
|
||||
if self.max_width == width && !self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.max_width = width;
|
||||
self.lines.clear();
|
||||
// todo remove leading space on broken lines
|
||||
// todo fix splitting very long words
|
||||
let mut line_start = 0;
|
||||
let mut line_end = 0;
|
||||
for (i, c) in self.text.char_indices() {
|
||||
if c == ' ' {
|
||||
let line = &self.text[line_start..i];
|
||||
let new_line_length = d.measure_text(line, self.font_size);
|
||||
if new_line_length <= self.max_width {
|
||||
line_end = i;
|
||||
} else {
|
||||
self.lines.push(line_start..line_end);
|
||||
line_start = line_end;
|
||||
}
|
||||
}
|
||||
if c == '\n' {
|
||||
let line = &self.text[line_start..i];
|
||||
let new_line_length = d.measure_text(line, self.font_size);
|
||||
if new_line_length <= self.max_width {
|
||||
self.lines.push(line_start..i);
|
||||
line_end = i + 1;
|
||||
line_start = i + 1;
|
||||
} else {
|
||||
self.lines.push(line_start..line_end);
|
||||
self.lines.push(line_end..(i + 1));
|
||||
line_start = i + 1;
|
||||
line_end = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = self.text.len();
|
||||
let line = &self.text[line_start..i];
|
||||
let new_line_length = d.measure_text(line, self.font_size);
|
||||
if new_line_length <= self.max_width {
|
||||
self.lines.push(line_start..i);
|
||||
} else {
|
||||
self.lines.push(line_start..line_end);
|
||||
self.lines.push(line_end..i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&self, d: &mut RaylibDrawHandle, x: i32, y: i32) {
|
||||
// d.draw_rectangle(x, y, self.max_width, 4, Color::RED);
|
||||
for (i, line) in self.lines.iter().enumerate() {
|
||||
let line = &self.text[line.clone()];
|
||||
let line_y = y + self.font_size * i as i32;
|
||||
d.draw_text(line, x, line_y, self.font_size, Color::WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Tooltip {
|
||||
text: Option<&'static str>,
|
||||
mouse_x: i32,
|
||||
mouse_y: i32,
|
||||
screen_width: i32,
|
||||
screen_height: i32,
|
||||
}
|
||||
|
||||
impl Tooltip {
|
||||
pub fn init_frame(&mut self, d: &RaylibHandle) {
|
||||
let p = d.get_mouse_position();
|
||||
*self = Self {
|
||||
text: None,
|
||||
mouse_x: p.x as i32,
|
||||
mouse_y: p.y as i32,
|
||||
screen_width: d.get_screen_width(),
|
||||
screen_height: d.get_screen_height(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add(&mut self, x: i32, y: i32, width: i32, height: i32, text: &'static str) {
|
||||
if self.mouse_x >= x
|
||||
&& self.mouse_y >= y
|
||||
&& self.mouse_x <= (x + width)
|
||||
&& self.mouse_y <= (y + height)
|
||||
{
|
||||
self.text = Some(text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.text = None;
|
||||
}
|
||||
|
||||
pub fn add_rec(&mut self, bounds: Rectangle, text: &'static str) {
|
||||
self.add(
|
||||
bounds.x as i32,
|
||||
bounds.y as i32,
|
||||
bounds.width as i32,
|
||||
bounds.height as i32,
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw(&self, d: &mut RaylibDrawHandle) {
|
||||
if let Some(text) = self.text {
|
||||
let font_size = 20;
|
||||
let margin = 4;
|
||||
let text_width = d.measure_text(text, font_size);
|
||||
let x = self
|
||||
.mouse_x
|
||||
.min(self.screen_width - text_width - margin * 2);
|
||||
let y = self
|
||||
.mouse_y
|
||||
.min(self.screen_height - font_size - margin * 2);
|
||||
d.draw_rectangle(
|
||||
x,
|
||||
y,
|
||||
text_width + margin * 2,
|
||||
font_size + margin * 2,
|
||||
BG_LIGHT,
|
||||
);
|
||||
d.draw_text(text, x + margin, y + margin, font_size, Color::WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_button(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> bool {
|
||||
let mouse_pos = mouse.pos();
|
||||
let bounds = Rectangle {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
width: width as f32,
|
||||
height: height as f32,
|
||||
};
|
||||
let hover = bounds.check_collision_point_rec(mouse_pos);
|
||||
let pressed = hover && mouse.left_click();
|
||||
d.draw_rectangle(x, y, width, height, widget_bg(hover));
|
||||
pressed
|
||||
}
|
||||
|
||||
pub fn text_button(
|
||||
d: &mut RaylibDrawHandle,
|
||||
mouse: &MouseInput,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
text: &str,
|
||||
) -> bool {
|
||||
let font_size = 20;
|
||||
let margin = font_size / 4;
|
||||
let height = font_size + margin * 2;
|
||||
let clicked = simple_button((d, mouse), x, y, width, height);
|
||||
d.draw_text(text, x + margin, y + margin, font_size, Color::WHITE);
|
||||
clicked
|
||||
}
|
||||
|
||||
pub fn tex32_button(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
(x, y): (i32, i32),
|
||||
texture: &Texture2D,
|
||||
(tooltip, text): (&mut Tooltip, &'static str),
|
||||
) -> bool {
|
||||
let size = 32;
|
||||
let clicked = simple_button((d, mouse), x, y, 32, 32);
|
||||
draw_scaled_texture(d, texture, x, y, 2.);
|
||||
tooltip.add(x, y, size, size, text);
|
||||
clicked
|
||||
}
|
||||
|
||||
pub fn simple_option_button<T>(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
bounds: Rectangle,
|
||||
option: T,
|
||||
current: &mut T,
|
||||
) -> bool
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
d.draw_rectangle_rec(bounds, widget_bg(&option == current));
|
||||
let mut changed = false;
|
||||
if mouse.left_click() && mouse.is_over(bounds) && current != &option {
|
||||
*current = option;
|
||||
changed = true;
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn text_input(
|
||||
d: &mut RaylibDrawHandle,
|
||||
mouse: &MouseInput,
|
||||
bounds: Rectangle,
|
||||
text: &mut String,
|
||||
is_selected: &mut bool,
|
||||
max_len: usize,
|
||||
editable: bool,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let font_size = 20;
|
||||
d.draw_rectangle_rec(bounds, widget_bg(*is_selected));
|
||||
d.draw_rectangle_rec(
|
||||
Rectangle::new(
|
||||
bounds.x + 2.,
|
||||
bounds.y + bounds.height - 5.,
|
||||
bounds.width - 4.,
|
||||
3.,
|
||||
),
|
||||
BG_DARK,
|
||||
);
|
||||
d.draw_text(
|
||||
text,
|
||||
bounds.x as i32 + 4,
|
||||
bounds.y as i32 + 4,
|
||||
font_size,
|
||||
Color::WHITE,
|
||||
);
|
||||
// blinking cursor
|
||||
if *is_selected && d.get_time().fract() < 0.5 {
|
||||
let width = d.measure_text(text, font_size);
|
||||
d.draw_rectangle(
|
||||
bounds.x as i32 + 6 + width,
|
||||
bounds.y as i32 + 4,
|
||||
2,
|
||||
font_size,
|
||||
Color::WHITE,
|
||||
);
|
||||
};
|
||||
if editable && mouse.left_click() && (mouse.is_over(bounds) || *is_selected) {
|
||||
*is_selected = !*is_selected;
|
||||
}
|
||||
|
||||
if *is_selected {
|
||||
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
|
||||
*is_selected = false;
|
||||
}
|
||||
if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) && !text.is_empty() {
|
||||
changed = true;
|
||||
text.pop();
|
||||
if d.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) {
|
||||
while let Some(c) = text.pop() {
|
||||
if c == ' ' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if text.len() < max_len {
|
||||
let char_code = unsafe { ffi::GetCharPressed() };
|
||||
let c = if char_code > 0 {
|
||||
char::from_u32(char_code as u32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(c) = c {
|
||||
changed = true;
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn scrollable_texture_option_button<T>(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
pos: Vector2,
|
||||
texture: &Texture2D,
|
||||
option: T,
|
||||
current: &mut T,
|
||||
border: f32,
|
||||
) -> Option<Scroll>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
let bounds = Rectangle {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: 32. + border * 2.,
|
||||
height: 32. + border * 2.,
|
||||
};
|
||||
d.draw_rectangle_rec(
|
||||
bounds,
|
||||
if &option == current {
|
||||
BG_WIDGET_ACTIVE
|
||||
} else {
|
||||
gray(16)
|
||||
},
|
||||
);
|
||||
d.draw_texture_ex(
|
||||
texture,
|
||||
pos + Vector2::new(border, border),
|
||||
0.,
|
||||
32. / texture.width as f32,
|
||||
Color::WHITE,
|
||||
);
|
||||
if mouse.is_over(bounds) {
|
||||
if mouse.left_click() {
|
||||
*current = option;
|
||||
}
|
||||
return mouse.scroll();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn draw_usize(
|
||||
d: &mut RaylibDrawHandle,
|
||||
textures: &Textures,
|
||||
mut number: usize,
|
||||
(x, y): (i32, i32),
|
||||
digits: i32,
|
||||
scale: i32,
|
||||
) {
|
||||
for i in 0..digits {
|
||||
d.draw_rectangle(x + 10 * i * scale, y, 8 * scale, 16 * scale, BG_LIGHT);
|
||||
}
|
||||
let mut i = 0;
|
||||
while (number != 0 || i == 0) && i < digits {
|
||||
let texture = textures.get(&format!("digit_{}", number % 10));
|
||||
let x = x + (digits - i - 1) * 10 * scale;
|
||||
draw_scaled_texture(d, texture, x, y, scale as f32);
|
||||
number /= 10;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_usize_small(
|
||||
d: &mut RaylibDrawHandle,
|
||||
textures: &Textures,
|
||||
mut num: usize,
|
||||
mut x: i32,
|
||||
y: i32,
|
||||
scale: f32,
|
||||
) {
|
||||
const MAX_DIGITS: usize = 8;
|
||||
let mut digits = [0; MAX_DIGITS];
|
||||
let mut i = 0;
|
||||
while (num != 0 || i == 0) && i < MAX_DIGITS {
|
||||
digits[MAX_DIGITS - i - 1] = num % 10;
|
||||
num /= 10;
|
||||
i += 1;
|
||||
}
|
||||
let texture = textures.get("digits_small");
|
||||
for &digit in &digits[(MAX_DIGITS - i)..] {
|
||||
let source = Rectangle::new(4. * digit as f32, 0., 4., 6.);
|
||||
let dest = Rectangle::new(x as f32, y as f32, 4. * scale, 6. * scale);
|
||||
d.draw_texture_pro(texture, source, dest, Vector2::zero(), 0., FG_MARBLE_VALUE);
|
||||
x += 4 * scale as i32;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slider(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
bounds: Rectangle,
|
||||
value: &mut u8,
|
||||
min: u8,
|
||||
max: u8,
|
||||
) -> bool {
|
||||
// draw state
|
||||
// the +1 makes the lowest state look slightly filled and the max state fully filled
|
||||
let percent = (*value - min + 1) as f32 / (max - min + 1) as f32;
|
||||
d.draw_rectangle_rec(bounds, BG_WIDGET);
|
||||
let mut filled_bounds = bounds;
|
||||
filled_bounds.width *= percent;
|
||||
d.draw_rectangle_rec(filled_bounds, Color::CYAN);
|
||||
// interaction
|
||||
if mouse.is_over(bounds) {
|
||||
if mouse.left_hold() {
|
||||
let percent = (mouse.pos().x - bounds.x) / bounds.width;
|
||||
let new_value = min + (percent * (max - min + 1) as f32) as u8;
|
||||
if *value != new_value {
|
||||
*value = new_value;
|
||||
}
|
||||
} else if mouse.scroll() == Some(Scroll::Up) && *value < max {
|
||||
*value += 1;
|
||||
} else if mouse.scroll() == Some(Scroll::Down) && *value > min {
|
||||
*value -= 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
151
marble_machinations/src/util.rs
Normal file
151
marble_machinations/src/util.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use std::{collections::HashMap, fs::read_dir, path::PathBuf};
|
||||
|
||||
use raylib::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Textures {
|
||||
map: HashMap<String, Texture2D>,
|
||||
}
|
||||
|
||||
impl Textures {
|
||||
pub fn load_dir(&mut self, folder: &str, rl: &mut RaylibHandle, thread: &RaylibThread) {
|
||||
for d in read_dir(folder).unwrap().flatten() {
|
||||
let path = d.path();
|
||||
if path.is_file() {
|
||||
let name = path.file_stem().unwrap().to_string_lossy();
|
||||
let texture = rl
|
||||
.load_texture(thread, &format!("{folder}/{name}.png"))
|
||||
.unwrap();
|
||||
self.map.insert(name.to_string(), texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> &Texture2D {
|
||||
self.map
|
||||
.get(name)
|
||||
.unwrap_or_else(|| self.map.get("missing").unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn userdata_dir() -> PathBuf {
|
||||
PathBuf::from("user")
|
||||
}
|
||||
|
||||
pub fn draw_scaled_texture(
|
||||
d: &mut RaylibDrawHandle,
|
||||
texture: &Texture2D,
|
||||
x: i32,
|
||||
y: i32,
|
||||
scale: f32,
|
||||
) {
|
||||
let pos = Vector2::new(x as f32, y as f32);
|
||||
d.draw_texture_ex(texture, pos, 0., scale, Color::WHITE);
|
||||
}
|
||||
|
||||
pub fn get_free_id<T>(items: &[T], id_fn: fn(&T) -> usize) -> usize {
|
||||
let mut id = 0;
|
||||
while items.iter().any(|i| id_fn(i) == id) {
|
||||
id += 1;
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn screen_centered_rect(rl: &RaylibHandle, width: i32, height: i32) -> Rectangle {
|
||||
let w = rl.get_screen_width();
|
||||
let h = rl.get_screen_height();
|
||||
Rectangle {
|
||||
x: (w / 2 - width / 2) as f32,
|
||||
y: (h / 2 - height / 2) as f32,
|
||||
width: width as f32,
|
||||
height: height as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screen_centered_rect_dyn(rl: &RaylibHandle, margin_x: i32, margin_y: i32) -> Rectangle {
|
||||
let w = rl.get_screen_width();
|
||||
let h = rl.get_screen_height();
|
||||
Rectangle {
|
||||
x: margin_x as f32,
|
||||
y: margin_y as f32,
|
||||
width: (w - margin_x * 2) as f32,
|
||||
height: (h - margin_y * 2) as f32,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MouseInput {
|
||||
pos: Vector2,
|
||||
left_click: bool,
|
||||
left_hold: bool,
|
||||
left_release: bool,
|
||||
right_hold: bool,
|
||||
scroll: Option<Scroll>,
|
||||
}
|
||||
|
||||
impl MouseInput {
|
||||
pub fn get(rl: &RaylibHandle) -> Self {
|
||||
Self {
|
||||
pos: rl.get_mouse_position(),
|
||||
left_click: rl.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT),
|
||||
left_hold: rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT),
|
||||
left_release: rl.is_mouse_button_released(MouseButton::MOUSE_BUTTON_LEFT),
|
||||
right_hold: rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_RIGHT),
|
||||
scroll: get_scroll(rl),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_over(&self, rect: Rectangle) -> bool {
|
||||
rect.check_collision_point_rec(self.pos)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> Vector2 {
|
||||
self.pos
|
||||
}
|
||||
|
||||
pub fn left_click(&self) -> bool {
|
||||
self.left_click
|
||||
}
|
||||
|
||||
pub fn left_hold(&self) -> bool {
|
||||
self.left_hold
|
||||
}
|
||||
|
||||
pub fn left_release(&self) -> bool {
|
||||
self.left_release
|
||||
}
|
||||
|
||||
pub fn right_hold(&self) -> bool {
|
||||
self.right_hold
|
||||
}
|
||||
|
||||
pub fn scroll(&self) -> Option<Scroll> {
|
||||
self.scroll
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Scroll {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
pub fn get_scroll(rl: &RaylibHandle) -> Option<Scroll> {
|
||||
const SCROLL_THRESHOLD: f32 = 0.5;
|
||||
let value = rl.get_mouse_wheel_move();
|
||||
if value > SCROLL_THRESHOLD {
|
||||
Some(Scroll::Up)
|
||||
} else if value < -SCROLL_THRESHOLD {
|
||||
Some(Scroll::Down)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rect(x: i32, y: i32, width: i32, height: i32) -> Rectangle {
|
||||
Rectangle::new(x as f32, y as f32, width as f32, height as f32)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue