From fc611711c92bc4d78c0f331da55362606fccaaa3 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Sat, 28 Dec 2024 18:10:21 -0500 Subject: [PATCH] fix: score resume from pause --- src/score.rs | 345 ++++++++++++++++++++++++++------------------------- 1 file changed, 176 insertions(+), 169 deletions(-) diff --git a/src/score.rs b/src/score.rs index 26104ce..02d6806 100644 --- a/src/score.rs +++ b/src/score.rs @@ -1,14 +1,12 @@ //! Score Management Module //! -//! This module handles all scoring-related functionality, including: -//! - Score tracking and display -//! - Serve mechanics and timing -//! - Ball spawning on points -//! - Score UI rendering -//! - Victory conditions and game reset -//! -//! The scoring system follows traditional ping-pong rules with -//! serve switching and deuce handling. +//! Handles scoring mechanics and display for a table tennis-style game. Features include: +//! - Score tracking and persistence across game states +//! - Traditional table tennis scoring rules (first to 11, win by 2) +//! - Alternating serve patterns with deuce handling +//! - Score display UI with automatic updates +//! - Victory condition checking +//! - Ball spawning and serve mechanics use crate::ball::{create_ball, Ball}; use crate::board::Wall; @@ -17,43 +15,50 @@ use bevy::prelude::*; use bevy_rapier2d::prelude::*; use rand::Rng; -/// Resource that tracks the game's scoring state. -/// This persists across state changes to maintain score history. +// ----- Resources ----- + +/// Resource that tracks game scoring state and serve mechanics. +/// This persists across state changes to maintain game progress. #[derive(Resource)] pub struct Score { - pub p1: u32, // Player 1's score - pub p2: u32, // Player 2's score - pub server_is_p1: bool, // Whether P1 is currently serving - serve_count: u32, // Number of serves since last switch - serve_timer: Timer, // Delay between point and next serve - pub should_serve: bool, // Whether we're waiting to serve + /// Player 1's current score + pub p1: u32, + /// Player 2's current score + pub p2: u32, + /// Indicates whether Player 1 is currently serving + pub server_is_p1: bool, + /// Tracks serves since last server switch + serve_count: u32, + /// Timer for delay between points + serve_timer: Timer, + /// Flag indicating a serve is pending + pub should_serve: bool, } impl Score { - /// Creates a new score state with initial values + /// Creates a new scoring state with initial values. + /// Server is randomly chosen at start. fn new() -> Self { Self { p1: 0, p2: 0, - // Randomly choose initial server server_is_p1: rand::thread_rng().gen_bool(0.5), serve_count: 0, - // 0.75 second delay before serving serve_timer: Timer::from_seconds(0.75, TimerMode::Once), should_serve: false, } } - /// Adds a point and handles serve switching logic + /// Awards a point and handles serve rotation logic. /// - /// # Arguments - /// * `p1_scored` - Whether player 1 scored the point + /// Implements official table tennis serve rules: + /// - Server changes every 2 points in normal play + /// - Server changes every point during deuce (10-10 or higher) /// - /// # Serve Switching Rules - /// - Normal play: Switch server every 2 points - /// - Deuce (10-10 or higher): Switch every point + /// # Arguments + /// * `p1_scored` - true if point goes to Player 1, false for Player 2 fn add_point(&mut self, p1_scored: bool) { - // Update score + // Update appropriate player's score if p1_scored { self.p1 += 1; } else { @@ -62,25 +67,26 @@ impl Score { self.serve_count += 1; - // Check for deuce conditions + // Check for deuce conditions (both players at 10+) let in_deuce = self.p1 >= 10 && self.p2 >= 10; let switch_threshold = if in_deuce { 1 } else { 2 }; - // Switch server if threshold reached + // Switch server if we've hit the threshold if self.serve_count >= switch_threshold { self.server_is_p1 = !self.server_is_p1; self.serve_count = 0; } } - /// Checks if the game has reached a victory condition + /// Checks if either player has won the game. /// - /// Victory is achieved when: - /// - A player reaches 11 or more points AND - /// - They have a lead of at least 2 points + /// Victory conditions (official table tennis rules): + /// 1. Score must be 11 or higher + /// 2. Must have a 2-point lead /// /// # Returns - /// `true` if either player has won, `false` if the game should continue + /// * `true` if either player has won + /// * `false` if game should continue pub fn check_victory(&self) -> bool { if self.p1 >= 11 && self.p1 >= self.p2 + 2 { return true; @@ -91,13 +97,13 @@ impl Score { false } - /// Resets the game state to initial values for a new game + /// Resets scoring state for a new game. /// - /// This: - /// - Clears both players' scores - /// - Randomly assigns a new server - /// - Resets serve count and timer - /// - Clears any pending serve flags + /// This resets: + /// - Both players' scores to 0 + /// - Serve count to 0 + /// - Randomly assigns initial server + /// - Clears any pending serve state pub fn reset(&mut self) { self.p1 = 0; self.p2 = 0; @@ -108,30 +114,34 @@ impl Score { } } -/// Component to identify score display text entities. -/// Used for both individual score texts and their container. +// ----- Components ----- + +/// Component to identify and differentiate score display UI elements. #[derive(Component)] struct ScoreText { kind: ScoreKind, } -/// Enum to differentiate between different score UI elements +/// Types of score display UI elements. enum ScoreKind { - P1, // Player 1's score text - P2, // Player 2's score text - Root, // The container node + P1, // Player 1's score display + P2, // Player 2's score display + Root, // Container element } -/// Initializes the Score resource at startup. -/// This runs once when the game starts and persists through all states. -fn init_score(mut commands: Commands) { - commands.insert_resource(Score::new()); -} +// ----- UI Creation and Management Systems ----- -/// Sets up the score display UI when entering Playing state. -/// The UI is only visible during actual gameplay. -fn setup_score_ui(mut commands: Commands) { - // Create UI root container +/// Creates the score display UI layout. +/// +/// Layout structure: +/// - Root container (centered, fixed width) +/// - Player 1 score (left side) +/// - Player 2 score (right side) +/// +/// # Arguments +/// * `commands` - Command buffer for entity creation +/// * `score` - Current score resource for initial values +fn setup_score_ui(mut commands: Commands, score: Res) { commands .spawn(( Node { @@ -143,51 +153,80 @@ fn setup_score_ui(mut commands: Commands) { flex_direction: FlexDirection::Row, ..default() }, - ScoreText { - kind: ScoreKind::Root, - }, // Mark for cleanup + ScoreText { kind: ScoreKind::Root }, )) .with_children(|parent| { - // Player 1 score text - parent.spawn(( - Text::new("0"), - TextFont { - font_size: 48.0, - ..default() - }, - TextColor(Color::WHITE), - Node { - margin: UiRect::right(Val::Px(20.0)), - ..default() - }, - ScoreText { - kind: ScoreKind::P1, - }, - )); - - // Player 2 score text - parent.spawn(( - Text::new("0"), - TextFont { - font_size: 48.0, - ..default() - }, - TextColor(Color::WHITE), - Node { - margin: UiRect::left(Val::Px(20.0)), - ..default() - }, - ScoreText { - kind: ScoreKind::P2, - }, - )); + spawn_player_score(parent, score.p1, ScoreKind::P1, UiRect::right(Val::Px(20.0))); + spawn_player_score(parent, score.p2, ScoreKind::P2, UiRect::left(Val::Px(20.0))); }); } -/// Spawns a new ball when: -/// - Entering Playing state initially -/// - Resuming from pause -/// - Starting a new game after victory +/// Helper function to spawn individual player score displays. +/// +/// # Arguments +/// * `parent` - Parent UI node to attach to +/// * `score` - Initial score value to display +/// * `kind` - Which player's score this represents +/// * `margin` - Margin settings for positioning +fn spawn_player_score( + parent: &mut ChildBuilder, + score: u32, + kind: ScoreKind, + margin: UiRect, +) { + parent.spawn(( + Text::new(score.to_string()), + TextFont { + font_size: 48.0, + ..default() + }, + TextColor(Color::WHITE), + Node { margin, ..default() }, + ScoreText { kind }, + )); +} + +/// Updates score display text to match current game state. +/// +/// This system: +/// - Runs continuously during gameplay +/// - Updates only when text doesn't match current score +/// - Ensures consistency after state transitions +fn update_score_display(score: Res, mut query: Query<(&mut Text, &ScoreText)>) { + for (mut text, score_text) in query.iter_mut() { + let current_score = match score_text.kind { + ScoreKind::P1 => score.p1, + ScoreKind::P2 => score.p2, + ScoreKind::Root => continue, + }; + + let score_text = current_score.to_string(); + if **text != score_text { + **text = score_text; + } + } +} + +/// Removes score display UI when leaving gameplay state. +fn cleanup_score_ui(mut commands: Commands, query: Query>) { + for entity in query.iter() { + commands.entity(entity).despawn_recursive(); + } +} + +// ----- Gameplay Systems ----- + +/// Creates initial Score resource. +fn init_score(mut commands: Commands) { + commands.insert_resource(Score::new()); +} + +/// Manages ball spawning for various game situations. +/// +/// Spawns ball: +/// - At start of new game +/// - After resuming from pause +/// - After each point (with serve delay) fn on_resume( mut commands: Commands, mut meshes: ResMut>, @@ -195,39 +234,17 @@ fn on_resume( score: Res, ball_query: Query>, ) { - // Only spawn if no ball exists and we're not in serve delay if ball_query.is_empty() && !score.should_serve { - create_ball( - &mut commands, - &mut meshes, - &mut materials, - score.server_is_p1, - ); + create_ball(&mut commands, &mut meshes, &mut materials, score.server_is_p1); } } -/// Checks for victory conditions and transitions to GameOver state. -/// This only runs during active gameplay. -fn check_victory( - score: Res, - mut commands: Commands, - mut next_state: ResMut>, - ball_query: Query>, -) { - if score.check_victory() { - // Despawn the ball to prevent further scoring - for entity in ball_query.iter() { - commands.entity(entity).despawn(); - } - // Transition to game over state - next_state.set(GameState::GameOver); - } -} - -/// Handles the delay between scoring and serving. -/// This provides a brief pause after points to: -/// - Let players see what happened -/// - Prepare for the next serve +/// Implements serve delay mechanics between points. +/// +/// This provides: +/// - Visual pause between points +/// - Time for players to prepare +/// - Consistent serve timing fn handle_serve_delay( time: Res