diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 073ac01..fb759f4 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -33,24 +33,29 @@ jobs: prettier --write . - name: Super-linter - uses: super-linter/super-linter/slim@v7 + uses: super-linter/super-linter/slim@v8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/pull_request_template.md|.github/ISSUE_TEMPLATE/*.md)" + VALIDATE_ALL_CODEBASE: true + FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/pull_request_template.md|.github/ISSUE_TEMPLATE/*.md|.github/workflows/*.(yml|yaml))" # Disable problematic validators + VALIDATE_BIOME_FORMAT: false VALIDATE_DOCKERFILE_HADOLINT: false VALIDATE_GIT_COMMITLINT: false + VALIDATE_GITHUB_ACTIONS_ZIZMOR: false # Enable fixers for PR events FIX_JSON: true FIX_JSON_PRETTIER: true FIX_MARKDOWN: true FIX_MARKDOWN_PRETTIER: true FIX_NATURAL_LANGUAGE: ${{ github.event_name == 'pull_request' }} + FIX_RUST_CLIPPY: true + # Custom options for linters + RUST_CLIPPY_COMMAND_OPTIONS: "--config max_width=160" - name: Commit and push fixes if: github.event_name == 'pull_request' && github.event.pull_request.head.ref != github.event.repository.default_branch - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: ${{ github.event.pull_request.head.ref }} commit_message: "Super-Linter: Fix linting issues" diff --git a/Cargo.toml b/Cargo.toml index 827c659..254149f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Rust-Pong" -version = "1.0.0" +version = "1.0.1" authors = ["Vianpyro"] edition = "2024" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5bb322 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Vianney Veremme + +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 3a7d956..c7e198a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,75 @@ -# Template +# Rust-Pong -Solid GitHub template repository +Welcome to the **Rust-Pong** repository! This repository contains a small Pong clone implemented in Rust to demonstrate modular game structure, audio, and simple UI. + +## 🚀 Getting Started + +To get started with this project: + +1. Clone the repository: + ```bash + git clone https://github.com/Vianpyro/Rust-Pong.git + cd Rust-Pong + ``` +2. Install the Rust toolchain if you don't already have it: +3. Build the project: + ```bash + cargo build + ``` +4. Run the game: + ```bash + cargo run --release + ``` + +> [!NOTE] +> +> - Sound effect files (sfx) are embedded in the binary, so you don't need to include them when distributing the executable. +> - If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. + +## 📁 Project Structure + +The repository contains the following directories and files (high level): + +- `assets/` - Game assets (sounds, images, etc.) +- `src/` - Application source code + - `audio/` - Audio handling + - `game/` - Game objects and physics (ball, racket, score) + - `player/` - Player and controller code + - `ui/` - Menus, HUD, and screens + - `main.rs` - Application entry point + - `main_state.rs`, `debug.rs` - Game state and debugging helpers +- `Cargo.toml` - Rust package manifest +- `LICENSE` - Project license (see file for terms) + +## 🛠 Features + +- Rust-based Pong clone demonstrating basic game loop, physics, and UI. +- Modular code organization (audio, game logic, players, UI). +- Lightweight and easy to extend for experimentation or learning. + +## 📖 Documentation + +The code is organized into clear modules under `src/`. For details, explore the following files and folders: + +- `src/game/` — core game logic and physics +- `src/audio/` — audio playback and resource handling +- `src/ui/` — UI screens (menu, HUD, pause, game over) + +Expand this readme as the project grows to include contribution guidelines, a development roadmap, and detailed architecture notes. + +## 🤝 Contributing + +Contributions are welcome. Suggested workflow: + +1. Fork the repository. +2. Create a feature branch: `git checkout -b feature/your-feature` +3. Make your changes and add tests where applicable. +4. Open a pull request to the main repository. + +When opening issues or PRs, provide reproduction steps and any relevant logs or OS details. + +## 📝 License + +See the [`LICENSE`](/LICENSE) file in this repository for license terms. + +Happy coding! 🎉 diff --git a/src/audio.rs b/src/audio/mod.rs similarity index 100% rename from src/audio.rs rename to src/audio/mod.rs diff --git a/src/debug.rs b/src/debug.rs index 580f53e..48eac93 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,5 +1,5 @@ -use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, PxScale, Text}; use ggez::{Context, GameResult, glam::Vec2}; +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, PxScale, Text}; pub struct DebugInfo { enabled: bool, diff --git a/src/ball.rs b/src/game/ball.rs similarity index 100% rename from src/ball.rs rename to src/game/ball.rs diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..9179b11 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,4 @@ +pub mod ball; +pub mod physics; +pub mod racket; +pub mod score; diff --git a/src/physics.rs b/src/game/physics.rs similarity index 93% rename from src/physics.rs rename to src/game/physics.rs index c7ea1c7..e554a0d 100644 --- a/src/physics.rs +++ b/src/game/physics.rs @@ -1,5 +1,5 @@ -use crate::ball::{BALL_SIZE, BALL_SPEED_INCREMENT, BALL_SPEED_MAX, Ball}; -use crate::racket::{RACKET_HEIGHT_HALF, RACKET_WIDTH_HALF, Racket}; +use crate::game::ball::{BALL_SIZE, BALL_SPEED_INCREMENT, BALL_SPEED_MAX, Ball}; +use crate::game::racket::{RACKET_HEIGHT_HALF, RACKET_WIDTH_HALF, Racket}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum Player { diff --git a/src/racket.rs b/src/game/racket.rs similarity index 91% rename from src/racket.rs rename to src/game/racket.rs index ad19e98..bca01ff 100644 --- a/src/racket.rs +++ b/src/game/racket.rs @@ -1,4 +1,4 @@ -use crate::controller::{Controller, RacketAction::*}; +use crate::player::controller::{Controller, RacketAction::*}; use ggez::graphics::{Canvas, Color, DrawParam, Mesh, Rect}; use ggez::{Context, GameResult}; @@ -33,7 +33,7 @@ impl Racket { canvas.draw(&self.racket_mesh, DrawParam::default().dest([self.position_x, self.position_y])); } - pub fn update(&mut self, input: &crate::controller::ControllerInput, delta_time: f32) { + pub fn update(&mut self, input: &crate::player::controller::ControllerInput, delta_time: f32) { match self.controller.get_action(input) { MoveUp => { self.position_y -= RACKET_SPEED * delta_time; diff --git a/src/score.rs b/src/game/score.rs similarity index 100% rename from src/score.rs rename to src/game/score.rs diff --git a/src/main.rs b/src/main.rs index 043b184..d5552f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,10 @@ #![windows_subsystem = "windows"] mod audio; -mod ball; -mod controller; mod debug; +mod game; mod main_state; -mod physics; -mod player_type; -mod racket; -mod score; +mod player; mod ui; use crate::main_state::MainState; diff --git a/src/main_state.rs b/src/main_state.rs index 3782ac7..5fcc7ce 100644 --- a/src/main_state.rs +++ b/src/main_state.rs @@ -5,9 +5,15 @@ // - Mouse Click: Select and cycle player type // - SPACE/ENTER: Start game +use crate::game::ball::*; +use crate::game::physics::*; +use crate::game::racket::*; +use crate::game::score::Score; +use crate::player::controller::ControllerInput; +use crate::player::player_type::PlayerType; use crate::ui::menu as ui_menu; -use crate::{audio::play_embedded_sound, ball::*, controller::ControllerInput, debug::DebugInfo, physics::*, player_type::PlayerType, racket::*, score::Score}; -use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use crate::{audio::play_embedded_sound, debug::DebugInfo}; +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect}; use ggez::{Context, GameResult, event, glam::Vec2, input::keyboard::KeyCode}; use std::collections::HashSet; @@ -69,16 +75,6 @@ impl MainState { }) } - fn draw_centered_title(&self, canvas: &mut Canvas, context: &mut Context, text: &str, color: Color) -> GameResult { - let (screen_width, screen_height) = context.gfx.drawable_size(); - let mut title = Text::new(text); - title.set_scale(screen_height / 10.0); - let title_dimensions = title.measure(context)?; - let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); - canvas.draw(&title, DrawParam::default().dest(title_position).color(color)); - Ok(()) - } - fn update_controllers(&mut self, context: &mut Context) -> GameResult { let screen_height = context.gfx.drawable_size().1; let screen_height_center = screen_height / 2.0; @@ -245,14 +241,16 @@ impl event::EventHandler for MainState { self.draw_playing(&mut canvas); } GameState::Paused => { - self.draw_paused(context, &mut canvas)?; + self.draw_playing(&mut canvas); + crate::ui::pause_screen::draw_pause_screen(context, &mut canvas)?; } GameState::GameOver { winner } => { - self.draw_game_over(context, &mut canvas, *winner)?; + self.draw_playing(&mut canvas); + crate::ui::game_over::draw_game_over(context, &mut canvas, *winner)?; } } - self.debug.draw(&mut canvas); + crate::ui::hud::draw_hud(context, &mut canvas, &self.debug)?; canvas.finish(context)?; Ok(()) } @@ -344,59 +342,4 @@ impl MainState { self.player_right.draw_on_canvas(canvas); self.ball.draw_on_canvas(canvas); } - - fn draw_game_over(&self, context: &mut Context, canvas: &mut Canvas, winner: Player) -> GameResult { - // First draw the game state - self.draw_playing(canvas); - - // Semi-transparent overlay - let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); - let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 180))?; - canvas.draw(&overlay_mesh, DrawParam::default()); - - let (screen_width, screen_height) = context.gfx.drawable_size(); - - // Winner text - let winner_text = match winner { - Player::Left => "Player 1 Wins!", - Player::Right => "Player 2 Wins!", - }; - self.draw_centered_title(canvas, context, winner_text, Color::WHITE)?; - - // Press to continue - let mut continue_text = Text::new("SPACE/ENTER: Menu | R: Restart | Esc: Menu"); - continue_text.set_scale(screen_height / 30.0); - let continue_dimensions = continue_text.measure(context)?; - let continue_position = Vec2::new((screen_width - continue_dimensions.x) / 2.0, screen_height * 0.65); - canvas.draw( - &continue_text, - DrawParam::default().dest(continue_position).color(Color::from_rgb(200, 200, 200)), - ); - - Ok(()) - } - - fn draw_paused(&self, context: &mut Context, canvas: &mut Canvas) -> GameResult { - // Draw current game state in the background - self.draw_playing(canvas); - - // Semi-transparent overlay - let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); - let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 160))?; - canvas.draw(&overlay_mesh, DrawParam::default()); - - let (screen_width, screen_height) = context.gfx.drawable_size(); - - // Paused title - self.draw_centered_title(canvas, context, "Paused", Color::WHITE)?; - - // Hints - let mut hint = Text::new("P: Resume | Esc: Menu"); - hint.set_scale(screen_height / 30.0); - let hint_dimensions = hint.measure(context)?; - let hint_position = Vec2::new((screen_width - hint_dimensions.x) / 2.0, screen_height * 0.65); - canvas.draw(&hint, DrawParam::default().dest(hint_position).color(Color::from_rgb(200, 200, 200))); - - Ok(()) - } } diff --git a/src/controller.rs b/src/player/controller.rs similarity index 99% rename from src/controller.rs rename to src/player/controller.rs index 5a1bdc2..bf82bd3 100644 --- a/src/controller.rs +++ b/src/player/controller.rs @@ -1,4 +1,4 @@ -use crate::racket::RACKET_HEIGHT_HALF; +use crate::game::racket::RACKET_HEIGHT_HALF; use ggez::{glam::Vec2, input::keyboard::KeyCode}; use std::collections::HashSet; diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..b79a90f --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,3 @@ +pub mod controller; +pub mod player_type; + diff --git a/src/player_type.rs b/src/player/player_type.rs similarity index 95% rename from src/player_type.rs rename to src/player/player_type.rs index 24f2f52..24a82ac 100644 --- a/src/player_type.rs +++ b/src/player/player_type.rs @@ -1,5 +1,5 @@ -use crate::controller::{AIController, Controller, HumanController}; -use crate::physics::Player; +use crate::game::physics::Player; +use crate::player::controller::{AIController, Controller, HumanController}; use ggez::input::keyboard::KeyCode; #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/src/ui/common.rs b/src/ui/common.rs new file mode 100644 index 0000000..77943c1 --- /dev/null +++ b/src/ui/common.rs @@ -0,0 +1,12 @@ +use ggez::{Context, GameResult, glam::Vec2}; +use ggez::graphics::{Canvas, Color, DrawParam, Text}; + +pub fn draw_centered_title(context: &mut Context, canvas: &mut Canvas, text: &str, color: Color) -> GameResult { + let (screen_width, screen_height) = context.gfx.drawable_size(); + let mut title = Text::new(text); + title.set_scale(screen_height / 10.0); + let title_dimensions = title.measure(context)?; + let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); + canvas.draw(&title, DrawParam::default().dest(title_position).color(color)); + Ok(()) +} diff --git a/src/ui/game_over.rs b/src/ui/game_over.rs new file mode 100644 index 0000000..b2d5130 --- /dev/null +++ b/src/ui/game_over.rs @@ -0,0 +1,33 @@ +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +use crate::game::physics::Player; + +pub fn draw_game_over(context: &mut Context, canvas: &mut Canvas, winner: Player) -> GameResult { + // Semi-transparent overlay + let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); + let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 180))?; + canvas.draw(&overlay_mesh, DrawParam::default()); + + let (screen_width, screen_height) = context.gfx.drawable_size(); + + // Winner text + let winner_text = match winner { + Player::Left => "Player 1 Wins!", + Player::Right => "Player 2 Wins!", + }; + + super::common::draw_centered_title(context, canvas, winner_text, Color::WHITE)?; + + // Press to continue + let mut continue_text = Text::new("R: Restart | Esc: Menu"); + continue_text.set_scale(screen_height / 30.0); + let continue_dimensions = continue_text.measure(context)?; + let continue_position = Vec2::new((screen_width - continue_dimensions.x) / 2.0, screen_height * 0.65); + canvas.draw( + &continue_text, + DrawParam::default().dest(continue_position).color(Color::from_rgb(200, 200, 200)), + ); + + Ok(()) +} diff --git a/src/ui/hud.rs b/src/ui/hud.rs new file mode 100644 index 0000000..96ac5c3 --- /dev/null +++ b/src/ui/hud.rs @@ -0,0 +1,10 @@ +use ggez::graphics::Canvas; +use ggez::{Context, GameResult}; + +use crate::debug::DebugInfo; + +pub fn draw_hud(_context: &mut Context, canvas: &mut Canvas, debug: &DebugInfo) -> GameResult { + // Delegate debug drawing to the DebugInfo helper + debug.draw(canvas); + Ok(()) +} diff --git a/src/ui/menu.rs b/src/ui/menu.rs index da268fa..b5f7768 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,7 +1,7 @@ use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; use ggez::{Context, GameResult, glam::Vec2}; -use crate::player_type::PlayerType; +use crate::player::player_type::PlayerType; // Layout ratios for the menu UI (tweak here to adjust spacing/size) const BOX_WIDTH_RATIO: f32 = 0.35; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b9a0e3e..db4cdcb 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1 +1,5 @@ +mod common; +pub mod game_over; +pub mod hud; pub mod menu; +pub mod pause_screen; diff --git a/src/ui/pause_screen.rs b/src/ui/pause_screen.rs new file mode 100644 index 0000000..0eec983 --- /dev/null +++ b/src/ui/pause_screen.rs @@ -0,0 +1,22 @@ +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +pub fn draw_pause_screen(context: &mut Context, canvas: &mut Canvas) -> GameResult { + // Semi-transparent overlay + let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); + let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 160))?; + canvas.draw(&overlay_mesh, DrawParam::default()); + + let (screen_width, screen_height) = context.gfx.drawable_size(); + + super::common::draw_centered_title(context, canvas, "Paused", Color::WHITE)?; + + // Hints + let mut hint = Text::new("P: Resume | Esc: Menu"); + hint.set_scale(screen_height / 30.0); + let hint_dimensions = hint.measure(context)?; + let hint_position = Vec2::new((screen_width - hint_dimensions.x) / 2.0, screen_height * 0.65); + canvas.draw(&hint, DrawParam::default().dest(hint_position).color(Color::from_rgb(200, 200, 200))); + + Ok(()) +}