use bevy::prelude::*; // Removed unused AppExit use std::time::Duration; // --- Game States --- #[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)] enum AppState { #[default] Playing, GameOver, } // --- Constants --- const WINDOW_WIDTH: f32 = 600.0; const WINDOW_HEIGHT: f32 = 800.0; 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); // Player bullet const ENEMY_BULLET_SIZE: Vec2 = Vec2::new(8.0, 8.0); // Enemy bullet const ENEMY_BULLET_SPEED: f32 = 300.0; const ENEMY_SHOOT_INTERVAL: f32 = 1.5; // Seconds between shots for an attacking enemy // Formation constants const FORMATION_ROWS: usize = 4; const FORMATION_COLS: usize = 8; const FORMATION_ENEMY_COUNT: usize = FORMATION_ROWS * FORMATION_COLS; const FORMATION_X_SPACING: f32 = 60.0; const FORMATION_Y_SPACING: f32 = 50.0; const FORMATION_BASE_Y: f32 = WINDOW_HEIGHT / 2.0 - 150.0; // Top area for formation const STARTING_LIVES: u32 = 3; const PLAYER_RESPAWN_DELAY: f32 = 2.0; const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0; // Collision thresholds const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) * 0.5; // 22.5 const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5; // 35.0 const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 = (ENEMY_BULLET_SIZE.x + PLAYER_SIZE.x) * 0.5; // ~19.0 // --- Components --- #[derive(Component)] struct Player { speed: f32, shoot_cooldown: Timer, } #[derive(Component)] struct Bullet; #[derive(Component)] struct Enemy { shoot_cooldown: Timer, } #[derive(Component)] struct Invincible { timer: Timer, } #[derive(Component)] struct FormationTarget { position: Vec3, } #[derive(Component, Clone, PartialEq)] enum EnemyState { Entering, // Flying onto the screen towards formation target InFormation, // Holding position in the formation Attacking, // Diving towards the player } #[derive(Component)] struct EnemyBullet; // --- Resources --- #[derive(Resource)] struct EnemySpawnTimer { timer: Timer, } #[derive(Resource)] struct PlayerLives { count: u32, } #[derive(Resource)] struct PlayerRespawnTimer { timer: Timer, } #[derive(Resource)] struct Score { value: u32, } #[derive(Resource)] struct CurrentStage { number: u32, waiting_for_clear: bool, // Flag to check if we should check for stage clear } #[derive(Resource)] struct FormationState { next_slot_index: usize, total_spawned_this_stage: usize, formation_complete: bool, // Flag to indicate if all enemies are in position } #[derive(Resource)] struct AttackDiveTimer { timer: Timer, } // --- Game Over UI --- #[derive(Component)] struct GameOverUI; fn setup_game_over_ui(mut commands: Commands) { println!("Entering GameOver state. Setting up UI."); commands.spawn(( TextBundle::from_section( "GAME OVER", TextStyle { font_size: 100.0, color: Color::WHITE, ..default() }, ) .with_style(Style { position_type: PositionType::Absolute, align_self: AlignSelf::Center, justify_self: JustifySelf::Center, top: Val::Percent(40.0), // Center vertically roughly ..default() }), GameOverUI, // Tag the UI element )); // TODO: Add "Press R to Restart" text later } fn cleanup_game_over_ui(mut commands: Commands, query: Query>) { println!("Exiting GameOver state. Cleaning up UI."); for entity in query.iter() { commands.entity(entity).despawn_recursive(); } } // --- Cleanup --- fn cleanup_game_entities( mut commands: Commands, bullet_query: Query>, enemy_query: Query>, // Optionally despawn player too, or handle separately if needed for restart // player_query: Query>, ) { println!("Exiting Playing state. Cleaning up game entities."); for entity in bullet_query.iter() { commands.entity(entity).despawn(); } for entity in enemy_query.iter() { commands.entity(entity).despawn(); } // for entity in player_query.iter() { // commands.entity(entity).despawn(); // } } // --- Stage Management --- // Helper to access world directly in check_stage_clear fn check_stage_clear(world: &mut World) { // Use manual resource access because we need mutable access to multiple resources // Separate checks to manage borrows correctly let mut should_clear = false; if let Some(stage) = world.get_resource::() { if stage.waiting_for_clear { // Create the query *after* checking the flag let mut enemy_query = world.query_filtered::>(); if enemy_query.iter(world).next().is_none() { should_clear = true; } } } if should_clear { // Get mutable resources only when needed if let Some(mut stage) = world.get_resource_mut::() { stage.number += 1; stage.waiting_for_clear = false; println!("Stage cleared! Starting Stage {}...", stage.number); } if let Some(mut formation_state) = world.get_resource_mut::() { formation_state.next_slot_index = 0; formation_state.total_spawned_this_stage = 0; formation_state.formation_complete = false; // Reset flag for new stage } if let Some(mut spawn_timer) = world.get_resource_mut::() { spawn_timer.timer.reset(); } } } // --- Enemy Attack Logic --- fn trigger_attack_dives( mut timer: ResMut, time: Res