diff --git a/src/game_state.rs b/src/game_state.rs index 35c6f11..d3efe5e 100644 --- a/src/game_state.rs +++ b/src/game_state.rs @@ -1,7 +1,9 @@ use bevy::prelude::*; -use crate::components::{Bullet, Enemy, EnemyBullet, GameOverUI, RestartMessage, StartButton, StartMenuUI}; -use crate::resources::RestartPressed; +use crate::components::{ + Bullet, Enemy, EnemyBullet, GameOverUI, HighScoreText, RestartMessage, StartButton, StartMenuUI, +}; +use crate::resources::{HighScore, RestartPressed, Score}; #[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)] pub enum AppState { @@ -16,7 +18,7 @@ const BUTTON_HOVER: Color = Color::srgb(0.2, 0.2, 0.7); // --- Game Over UI --- -pub fn setup_game_over_ui(mut commands: Commands) { +pub fn setup_game_over_ui(mut commands: Commands, high_score: Res, score: Res) { commands.spawn(( Text::new("GAME OVER"), TextFont { @@ -33,6 +35,25 @@ pub fn setup_game_over_ui(mut commands: Commands) { }, GameOverUI, )); + commands.spawn(( + Text::new(format!( + "Best: {} Your Score: {}", + high_score.value, score.value + )), + TextFont { + font_size: 32.0, + ..default() + }, + TextColor(Color::WHITE), + Node { + position_type: PositionType::Absolute, + align_self: AlignSelf::Center, + justify_self: JustifySelf::Center, + top: Val::Percent(48.0), + ..default() + }, + HighScoreText, + )); commands.spawn(( Text::new("Press R to Restart"), TextFont { @@ -53,7 +74,7 @@ pub fn setup_game_over_ui(mut commands: Commands) { pub fn cleanup_game_over_ui( mut commands: Commands, - query: Query, With)>>, + query: Query, With, With)>>, ) { for entity in &query { commands.entity(entity).despawn(); @@ -73,7 +94,7 @@ pub fn cleanup_game_entities( // --- Start Menu UI --- -pub fn setup_start_menu_ui(mut commands: Commands) { +pub fn setup_start_menu_ui(mut commands: Commands, high_score: Res) { commands .spawn(( Node { @@ -99,6 +120,19 @@ pub fn setup_start_menu_ui(mut commands: Commands) { ..default() }, )); + parent.spawn(( + Text::new(format!("Best: {}", high_score.value)), + TextFont { + font_size: 32.0, + ..default() + }, + TextColor(Color::WHITE), + Node { + margin: UiRect::bottom(Val::Px(30.0)), + ..default() + }, + HighScoreText, + )); parent .spawn(( Button, @@ -127,7 +161,10 @@ pub fn setup_start_menu_ui(mut commands: Commands) { }); } -pub fn cleanup_start_menu_ui(mut commands: Commands, query: Query>) { +pub fn cleanup_start_menu_ui( + mut commands: Commands, + query: Query, With)>>, +) { for entity in &query { commands.entity(entity).despawn(); } diff --git a/src/lib.rs b/src/lib.rs index 9c16d49..f6188f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod components; pub mod constants; pub mod enemy; pub mod game_state; +pub mod persistence; pub mod player; pub mod resources; pub mod stage; @@ -15,8 +16,7 @@ pub mod starfield; pub mod systems; use bullet::{ - check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets, - move_enemy_bullets, + check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets, move_enemy_bullets, }; use components::TractorBeam; use constants::{PLAYER_RESPAWN_DELAY, STARTING_LIVES, WINDOW_HEIGHT, WINDOW_WIDTH}; @@ -34,14 +34,14 @@ use player::{ player_shoot, respawn_player, }; use resources::{ - AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives, + AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, HighScore, PlayerLives, PlayerRespawnTimer, RestartPressed, Score, StageConfigurations, }; use stage::check_stage_clear; use starfield::scroll_starfield; use systems::{ - animate_explosion, player_exists, player_vulnerable, setup, should_respawn_player, - update_window_title, + animate_explosion, check_high_score, player_exists, player_vulnerable, setup, + should_respawn_player, update_window_title, }; pub fn run() { @@ -73,6 +73,9 @@ pub fn run() { timer: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Once), }) .init_resource::() + .insert_resource(HighScore { + value: persistence::load(&persistence::save_path()).value, + }) .init_resource::() .init_resource::() .init_resource::() @@ -121,7 +124,10 @@ pub fn run() { start_menu_button_system.run_if(in_state(AppState::StartMenu)), ) // Game over. - .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui) + .add_systems( + OnEnter(AppState::GameOver), + (setup_game_over_ui, check_high_score), + ) .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui) .add_systems( Update, diff --git a/src/systems.rs b/src/systems.rs index 563cf7d..812f1a6 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -2,9 +2,12 @@ use bevy::prelude::*; use std::time::Duration; use crate::components::{Explosion, Invincible, Player}; -use crate::constants::{EXPLOSION_BASE_SIZE, EXPLOSION_COLOR, EXPLOSION_DURATION, EXPLOSION_MAX_SIZE}; +use crate::constants::{ + EXPLOSION_BASE_SIZE, EXPLOSION_COLOR, EXPLOSION_DURATION, EXPLOSION_MAX_SIZE, +}; +use crate::persistence; use crate::player::spawn_player_ship; -use crate::resources::{CurrentStage, PlayerLives, Score, is_special_stage}; +use crate::resources::{is_special_stage, CurrentStage, HighScore, PlayerLives, Score}; use crate::starfield::spawn_starfield; pub fn setup(mut commands: Commands) { @@ -27,6 +30,17 @@ pub fn should_respawn_player(lives: Res, query: Query<&Player>) -> query.is_empty() && lives.count > 0 } +// --- High Score --- + +pub fn check_high_score(score: Res, mut high_score: ResMut) { + if score.value > high_score.value { + high_score.value = score.value; + if let Err(e) = persistence::save(&high_score, &persistence::save_path()) { + eprintln!("Failed to save high score: {e}"); + } + } +} + // --- HUD --- pub fn update_window_title(