diff --git a/README.md b/README.md index 4166dbc..7989a47 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,14 @@ nix develop --command bash -c "cargo build" * ~~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. +* ~~**Scoring:**~~ **(DONE)** + * ~~Add a `Score` resource.~~ + * ~~Increment the score in `check_bullet_collisions` when an enemy is hit.~~ * Consider different point values for different enemy types or hitting enemies during dives later. -* **Levels/Stages:** - * Add a `CurrentStage` resource. - * Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation). - * Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations). +* ~~**Levels/Stages:**~~ **(DONE)** + * ~~Add a `CurrentStage` resource.~~ + * ~~Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation).~~ + * ~~Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations).~~ **2. Enemy Behavior - Formations & Attack Patterns:** diff --git a/src/main.rs b/src/main.rs index c561ff6..5970697 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,13 @@ const PLAYER_SIZE: Vec2 = Vec2::new(30.0, 30.0); const ENEMY_SIZE: Vec2 = Vec2::new(40.0, 40.0); const BULLET_SIZE: Vec2 = Vec2::new(5.0, 15.0); +// Formation constants +const FORMATION_ROWS: usize = 4; +const FORMATION_COLS: usize = 8; +const FORMATION_ENEMY_COUNT: usize = FORMATION_ROWS * FORMATION_COLS; +const FORMATION_X_SPACING: f32 = 60.0; +const FORMATION_Y_SPACING: f32 = 50.0; +const FORMATION_BASE_Y: f32 = WINDOW_HEIGHT / 2.0 - 150.0; // Top area for formation const STARTING_LIVES: u32 = 3; const PLAYER_RESPAWN_DELAY: f32 = 2.0; const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0; @@ -40,9 +47,13 @@ struct Enemy; #[derive(Component)] struct Invincible { - timer: Timer, + timer: Timer, } +#[derive(Component)] +struct FormationTarget { + position: Vec3, +} // --- Resources --- #[derive(Resource)] struct EnemySpawnTimer { @@ -56,9 +67,24 @@ struct PlayerLives { #[derive(Resource)] struct PlayerRespawnTimer { - timer: Timer, + timer: Timer, +} +#[derive(Resource)] +struct Score { + value: u32, } +#[derive(Resource)] +struct CurrentStage { + number: u32, + waiting_for_clear: bool, // Flag to check if we should check for stage clear +} + +#[derive(Resource)] +struct FormationState { + next_slot_index: usize, + total_spawned_this_stage: usize, +} // --- Game Over UI --- #[derive(Component)] @@ -112,7 +138,41 @@ fn cleanup_game_entities( } // for entity in player_query.iter() { // commands.entity(entity).despawn(); - // } + // } +} + +// --- Stage Management --- + +// Helper to access world directly in check_stage_clear +fn check_stage_clear(world: &mut World) { + // Use manual resource access because we need mutable access to multiple resources + // Separate checks to manage borrows correctly + let mut should_clear = false; + if let Some(stage) = world.get_resource::() { + if stage.waiting_for_clear { + // Create the query *after* checking the flag + let mut enemy_query = world.query_filtered::>(); + if enemy_query.iter(world).next().is_none() { + should_clear = true; + } + } + } + + if should_clear { + // Get mutable resources only when needed + if let Some(mut stage) = world.get_resource_mut::() { + stage.number += 1; + stage.waiting_for_clear = false; + println!("Stage cleared! Starting Stage {}...", stage.number); + } + if let Some(mut formation_state) = world.get_resource_mut::() { + formation_state.next_slot_index = 0; + formation_state.total_spawned_this_stage = 0; + } + if let Some(mut spawn_timer) = world.get_resource_mut::() { + spawn_timer.timer.reset(); + } + } } fn main() { @@ -121,7 +181,7 @@ fn main() { .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - title: "Galaga :: Lives: 3".into(), // Initial title + title: "Galaga :: Stage: 1 Lives: 3 Score: 0".into(), // Initial title with stage resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(), ..default() }), @@ -134,9 +194,12 @@ fn main() { timer: Timer::new(Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), TimerMode::Once) }) .insert_resource(EnemySpawnTimer { - timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating), - }) - // Add Systems + timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating), + }) + .insert_resource(Score { value: 0 }) // Initialize score + .insert_resource(CurrentStage { number: 1, waiting_for_clear: false }) // Initialize stage and flag + .insert_resource(FormationState { next_slot_index: 0, total_spawned_this_stage: 0 }) // Initialize formation state + // Add Systems .add_systems(Startup, setup) // Systems running only when Playing .add_systems(Update, ( @@ -148,9 +211,11 @@ fn main() { check_bullet_collisions, 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))) + manage_invincibility, + // check_stage_clear is now added separately below using .pipe(system) + // Game Over check is now implicit in check_player_enemy_collisions + ).run_if(in_state(AppState::Playing))) + .add_systems(Update, check_stage_clear.run_if(in_state(AppState::Playing))) // Add world-based system directly // Systems running regardless of state (or managing state transitions) .add_systems(Update, ( update_window_title, // Keep title updated @@ -283,60 +348,112 @@ fn move_bullets( fn spawn_enemies( mut commands: Commands, - time: Res