feat: Implement Game Over state and UI cleanup

This commit is contained in:
Harald Hoyer 2025-04-05 00:13:56 +02:00
parent 9e5addb59d
commit 0f4737fffd
2 changed files with 88 additions and 15 deletions

View file

@ -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.

View file

@ -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<Entity, With<GameOverUI>>) {
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<Entity, With<Bullet>>,
enemy_query: Query<Entity, With<Enemy>>,
// Optionally despawn player too, or handle separately if needed for restart
// player_query: Query<Entity, With<Player>>,
) {
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::<AppState>() // 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<PlayerLives>,
mut respawn_timer: ResMut<PlayerRespawnTimer>,
mut next_state: ResMut<NextState<AppState>>, // Resource to change state
// Query player without Invincible component - relies on run_if condition too
player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>,
enemy_query: Query<(Entity, &Transform), With<Enemy>>,
@ -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;
}
}
}
}
}