From 0f4737fffd4115440951c6519d0b5bf15afd87eb Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Sat, 5 Apr 2025 00:13:56 +0200 Subject: [PATCH] feat: Implement Game Over state and UI cleanup --- README.md | 8 ++--- src/main.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 46668f9..4166dbc 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ nix develop --command bash -c "cargo build" * ~~Add a `PlayerLives` resource (e.g., starting with 3).~~ * ~~Modify `check_player_enemy_collisions`: Instead of just printing, decrement the lives count.~~ * ~~Implement player destruction (despawn the player sprite) and respawn logic (maybe after a short delay, with temporary invincibility).~~ -* **Game Over State:** - * Use Bevy's `States` (e.g., `Playing`, `GameOver`). - * Transition to `GameOver` when `PlayerLives` reaches zero. - * In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option. +* ~~**Game Over State:**~~ **(DONE)** + * ~~Use Bevy's `States` (e.g., `Playing`, `GameOver`).~~ + * ~~Transition to `GameOver` when `PlayerLives` reaches zero.~~ + * ~~In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option.~~ * **Scoring:** * Add a `Score` resource. * Increment the score in `check_bullet_collisions` when an enemy is hit. diff --git a/src/main.rs b/src/main.rs index 09f0bf8..c561ff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,12 @@ -use bevy::prelude::*; +use bevy::prelude::*; // Removed unused AppExit use std::time::Duration; - +// --- Game States --- +#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)] +enum AppState { + #[default] + Playing, + GameOver, +} // --- Constants --- const WINDOW_WIDTH: f32 = 600.0; const WINDOW_HEIGHT: f32 = 800.0; @@ -53,9 +59,65 @@ struct PlayerRespawnTimer { timer: Timer, } +// --- Game Over UI --- + +#[derive(Component)] +struct GameOverUI; + +fn setup_game_over_ui(mut commands: Commands) { + println!("Entering GameOver state. Setting up UI."); + commands.spawn(( + TextBundle::from_section( + "GAME OVER", + TextStyle { + font_size: 100.0, + color: Color::WHITE, + ..default() + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + align_self: AlignSelf::Center, + justify_self: JustifySelf::Center, + top: Val::Percent(40.0), // Center vertically roughly + ..default() + }), + GameOverUI, // Tag the UI element + )); + // TODO: Add "Press R to Restart" text later +} + +fn cleanup_game_over_ui(mut commands: Commands, query: Query>) { + println!("Exiting GameOver state. Cleaning up UI."); + for entity in query.iter() { + commands.entity(entity).despawn_recursive(); + } +} + +// --- Cleanup --- + +fn cleanup_game_entities( + mut commands: Commands, + bullet_query: Query>, + enemy_query: Query>, + // Optionally despawn player too, or handle separately if needed for restart + // player_query: Query>, +) { + println!("Exiting Playing state. Cleaning up game entities."); + for entity in bullet_query.iter() { + commands.entity(entity).despawn(); + } + for entity in enemy_query.iter() { + commands.entity(entity).despawn(); + } + // for entity in player_query.iter() { + // commands.entity(entity).despawn(); + // } +} fn main() { - App::new() + App::new() // Start App builder + .init_state::() // Initialize the AppState *after* App::new() .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { @@ -76,19 +138,29 @@ fn main() { }) // Add Systems .add_systems(Startup, setup) + // Systems running only when Playing .add_systems(Update, ( move_player, - player_shoot.run_if(player_exists), // Only shoot if player exists + player_shoot.run_if(player_exists), move_bullets, move_enemies, spawn_enemies, check_bullet_collisions, - check_player_enemy_collisions.run_if(player_vulnerable), // Only check if player exists and is not invincible - respawn_player.run_if(should_respawn_player), // Conditionally run respawn - manage_invincibility, // Handle invincibility timer and blinking - update_window_title, // Show lives in window title - // (Game Over check would go here later) + check_player_enemy_collisions.run_if(player_vulnerable), + respawn_player.run_if(should_respawn_player), + manage_invincibility, + // Game Over check is now implicit in check_player_enemy_collisions + ).run_if(in_state(AppState::Playing))) + // Systems running regardless of state (or managing state transitions) + .add_systems(Update, ( + update_window_title, // Keep title updated + // Add system to check for restart input in GameOver state later + bevy::window::close_on_esc, // Allow closing anytime )) + // Systems for entering/exiting states + .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui) + .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui) + .add_systems(OnExit(AppState::Playing), cleanup_game_entities) // Cleanup when leaving Playing .run(); } @@ -275,6 +347,7 @@ fn check_player_enemy_collisions( mut commands: Commands, mut lives: ResMut, mut respawn_timer: ResMut, + mut next_state: ResMut>, // Resource to change state // Query player without Invincible component - relies on run_if condition too player_query: Query<(Entity, &Transform), (With, Without)>, enemy_query: Query<(Entity, &Transform), With>, @@ -300,7 +373,7 @@ fn check_player_enemy_collisions( println!("Respawn timer started."); } else { println!("GAME OVER!"); - // Game Over logic would go here - e.g., transition to a GameOver state + next_state.set(AppState::GameOver); // Transition to GameOver state } // Important: Break after handling one collision per frame for the player break; @@ -351,7 +424,7 @@ fn manage_invincibility( // Ensure player is visible when invincibility ends if let Some(ref mut vis) = visibility { **vis = Visibility::Visible; - } + } } } }