diff --git a/src/audio.rs b/src/audio.rs index 431fa33..e95316f 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -22,7 +22,7 @@ pub struct MusicPlugin; /// /// The state persists across game state changes to maintain user preferences /// for music playback. -#[derive(Resource)] +#[derive(Resource, Default)] struct MusicState { /// Indicates if music should be playing (true) or muted (false) playing: bool, @@ -31,15 +31,6 @@ struct MusicState { handle: Option>, } -impl Default for MusicState { - fn default() -> Self { - Self { - playing: false, // Start with music enabled by default - handle: None, // No audio instance at initialization - } - } -} - impl Plugin for MusicPlugin { fn build(&self, app: &mut App) { app.add_plugins(AudioPlugin) diff --git a/src/ball.rs b/src/ball.rs index b0ca3e1..4f676c7 100644 --- a/src/ball.rs +++ b/src/ball.rs @@ -1,122 +1,214 @@ //! Ball Physics Module //! -//! This module handles all aspects of the game ball, including: -//! - Physical properties and behavior -//! - Spawn and cleanup logic -//! - Velocity maintenance -//! - Collision detection and response +//! This module implements the core ball mechanics for the Pong game using the Bevy game engine +//! and Rapier2D physics engine. It handles all aspects of the ball's behavior including: //! -//! The ball uses Rapier2D physics for realistic movement and collisions. +//! - Ball creation and initialization +//! - Physics properties and collision response +//! - Velocity management and speed constraints +//! - Cleanup and state management +//! - Collision detection and event handling +//! +//! The ball uses Rapier2D's rigid body physics system for realistic movement and collisions, +//! with carefully tuned parameters to ensure engaging gameplay while maintaining physical plausibility. use crate::GameState; use bevy::app::{App, Plugin, Update}; use bevy::prelude::*; use bevy_rapier2d::prelude::*; -/// Physical properties of the ball that define its behavior -const BALL_SIZE: f32 = 0.3; // Diameter in world units -const BALL_SPEED: f32 = 10.0; // Constant speed the ball should maintain -const SPEED_TOLERANCE: f32 = 0.5; // Allowed speed variation before correction -const RESTITUTION: f32 = 1.5; // Bounciness (>1 means gaining energy) -const BALL_MASS: f32 = 0.1; // Mass affects collision response +/// Physical properties and gameplay constants for the ball +/// +/// These constants define both the visual and physical characteristics of the ball, +/// carefully tuned to provide satisfying gameplay mechanics while maintaining +/// physical plausibility. +const BALL_SIZE: f32 = 0.3; // Ball diameter in world units (small enough for precise gameplay) +const MIN_VELOCITY: f32 = 7.0; // Minimum ball speed (ensures game keeps moving) +const MAX_VELOCITY: f32 = 20.0; // Maximum ball speed (prevents ball from becoming too fast) +const RESTITUTION: f32 = 0.9; // Bounce elasticity (slightly inelastic for better control) +const BALL_MASS: f32 = 0.0027; // Ball mass (tuned for realistic collision responses) -/// Marker component to identify ball entities. -/// Used for querying and managing ball-specific behavior. +/// Marker component for identifying ball entities in the game world. +/// +/// This component is used as a tag to: +/// - Query for ball entities in systems +/// - Filter collision events involving the ball +/// - Manage ball-specific behavior and cleanup +/// +/// # Example Usage +/// ```rust +/// // Query for ball entities +/// fn ball_system(query: Query<&Transform, With>) { +/// for transform in query.iter() { +/// // Process ball position +/// } +/// } +/// ``` #[derive(Component)] pub struct Ball; -/// Creates a new ball entity with all necessary components for physics and rendering. +/// Creates a new ball entity with complete physics and rendering setup. +/// +/// This function creates a ball entity configured with: +/// - Visual representation (white circle mesh) +/// - Physics body and collider +/// - Initial velocity based on serving direction +/// - Collision properties and response settings +/// - Physics modifiers for gameplay behavior /// /// # Arguments -/// * `commands` - Command buffer for entity creation -/// * `meshes` - Asset storage for the ball's mesh -/// * `materials` - Asset storage for the ball's material -/// * `served_by_p1` - Direction ball should initially move (true = right, false = left) +/// * `commands` - Command buffer for entity creation and component insertion +/// * `meshes` - Asset storage for managing the ball's visual mesh +/// * `materials` - Asset storage for managing the ball's material/color +/// * `served_by_p1` - Boolean flag indicating serve direction (true = right, false = left) +/// +/// # Physics Configuration +/// The ball is configured with: +/// - Dynamic rigid body for physics-based movement +/// - Zero friction to maintain momentum +/// - Zero gravity for 2D pong mechanics +/// - Continuous collision detection for reliability +/// - Custom mass and restitution for desired bounce behavior +/// +/// # Example +/// ```rust +/// create_ball(&mut commands, &mut meshes, &mut materials, true); // Serve to the right +/// ``` pub fn create_ball( commands: &mut Commands, meshes: &mut ResMut>, materials: &mut ResMut>, served_by_p1: bool, ) { - // Determine initial direction based on server + // Calculate initial direction and velocity let direction = if served_by_p1 { 1 } else { -1 }; + let initial_velocity = Vec2::new(MIN_VELOCITY * direction as f32, 0.0); - commands.spawn(( - // Identification and visual components - Ball, // Marker component - Mesh2d(meshes.add(Circle::new(BALL_SIZE / 2.0))), // Visual shape - MeshMaterial2d(materials.add(ColorMaterial::from(Color::WHITE))), // Color - Transform::from_xyz(0.0, 0.0, 0.0), // Start at center - // Physics body configuration - RigidBody::Dynamic, // Moves based on physics - Collider::ball(BALL_SIZE / 2.0), // Circular collision shape - // Initial velocity (horizontal only) - Velocity::linear(Vec2::new(BALL_SPEED * direction as f32, 0.0)), - // Collision response properties - Restitution { + commands + .spawn(Ball) + // Visual Components + // Creates a circular mesh for rendering with appropriate size + .insert(Mesh2d(meshes.add(Circle::new(BALL_SIZE / 2.0)))) + // Applies white color material to the ball + .insert(MeshMaterial2d( + materials.add(ColorMaterial::from(Color::WHITE)), + )) + // Positions ball at center of screen initially + .insert(Transform::from_xyz(0.0, 0.0, 0.0)) + // Physics Body Configuration + // Sets up dynamic rigid body for physics simulation + .insert(RigidBody::Dynamic) + // Creates circular collider matching visual size + .insert(Collider::ball(BALL_SIZE / 2.0)) + // Sets initial movement velocity + .insert(Velocity::linear(initial_velocity)) + // Collision Properties + // Configures bounce behavior + .insert(Restitution { coefficient: RESTITUTION, - combine_rule: CoefficientCombineRule::Max, // Use highest restitution in collisions - }, - Friction { - coefficient: 0.0, // Frictionless - combine_rule: CoefficientCombineRule::Min, // Use lowest friction in collisions - }, - // Physics behavior modifiers - Damping { - linear_damping: 0.0, // No speed loss over time - angular_damping: 0.0, // No rotation slowdown - }, - GravityScale(0.0), // Ignore gravity - // Collision detection settings - Ccd::enabled(), // Continuous collision detection - ActiveCollisionTypes::all(), // Detect all collision types - ActiveEvents::COLLISION_EVENTS, // Generate collision events - AdditionalMassProperties::Mass(BALL_MASS), // Set specific mass - )); + combine_rule: CoefficientCombineRule::Max, + }) + // Removes friction for consistent movement + .insert(Friction { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }) + // Physics Modifiers + // Disables velocity damping to maintain speed + .insert(Damping { + linear_damping: 0.0, + angular_damping: 0.0, + }) + // Removes gravity effect + .insert(GravityScale(0.0)) + // Collision Detection Setup + // Enables continuous collision detection for fast movement + .insert(Ccd::enabled()) + // Prevents physics sleep for consistent behavior + .insert(Sleeping::disabled()) + // Enables all collision types for comprehensive detection + .insert(ActiveCollisionTypes::all()) + // Enables collision event generation + .insert(ActiveEvents::COLLISION_EVENTS) + // Sets mass for collision response calculations + .insert(AdditionalMassProperties::Mass(BALL_MASS)); } -/// System to clean up the ball when exiting the Playing state. -/// This prevents the ball from persisting in other game states. +/// System that removes the ball entity when exiting the Playing state. +/// +/// This cleanup system ensures that: +/// - Ball is properly despawned when leaving gameplay +/// - No ball entities persist in other game states +/// - Memory is properly freed +/// - Game state transitions are clean +/// +/// # System Parameters +/// * `commands` - Command buffer for entity manipulation +/// * `ball_query` - Query to find ball entities for cleanup fn cleanup_ball(mut commands: Commands, ball_query: Query>) { for entity in ball_query.iter() { commands.entity(entity).despawn(); } } -/// System that maintains constant ball speed regardless of collisions. +/// System that maintains the ball's velocity within gameplay constraints. +/// +/// This system ensures that: +/// - Ball never moves too slowly (maintains minimum speed) +/// - Ball never moves too quickly (caps maximum speed) +/// - Direction is preserved when adjusting speed +/// - Ball maintains consistent gameplay feel /// -/// This is necessary because: -/// 1. Collisions can change the ball's speed -/// 2. We want the game to maintain a consistent pace -/// 3. Numerical errors can accumulate over time +/// The system runs every frame during gameplay to: +/// 1. Check current ball speed +/// 2. Compare against min/max bounds +/// 3. Adjust if necessary while preserving direction +/// 4. Handle edge cases (like zero velocity) /// -/// The system checks the current speed against BALL_SPEED and -/// corrects it if it deviates beyond SPEED_TOLERANCE. +/// # Physics Notes +/// - Uses vector normalization to preserve direction +/// - Handles potential division by zero +/// - Maintains speed constraints for consistent gameplay fn maintain_ball_velocity(mut query: Query<&mut Velocity, With>) { for mut velocity in query.iter_mut() { let current_velocity = velocity.linvel; let current_speed = current_velocity.length(); - // Correct speed if it's outside tolerance range - if (current_speed - BALL_SPEED).abs() > SPEED_TOLERANCE { - // Maintain direction but normalize speed - velocity.linvel = current_velocity.normalize() * BALL_SPEED; + // Only adjust non-zero velocities to prevent normalization issues + if current_speed != 0.0 { + // Determine new speed based on constraints + let new_speed = if current_speed.abs() < MIN_VELOCITY { + MIN_VELOCITY // Enforce minimum speed + } else if current_speed.abs() > MAX_VELOCITY { + MAX_VELOCITY // Cap maximum speed + } else { + current_speed // Maintain current speed if within bounds + }; + + // Apply new speed while preserving direction + velocity.linvel = current_velocity.normalize() * new_speed; } } } -/// Plugin that manages all ball-related systems. +/// Plugin that manages all ball-related systems and behavior. +/// +/// This plugin integrates the ball systems into the game by: +/// - Adding cleanup system for state transitions +/// - Adding velocity maintenance system for gameplay +/// - Organizing ball-related functionality +/// +/// The plugin ensures proper initialization and cleanup of ball +/// mechanics while maintaining clean integration with the game's +/// state system. pub struct BallPlugin; impl Plugin for BallPlugin { fn build(&self, app: &mut App) { app - // Clean up ball when leaving Playing state + // Add cleanup system for state transitions .add_systems(OnExit(GameState::Playing), cleanup_ball) - // Maintain ball velocity during gameplay - .add_systems( - Update, - maintain_ball_velocity.run_if(in_state(GameState::Playing)), - ); + // Add velocity maintenance system during gameplay updates + .add_systems(Update, maintain_ball_velocity); } } diff --git a/src/board.rs b/src/board.rs index d80042c..f829fd1 100644 --- a/src/board.rs +++ b/src/board.rs @@ -38,7 +38,7 @@ const DASH_GAP: f32 = 0.4; // Gap between dashes /// Physics settings for the walls. /// Walls are bouncy to create more interesting gameplay. -const WALL_RESTITUTION: f32 = 1.5; // Wall bounciness (>1 means adding energy) +const WALL_RESTITUTION: f32 = 2.0; // Wall bounciness (>1 means adding energy) /// Creates the black background color resource. /// This sets the clear color for the game's rendering. diff --git a/src/player.rs b/src/player.rs index bb52310..f4ece98 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,71 +1,216 @@ //! Player Module //! -//! This module handles both human and AI-controlled paddles, including: -//! - Paddle physics and movement -//! - Player input handling -//! - AI behavior and decision making -//! - Ball collision and bounce mechanics -//! -//! The module supports both human input (Player 1) and AI control (Player 2). +//! This module implements the player paddle mechanics for the Pong game, including both +//! human-controlled and AI-controlled paddles. use crate::ball::Ball; use crate::GameState; use bevy::app::{App, Plugin, Startup, Update}; use bevy::prelude::*; +use bevy::render::mesh::Indices; +use bevy::render::render_asset::RenderAssetUsages; +use bevy::render::render_resource::PrimitiveTopology; use bevy_rapier2d::prelude::*; +use std::time::Duration; -/// Constants defining the player paddles' physical and gameplay properties -const PADDLE_WIDTH: f32 = 0.5; // Width in world units -const PADDLE_HEIGHT: f32 = 2.0; // Height in world units -const PADDLE_SPEED: f32 = 5.0; // Movement speed in units per second -const MAX_BOUNCE_ANGLE: f32 = 1.0; // Maximum angle ball can bounce (radians) -const LEFT_PADDLE_X: f32 = -7.65; // X position of left paddle -const RIGHT_PADDLE_X: f32 = 7.65; // X position of right paddle +/// Configuration constants for paddle physics and gameplay +#[derive(Debug, Resource)] +pub struct PaddleConfig { + /// Movement speed in world units per second + pub speed: f32, + /// X-coordinate for left paddle position + pub left_x: f32, + /// X-coordinate for right paddle position + pub right_x: f32, + /// Total height of the paddle + pub height: f32, + /// Depth of the paddle's curve + pub curve_depth: f32, + /// Number of segments used to create the curved shape + pub segments: usize, + /// Mass of the paddle for physics calculations + pub mass: f32, + /// Duration of punch animation in seconds + pub punch_duration: f32, + /// Distance paddle moves during punch + pub punch_distance: f32, +} -/// Update rate for AI decisions to simulate human-like reaction time -const AI_UPDATE_RATE: f32 = 0.3; // Updates 3.33 times per second +impl Default for PaddleConfig { + fn default() -> Self { + Self { + speed: 20.0, + left_x: -7.65, + right_x: 7.65, + height: 2.0, + curve_depth: 0.3, + segments: 100, + mass: 0.1, + punch_duration: 0.05, + punch_distance: 0.15, + } + } +} + +/// Configuration for AI difficulty tuning +#[derive(Debug, Resource)] +pub struct AiConfig { + /// Time between AI decisions (seconds) + pub update_rate: f32, + /// Deadzone for movement to prevent jitter + pub movement_deadzone: f32, + /// Offset from center for optimal hit point + pub hit_point_offset: f32, + /// Chance to make a prediction error (0.0 - 1.0) + pub error_chance: f32, + /// Maximum prediction error amount in world units + pub max_error: f32, + /// Chance to completely miss the ball (0.0 - 1.0) + pub miss_chance: f32, +} + +/// Configuration for a challenging AI opponent +/// +/// While challenging, this AI is intentionally not perfect: +/// - It can be baited into wrong movements +/// - It occasionally misreads steep angles +/// - It has a slight delay before position adjustments +/// - It sometimes misses extremely fast shots +impl Default for AiConfig { + fn default() -> Self { + Self { + // Time between position adjustments + // Fast enough to handle most shots, but with enough delay + // that quick direction changes can catch it out of position + update_rate: 0.3, + + // Minimum distance before adjusting position + // Small enough to maintain precise positioning for returns, + // but creates brief windows where slight misalignments can + // be exploited + movement_deadzone: 0.08, + + // Preferred hitting position relative to paddle center + // Aims slightly off-center to maintain better control, + // but this creates small gaps near the paddle edges that + // can be targeted + hit_point_offset: 0.4, + + // Chance to misread the ball's trajectory + // Most noticeable when handling steep angles or after + // multiple bounces, representing the limits of its + // prediction capabilities + error_chance: 0.12, + + // Maximum prediction error magnitude + // When errors occur, they're significant enough to create + // scoring opportunities if the player is positioned to + // capitalize on them + max_error: 1.0, + + // Chance to completely miss the ball + // Primarily occurs during very fast exchanges or when + // the ball approaches at extreme angles, simulating + // the challenge of handling powerful shots + miss_chance: 0.05, + } + } +} /// Component that identifies which player a paddle belongs to -#[derive(Component)] +#[derive(Component, Clone, Debug)] pub enum Player { P1, // Human player (left paddle) P2, // AI player (right paddle) } +/// Represents the current movement state of the AI paddle +#[derive(Debug)] +enum MovementState { + Idle, + MovingUp(f32), // Contains target Y position + MovingDown(f32), // Contains target Y position +} + /// Component for AI-controlled paddles that simulates human-like input behavior -#[derive(Component)] +#[derive(Component, Debug)] struct AiPaddle { - update_timer: Timer, // Controls AI decision rate - move_up: bool, // Simulates W key press - move_down: bool, // Simulates S key press + /// Timer to control AI decision rate + update_timer: Timer, + /// Timer for upward movement duration + move_up_timer: Timer, + /// Timer for downward movement duration + move_down_timer: Timer, + /// Current movement state + movement_state: MovementState, + /// Last predicted intersection point + last_prediction: Option, } impl Default for AiPaddle { fn default() -> Self { Self { - update_timer: Timer::from_seconds(AI_UPDATE_RATE, TimerMode::Repeating), - move_up: false, - move_down: false, + update_timer: Timer::from_seconds( + AiConfig::default().update_rate, + TimerMode::Repeating, + ), + move_up_timer: Timer::from_seconds(0.0, TimerMode::Once), + move_down_timer: Timer::from_seconds(0.0, TimerMode::Once), + movement_state: MovementState::Idle, + last_prediction: None, } } } -/// Calculates where the ball will intersect with a paddle's x-position -/// -/// # Arguments -/// * `ball_pos` - Current ball position -/// * `ball_vel` - Current ball velocity -/// * `paddle_x` - X-coordinate of paddle to check -/// -/// # Returns -/// * `Some(y)` - Predicted Y intersection if ball is moving toward paddle -/// * `None` - If ball is moving away from paddle +/// Component to track paddle punch state and animation +#[derive(Component, Debug)] +struct PunchState { + /// Timer for punch animation duration + timer: Timer, + /// Whether paddle is currently in punch state + is_punching: bool, + /// Original x position to return to after punch + rest_x: f32, +} + +impl Default for PunchState { + fn default() -> Self { + Self { + timer: Timer::from_seconds(PaddleConfig::default().punch_duration, TimerMode::Once), + is_punching: false, + rest_x: 0.0, + } + } +} + +/// Calculate the duration needed to move to a target position +fn calculate_movement_duration( + current_pos: f32, + target_pos: f32, + speed: f32, + min_duration: f32, + max_duration: f32, +) -> f32 { + let distance = (target_pos - current_pos).abs(); + let base_duration = distance / speed; + + // Add small random variation for more human-like behavior + let variation = rand::random::() * 0.1; // Up to 10% variation + let duration = base_duration * (1.0 + variation); + + // Clamp duration between minimum and maximum values + duration.clamp(min_duration, max_duration) +} + +/// Predicts where the ball will intersect with a paddle's x-position fn predict_intersection(ball_pos: Vec2, ball_vel: Vec2, paddle_x: f32) -> Option { - // Only predict if ball is moving toward the paddle - if (paddle_x > ball_pos.x && ball_vel.x > 0.0) || (paddle_x < ball_pos.x && ball_vel.x < 0.0) { - // Calculate time until x-intersection + // Check if ball is moving toward paddle + let moving_toward = + (paddle_x > ball_pos.x && ball_vel.x > 0.0) || (paddle_x < ball_pos.x && ball_vel.x < 0.0); + + if moving_toward { + // Calculate intersection time and position let time = (paddle_x - ball_pos.x) / ball_vel.x; - // Calculate y-position at intersection let y = ball_pos.y + (ball_vel.y * time); Some(y) } else { @@ -74,38 +219,113 @@ fn predict_intersection(ball_pos: Vec2, ball_vel: Vec2, paddle_x: f32) -> Option } /// System that controls AI paddle movement by simulating human-like input -/// -/// The AI: -/// 1. Predicts ball intersection point -/// 2. Updates movement decision at human-like intervals -/// 3. Moves paddle toward predicted position fn ai_decision_making( time: Res