diff --git a/README.md b/README.md index 654bada..c2bb897 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ nix develop --command bash -c "cargo build" * ~~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. **(DONE - Stop implemented by removing FormationTarget)** * **Enemy Attack Dives:** - * Give enemies an `Attacking` state/component. - * Periodically trigger enemies in the formation to switch to the `Attacking` state. - * Define attack paths (swooping dives towards the player area). - * Make enemies fire bullets (downwards or towards the player) during their dives. - * After an attack dive, enemies could return to their formation position or fly off-screen. + * ~~Give enemies an `Attacking` state/component.~~ **(DONE)** + * ~~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). **(Basic downward dive implemented)** + * ~~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. **(Fly off-screen implemented)** * **Enemy Variety:** * Introduce different types of enemies (e.g., using different components or an enum). * Assign different behaviors, point values, and maybe sprites to each type. diff --git a/src/main.rs b/src/main.rs index 94567d1..6e50362 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,10 @@ 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 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; @@ -31,6 +34,7 @@ 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)] @@ -43,24 +47,29 @@ struct Player { struct Bullet; #[derive(Component)] -struct Enemy; +struct Enemy { + shoot_cooldown: Timer, +} #[derive(Component)] struct Invincible { - timer: Timer, + timer: Timer, } #[derive(Component)] struct FormationTarget { - position: Vec3, + 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 (to be implemented) + 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 { @@ -74,28 +83,29 @@ struct PlayerLives { #[derive(Resource)] struct PlayerRespawnTimer { - timer: Timer, + timer: Timer, } #[derive(Resource)] struct Score { - value: u32, + value: u32, } #[derive(Resource)] struct CurrentStage { - number: u32, - waiting_for_clear: bool, // Flag to check if we should check for stage clear + 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, + 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, + timer: Timer, } // --- Game Over UI --- @@ -103,88 +113,256 @@ struct AttackDiveTimer { 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 + 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(); - } + 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>, + 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(); - // } + 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; - } - } - } + // Use manual resource access because we need mutable access to multiple resources + // Separate checks to manage borrows correctly + let mut should_clear = false; + if let Some(stage) = world.get_resource::() { + if stage.waiting_for_clear { + // Create the query *after* checking the flag + let mut enemy_query = world.query_filtered::>(); + if enemy_query.iter(world).next().is_none() { + should_clear = true; + } + } + } - if should_clear { - // Get mutable resources only when needed - if let Some(mut stage) = world.get_resource_mut::() { - stage.number += 1; - stage.waiting_for_clear = false; - println!("Stage cleared! Starting Stage {}...", stage.number); - } - if let Some(mut formation_state) = world.get_resource_mut::() { - formation_state.next_slot_index = 0; - formation_state.total_spawned_this_stage = 0; - } - if let Some(mut spawn_timer) = world.get_resource_mut::() { - spawn_timer.timer.reset(); - } - } + 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