diff --git a/.gitignore b/.gitignore index eca2202..9092d51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,2 @@ -# Rust build junk -target/ -debug/ -flycheck* -*.rs.bk -*.pdb -*.exe -*.dll - -# Editor junk -.vscode/ -.idea/ +/target +/.helix # I use Helix btw diff --git a/Cargo.lock b/Cargo.lock index 22fd879..785ac7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -44,12 +94,58 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "crc32fast" version = "1.5.0" @@ -117,6 +213,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "image" version = "0.24.9" @@ -130,6 +232,12 @@ dependencies = [ "png", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "libc" version = "0.2.177" @@ -211,6 +319,12 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "png" version = "0.17.16" @@ -224,12 +338,30 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + [[package]] name = "quad-rand" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -237,18 +369,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] -name = "simple_menu" -version = "0.2.3" +name = "simple-menu" +version = "0.2.4" dependencies = [ + "clap", "macroquad", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "ttf-parser" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "winapi" version = "0.3.9" @@ -270,3 +432,18 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 566736a..fad8de2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,16 @@ [package] -name = "simple_menu" -version = "0.2.3" +name = "simple-menu" +version = "0.2.4" edition = "2024" [dependencies] +clap = { version = "4.6.7", features = ["derive"] } macroquad = "0.4" + +[profile.release] +lto = "fat" +panic = "abort" +opt-level = 3 +codegen-units = 1 +strip = true + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..36930cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Zot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d77e395..67e6c13 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,90 @@ -# Simple Menu -![screenshot](preview_menu.png) -[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) +# simple-menu -a pretty simple menu you can navigate, it includes a little partical simulator, settings and a starting screen! +![preview](preview_menu.png) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Rust Edition](https://img.shields.io/badge/Rust-2024%20Edition-orange.svg)](https://www.rust-lang.org/) + +> "I click button, I happy." + +A ridiculously over-engineered, lightweight menu playground and bouncing dot simulator written in Rust with Macroquad. + +It kind of looks like 2001 in a nutshell, but now with clap CLI arguments and release profile optimizations that treat 20 bouncing circles like a AAA physics engine. --- -## Tech Stack +## What is this? -- Language: Rust -- Main libs/frameworks: Macroquad +Calling it a "game" is definitely a stretch, but it has three screens and buttons you can click: + +- **Main Menu**: Shows the version and a big START button. When you click it, it prints `game started!` to stdout and throws you into the action. +- **Game Prototype**: A gray box with "Cute Dots" bouncing around inside. If 20 dots aren't enough, smash the **dots +** button to spawn more. +- **Settings**: A placeholder paradise where you can cycle the background through Purple, Red, Green, Blue, and Pink, or toggle Developer Mode. --- -## Features +## Over-Engineered Improvements -![done](https://img.shields.io/badge/status-done-brightgreen) -- 3 menues you can navigate -- each menu has it`s core features in place ( Start screen with start button, Game screen with partical simulator and the Settings to tweak stuff) +Recently forked and given far more engineering discipline than a dot simulator probably deserves: -![wip](https://img.shields.io/badge/status-WIP-blue) -- making it pretty... it kind of looks like 2001 in a nut shell -- giving more settings & options for both the Settings & Game tab -- a game title (i keep forgetting that) +- **Clap CLI integration**: You can now pass `--dev-mode` directly from your terminal to launch straight into the developer HUD. +- **Aggressive compiler optimizations**: Configured `Cargo.toml` with `opt-level = 3`, `lto = "fat"`, `codegen-units = 1`, `panic = "abort"`, and `strip = true`. Because 20 bouncing dots deserve bare-metal performance. +- **Modular crate split**: Decoupled `src/lib.rs` from `src/main.rs`. Now `CurrentState`, `BgColor`, and `CuteDot` live in their own library crate. +- **Developer HUD**: Displays live mouse coordinates `(x, y)`, total dot count, and an FPS counter that updates every 10 frames to avoid burning out your screen. --- -### Installation +## Running It + +Make sure you have Rust installed. ```bash -# clone the repo -git clone https://github.com/USERNAME/REPO_NAME.git -cd REPO_NAME +# Clone the repository +git clone https://github.com/Jacob-Dayan/simple-menu.git +cd simple-menu + +# Run it +cargo run +``` + +### CLI Options + +Thanks to `clap`, there are actual command-line flags: + +```bash +# Launch directly with the developer overlay active +cargo run -- --dev-mode + +# Print version +cargo run -- --version + +# Print help +cargo run -- --help +``` + +--- + +## How to "Play" + +| Screen | Buttons | What happens | +| :--- | :--- | :--- | +| Main Menu | **START** | Takes you to the simulation. | +| Game | **dots +** | Spawns another dot. | +| Game | **Settings** | Opens the settings. | +| Settings | **Choose color** | Cycles background color. | +| Settings | **Dev: true/false** | Toggles the HUD showing FPS and mouse coordinates. | +| Settings | **Back to game** | Takes you back to the bouncing dots. | + +--- + +## Tech Stack + +- **Rust** (2024 edition) +- **Macroquad** (for rendering windows, rectangles, and circles) +- **Clap** (for parsing two CLI flags with maximum type safety) + +--- + +## License + +MIT. Do whatever you want with it. diff --git a/preview_menu.png b/preview_menu.png index dc0ceb9..c0d7356 100644 Binary files a/preview_menu.png and b/preview_menu.png differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..82841d9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,127 @@ +pub use macroquad::prelude::*; + +/// I mean if you needed a documentation for that enum, +/// the problem is not the developer didn't document it; +/// it's you. +pub enum CurrentState { + Game, + MainMenu, + Settings, +} + +#[derive(Clone, Copy, Debug)] +pub enum BgColor { + Red, + Green, + Blue, + Pink, + Purple, +} + +/// Some boilerplate for changing color A -> B +impl BgColor { + pub fn next(self) -> BgColor { + match self { + BgColor::Red => BgColor::Green, + BgColor::Green => BgColor::Blue, + BgColor::Blue => BgColor::Pink, + BgColor::Pink => BgColor::Purple, + BgColor::Purple => BgColor::Red, + } + } +} + +/// Just a more idiomatic way to convert one data type to another +/// In that case: BgColor -> Color +impl From for Color { + fn from(value: BgColor) -> Self { + match value { + BgColor::Red => RED, + BgColor::Green => GREEN, + BgColor::Blue => BLUE, + BgColor::Pink => PINK, + BgColor::Purple => PURPLE, + } + } +} + +/// simple struct to represent a cute dot +#[derive(Clone, Debug)] +pub struct CuteDot { + pub pos: Vec2, + pub vel: Vec2, + pub radius: f32, + pub color: Color, +} + +impl Default for CuteDot { + fn default() -> Self { + Self { + pos: Vec2::ZERO, + vel: vec2(50.0, 50.0), + radius: 5.0, + color: YELLOW, + } + } +} + +impl CuteDot { + pub fn new(pos: Vec2) -> Self { + Self { + pos, + ..Default::default() + } + } + + /// Spawns a dot randomly inside the given boundary box. + /// Because doing math to place dots by hand was getting old. + pub fn spawn_in_bounds(bounds: Rect) -> Self { + let radius = 5.0; + let x = macroquad::rand::gen_range(bounds.left() + radius, bounds.right() - radius); + let y = macroquad::rand::gen_range(bounds.top() + radius, bounds.bottom() - radius); + Self { + pos: vec2(x, y), + radius, + ..Default::default() + } + } + + /// Updates the dot's position and bounces off the boundary walls. + /// If you find a bug here, no you didn't. + pub fn update(&mut self, dt: f32, bounds: Rect) { + self.pos += self.vel * dt; + + // each of these variables represents the boundaries of the area the dot can move in + let left = bounds.left() + self.radius; + let right = bounds.right() - self.radius; + let top = bounds.top() + self.radius; + let bottom = bounds.bottom() - self.radius; + + // below is simple collision detection with the area boundaries + if self.pos.x < left { + self.pos.x = left; + self.vel.x *= -1.0; + } else if self.pos.x > right { + self.pos.x = right; + self.vel.x *= -1.0; + } + + if self.pos.y < top { + self.pos.y = top; + self.vel.y *= -1.0; + } else if self.pos.y > bottom { + self.pos.y = bottom; + self.vel.y *= -1.0; + } + } + + pub fn draw(&self) { + draw_circle(self.pos.x, self.pos.y, self.radius, self.color); + draw_circle_lines(self.pos.x, self.pos.y, self.radius, 2.0, BLACK); + } +} + +/// Helper to spawn a dot and push it directly into a collection +pub fn spawn_dot_in_box(dots: &mut Vec, bounds: Rect) { + dots.push(CuteDot::spawn_in_bounds(bounds)); +} diff --git a/src/main.rs b/src/main.rs index 906b460..c71a24e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,256 +1,188 @@ -// tried to make comments because people might read this code and not understand it otherwise -// don't worry neither do i fully understand it sometimes -use macroquad::prelude::*; -enum CurrentState { - Game, - MainMenu, - Settings, -} -#[derive(Clone, Copy, Debug)] -enum BgColor { - RED, - GREEN, - BLUE, - PINK, - PURPLE, -} -// okay the following 2? i couldn't explain, like i'm trying to be somewhat professional BUT WHO THOUGHT MAKING COLOR GO FROM A TO B COULD BE THIS HARD FFS -impl BgColor { - fn next(self) -> BgColor { - match self { - BgColor::RED => BgColor::GREEN, - BgColor::GREEN => BgColor::BLUE, - BgColor::BLUE => BgColor::PINK, - BgColor::PINK => BgColor::PURPLE, - BgColor::PURPLE => BgColor::RED, - } - } -} -impl BgColor { - fn to_color(self) -> Color { - match self { - BgColor::RED => RED, - BgColor::GREEN => GREEN, - BgColor::BLUE => BLUE, - BgColor::PINK => PINK, - BgColor::PURPLE => PURPLE, - } - } -} -// simple struct to represent a cute dot -struct CuteDot { - pos: Vec2, - vel: Vec2, - radius: f32, - color: Color, -} -// calling them cute dots bc why not -fn new_cute_dot(pos: Vec2) -> CuteDot { - CuteDot { - pos, - vel: vec2(50.0, 50.0), - radius: 5.0, - color: YELLOW, - } -} -fn update_cute_dot(dot: &mut CuteDot, dt: f32, area_pos: Vec2, area_dim: Vec2) { - dot.pos += dot.vel * dt; -// each of these variables represents the boundaries of the area the dot can move in - let left = area_pos.x + dot.radius; - let right = area_pos.x + area_dim.x - dot.radius; - let top = area_pos.y + dot.radius; - let bottom = area_pos.y + area_dim.y - dot.radius; - - // below is simple collision detection with the area boundaries - if dot.pos.x < left { - dot.pos.x = left; - dot.vel.x *= -1.0; - } else if dot.pos.x > right { - dot.pos.x = right; - dot.vel.x *= -1.0; - } +//! I tried to document this code so it would be easier to contribue and maintain the game, but I ran into a problem! +//! I barely manage to maintain it! +//! good luck I guess +//! (no budget for actual emoji, deal with that) - if dot.pos.y < top { - dot.pos.y = top; - dot.vel.y *= -1.0; - } else if dot.pos.y > bottom { - dot.pos.y = bottom; - dot.vel.y *= -1.0; - } -} +use simple_menu::*; -fn draw_cute_dot(dot: &CuteDot) { - draw_circle(dot.pos.x, dot.pos.y, dot.radius, dot.color); - draw_circle_lines(dot.pos.x, dot.pos.y, dot.radius, 2.0, BLACK); +const DEFAULT_DOTS_COUNT: usize = 20; +const GAME_AREA_SIZE: Vec2 = vec2(500.0, 300.0); +const BTN_SIZE: Vec2 = vec2(200.0, 60.0); +const COLOR_BTN_SIZE: Vec2 = vec2(200.0, 50.0); +const SCREEN_OUTLINE_THICKNESS: f32 = 10.0; +const GAME_OUTLINE_THICKNESS: f32 = 10.0; +const BTN_OUTLINE_THICKNESS: f32 = 4.0; +const FPS_UPDATE_INTERVAL: i32 = 10; + +fn game_bounds() -> Rect { + Rect::new( + (screen_width() - GAME_AREA_SIZE.x) / 2.0, + (screen_height() - GAME_AREA_SIZE.y) / 2.0, + GAME_AREA_SIZE.x, + GAME_AREA_SIZE.y, + ) } -fn spawn_dot_in_box(dots: &mut Vec, game_pos: Vec2, game_dim: Vec2) { - let radius = 5.0; - let x = macroquad::rand::gen_range(game_pos.x + radius, game_pos.x + game_dim.x - radius); - let y = macroquad::rand::gen_range(game_pos.y + radius, game_pos.y + game_dim.y - radius); - - let mut dot = new_cute_dot(vec2(x, y)); - dot.radius = radius; - dots.push(dot); + +fn centered_rect(center_x: f32, y: f32, size: Vec2) -> Rect { + Rect::new(center_x - size.x / 2.0, y, size.x, size.y) } -fn text_dimensions(text: &str, font_size: f32) -> Vec2 { - let dims = measure_text(text, None, font_size as u16, 1.0); - return vec2(dims.width, dims.height); +// FORMERLY "WORK IN PROGRESS FUNCTION, DO NOT USE YET" +// The existential crisis has been resolved: it now returns TextDimensions with actual width +// and height fields instead of stuffing dimensions into a Vec2 like a barbarian. +fn text_dimensions(text: &str, font_size: f32) -> TextDimensions { + measure_text(text, None, font_size as u16, 1.0) } -// WORK IN PROGRESS FUNCTION, DO NOT USE YET -// actually never mind it's done now -// i dont really like how it returns a vec2 when it could return a struct with width and height fields but oh well -// i might change that later -// please ignore the mess that is this function +fn centered_text_pos(text: &str, center_x: f32, center_y: f32, font_size: f32) -> Vec2 { + let dims = text_dimensions(text, font_size); + vec2(center_x - dims.width / 2.0, center_y - dims.height / 2.0) +} fn game_msg(text: &str, position: Vec2, font_size: f32) { - draw_text(text, position.x, position.y, font_size as f32, WHITE); + draw_text(text, position.x, position.y, font_size, WHITE); } + //this function draws an outline around the screen, used for visual effect -fn screen_outline(x: f32, y: f32, w: f32, h: f32) { - draw_rectangle_lines(x, y, w, h, 10.0, BLACK); +fn screen_outline() { + draw_rectangle_lines( + 0.0, + 0.0, + screen_width(), + screen_height(), + SCREEN_OUTLINE_THICKNESS, + BLACK, + ); } -// simple button function, returns true if clicked yes i know it's basic but hey it works -fn button(x: f32, y: f32, w: f32, h: f32, label: &str) -> bool { + +/// A function that checks if pressed a button, +/// or if the mouse is currently hoverring the button -> +/// The button becomes light gray (until unhovered) +fn button(rect: Rect, label: &str) -> bool { let (mx, my) = mouse_position(); - let inside_x = mx >= x && mx <= x + w; - let inside_y = my >= y && my <= y + h; - let hovered = inside_x && inside_y; + let hovered = rect.contains(vec2(mx, my)); - if hovered { - draw_rectangle(x, y, w, h, LIGHTGRAY); - draw_text(label, x + 10.0, y + h * 0.6, 31.0, BLACK); + let (bg, font_size) = if hovered { + (LIGHTGRAY, 31.0) } else { - draw_rectangle(x, y, w, h, GRAY); - draw_text(label, x + 10.0, y + h * 0.6, 30.0, BLACK); - } - draw_rectangle_lines(x, y, w, h, 4.0, BLACK); + (GRAY, 30.0) + }; + + draw_rectangle(rect.x, rect.y, rect.w, rect.h, bg); + draw_text( + label, + rect.x + 10.0, + rect.y + rect.h * 0.6, + font_size, + BLACK, + ); + draw_rectangle_lines(rect.x, rect.y, rect.w, rect.h, BTN_OUTLINE_THICKNESS, BLACK); hovered && is_mouse_button_pressed(MouseButton::Left) } -fn dev_mode_display(dev_mode: bool, mouse: (f32, f32), fps: i32, amount_of_dots: i32) { + +fn dev_mode_display(mouse: (f32, f32), fps: i32, dots_count: usize) { // displays mouse coordinates and a dev mode message when dev mode is active // the function is called every frame, but only draws when dev_mode is true // which is efficient enough for this simple use case // but i should probably add some throttling or optimization if this were to be used in a more complex application oh well - if dev_mode { - let dev_txt_dim = text_dimensions("developer mode active", 20.0); - let cords_txt_dim = text_dimensions(&format!("x: {} | y: {}", mouse.0, mouse.1), 20.0); - let fps_txt_dim = text_dimensions(&format!("FPS: {}", fps), 20.0); - let dots_txt_dim = text_dimensions(&format!("dots: {}", amount_of_dots), 20.0); + let entries = [ + format!("x: {} | y: {}", mouse.0, mouse.1), + "developer mode active".to_string(), + format!("FPS: {fps}"), + format!("dots: {dots_count}"), + ]; - let dev_dim = vec2(dev_txt_dim.x, dev_txt_dim.y); - let fps_dim = vec2(fps_txt_dim.x, fps_txt_dim.y); - let cords_dim = vec2(cords_txt_dim.x, cords_txt_dim.y); - let dots_dim = vec2(dots_txt_dim.x, dots_txt_dim.y); + let font_size = 20.0; + let base_x = screen_width() * 0.85; - draw_text( - &format!("x: {} | y: {}", mouse.0, mouse.1), - screen_width() * 0.85 - cords_dim.x / 2.0, - screen_height() * 0.05 - cords_dim.y / 2.0, - 20.0, - BLACK, - ); - - draw_text( - &format!("developer mode active"), - screen_width() * 0.85 - dev_dim.x / 2.0, - screen_height() * 0.1 - dev_dim.y / 2.0, - 20.0, - BLACK, - ); + for (idx, line) in entries.iter().enumerate() { + let dims = text_dimensions(line, font_size); // although the fps is drawn every frame, it only updates every 10 frames in the main loop to reduce performance impact and change frequency // probably a less complex way to do this but it works for now + let y_pos = screen_height() * (0.05 * (idx + 1) as f32); draw_text( - &format!("FPS: {}", fps), - screen_width() * 0.85 - fps_dim.x / 2.0, - screen_height() * 0.15 - fps_dim.y / 2.0, - 20.0, - BLACK, - ); - -draw_text( - &format!("dots: {}", amount_of_dots), - screen_width() * 0.85 - dots_dim.x / 2.0, - screen_height() * 0.2 - dots_dim.y / 2.0, - 20.0, + line, + base_x - dims.width / 2.0, + y_pos - dims.height / 2.0, + font_size, BLACK, ); } } -#[macroquad::main("i click button, i happy")] +use clap::Parser; + +#[derive(Parser)] +#[command(version)] +struct Cli { + #[arg(skip = env!("CARGO_PKG_VERSION"))] + version: &'static str, + #[arg(long)] + dev_mode: bool, + #[arg(long)] + dots_count: Option, +} + +#[macroquad::main("I click button, I happy")] async fn main() { + let cli = Cli::parse(); + // fps tracking variables - let mut fps: i32 = 60; - let mut i: i32 = 0; - let max: i32 = 10; + let mut fps = 60; + let mut i = 0; // lots of variables to keep track of the state of the game - let version = "0.2.3"; - let mut dev_mode = false; + let mut dev_mode = cli.dev_mode; let mut state = CurrentState::MainMenu; - let mut current_color: BgColor = BgColor::PURPLE; + let mut current_color = BgColor::Purple; - let mut dots: Vec = Vec::new(); - let mut amount_of_dots: i32 = 20; - - let rec_dim = vec2(500.0, 300.0); - let rec_pos = vec2( - screen_width() / 2.0 - rec_dim.x / 2.0, - screen_height() / 2.0 - rec_dim.y / 2.0, - ); + let mut dots_count = cli.dots_count.unwrap_or(DEFAULT_DOTS_COUNT); + let mut dots: Vec = Vec::with_capacity(dots_count); + let initial_bounds = game_bounds(); - for _ in 0..amount_of_dots { - let radius = 5.0; - let x = macroquad::rand::gen_range(rec_pos.x + radius, rec_pos.x + rec_dim.x - radius); - let y = macroquad::rand::gen_range(rec_pos.y + radius, rec_pos.y + rec_dim.y - radius); - let mut dot = new_cute_dot(vec2(x, y)); - dot.radius = radius; - dots.push(dot); + for _ in 0..dots_count { + spawn_dot_in_box(&mut dots, initial_bounds); } loop { let dt = get_frame_time(); - clear_background(current_color.to_color()); + clear_background(Color::from(current_color)); // for fps display in dev mode i += 1; - if i >= max { + if i >= FPS_UPDATE_INTERVAL { i = 0; fps = get_fps(); } - // get screen dimensions and button dimensions - let screen = vec2(screen_width(), screen_height()); - let btn_size = vec2(200.0, 60.0); - let outline_size = vec2(screen_width(), screen_height()); - let mouse: (f32, f32) = mouse_position(); - // center position: screen/2 - size/2 - // this is used to center the button on the screen - let btn_pos = vec2( - screen.x / 2.0 - btn_size.x / 2.0, - screen.y / 2.0 - btn_size.y / 2.0, - ); - let outline_pos = vec2( - screen.x / 2.0 - outline_size.x / 2.0, - screen.y / 2.0 - outline_size.y / 2.0, - ); + // draw the outline around the screen - screen_outline(outline_pos.x, outline_pos.y, screen.x, screen.y); + screen_outline(); + // display dev mode info if active - dev_mode_display(dev_mode, mouse, fps, amount_of_dots); + if dev_mode { + dev_mode_display(mouse_position(), fps, dots_count); + } + // this match statement handles the different states of the game, note how each state has its own UI and functionality match state { CurrentState::MainMenu => { - let v_text = version; - let v_text_dims = measure_text(v_text, None, 20, 1.0); + let v_text = cli.version; + let v_text_dims = text_dimensions(v_text, 20.0); let game_text_pos = vec2(10.0, v_text_dims.height * 3.0); game_msg(v_text, game_text_pos, 40.0); - if button(btn_pos.x, btn_pos.y, btn_size.x, btn_size.y, "START") { + // center position: screen/2 - size/2 + // this is used to center the button on the screen + let btn_rect = centered_rect( + screen_width() / 2.0, + (screen_height() - BTN_SIZE.y) / 2.0, + BTN_SIZE, + ); + + if button(btn_rect, "START") { println!("game started!"); state = CurrentState::Game; } @@ -258,71 +190,82 @@ async fn main() { CurrentState::Game => { // actually calling it a game is a bit of a stretch let game_text = "game prototype:"; - let game_text_dims = measure_text(game_text, None, 40, 1.0); - let game_text_pos = vec2( - screen_width() / 2.0 - game_text_dims.width / 2.0, - screen_height() / 4.0 - game_text_dims.height / 2.0, - ); - let game_dim = vec2(500.0, 300.0); - let game_pos = vec2( - screen_width() / 2.0 - game_dim.x / 2.0, - screen_height() / 2.0 - game_dim.y / 2.0, - ); - let outline_size = 10.0; + let game_bounds = game_bounds(); - draw_rectangle(game_pos.x, game_pos.y, game_dim.x, game_dim.y, LIGHTGRAY); + draw_rectangle( + game_bounds.x, + game_bounds.y, + game_bounds.w, + game_bounds.h, + LIGHTGRAY, + ); + let game_text_pos = + centered_text_pos(game_text, screen_width() / 2.0, screen_height() / 4.0, 40.0); game_msg(game_text, game_text_pos, 40.0); -// update and draw each cute dot - let inside_game_pos = game_pos - vec2(outline_size / 2.0, outline_size / 2.0); + // update and draw each cute dot for dot in &mut dots { - update_cute_dot(dot, dt, inside_game_pos, game_dim); - } - for dot in &dots { - draw_cute_dot(dot); + dot.update(dt, game_bounds); + dot.draw(); } + // draw the game area outline after drawing the dots to ensure it's on top - draw_rectangle_lines(game_pos.x, game_pos.y, game_dim.x, game_dim.y, outline_size, BLACK); - if button(screen_width() / 4.0, screen_height() - 100.0, btn_size.x, btn_size.y, "dots +") { - amount_of_dots += 1; - spawn_dot_in_box(&mut dots, inside_game_pos, game_dim); - println!("dots increased to {}", amount_of_dots); + draw_rectangle_lines( + game_bounds.x, + game_bounds.y, + game_bounds.w, + game_bounds.h, + GAME_OUTLINE_THICKNESS, + BLACK, + ); + + if button( + Rect::new( + screen_width() / 4.0, + screen_height() - 100.0, + BTN_SIZE.x, + BTN_SIZE.y, + ), + "dots +", + ) { + dots_count += 1; + spawn_dot_in_box(&mut dots, game_bounds); + println!("dots increased to {dots_count}"); } - if button(10.0, 10.0, btn_size.x, btn_size.y, "Settings") { + + if button(Rect::new(10.0, 10.0, BTN_SIZE.x, BTN_SIZE.y), "Settings") { state = CurrentState::Settings; } } CurrentState::Settings => { // i might add more settings later but for now this is fine // also the settings text at the bottom is just a placeholder for now - let settings_text = "change your settings as you desire! (SETTINGS SOON!!)"; - let settings_text_dims = measure_text(settings_text, None, 30, 1.0); - let settings_text_pos: Vec2 = vec2( - screen_width() / 2.0 - settings_text_dims.width / 2.0, - screen_height() - settings_text_dims.height / 2.0, + let settings_text = "change your settings as you desire!"; + let settings_dims = text_dimensions(settings_text, 30.0); + let settings_text_pos = vec2( + (screen_width() - settings_dims.width) / 2.0, + screen_height() - settings_dims.height / 2.0, ); game_msg(settings_text, settings_text_pos, 30.0); - let color_btn_size = vec2(200.0, 50.0); + if button( - screen_width() / 2.0 - btn_size.x / 2.0, - 125.0, - color_btn_size.x, - color_btn_size.y, - &format!("Dev: {}", dev_mode), + centered_rect(screen_width() / 2.0, 125.0, COLOR_BTN_SIZE), + &format!("Dev: {}", if dev_mode { "activated" } else { "disabled" }), ) { dev_mode = !dev_mode; } + if button( - screen_width() / 2.0 - btn_size.x / 2.0, - 50.0, - color_btn_size.x, - color_btn_size.y, + centered_rect(screen_width() / 2.0, 50.0, COLOR_BTN_SIZE), "Choose color", ) { current_color = current_color.next(); } - if button(10.0, 10.0, btn_size.x, btn_size.y, "Back to game") { + if button( + Rect::new(10.0, 10.0, BTN_SIZE.x, BTN_SIZE.y), + "Back to game", + ) { state = CurrentState::Game; } } @@ -330,5 +273,3 @@ async fn main() { next_frame().await; } } -// hey we made it to the end! congrats! -// hope you found what you were looking for :D diff --git a/target/CACHEDIR.TAG b/target/CACHEDIR.TAG deleted file mode 100644 index 20d7c31..0000000 --- a/target/CACHEDIR.TAG +++ /dev/null @@ -1,3 +0,0 @@ -Signature: 8a477f597d28d172789f06886806bc55 -# This file is a cache directory tag created by cargo. -# For information about cache directory tags see https://bford.info/cachedir/