Compare commits
	
		
			10 commits
		
	
	
		
			0f4737fffd
			...
			c525b376b0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c525b376b0 | |||
| 0b8527b955 | |||
| 3dbfb9dac1 | |||
| ef055fe3c5 | |||
| e8dceaca27 | |||
| 6acfee2f95 | |||
| 45482f9e11 | |||
| 66fd1e8b1b | |||
| 4256b0046c | |||
| fe5579727f | 
					 13 changed files with 1165 additions and 430 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
/target
 | 
					/target
 | 
				
			||||||
/.direnv
 | 
					/.direnv
 | 
				
			||||||
/logs
 | 
					/logs
 | 
				
			||||||
 | 
					/.idea
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								.roo/mcp.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.roo/mcp.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "mcpServers": {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										36
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
					@ -47,35 +47,35 @@ nix develop --command bash -c "cargo build"
 | 
				
			||||||
    *   ~~Use Bevy's `States` (e.g., `Playing`, `GameOver`).~~
 | 
					    *   ~~Use Bevy's `States` (e.g., `Playing`, `GameOver`).~~
 | 
				
			||||||
    *   ~~Transition to `GameOver` when `PlayerLives` reaches zero.~~
 | 
					    *   ~~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.~~
 | 
					    *   ~~In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option.~~
 | 
				
			||||||
*   **Scoring:**
 | 
					*   ~~**Scoring:**~~ **(DONE)**
 | 
				
			||||||
    *   Add a `Score` resource.
 | 
					    *   ~~Add a `Score` resource.~~
 | 
				
			||||||
    *   Increment the score in `check_bullet_collisions` when an enemy is hit.
 | 
					    *   ~~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.
 | 
					    *   Consider different point values for different enemy types or hitting enemies during dives later.
 | 
				
			||||||
*   **Levels/Stages:**
 | 
					*   ~~**Levels/Stages:**~~ **(DONE)**
 | 
				
			||||||
    *   Add a `CurrentStage` resource.
 | 
					    *   ~~Add a `CurrentStage` resource.~~
 | 
				
			||||||
    *   Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation).
 | 
					    *   ~~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).
 | 
					    *   ~~Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations).~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**2. Enemy Behavior - Formations & Attack Patterns:**
 | 
					**2. Enemy Behavior - Formations & Attack Patterns:**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*   **Enemy Formations:** This is a core Galaga feature.
 | 
					*   **Enemy Formations:** This is a core Galaga feature.
 | 
				
			||||||
    *   Define target positions for the enemy formation on screen.
 | 
					    *   ~~Define target positions for the enemy formation on screen.~~ **(DONE)**
 | 
				
			||||||
    *   Give enemies an `Entering` state/component: They fly onto the screen following predefined paths (curves, waypoints).
 | 
					    *   ~~Give enemies an `Entering` state/component: They fly onto the screen following predefined paths (curves, waypoints).~~ **(DONE - Basic linear path implemented)**
 | 
				
			||||||
    *   Give enemies a `Formation` state/component: Once they reach their target position, they stop and hold formation.
 | 
					    *   Give enemies a `Formation` state/component: Once they reach their target position, they stop and hold formation. **(DONE - Stop implemented by removing FormationTarget)**
 | 
				
			||||||
*   **Enemy Attack Dives:**
 | 
					*   **Enemy Attack Dives:**
 | 
				
			||||||
    *   Give enemies an `Attacking` state/component.
 | 
					    *   ~~Give enemies an `Attacking` state/component.~~ **(DONE)**
 | 
				
			||||||
    *   Periodically trigger enemies in the formation to switch to the `Attacking` state.
 | 
					    *   ~~Periodically trigger enemies in the formation to switch to the `Attacking` state.~~ **(DONE - Random selection after formation complete)**
 | 
				
			||||||
    *   Define attack paths (swooping dives towards the player area).
 | 
					    *   ~~Define attack paths (swooping dives towards the player area).~~ **(DONE - Basic swoop towards center implemented)**
 | 
				
			||||||
    *   Make enemies fire bullets (downwards or towards the player) during their dives.
 | 
					    *   ~~Make enemies fire bullets (downwards or towards the player) during their dives.~~ **(DONE - Downward)**
 | 
				
			||||||
    *   After an attack dive, enemies could return to their formation position or fly off-screen.
 | 
					    *   After an attack dive, enemies could return to their formation position or fly off-screen. **(Fly off-screen implemented)**
 | 
				
			||||||
*   **Enemy Variety:**
 | 
					*   **Enemy Variety:**
 | 
				
			||||||
    *   Introduce different types of enemies (e.g., using different components or an enum).
 | 
					    *   ~~Introduce different types of enemies (e.g., using different components or an enum).~~ **(DONE - Added EnemyType enum and field)**
 | 
				
			||||||
    *   Assign different behaviors, point values, and maybe sprites to each type.
 | 
					    *   ~~Assign different behaviors, point values, and maybe sprites to each type.~~ **(DONE - Behaviors, points & color based on type)**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**3. Advanced Galaga Mechanics:**
 | 
					**3. Advanced Galaga Mechanics:**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*   **Boss Galaga & Capture Beam:**
 | 
					*   **Boss Galaga & Capture Beam:**
 | 
				
			||||||
    *   Create a "Boss" enemy type.
 | 
					    *   ~~Create a "Boss" enemy type.~~ **(DONE - Added to Enum, handled in matches)**
 | 
				
			||||||
    *   Implement the tractor beam attack (visual effect, player capture logic).
 | 
					    *   Implement the tractor beam attack (visual effect, player capture logic).
 | 
				
			||||||
    *   Player needs a `Captured` state.
 | 
					    *   Player needs a `Captured` state.
 | 
				
			||||||
    *   Logic for the Boss to return captured ships to the formation.
 | 
					    *   Logic for the Boss to return captured ships to the formation.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										113
									
								
								src/bullet.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/bullet.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::{Bullet, Enemy, EnemyBullet, EnemyType};
 | 
				
			||||||
 | 
					use crate::constants::{
 | 
				
			||||||
 | 
					    BULLET_ENEMY_COLLISION_THRESHOLD, BULLET_SIZE, BULLET_SPEED, ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD,
 | 
				
			||||||
 | 
					    ENEMY_BULLET_SIZE, ENEMY_BULLET_SPEED, WINDOW_HEIGHT,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::resources::{PlayerLives, PlayerRespawnTimer, Score};
 | 
				
			||||||
 | 
					use crate::game_state::AppState;
 | 
				
			||||||
 | 
					use crate::components::Player; // Needed for check_enemy_bullet_player_collisions
 | 
				
			||||||
 | 
					use crate::components::Invincible; // Needed for check_enemy_bullet_player_collisions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Player Bullet Systems ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn move_bullets(
 | 
				
			||||||
 | 
					    mut query: Query<(Entity, &mut Transform), With<Bullet>>,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (entity, mut transform) in query.iter_mut() {
 | 
				
			||||||
 | 
					        transform.translation.y += BULLET_SPEED * time.delta_seconds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if transform.translation.y > WINDOW_HEIGHT / 2.0 + BULLET_SIZE.y {
 | 
				
			||||||
 | 
					            commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn check_bullet_collisions(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    bullet_query: Query<(Entity, &Transform), With<Bullet>>,
 | 
				
			||||||
 | 
					    enemy_query: Query<(Entity, &Transform, &Enemy), With<Enemy>>, // Fetch Enemy component too
 | 
				
			||||||
 | 
					    mut score: ResMut<Score>,                                      // Add Score resource
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (bullet_entity, bullet_transform) in bullet_query.iter() {
 | 
				
			||||||
 | 
					        for (enemy_entity, enemy_transform, enemy) in enemy_query.iter() {
 | 
				
			||||||
 | 
					            // Get Enemy component
 | 
				
			||||||
 | 
					            let distance = bullet_transform
 | 
				
			||||||
 | 
					                .translation
 | 
				
			||||||
 | 
					                .distance(enemy_transform.translation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if distance < BULLET_ENEMY_COLLISION_THRESHOLD {
 | 
				
			||||||
 | 
					                commands.entity(bullet_entity).despawn();
 | 
				
			||||||
 | 
					                commands.entity(enemy_entity).despawn();
 | 
				
			||||||
 | 
					                // Increment score based on enemy type
 | 
				
			||||||
 | 
					                let points = match enemy.enemy_type {
 | 
				
			||||||
 | 
					                    EnemyType::Grunt => 100,
 | 
				
			||||||
 | 
					                    EnemyType::Boss => 100, // Same points as Grunt for now
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                score.value += points;
 | 
				
			||||||
 | 
					                println!("Enemy hit by player bullet! Score: {}", score.value); // Log score update
 | 
				
			||||||
 | 
					                break; // Bullet only hits one enemy
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Enemy Bullet Systems ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn move_enemy_bullets(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut query: Query<(Entity, &mut Transform), With<EnemyBullet>>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (entity, mut transform) in query.iter_mut() {
 | 
				
			||||||
 | 
					        transform.translation.y -= ENEMY_BULLET_SPEED * time.delta_seconds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Despawn if off screen (bottom)
 | 
				
			||||||
 | 
					        if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_BULLET_SIZE.y {
 | 
				
			||||||
 | 
					            commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Check collisions between enemy bullets and the player
 | 
				
			||||||
 | 
					pub fn check_enemy_bullet_player_collisions(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    mut lives: ResMut<PlayerLives>,
 | 
				
			||||||
 | 
					    mut respawn_timer: ResMut<PlayerRespawnTimer>,
 | 
				
			||||||
 | 
					    mut next_state: ResMut<NextState<AppState>>,
 | 
				
			||||||
 | 
					    // Player query matching the run_if condition (player exists and is vulnerable)
 | 
				
			||||||
 | 
					    player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>,
 | 
				
			||||||
 | 
					    enemy_bullet_query: Query<(Entity, &Transform), With<EnemyBullet>>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    if let Ok((player_entity, player_transform)) = player_query.get_single() {
 | 
				
			||||||
 | 
					        for (bullet_entity, bullet_transform) in enemy_bullet_query.iter() {
 | 
				
			||||||
 | 
					            let distance = player_transform
 | 
				
			||||||
 | 
					                .translation
 | 
				
			||||||
 | 
					                .distance(bullet_transform.translation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if distance < ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD {
 | 
				
			||||||
 | 
					                println!("Player hit by enemy bullet!");
 | 
				
			||||||
 | 
					                commands.entity(bullet_entity).despawn(); // Despawn bullet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                lives.count = lives.count.saturating_sub(1);
 | 
				
			||||||
 | 
					                println!("Lives remaining: {}", lives.count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                commands.entity(player_entity).despawn(); // Despawn player
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if lives.count > 0 {
 | 
				
			||||||
 | 
					                    respawn_timer.timer.reset();
 | 
				
			||||||
 | 
					                    respawn_timer.timer.unpause();
 | 
				
			||||||
 | 
					                    println!("Respawn timer started.");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    println!("GAME OVER!");
 | 
				
			||||||
 | 
					                    next_state.set(AppState::GameOver);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                // Break because the player can only be hit once per frame
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/components.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/components.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					use std::time::Duration; // Needed for Timer in Player
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Components ---
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct Player {
 | 
				
			||||||
 | 
					    pub speed: f32,
 | 
				
			||||||
 | 
					    pub shoot_cooldown: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct Bullet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)] // Added derive for common traits
 | 
				
			||||||
 | 
					pub enum EnemyType {
 | 
				
			||||||
 | 
					    Grunt,
 | 
				
			||||||
 | 
					    Boss, // Added Boss type
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct Enemy {
 | 
				
			||||||
 | 
					    pub enemy_type: EnemyType, // Add type field
 | 
				
			||||||
 | 
					    pub shoot_cooldown: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct Invincible {
 | 
				
			||||||
 | 
					    pub timer: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct FormationTarget {
 | 
				
			||||||
 | 
					    pub position: Vec3,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Enum defining different ways an enemy can attack
 | 
				
			||||||
 | 
					#[derive(Component, Clone, Copy, PartialEq, Debug)]
 | 
				
			||||||
 | 
					pub enum AttackPattern {
 | 
				
			||||||
 | 
					    SwoopDive,      // Original pattern: dive towards center, then off screen
 | 
				
			||||||
 | 
					    DirectDive,     // Dive straight down
 | 
				
			||||||
 | 
					    Kamikaze(Vec3), // Dive towards a specific target location (e.g., player's last known position) - Needs target Vec3
 | 
				
			||||||
 | 
					    // Add more patterns later (e.g., FigureEight, Looping)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component, Clone, PartialEq, Debug)] // Added Debug derive
 | 
				
			||||||
 | 
					pub enum EnemyState {
 | 
				
			||||||
 | 
					    Entering,                // Flying onto the screen towards formation target
 | 
				
			||||||
 | 
					    InFormation,             // Holding position in the formation
 | 
				
			||||||
 | 
					    Attacking(AttackPattern), // Diving towards the player using a specific pattern
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct EnemyBullet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Game Over UI Component (might move to ui.rs later if more UI exists)
 | 
				
			||||||
 | 
					#[derive(Component)]
 | 
				
			||||||
 | 
					pub struct GameOverUI;
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/constants.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/constants.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					use bevy::math::Vec2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Constants ---
 | 
				
			||||||
 | 
					pub const WINDOW_WIDTH: f32 = 600.0;
 | 
				
			||||||
 | 
					pub const WINDOW_HEIGHT: f32 = 800.0;
 | 
				
			||||||
 | 
					pub const PLAYER_SPEED: f32 = 300.0;
 | 
				
			||||||
 | 
					pub const BULLET_SPEED: f32 = 500.0;
 | 
				
			||||||
 | 
					pub const ENEMY_SPEED: f32 = 100.0;
 | 
				
			||||||
 | 
					pub const PLAYER_SIZE: Vec2 = Vec2::new(30.0, 30.0);
 | 
				
			||||||
 | 
					pub const ENEMY_SIZE: Vec2 = Vec2::new(40.0, 40.0);
 | 
				
			||||||
 | 
					pub const BULLET_SIZE: Vec2 = Vec2::new(5.0, 15.0);
 | 
				
			||||||
 | 
					// Player bullet
 | 
				
			||||||
 | 
					pub const ENEMY_BULLET_SIZE: Vec2 = Vec2::new(8.0, 8.0);
 | 
				
			||||||
 | 
					// Enemy bullet
 | 
				
			||||||
 | 
					pub const ENEMY_BULLET_SPEED: f32 = 300.0;
 | 
				
			||||||
 | 
					pub const ENEMY_SHOOT_INTERVAL: f32 = 1.5;
 | 
				
			||||||
 | 
					// Formation constants
 | 
				
			||||||
 | 
					const FORMATION_ROWS: usize = 4;
 | 
				
			||||||
 | 
					pub const FORMATION_COLS: usize = 8;
 | 
				
			||||||
 | 
					pub const FORMATION_ENEMY_COUNT: usize = FORMATION_ROWS * FORMATION_COLS;
 | 
				
			||||||
 | 
					pub const FORMATION_X_SPACING: f32 = 60.0;
 | 
				
			||||||
 | 
					pub const FORMATION_Y_SPACING: f32 = 50.0;
 | 
				
			||||||
 | 
					pub const FORMATION_BASE_Y: f32 = WINDOW_HEIGHT / 2.0 - 150.0;
 | 
				
			||||||
 | 
					// Top area for formation
 | 
				
			||||||
 | 
					pub const STARTING_LIVES: u32 = 3;
 | 
				
			||||||
 | 
					pub const PLAYER_RESPAWN_DELAY: f32 = 2.0;
 | 
				
			||||||
 | 
					pub const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0;
 | 
				
			||||||
 | 
					// Collision thresholds
 | 
				
			||||||
 | 
					pub const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) * 0.5;
 | 
				
			||||||
 | 
					// 22.5
 | 
				
			||||||
 | 
					pub const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5;
 | 
				
			||||||
 | 
					// 35.0
 | 
				
			||||||
 | 
					pub const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 = (ENEMY_BULLET_SIZE.x + PLAYER_SIZE.x) * 0.5;
 | 
				
			||||||
							
								
								
									
										368
									
								
								src/enemy.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										368
									
								
								src/enemy.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,368 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget};
 | 
				
			||||||
 | 
					use crate::constants::{ // Added WINDOW_WIDTH
 | 
				
			||||||
 | 
					    ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD, ENEMY_BULLET_SIZE, ENEMY_BULLET_SPEED,
 | 
				
			||||||
 | 
					    ENEMY_SHOOT_INTERVAL, ENEMY_SIZE, ENEMY_SPEED, FORMATION_BASE_Y, FORMATION_COLS,
 | 
				
			||||||
 | 
					    FORMATION_ENEMY_COUNT, FORMATION_X_SPACING, FORMATION_Y_SPACING, WINDOW_HEIGHT, WINDOW_WIDTH,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::resources::{
 | 
				
			||||||
 | 
					    AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives,
 | 
				
			||||||
 | 
					    PlayerRespawnTimer, StageConfigurations, // Make sure StageConfigurations is imported if not already
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::game_state::AppState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn spawn_enemies(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut timer: ResMut<EnemySpawnTimer>,
 | 
				
			||||||
 | 
					    mut stage: ResMut<CurrentStage>,
 | 
				
			||||||
 | 
					    mut formation_state: ResMut<FormationState>,
 | 
				
			||||||
 | 
					    stage_configs: Res<StageConfigurations>, // Use imported name
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Get current stage config, looping if stage number exceeds defined configs
 | 
				
			||||||
 | 
					    let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
 | 
				
			||||||
 | 
					    let current_config = &stage_configs.stages[config_index];
 | 
				
			||||||
 | 
					    let stage_enemy_count = current_config.enemy_count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tick the timer every frame
 | 
				
			||||||
 | 
					    timer.timer.tick(time.delta());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only spawn if we haven't spawned the full formation for this stage yet
 | 
				
			||||||
 | 
					    // AND the timer just finished this frame
 | 
				
			||||||
 | 
					    if formation_state.total_spawned_this_stage < stage_enemy_count
 | 
				
			||||||
 | 
					        && timer.timer.just_finished()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let slot_index = formation_state.next_slot_index;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Ensure slot_index is within bounds of the formation layout
 | 
				
			||||||
 | 
					        if slot_index >= current_config.formation_layout.positions.len() {
 | 
				
			||||||
 | 
					             println!("Warning: slot_index {} out of bounds for formation layout '{}' (size {})",
 | 
				
			||||||
 | 
					                slot_index, current_config.formation_layout.name, current_config.formation_layout.positions.len());
 | 
				
			||||||
 | 
					             // Optionally, reset the timer and skip spawning this frame, or handle differently
 | 
				
			||||||
 | 
					             timer.timer.reset();
 | 
				
			||||||
 | 
					             return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get target position from the stage's formation layout
 | 
				
			||||||
 | 
					        let target_pos = current_config.formation_layout.positions[slot_index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Spawn position (random at the top) - Corrected to use WINDOW_WIDTH
 | 
				
			||||||
 | 
					        let spawn_x = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - ENEMY_SIZE.x);
 | 
				
			||||||
 | 
					        let spawn_y = WINDOW_HEIGHT / 2.0 + ENEMY_SIZE.y / 2.0; // Spawn slightly above screen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Determine enemy type (can be randomized or based on stage config later)
 | 
				
			||||||
 | 
					        let enemy_type = EnemyType::Grunt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Determine sprite color based on type
 | 
				
			||||||
 | 
					        let sprite_color = match enemy_type {
 | 
				
			||||||
 | 
					            EnemyType::Grunt => Color::rgb(1.0, 0.2, 0.2),
 | 
				
			||||||
 | 
					            EnemyType::Boss => Color::rgb(0.8, 0.2, 1.0),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        commands.spawn((
 | 
				
			||||||
 | 
					            SpriteBundle {
 | 
				
			||||||
 | 
					                sprite: Sprite {
 | 
				
			||||||
 | 
					                    color: sprite_color,
 | 
				
			||||||
 | 
					                    custom_size: Some(ENEMY_SIZE),
 | 
				
			||||||
 | 
					                    ..default()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                transform: Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
 | 
				
			||||||
 | 
					                ..default()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Enemy {
 | 
				
			||||||
 | 
					                enemy_type,
 | 
				
			||||||
 | 
					                // Use shoot interval from stage config
 | 
				
			||||||
 | 
					                shoot_cooldown: Timer::new(
 | 
				
			||||||
 | 
					                    Duration::from_secs_f32(current_config.enemy_shoot_interval),
 | 
				
			||||||
 | 
					                    TimerMode::Once,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            FormationTarget { position: target_pos },
 | 
				
			||||||
 | 
					            EnemyState::Entering,
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use stage_enemy_count for cycling index
 | 
				
			||||||
 | 
					        formation_state.next_slot_index = (slot_index + 1) % stage_enemy_count;
 | 
				
			||||||
 | 
					        formation_state.total_spawned_this_stage += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mark that we are now waiting for enemies to be cleared
 | 
				
			||||||
 | 
					        stage.waiting_for_clear = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Reset timer only if we are still spawning more enemies for the formation
 | 
				
			||||||
 | 
					        if formation_state.total_spawned_this_stage < stage_enemy_count {
 | 
				
			||||||
 | 
					            timer.timer.reset();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            println!(
 | 
				
			||||||
 | 
					                "Full formation ({}) spawned for Stage {}",
 | 
				
			||||||
 | 
					                current_config.formation_layout.name, stage.number
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn move_enemies(
 | 
				
			||||||
 | 
					    mut entering_query: Query<
 | 
				
			||||||
 | 
					        (Entity, &mut Transform, &FormationTarget, &mut EnemyState),
 | 
				
			||||||
 | 
					        (With<Enemy>, With<FormationTarget>),
 | 
				
			||||||
 | 
					    >,
 | 
				
			||||||
 | 
					    mut attacking_query: Query<
 | 
				
			||||||
 | 
					        (Entity, &mut Transform, &EnemyState, &Enemy), // Add &Enemy here
 | 
				
			||||||
 | 
					        (With<Enemy>, Without<FormationTarget>),
 | 
				
			||||||
 | 
					    >, // Query potential attackers
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    stage: Res<CurrentStage>,
 | 
				
			||||||
 | 
					    stage_configs: Res<StageConfigurations>, // Add stage configurations
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Get current stage config for speed multiplier
 | 
				
			||||||
 | 
					    let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
 | 
				
			||||||
 | 
					    let current_config = &stage_configs.stages[config_index];
 | 
				
			||||||
 | 
					    let speed_multiplier = current_config.enemy_speed_multiplier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Calculate speeds for this frame
 | 
				
			||||||
 | 
					    let base_speed = ENEMY_SPEED * speed_multiplier;
 | 
				
			||||||
 | 
					    let attack_speed = base_speed * 1.5; // Attackers are faster
 | 
				
			||||||
 | 
					    let arrival_threshold = base_speed * time.delta_seconds() * 1.1; // Threshold for reaching formation target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // --- Handle Entering Enemies ---
 | 
				
			||||||
 | 
					    for (entity, mut transform, target, mut state) in entering_query.iter_mut() {
 | 
				
			||||||
 | 
					        // Ensure we only process Entering state here, though query filters it mostly
 | 
				
			||||||
 | 
					        if *state == EnemyState::Entering {
 | 
				
			||||||
 | 
					            let current_pos = transform.translation;
 | 
				
			||||||
 | 
					            let target_pos = target.position;
 | 
				
			||||||
 | 
					            let direction = target_pos - current_pos;
 | 
				
			||||||
 | 
					            let distance = direction.length();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if distance < arrival_threshold {
 | 
				
			||||||
 | 
					                // Arrived at target
 | 
				
			||||||
 | 
					                transform.translation = target_pos;
 | 
				
			||||||
 | 
					                commands.entity(entity).remove::<FormationTarget>(); // Remove target component
 | 
				
			||||||
 | 
					                *state = EnemyState::InFormation; // Change state
 | 
				
			||||||
 | 
					                println!(
 | 
				
			||||||
 | 
					                    "Enemy {:?} reached formation target and is now InFormation.",
 | 
				
			||||||
 | 
					                    entity
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Move towards target using base_speed
 | 
				
			||||||
 | 
					                let move_delta = direction.normalize() * base_speed * time.delta_seconds();
 | 
				
			||||||
 | 
					                transform.translation += move_delta;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // --- Handle Attacking Enemies ---
 | 
				
			||||||
 | 
					    // Note: attack_speed calculated above using multiplier
 | 
				
			||||||
 | 
					    for (entity, mut transform, state, _enemy) in attacking_query.iter_mut() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Match on the specific attack pattern using matches! and then get the pattern
 | 
				
			||||||
 | 
					        if matches!(state, EnemyState::Attacking(_)) {
 | 
				
			||||||
 | 
					             if let EnemyState::Attacking(pattern) = state { // Get the pattern safely now
 | 
				
			||||||
 | 
					            let delta_seconds = time.delta_seconds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match pattern {
 | 
				
			||||||
 | 
					                AttackPattern::SwoopDive => {
 | 
				
			||||||
 | 
					                    // Original Swooping Dive Logic
 | 
				
			||||||
 | 
					                    let vertical_movement = attack_speed * delta_seconds;
 | 
				
			||||||
 | 
					                    let horizontal_speed_factor = 0.5;
 | 
				
			||||||
 | 
					                    let horizontal_movement = if transform.translation.x < 0.0 {
 | 
				
			||||||
 | 
					                        attack_speed * horizontal_speed_factor * delta_seconds
 | 
				
			||||||
 | 
					                    } else if transform.translation.x > 0.0 {
 | 
				
			||||||
 | 
					                        -attack_speed * horizontal_speed_factor * delta_seconds
 | 
				
			||||||
 | 
					                    } else { 0.0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    transform.translation.y -= vertical_movement;
 | 
				
			||||||
 | 
					                    transform.translation.x += horizontal_movement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Prevent overshooting center
 | 
				
			||||||
 | 
					                    if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) ||
 | 
				
			||||||
 | 
					                       (transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) {
 | 
				
			||||||
 | 
					                        transform.translation.x = 0.0;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                AttackPattern::DirectDive => {
 | 
				
			||||||
 | 
					                    // Move straight down
 | 
				
			||||||
 | 
					                    transform.translation.y -= attack_speed * delta_seconds;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                AttackPattern::Kamikaze(target_pos) => {
 | 
				
			||||||
 | 
					                    // Move towards the target position
 | 
				
			||||||
 | 
					                    let direction = *target_pos - transform.translation;
 | 
				
			||||||
 | 
					                    let distance = direction.length();
 | 
				
			||||||
 | 
					                    let kamikaze_threshold = attack_speed * delta_seconds * 1.1; // Threshold to stop near target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if distance > kamikaze_threshold {
 | 
				
			||||||
 | 
					                        let move_delta = direction.normalize() * attack_speed * delta_seconds;
 | 
				
			||||||
 | 
					                        transform.translation += move_delta;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        // Optionally stop or continue past target - for now, just stop moving towards it
 | 
				
			||||||
 | 
					                        // Could also despawn here if desired upon reaching target
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                // Add cases for other patterns here
 | 
				
			||||||
 | 
					             } // Close inner if let
 | 
				
			||||||
 | 
					            } // Closes match enemy.enemy_type
 | 
				
			||||||
 | 
					        } // Closes if *state == EnemyState::Attacking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Despawn if off screen (This should be inside the loop)
 | 
				
			||||||
 | 
					        if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
 | 
				
			||||||
 | 
					            println!(
 | 
				
			||||||
 | 
					                "Despawning enemy {:?} that went off screen.", // Generic message as it could be InFormation or Attacking
 | 
				
			||||||
 | 
					                entity
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					            // TODO: Later, attacking enemies might return to formation or loop
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } // Closes for loop
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// System to check if all spawned enemies have reached their formation position
 | 
				
			||||||
 | 
					pub fn check_formation_complete(
 | 
				
			||||||
 | 
					    mut formation_state: ResMut<FormationState>,
 | 
				
			||||||
 | 
					    enemy_query: Query<&EnemyState, With<Enemy>>,
 | 
				
			||||||
 | 
					    mut attack_dive_timer: ResMut<AttackDiveTimer>,
 | 
				
			||||||
 | 
					    stage: Res<CurrentStage>, // Need current stage
 | 
				
			||||||
 | 
					    stage_configs: Res<StageConfigurations>, // Need stage configs
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Only run the check if the formation isn't already marked as complete
 | 
				
			||||||
 | 
					    if !formation_state.formation_complete {
 | 
				
			||||||
 | 
					        // Get current stage config
 | 
				
			||||||
 | 
					        let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
 | 
				
			||||||
 | 
					        let current_config = &stage_configs.stages[config_index];
 | 
				
			||||||
 | 
					        let stage_enemy_count = current_config.enemy_count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if all enemies for *this stage* have been spawned
 | 
				
			||||||
 | 
					        if formation_state.total_spawned_this_stage == stage_enemy_count {
 | 
				
			||||||
 | 
					            // Check if any enemies are still in the Entering state
 | 
				
			||||||
 | 
					            let mut any_entering = false;
 | 
				
			||||||
 | 
					            for state in enemy_query.iter() {
 | 
				
			||||||
 | 
					                // Use matches! macro for safety
 | 
				
			||||||
 | 
					                if matches!(state, EnemyState::Entering) {
 | 
				
			||||||
 | 
					                    any_entering = true;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If all spawned and none are entering, formation is complete
 | 
				
			||||||
 | 
					            if !any_entering {
 | 
				
			||||||
 | 
					                 println!(
 | 
				
			||||||
 | 
					                    "Formation complete for Stage {}! Setting attack timer. (Spawned={})",
 | 
				
			||||||
 | 
					                    stage.number, formation_state.total_spawned_this_stage
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                formation_state.formation_complete = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Set timer duration based on stage config
 | 
				
			||||||
 | 
					                let dive_interval = Duration::from_secs_f32(current_config.attack_dive_interval);
 | 
				
			||||||
 | 
					                attack_dive_timer.timer.set_duration(dive_interval);
 | 
				
			||||||
 | 
					                attack_dive_timer.timer.reset();
 | 
				
			||||||
 | 
					                attack_dive_timer.timer.unpause();
 | 
				
			||||||
 | 
					                println!("Attack timer set to {:?} duration, unpaused and reset.", dive_interval);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::AttackPattern; // Import the new enum
 | 
				
			||||||
 | 
					use crate::components::Player; // Import Player for Kamikaze target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn trigger_attack_dives(
 | 
				
			||||||
 | 
					    mut timer: ResMut<AttackDiveTimer>,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut enemy_query: Query<(Entity, &mut EnemyState), With<Enemy>>, // Renamed for clarity
 | 
				
			||||||
 | 
					    formation_state: Res<FormationState>,
 | 
				
			||||||
 | 
					    stage: Res<CurrentStage>, // Need current stage
 | 
				
			||||||
 | 
					    stage_configs: Res<StageConfigurations>, // Need stage configs
 | 
				
			||||||
 | 
					    player_query: Query<&Transform, With<Player>>, // Need player position for Kamikaze
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    timer.timer.tick(time.delta());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only proceed if the timer finished AND the formation is complete
 | 
				
			||||||
 | 
					    if timer.timer.just_finished() && formation_state.formation_complete {
 | 
				
			||||||
 | 
					        // Find all enemies currently in formation
 | 
				
			||||||
 | 
					        let mut available_enemies: Vec<Entity> = Vec::new();
 | 
				
			||||||
 | 
					        // Get the current stage config
 | 
				
			||||||
 | 
					        let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
 | 
				
			||||||
 | 
					        let current_config = &stage_configs.stages[config_index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Find all enemies currently in formation
 | 
				
			||||||
 | 
					        let mut available_enemies: Vec<Entity> = Vec::new();
 | 
				
			||||||
 | 
					        for (entity, state) in enemy_query.iter() {
 | 
				
			||||||
 | 
					            // Check the state correctly
 | 
				
			||||||
 | 
					            if matches!(state, EnemyState::InFormation) {
 | 
				
			||||||
 | 
					                available_enemies.push(entity);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // If there are enemies available, pick one randomly
 | 
				
			||||||
 | 
					        if !available_enemies.is_empty() && !current_config.attack_patterns.is_empty() {
 | 
				
			||||||
 | 
					            let random_index = fastrand::usize(..available_enemies.len());
 | 
				
			||||||
 | 
					            let chosen_entity = available_enemies[random_index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Select a random attack pattern for this stage
 | 
				
			||||||
 | 
					            let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
 | 
				
			||||||
 | 
					            let mut selected_pattern = current_config.attack_patterns[pattern_index]; // Copy the pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If Kamikaze, get player position (if player exists)
 | 
				
			||||||
 | 
					            if let AttackPattern::Kamikaze(_) = selected_pattern {
 | 
				
			||||||
 | 
					                if let Ok(player_transform) = player_query.get_single() {
 | 
				
			||||||
 | 
					                     selected_pattern = AttackPattern::Kamikaze(player_transform.translation);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Fallback if player doesn't exist (e.g., just died)
 | 
				
			||||||
 | 
					                    selected_pattern = AttackPattern::DirectDive; // Or SwoopDive
 | 
				
			||||||
 | 
					                    println!("Kamikaze target not found, falling back to DirectDive");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get the chosen enemy's state mutably and change it
 | 
				
			||||||
 | 
					            if let Ok((_, mut state)) = enemy_query.get_mut(chosen_entity) {
 | 
				
			||||||
 | 
					                println!("Enemy {:?} starting attack dive with pattern {:?}!", chosen_entity, selected_pattern);
 | 
				
			||||||
 | 
					                *state = EnemyState::Attacking(selected_pattern); // Set state with pattern
 | 
				
			||||||
 | 
					                // Timer duration is handled elsewhere (e.g., check_formation_complete)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn enemy_shoot(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    // Query attacking enemies: need their transform and mutable Enemy component for the timer
 | 
				
			||||||
 | 
					    mut enemy_query: Query<(&Transform, &mut Enemy, &EnemyState), Without<FormationTarget>>, // Query remains the same
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (transform, mut enemy, state) in enemy_query.iter_mut() {
 | 
				
			||||||
 | 
					        // Only shoot if in any Attacking state (pattern doesn't matter for shooting)
 | 
				
			||||||
 | 
					        if matches!(state, EnemyState::Attacking(_)) { // Use matches! macro
 | 
				
			||||||
 | 
					            enemy.shoot_cooldown.tick(time.delta());
 | 
				
			||||||
 | 
					            if enemy.shoot_cooldown.finished() {
 | 
				
			||||||
 | 
					                // println!("Enemy {:?} firing!", transform.translation); // Less verbose logging
 | 
				
			||||||
 | 
					                let bullet_start_pos = transform.translation
 | 
				
			||||||
 | 
					                    - Vec3::Y * (ENEMY_SIZE.y / 2.0 + ENEMY_BULLET_SIZE.y / 2.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                commands.spawn((
 | 
				
			||||||
 | 
					                    SpriteBundle {
 | 
				
			||||||
 | 
					                        sprite: Sprite {
 | 
				
			||||||
 | 
					                            color: Color::rgb(1.0, 0.5, 0.5),
 | 
				
			||||||
 | 
					                            custom_size: Some(ENEMY_BULLET_SIZE),
 | 
				
			||||||
 | 
					                            ..default()
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        transform: Transform::from_translation(bullet_start_pos),
 | 
				
			||||||
 | 
					                        ..default()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    EnemyBullet,
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Reset the timer for the next shot
 | 
				
			||||||
 | 
					                enemy.shoot_cooldown.reset();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Check collisions between enemy bullets and the player
 | 
				
			||||||
 | 
					// Moved to player.rs as it affects player state directly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New run condition: Check if the formation is complete
 | 
				
			||||||
 | 
					pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
 | 
				
			||||||
 | 
					    formation_state.formation_complete
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										68
									
								
								src/game_state.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/game_state.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					use crate::components::{Bullet, Enemy, GameOverUI}; // Import necessary components
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Game States ---
 | 
				
			||||||
 | 
					#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
 | 
				
			||||||
 | 
					pub enum AppState {
 | 
				
			||||||
 | 
					    #[default]
 | 
				
			||||||
 | 
					    Playing,
 | 
				
			||||||
 | 
					    GameOver,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Game Over UI ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub 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 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cleanup system when exiting the Playing state
 | 
				
			||||||
 | 
					pub fn cleanup_game_entities(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    bullet_query: Query<Entity, With<Bullet>>,
 | 
				
			||||||
 | 
					    enemy_bullet_query: Query<Entity, With<crate::components::EnemyBullet>>, // Need to specify crate::components
 | 
				
			||||||
 | 
					    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_bullet_query.iter() { // Also despawn enemy bullets
 | 
				
			||||||
 | 
					        commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for entity in enemy_query.iter() {
 | 
				
			||||||
 | 
					        commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // for entity in player_query.iter() {
 | 
				
			||||||
 | 
					    //     commands.entity(entity).despawn();
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										505
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										505
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
					@ -1,441 +1,136 @@
 | 
				
			||||||
use bevy::prelude::*; // Removed unused AppExit
 | 
					use bevy::prelude::*;
 | 
				
			||||||
use std::time::Duration;
 | 
					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;
 | 
					 | 
				
			||||||
const PLAYER_SPEED: f32 = 300.0;
 | 
					 | 
				
			||||||
const BULLET_SPEED: f32 = 500.0;
 | 
					 | 
				
			||||||
const ENEMY_SPEED: f32 = 100.0;
 | 
					 | 
				
			||||||
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);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const STARTING_LIVES: u32 = 3;
 | 
					pub mod components;
 | 
				
			||||||
const PLAYER_RESPAWN_DELAY: f32 = 2.0;
 | 
					pub mod constants;
 | 
				
			||||||
const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0;
 | 
					pub mod resources;
 | 
				
			||||||
 | 
					pub mod game_state;
 | 
				
			||||||
 | 
					pub mod player;
 | 
				
			||||||
 | 
					pub mod enemy;
 | 
				
			||||||
 | 
					pub mod bullet;
 | 
				
			||||||
 | 
					pub mod stage;
 | 
				
			||||||
 | 
					pub mod systems;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Collision thresholds
 | 
					use constants::{PLAYER_RESPAWN_DELAY, STARTING_LIVES, WINDOW_HEIGHT, WINDOW_WIDTH};
 | 
				
			||||||
const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) * 0.5; // 22.5
 | 
					use resources::{ // Added StageConfigurations
 | 
				
			||||||
const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5; // 35.0
 | 
					    AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives,
 | 
				
			||||||
 | 
					    PlayerRespawnTimer, Score, StageConfigurations,
 | 
				
			||||||
// --- Components ---
 | 
					};
 | 
				
			||||||
#[derive(Component)]
 | 
					use game_state::{
 | 
				
			||||||
struct Player {
 | 
					    cleanup_game_entities, cleanup_game_over_ui, setup_game_over_ui, AppState,
 | 
				
			||||||
    speed: f32,
 | 
					};
 | 
				
			||||||
    shoot_cooldown: Timer,
 | 
					use player::{
 | 
				
			||||||
}
 | 
					    check_player_enemy_collisions, manage_invincibility, move_player, player_shoot,
 | 
				
			||||||
 | 
					    respawn_player,
 | 
				
			||||||
#[derive(Component)]
 | 
					};
 | 
				
			||||||
struct Bullet;
 | 
					use enemy::{
 | 
				
			||||||
 | 
					    check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies,
 | 
				
			||||||
#[derive(Component)]
 | 
					    trigger_attack_dives,
 | 
				
			||||||
struct Enemy;
 | 
					};
 | 
				
			||||||
 | 
					use bullet::{
 | 
				
			||||||
#[derive(Component)]
 | 
					    check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets,
 | 
				
			||||||
struct Invincible {
 | 
					    move_enemy_bullets,
 | 
				
			||||||
    timer: Timer,
 | 
					};
 | 
				
			||||||
}
 | 
					use stage::check_stage_clear;
 | 
				
			||||||
 | 
					use systems::{player_exists, player_vulnerable, setup, should_respawn_player, update_window_title};
 | 
				
			||||||
// --- Resources ---
 | 
					 | 
				
			||||||
#[derive(Resource)]
 | 
					 | 
				
			||||||
struct EnemySpawnTimer {
 | 
					 | 
				
			||||||
    timer: Timer,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Resource)]
 | 
					 | 
				
			||||||
struct PlayerLives {
 | 
					 | 
				
			||||||
    count: u32,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Resource)]
 | 
					 | 
				
			||||||
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() {
 | 
					fn main() {
 | 
				
			||||||
    App::new() // Start App builder
 | 
					    App::new()
 | 
				
			||||||
        .init_state::<AppState>() // Initialize the AppState *after* App::new()
 | 
					        .init_state::<AppState>() // Initialize the AppState
 | 
				
			||||||
        .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
 | 
					        .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
 | 
				
			||||||
        .add_plugins(DefaultPlugins.set(WindowPlugin {
 | 
					        .add_plugins(DefaultPlugins.set(WindowPlugin {
 | 
				
			||||||
            primary_window: Some(Window {
 | 
					            primary_window: Some(Window {
 | 
				
			||||||
                title: "Galaga :: Lives: 3".into(), // Initial title
 | 
					                title: "Galaga :: Stage: 1 Lives: 3 Score: 0".into(), // Initial title
 | 
				
			||||||
                resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
 | 
					                resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
 | 
				
			||||||
                ..default()
 | 
					                ..default()
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            ..default()
 | 
					            ..default()
 | 
				
			||||||
        }))
 | 
					        }))
 | 
				
			||||||
        // Add Resources
 | 
					        // Add Resources
 | 
				
			||||||
        .insert_resource(PlayerLives { count: STARTING_LIVES })
 | 
					        .insert_resource(PlayerLives {
 | 
				
			||||||
 | 
					            count: STARTING_LIVES,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .insert_resource(PlayerRespawnTimer {
 | 
					        .insert_resource(PlayerRespawnTimer {
 | 
				
			||||||
             // Start paused and finished
 | 
					            timer: Timer::new(
 | 
				
			||||||
            timer: Timer::new(Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), TimerMode::Once)
 | 
					                Duration::from_secs_f32(PLAYER_RESPAWN_DELAY),
 | 
				
			||||||
 | 
					                TimerMode::Once,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .insert_resource(EnemySpawnTimer {
 | 
					        .insert_resource(EnemySpawnTimer {
 | 
				
			||||||
            timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating),
 | 
					            timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating),
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					        .insert_resource(Score { value: 0 })
 | 
				
			||||||
 | 
					        .insert_resource(CurrentStage {
 | 
				
			||||||
 | 
					            number: 1,
 | 
				
			||||||
 | 
					            waiting_for_clear: false,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .insert_resource(FormationState {
 | 
				
			||||||
 | 
					            next_slot_index: 0,
 | 
				
			||||||
 | 
					            total_spawned_this_stage: 0,
 | 
				
			||||||
 | 
					            formation_complete: false,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .insert_resource(StageConfigurations::default()) // Add stage configurations
 | 
				
			||||||
 | 
					        .insert_resource(AttackDiveTimer {
 | 
				
			||||||
 | 
					            timer: { // Correctly assign the block expression to the timer field
 | 
				
			||||||
 | 
					                let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating); // Default duration, will be overwritten by stage config
 | 
				
			||||||
 | 
					                timer.pause(); // Start paused
 | 
				
			||||||
 | 
					                timer
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .insert_resource(AttackDiveTimer {
 | 
				
			||||||
 | 
					            timer: {
 | 
				
			||||||
 | 
					                let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating);
 | 
				
			||||||
 | 
					                timer.pause(); // Start paused
 | 
				
			||||||
 | 
					                timer
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        // Add Systems
 | 
					        // Add Systems
 | 
				
			||||||
        .add_systems(Startup, setup)
 | 
					        .add_systems(Startup, setup)
 | 
				
			||||||
        // Systems running only when Playing
 | 
					        // Systems running only when Playing
 | 
				
			||||||
        .add_systems(Update, (
 | 
					        .add_systems(
 | 
				
			||||||
 | 
					            Update,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                // Player systems
 | 
				
			||||||
                move_player,
 | 
					                move_player,
 | 
				
			||||||
                player_shoot.run_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),
 | 
					                check_player_enemy_collisions.run_if(player_vulnerable),
 | 
				
			||||||
                respawn_player.run_if(should_respawn_player),
 | 
					                respawn_player.run_if(should_respawn_player),
 | 
				
			||||||
                manage_invincibility,
 | 
					                manage_invincibility,
 | 
				
			||||||
            // Game Over check is now implicit in check_player_enemy_collisions
 | 
					                // Bullet systems
 | 
				
			||||||
        ).run_if(in_state(AppState::Playing)))
 | 
					                move_bullets,
 | 
				
			||||||
 | 
					                check_bullet_collisions,
 | 
				
			||||||
 | 
					                move_enemy_bullets,
 | 
				
			||||||
 | 
					                check_enemy_bullet_player_collisions.run_if(player_vulnerable),
 | 
				
			||||||
 | 
					                // Enemy systems
 | 
				
			||||||
 | 
					                spawn_enemies,
 | 
				
			||||||
 | 
					                move_enemies,
 | 
				
			||||||
 | 
					                enemy_shoot, // Consider run_if attacking state? (Handled internally for now)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .run_if(in_state(AppState::Playing)),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        // Formation/Attack/Stage systems (some need specific ordering or run conditions)
 | 
				
			||||||
 | 
					        .add_systems(
 | 
				
			||||||
 | 
					            Update,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                check_formation_complete,
 | 
				
			||||||
 | 
					                trigger_attack_dives.run_if(is_formation_complete), // Run only when formation is complete
 | 
				
			||||||
 | 
					                check_stage_clear, // Uses world access, run separately
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .chain() // Ensure these run in order if needed, check_formation first
 | 
				
			||||||
 | 
					                .run_if(in_state(AppState::Playing)),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        // Systems running regardless of state (or managing state transitions)
 | 
					        // Systems running regardless of state (or managing state transitions)
 | 
				
			||||||
        .add_systems(Update, (
 | 
					        .add_systems(
 | 
				
			||||||
 | 
					            Update,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
                update_window_title, // Keep title updated
 | 
					                update_window_title, // Keep title updated
 | 
				
			||||||
            // Add system to check for restart input in GameOver state later
 | 
					                // TODO: Add system to check for restart input in GameOver state
 | 
				
			||||||
                bevy::window::close_on_esc, // Allow closing anytime
 | 
					                bevy::window::close_on_esc, // Allow closing anytime
 | 
				
			||||||
        ))
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        // Systems for entering/exiting states
 | 
					        // Systems for entering/exiting states
 | 
				
			||||||
        .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui)
 | 
					        .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui)
 | 
				
			||||||
        .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui)
 | 
					        .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui)
 | 
				
			||||||
        .add_systems(OnExit(AppState::Playing), cleanup_game_entities) // Cleanup when leaving Playing
 | 
					        .add_systems(OnExit(AppState::Playing), cleanup_game_entities) // Cleanup when leaving Playing
 | 
				
			||||||
        .run();
 | 
					        .run();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Run Conditions ---
 | 
					 | 
				
			||||||
fn player_exists(query: Query<&Player>) -> bool {
 | 
					 | 
				
			||||||
    !query.is_empty()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn player_vulnerable(query: Query<&Player, Without<Invincible>>) -> bool {
 | 
					 | 
				
			||||||
    !query.is_empty()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn should_respawn_player(
 | 
					 | 
				
			||||||
    lives: Res<PlayerLives>,
 | 
					 | 
				
			||||||
    player_query: Query<&Player>,
 | 
					 | 
				
			||||||
) -> bool {
 | 
					 | 
				
			||||||
    player_query.is_empty() && lives.count > 0
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Systems ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn setup(mut commands: Commands) {
 | 
					 | 
				
			||||||
    commands.spawn(Camera2dBundle::default());
 | 
					 | 
				
			||||||
    spawn_player_ship(&mut commands); // Use helper function
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Helper to spawn player (used in setup and respawn)
 | 
					 | 
				
			||||||
fn spawn_player_ship(commands: &mut Commands) {
 | 
					 | 
				
			||||||
     commands.spawn((
 | 
					 | 
				
			||||||
        SpriteBundle {
 | 
					 | 
				
			||||||
            sprite: Sprite {
 | 
					 | 
				
			||||||
                color: Color::rgb(0.0, 0.5, 1.0),
 | 
					 | 
				
			||||||
                custom_size: Some(PLAYER_SIZE),
 | 
					 | 
				
			||||||
                ..default()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            transform: Transform::from_translation(Vec3::new(0.0, -WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0, 0.0)),
 | 
					 | 
				
			||||||
            ..default()
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        Player {
 | 
					 | 
				
			||||||
            speed: PLAYER_SPEED,
 | 
					 | 
				
			||||||
            shoot_cooldown: Timer::new(Duration::from_secs_f32(0.3), TimerMode::Once),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        // Player starts invincible for a short time
 | 
					 | 
				
			||||||
        Invincible {
 | 
					 | 
				
			||||||
            timer: Timer::new(Duration::from_secs_f32(PLAYER_INVINCIBILITY_DURATION), TimerMode::Once)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ));
 | 
					 | 
				
			||||||
    println!("Player spawned!");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn move_player(
 | 
					 | 
				
			||||||
    keyboard_input: Res<ButtonInput<KeyCode>>,
 | 
					 | 
				
			||||||
    mut query: Query<(&mut Transform, &Player)>,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    // Using get_single_mut handles the case where player might not exist yet (or was just destroyed)
 | 
					 | 
				
			||||||
    if let Ok((mut transform, player)) = query.get_single_mut() {
 | 
					 | 
				
			||||||
        let mut direction = 0.0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
 | 
					 | 
				
			||||||
            direction -= 1.0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
 | 
					 | 
				
			||||||
            direction += 1.0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        transform.translation.x += direction * player.speed * time.delta_seconds();
 | 
					 | 
				
			||||||
        let half_player_width = PLAYER_SIZE.x / 2.0;
 | 
					 | 
				
			||||||
        transform.translation.x = transform.translation.x.clamp(-WINDOW_WIDTH / 2.0 + half_player_width, WINDOW_WIDTH / 2.0 - half_player_width);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn player_shoot(
 | 
					 | 
				
			||||||
    keyboard_input: Res<ButtonInput<KeyCode>>,
 | 
					 | 
				
			||||||
    mut query: Query<(&Transform, &mut Player)>, // Should only run if player exists due to run_if
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
   if let Ok((player_transform, mut player)) = query.get_single_mut() {
 | 
					 | 
				
			||||||
        player.shoot_cooldown.tick(time.delta());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (keyboard_input.just_pressed(KeyCode::Space) || keyboard_input.just_pressed(KeyCode::ArrowUp))
 | 
					 | 
				
			||||||
            && player.shoot_cooldown.finished() {
 | 
					 | 
				
			||||||
            player.shoot_cooldown.reset();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let bullet_start_pos = player_transform.translation + Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            commands.spawn((
 | 
					 | 
				
			||||||
                SpriteBundle {
 | 
					 | 
				
			||||||
                    sprite: Sprite {
 | 
					 | 
				
			||||||
                        color: Color::rgb(1.0, 1.0, 1.0),
 | 
					 | 
				
			||||||
                        custom_size: Some(BULLET_SIZE),
 | 
					 | 
				
			||||||
                        ..default()
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    transform: Transform::from_translation(bullet_start_pos),
 | 
					 | 
				
			||||||
                    ..default()
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                Bullet,
 | 
					 | 
				
			||||||
            ));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn move_bullets(
 | 
					 | 
				
			||||||
    mut query: Query<(Entity, &mut Transform), With<Bullet>>,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    for (entity, mut transform) in query.iter_mut() {
 | 
					 | 
				
			||||||
        transform.translation.y += BULLET_SPEED * time.delta_seconds();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if transform.translation.y > WINDOW_HEIGHT / 2.0 + BULLET_SIZE.y {
 | 
					 | 
				
			||||||
            commands.entity(entity).despawn();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn spawn_enemies(
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
    mut timer: ResMut<EnemySpawnTimer>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    timer.timer.tick(time.delta());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if timer.timer.just_finished() {
 | 
					 | 
				
			||||||
        let x_pos = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - ENEMY_SIZE.x);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        commands.spawn((
 | 
					 | 
				
			||||||
            SpriteBundle {
 | 
					 | 
				
			||||||
                sprite: Sprite {
 | 
					 | 
				
			||||||
                    color: Color::rgb(1.0, 0.2, 0.2),
 | 
					 | 
				
			||||||
                    custom_size: Some(ENEMY_SIZE),
 | 
					 | 
				
			||||||
                    ..default()
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                transform: Transform::from_translation(Vec3::new(
 | 
					 | 
				
			||||||
                    x_pos,
 | 
					 | 
				
			||||||
                    WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y / 2.0,
 | 
					 | 
				
			||||||
                    0.0
 | 
					 | 
				
			||||||
                )),
 | 
					 | 
				
			||||||
                ..default()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Enemy,
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn move_enemies(
 | 
					 | 
				
			||||||
    mut query: Query<(Entity, &mut Transform), With<Enemy>>,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    for (entity, mut transform) in query.iter_mut() {
 | 
					 | 
				
			||||||
        transform.translation.y -= ENEMY_SPEED * time.delta_seconds();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
 | 
					 | 
				
			||||||
            commands.entity(entity).despawn();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn check_bullet_collisions(
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
    bullet_query: Query<(Entity, &Transform), With<Bullet>>,
 | 
					 | 
				
			||||||
    enemy_query: Query<(Entity, &Transform), With<Enemy>>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    for (bullet_entity, bullet_transform) in bullet_query.iter() {
 | 
					 | 
				
			||||||
        for (enemy_entity, enemy_transform) in enemy_query.iter() {
 | 
					 | 
				
			||||||
            let distance = bullet_transform.translation.distance(enemy_transform.translation);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if distance < BULLET_ENEMY_COLLISION_THRESHOLD {
 | 
					 | 
				
			||||||
                commands.entity(bullet_entity).despawn();
 | 
					 | 
				
			||||||
                commands.entity(enemy_entity).despawn();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Modified Collision Check for Player
 | 
					 | 
				
			||||||
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>>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    // This system only runs if player exists and is not invincible, due to run_if
 | 
					 | 
				
			||||||
    if let Ok((player_entity, player_transform)) = player_query.get_single() {
 | 
					 | 
				
			||||||
        for (enemy_entity, enemy_transform) in enemy_query.iter() {
 | 
					 | 
				
			||||||
            let distance = player_transform.translation.distance(enemy_transform.translation);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if distance < PLAYER_ENEMY_COLLISION_THRESHOLD {
 | 
					 | 
				
			||||||
                println!("Player hit!");
 | 
					 | 
				
			||||||
                commands.entity(enemy_entity).despawn(); // Despawn enemy
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                lives.count = lives.count.saturating_sub(1); // Decrement lives safely
 | 
					 | 
				
			||||||
                println!("Lives remaining: {}", lives.count);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                commands.entity(player_entity).despawn(); // Despawn player
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if lives.count > 0 {
 | 
					 | 
				
			||||||
                    // Start the respawn timer
 | 
					 | 
				
			||||||
                    respawn_timer.timer.reset();
 | 
					 | 
				
			||||||
                    respawn_timer.timer.unpause();
 | 
					 | 
				
			||||||
                    println!("Respawn timer started.");
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    println!("GAME OVER!");
 | 
					 | 
				
			||||||
                    next_state.set(AppState::GameOver); // Transition to GameOver state
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                // Important: Break after handling one collision per frame for the player
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New System: Respawn Player
 | 
					 | 
				
			||||||
fn respawn_player(
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
    mut respawn_timer: ResMut<PlayerRespawnTimer>,
 | 
					 | 
				
			||||||
    // No player query needed here due to run_if condition
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    // Tick the timer only if it's actually running
 | 
					 | 
				
			||||||
    if respawn_timer.timer.tick(time.delta()).just_finished() {
 | 
					 | 
				
			||||||
         println!("Respawn timer finished. Spawning player.");
 | 
					 | 
				
			||||||
         spawn_player_ship(&mut commands);
 | 
					 | 
				
			||||||
         respawn_timer.timer.pause(); // Pause timer until next death
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New System: Manage Invincibility
 | 
					 | 
				
			||||||
fn manage_invincibility(
 | 
					 | 
				
			||||||
    mut commands: Commands,
 | 
					 | 
				
			||||||
    time: Res<Time>,
 | 
					 | 
				
			||||||
    mut query: Query<(Entity, &mut Invincible, Option<&mut Visibility>), With<Player>>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    for (entity, mut invincible, mut visibility) in query.iter_mut() {
 | 
					 | 
				
			||||||
        invincible.timer.tick(time.delta());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Blinking effect (optional)
 | 
					 | 
				
			||||||
        if let Some(ref mut vis) = visibility {
 | 
					 | 
				
			||||||
             // Blink roughly 5 times per second
 | 
					 | 
				
			||||||
            let elapsed_secs = invincible.timer.elapsed_secs();
 | 
					 | 
				
			||||||
            **vis = if (elapsed_secs * 10.0).floor() % 2.0 == 0.0 {
 | 
					 | 
				
			||||||
                Visibility::Visible
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                Visibility::Hidden
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        } 
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if invincible.timer.finished() {
 | 
					 | 
				
			||||||
            println!("Invincibility finished.");
 | 
					 | 
				
			||||||
            commands.entity(entity).remove::<Invincible>();
 | 
					 | 
				
			||||||
             // Ensure player is visible when invincibility ends
 | 
					 | 
				
			||||||
             if let Some(ref mut vis) = visibility {
 | 
					 | 
				
			||||||
                  **vis = Visibility::Visible;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New System: Update Window Title with Lives
 | 
					 | 
				
			||||||
fn update_window_title(
 | 
					 | 
				
			||||||
    lives: Res<PlayerLives>,
 | 
					 | 
				
			||||||
    mut windows: Query<&mut Window>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    if lives.is_changed() {
 | 
					 | 
				
			||||||
        let mut window = windows.single_mut();
 | 
					 | 
				
			||||||
        window.title = format!("Galaga :: Lives: {}", lives.count);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/player.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/player.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,189 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::{Bullet, Enemy, Invincible, Player};
 | 
				
			||||||
 | 
					use crate::constants::{
 | 
				
			||||||
 | 
					    BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE,
 | 
				
			||||||
 | 
					    PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::game_state::AppState;
 | 
				
			||||||
 | 
					use crate::resources::{PlayerLives, PlayerRespawnTimer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper to spawn player (used in setup and respawn)
 | 
				
			||||||
 | 
					pub fn spawn_player_ship(commands: &mut Commands) {
 | 
				
			||||||
 | 
					    commands.spawn((
 | 
				
			||||||
 | 
					        SpriteBundle {
 | 
				
			||||||
 | 
					            sprite: Sprite {
 | 
				
			||||||
 | 
					                color: Color::rgb(0.0, 0.5, 1.0),
 | 
				
			||||||
 | 
					                custom_size: Some(PLAYER_SIZE),
 | 
				
			||||||
 | 
					                ..default()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            transform: Transform::from_translation(Vec3::new(
 | 
				
			||||||
 | 
					                0.0,
 | 
				
			||||||
 | 
					                -WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0,
 | 
				
			||||||
 | 
					                0.0,
 | 
				
			||||||
 | 
					            )),
 | 
				
			||||||
 | 
					            ..default()
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        Player {
 | 
				
			||||||
 | 
					            speed: PLAYER_SPEED,
 | 
				
			||||||
 | 
					            shoot_cooldown: Timer::new(Duration::from_secs_f32(0.3), TimerMode::Once),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // Player starts invincible for a short time
 | 
				
			||||||
 | 
					        Invincible {
 | 
				
			||||||
 | 
					            timer: Timer::new(
 | 
				
			||||||
 | 
					                Duration::from_secs_f32(PLAYER_INVINCIBILITY_DURATION),
 | 
				
			||||||
 | 
					                TimerMode::Once,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ));
 | 
				
			||||||
 | 
					    println!("Player spawned!");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn move_player(
 | 
				
			||||||
 | 
					    keyboard_input: Res<ButtonInput<KeyCode>>,
 | 
				
			||||||
 | 
					    mut query: Query<(&mut Transform, &Player)>,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Using get_single_mut handles the case where player might not exist yet (or was just destroyed)
 | 
				
			||||||
 | 
					    if let Ok((mut transform, player)) = query.get_single_mut() {
 | 
				
			||||||
 | 
					        let mut direction = 0.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
 | 
				
			||||||
 | 
					            direction -= 1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
 | 
				
			||||||
 | 
					            direction += 1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        transform.translation.x += direction * player.speed * time.delta_seconds();
 | 
				
			||||||
 | 
					        let half_player_width = PLAYER_SIZE.x / 2.0;
 | 
				
			||||||
 | 
					        transform.translation.x = transform.translation.x.clamp(
 | 
				
			||||||
 | 
					            -WINDOW_WIDTH / 2.0 + half_player_width,
 | 
				
			||||||
 | 
					            WINDOW_WIDTH / 2.0 - half_player_width,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn player_shoot(
 | 
				
			||||||
 | 
					    keyboard_input: Res<ButtonInput<KeyCode>>,
 | 
				
			||||||
 | 
					    mut query: Query<(&Transform, &mut Player)>, // Should only run if player exists due to run_if
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    if let Ok((player_transform, mut player)) = query.get_single_mut() {
 | 
				
			||||||
 | 
					        player.shoot_cooldown.tick(time.delta());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (keyboard_input.just_pressed(KeyCode::Space)
 | 
				
			||||||
 | 
					            || keyboard_input.just_pressed(KeyCode::ArrowUp))
 | 
				
			||||||
 | 
					            && player.shoot_cooldown.finished()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            player.shoot_cooldown.reset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let bullet_start_pos = player_transform.translation
 | 
				
			||||||
 | 
					                + Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            commands.spawn((
 | 
				
			||||||
 | 
					                SpriteBundle {
 | 
				
			||||||
 | 
					                    sprite: Sprite {
 | 
				
			||||||
 | 
					                        color: Color::rgb(1.0, 1.0, 1.0),
 | 
				
			||||||
 | 
					                        custom_size: Some(BULLET_SIZE),
 | 
				
			||||||
 | 
					                        ..default()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    transform: Transform::from_translation(bullet_start_pos),
 | 
				
			||||||
 | 
					                    ..default()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                Bullet,
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Modified Collision Check for Player vs Enemy
 | 
				
			||||||
 | 
					pub 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>>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // This system only runs if player exists and is not invincible, due to run_if
 | 
				
			||||||
 | 
					    if let Ok((player_entity, player_transform)) = player_query.get_single() {
 | 
				
			||||||
 | 
					        for (enemy_entity, enemy_transform) in enemy_query.iter() {
 | 
				
			||||||
 | 
					            let distance = player_transform
 | 
				
			||||||
 | 
					                .translation
 | 
				
			||||||
 | 
					                .distance(enemy_transform.translation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if distance < PLAYER_ENEMY_COLLISION_THRESHOLD {
 | 
				
			||||||
 | 
					                println!("Player hit by enemy!");
 | 
				
			||||||
 | 
					                commands.entity(enemy_entity).despawn(); // Despawn enemy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                lives.count = lives.count.saturating_sub(1); // Decrement lives safely
 | 
				
			||||||
 | 
					                println!("Lives remaining: {}", lives.count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                commands.entity(player_entity).despawn(); // Despawn player
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if lives.count > 0 {
 | 
				
			||||||
 | 
					                    // Start the respawn timer
 | 
				
			||||||
 | 
					                    respawn_timer.timer.reset();
 | 
				
			||||||
 | 
					                    respawn_timer.timer.unpause();
 | 
				
			||||||
 | 
					                    println!("Respawn timer started.");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    println!("GAME OVER!");
 | 
				
			||||||
 | 
					                    next_state.set(AppState::GameOver); // Transition to GameOver state
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                // Important: Break after handling one collision per frame for the player
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New System: Respawn Player
 | 
				
			||||||
 | 
					pub fn respawn_player(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut respawn_timer: ResMut<PlayerRespawnTimer>,
 | 
				
			||||||
 | 
					    // No player query needed here due to run_if condition
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Tick the timer only if it's actually running
 | 
				
			||||||
 | 
					    if respawn_timer.timer.tick(time.delta()).just_finished() {
 | 
				
			||||||
 | 
					        println!("Respawn timer finished. Spawning player.");
 | 
				
			||||||
 | 
					        spawn_player_ship(&mut commands);
 | 
				
			||||||
 | 
					        respawn_timer.timer.pause(); // Pause timer until next death
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New System: Manage Invincibility
 | 
				
			||||||
 | 
					pub fn manage_invincibility(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    time: Res<Time>,
 | 
				
			||||||
 | 
					    mut query: Query<(Entity, &mut Invincible, Option<&mut Visibility>), With<Player>>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (entity, mut invincible, mut visibility) in query.iter_mut() {
 | 
				
			||||||
 | 
					        invincible.timer.tick(time.delta());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Blinking effect (optional)
 | 
				
			||||||
 | 
					        if let Some(ref mut vis) = visibility {
 | 
				
			||||||
 | 
					            // Blink roughly 5 times per second
 | 
				
			||||||
 | 
					            let elapsed_secs = invincible.timer.elapsed_secs();
 | 
				
			||||||
 | 
					            **vis = if (elapsed_secs * 10.0).floor() % 2.0 == 0.0 {
 | 
				
			||||||
 | 
					                Visibility::Visible
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Visibility::Hidden
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if invincible.timer.finished() {
 | 
				
			||||||
 | 
					            println!("Invincibility finished.");
 | 
				
			||||||
 | 
					            commands.entity(entity).remove::<Invincible>();
 | 
				
			||||||
 | 
					            // Ensure player is visible when invincibility ends
 | 
				
			||||||
 | 
					            if let Some(ref mut vis) = visibility {
 | 
				
			||||||
 | 
					                **vis = Visibility::Visible;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										126
									
								
								src/resources.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/resources.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,126 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					use std::time::Duration; // Needed for Timer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Resources ---
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct EnemySpawnTimer {
 | 
				
			||||||
 | 
					    pub timer: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct PlayerLives {
 | 
				
			||||||
 | 
					    pub count: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct PlayerRespawnTimer {
 | 
				
			||||||
 | 
					    pub timer: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New struct to define formation positions
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct FormationLayout {
 | 
				
			||||||
 | 
					    pub name: String, // Optional name for debugging/identification
 | 
				
			||||||
 | 
					    pub positions: Vec<Vec3>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Default implementation for easy initialization
 | 
				
			||||||
 | 
					impl Default for FormationLayout {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        // Default to the original grid formation for now
 | 
				
			||||||
 | 
					        let mut positions = Vec::with_capacity(crate::constants::FORMATION_ENEMY_COUNT);
 | 
				
			||||||
 | 
					        for i in 0..crate::constants::FORMATION_ENEMY_COUNT {
 | 
				
			||||||
 | 
					            let row = i / crate::constants::FORMATION_COLS;
 | 
				
			||||||
 | 
					            let col = i % crate::constants::FORMATION_COLS;
 | 
				
			||||||
 | 
					            let target_x = (col as f32 - (crate::constants::FORMATION_COLS as f32 - 1.0) / 2.0) * crate::constants::FORMATION_X_SPACING;
 | 
				
			||||||
 | 
					            let target_y = crate::constants::FORMATION_BASE_Y - (row as f32 * crate::constants::FORMATION_Y_SPACING);
 | 
				
			||||||
 | 
					            positions.push(Vec3::new(target_x, target_y, 0.0));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        FormationLayout {
 | 
				
			||||||
 | 
					            name: "Default Grid".to_string(),
 | 
				
			||||||
 | 
					            positions,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Configuration for a single stage
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct StageConfig {
 | 
				
			||||||
 | 
					    pub formation_layout: FormationLayout,
 | 
				
			||||||
 | 
					    pub enemy_count: usize, // Allow overriding enemy count per stage
 | 
				
			||||||
 | 
					    pub attack_patterns: Vec<crate::components::AttackPattern>, // Possible patterns for this stage
 | 
				
			||||||
 | 
					    pub attack_dive_interval: f32, // Time between attack dives for this stage
 | 
				
			||||||
 | 
					    pub enemy_speed_multiplier: f32, // Speed multiplier for this stage
 | 
				
			||||||
 | 
					    pub enemy_shoot_interval: f32, // Shoot interval for this stage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Resource to hold all stage configurations
 | 
				
			||||||
 | 
					#[derive(Resource, Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct StageConfigurations {
 | 
				
			||||||
 | 
					    pub stages: Vec<StageConfig>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for StageConfigurations {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        use crate::components::AttackPattern;
 | 
				
			||||||
 | 
					        use crate::constants::*; // Import constants for default values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Define configurations for a few stages
 | 
				
			||||||
 | 
					        let stage1 = StageConfig {
 | 
				
			||||||
 | 
					            formation_layout: FormationLayout::default(), // Use the default grid
 | 
				
			||||||
 | 
					            enemy_count: FORMATION_ENEMY_COUNT,
 | 
				
			||||||
 | 
					            attack_patterns: vec![AttackPattern::SwoopDive],
 | 
				
			||||||
 | 
					            attack_dive_interval: 3.0,
 | 
				
			||||||
 | 
					            enemy_speed_multiplier: 1.0,
 | 
				
			||||||
 | 
					            enemy_shoot_interval: ENEMY_SHOOT_INTERVAL,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let stage2_layout = {
 | 
				
			||||||
 | 
					            let mut positions = Vec::new();
 | 
				
			||||||
 | 
					            let radius = WINDOW_WIDTH / 4.0;
 | 
				
			||||||
 | 
					            let center_y = FORMATION_BASE_Y - 50.0;
 | 
				
			||||||
 | 
					            let count = 16; // Example: Fewer enemies in a circle
 | 
				
			||||||
 | 
					            for i in 0..count {
 | 
				
			||||||
 | 
					                let angle = (i as f32 / count as f32) * 2.0 * std::f32::consts::PI;
 | 
				
			||||||
 | 
					                positions.push(Vec3::new(angle.cos() * radius, center_y + angle.sin() * radius, 0.0));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            FormationLayout { name: "Circle".to_string(), positions }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let stage2 = StageConfig {
 | 
				
			||||||
 | 
					            formation_layout: stage2_layout,
 | 
				
			||||||
 | 
					            enemy_count: 16,
 | 
				
			||||||
 | 
					            attack_patterns: vec![AttackPattern::SwoopDive, AttackPattern::DirectDive], // Add direct dive
 | 
				
			||||||
 | 
					            attack_dive_interval: 2.5, // Faster dives
 | 
				
			||||||
 | 
					            enemy_speed_multiplier: 1.2, // Faster enemies
 | 
				
			||||||
 | 
					            enemy_shoot_interval: ENEMY_SHOOT_INTERVAL * 0.8, // Faster shooting
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add more stages here...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        StageConfigurations {
 | 
				
			||||||
 | 
					            stages: vec![stage1, stage2], // Add more stages as needed
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct Score {
 | 
				
			||||||
 | 
					    pub value: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct CurrentStage {
 | 
				
			||||||
 | 
					    pub number: u32,
 | 
				
			||||||
 | 
					    pub waiting_for_clear: bool, // Flag to check if we should check for stage clear
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct FormationState {
 | 
				
			||||||
 | 
					    pub next_slot_index: usize,
 | 
				
			||||||
 | 
					    pub total_spawned_this_stage: usize,
 | 
				
			||||||
 | 
					    pub formation_complete: bool, // Flag to indicate if all enemies are in position
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					pub struct AttackDiveTimer {
 | 
				
			||||||
 | 
					    pub timer: Timer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/stage.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/stage.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::Enemy;
 | 
				
			||||||
 | 
					use crate::resources::{CurrentStage, EnemySpawnTimer, FormationState};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper to access world directly in check_stage_clear
 | 
				
			||||||
 | 
					pub 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::<CurrentStage>() {
 | 
				
			||||||
 | 
					        if stage.waiting_for_clear {
 | 
				
			||||||
 | 
					            // Create the query *after* checking the flag
 | 
				
			||||||
 | 
					            let mut enemy_query = world.query_filtered::<Entity, With<Enemy>>();
 | 
				
			||||||
 | 
					            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::<CurrentStage>() {
 | 
				
			||||||
 | 
					            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::<FormationState>() {
 | 
				
			||||||
 | 
					            formation_state.next_slot_index = 0;
 | 
				
			||||||
 | 
					            formation_state.total_spawned_this_stage = 0;
 | 
				
			||||||
 | 
					            formation_state.formation_complete = false; // Reset flag for new stage
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(mut spawn_timer) = world.get_resource_mut::<EnemySpawnTimer>() {
 | 
				
			||||||
 | 
					            spawn_timer.timer.reset();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/systems.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/systems.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					use bevy::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::components::{Invincible, Player};
 | 
				
			||||||
 | 
					use crate::resources::{CurrentStage, FormationState, PlayerLives, Score};
 | 
				
			||||||
 | 
					use crate::player::spawn_player_ship; // Import the helper function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Setup ---
 | 
				
			||||||
 | 
					pub fn setup(mut commands: Commands) {
 | 
				
			||||||
 | 
					    commands.spawn(Camera2dBundle::default());
 | 
				
			||||||
 | 
					    spawn_player_ship(&mut commands); // Use helper function from player module
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Run Conditions ---
 | 
				
			||||||
 | 
					pub fn player_exists(query: Query<&Player>) -> bool {
 | 
				
			||||||
 | 
					    !query.is_empty()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn player_vulnerable(query: Query<&Player, Without<Invincible>>) -> bool {
 | 
				
			||||||
 | 
					    !query.is_empty()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn should_respawn_player(lives: Res<PlayerLives>, player_query: Query<&Player>) -> bool {
 | 
				
			||||||
 | 
					    player_query.is_empty() && lives.count > 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Moved is_formation_complete to enemy.rs as it's closely related to enemy logic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- General Systems ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update Window Title with Lives, Score, and Stage
 | 
				
			||||||
 | 
					pub fn update_window_title(
 | 
				
			||||||
 | 
					    lives: Res<PlayerLives>,
 | 
				
			||||||
 | 
					    score: Res<Score>,
 | 
				
			||||||
 | 
					    stage: Res<CurrentStage>, // Add CurrentStage resource
 | 
				
			||||||
 | 
					    mut windows: Query<&mut Window>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Update if lives, score, or stage changed
 | 
				
			||||||
 | 
					    if lives.is_changed() || score.is_changed() || stage.is_changed() {
 | 
				
			||||||
 | 
					        let mut window = windows.single_mut();
 | 
				
			||||||
 | 
					        window.title = format!(
 | 
				
			||||||
 | 
					            "Galaga :: Stage: {} Lives: {} Score: {}",
 | 
				
			||||||
 | 
					            stage.number, lives.count, score.value
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue