diff --git a/.gitignore b/.gitignore index 7e17b2b..2768870 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.direnv /logs /.idea +result diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..588f96f --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,11 @@ +{ + "servers": { + "cratedocs": { + "type": "stdio", + "command": "/home/harald/.cargo/bin/cratedocs", + "args": [ + "stdio" + ] + } + } +} \ No newline at end of file diff --git a/.goosehints b/AGENT.md similarity index 100% rename from .goosehints rename to AGENT.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 2145a5c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -.goosehints \ No newline at end of file diff --git a/README.md b/README.md index 8b33cbf..869b4fa 100644 --- a/README.md +++ b/README.md @@ -34,73 +34,3 @@ nix develop --command bash -c "cargo build" 2. Clone the repository. 3. Navigate to the project directory. 4. Run the game using the command: `nix develop --command bash -c "cargo run"` - -# TODO - -**1. Core Gameplay Loop & State Management:** - -* ~~**Player Lives:**~~ **(DONE)** - * ~~Add a `PlayerLives` resource (e.g., starting with 3).~~ - * ~~Modify `check_player_enemy_collisions`: Instead of just printing, decrement the lives count.~~ - * ~~Implement player destruction (despawn the player sprite) and respawn logic (maybe after a short delay, with temporary invincibility).~~ -* ~~**Game Over State:**~~ **(DONE)** - * ~~Use Bevy's `States` (e.g., `Playing`, `GameOver`).~~ - * ~~Transition to `GameOver` when `PlayerLives` reaches zero.~~ - * ~~In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option.~~ -* ~~**Scoring:**~~ **(DONE)** - * ~~Add a `Score` resource.~~ - * ~~Increment the score in `check_bullet_collisions` when an enemy is hit.~~ - * Consider different point values for different enemy types or hitting enemies during dives later. -* ~~**Levels/Stages:**~~ **(DONE)** - * ~~Add a `CurrentStage` resource.~~ - * ~~Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation).~~ - * ~~Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations).~~ - -**2. Enemy Behavior - Formations & Attack Patterns:** - -* **Enemy Formations:** This is a core Galaga feature. - * ~~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).~~ **(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.~~ **(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).~~ **(DONE - Basic swoop towards center 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).~~ **(DONE - Added EnemyType enum and field)** - * ~~Assign different behaviors, point values, and maybe sprites to each type.~~ **(DONE - Behaviors, points & color based on type)** - -**3. Advanced Galaga Mechanics:** - -* **Boss Galaga & Capture Beam:** - * ~~Create a "Boss" enemy type.~~ **(DONE - Added to Enum, handled in matches)** - * Implement the tractor beam attack (visual effect, player capture logic). - * Player needs a `Captured` state. - * Logic for the Boss to return captured ships to the formation. -* **Dual Fighter (Rescuing Captured Ship):** - * Allow the player to shoot a Boss Galaga attempting to capture or one holding a captured ship. - * Implement the logic to free the captured ship. - * Implement the dual fighter mode (controlling two ships, firing two bullets). This involves significant changes to player control and shooting systems. -* **Challenging Stages:** - * Implement a special stage type (e.g., every few levels). - * Enemies fly in intricate patterns without shooting. - * Award bonus points for destroying all enemies in the stage. - -**4. Polish and User Interface:** - -* **Visuals:** - * Replace placeholder shapes with actual sprites using `SpriteBundle` or `SpriteSheetBundle`. Create distinct looks for the player, different enemy types, bullets. - * Add explosion effects/animations when enemies or the player are destroyed. - * Implement a scrolling starfield background for a more classic space feel. -* **Audio:** - * Integrate `bevy_audio`. - * Add sound effects for player shooting, enemy firing, explosions, player death, capturing, etc. - * Add background music that might change per stage or state (e.g., main gameplay vs. challenging stage). -* **UI:** - * Use `bevy_ui` to display the current Score, Lives remaining, and Stage number on screen during gameplay. - * Display High Score? - * Create a Start Menu state. - -Starting with the Core Gameplay Loop (Lives, Game Over, Score, basic Stages) and then moving onto Enemy Formations and Attack Patterns would likely provide the biggest steps towards a Galaga feel. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a41b72a --- /dev/null +++ b/TODO.md @@ -0,0 +1,69 @@ +# TODO + +**1. Core Gameplay Loop & State Management:** + +* ~~**Player Lives:**~~ **(DONE)** + * ~~Add a `PlayerLives` resource (e.g., starting with 3).~~ + * ~~Modify `check_player_enemy_collisions`: Instead of just printing, decrement the lives count.~~ + * ~~Implement player destruction (despawn the player sprite) and respawn logic (maybe after a short delay, with temporary invincibility).~~ +* ~~**Game Over State:**~~ **(DONE)** + * ~~Use Bevy's `States` (e.g., `Playing`, `GameOver`).~~ + * ~~Transition to `GameOver` when `PlayerLives` reaches zero.~~ + * ~~In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option.~~ +* ~~**Scoring:**~~ **(DONE)** + * ~~Add a `Score` resource.~~ + * ~~Increment the score in `check_bullet_collisions` when an enemy is hit.~~ + * Consider different point values for different enemy types or hitting enemies during dives later. +* ~~**Levels/Stages:**~~ **(DONE)** + * ~~Add a `CurrentStage` resource.~~ + * ~~Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation).~~ + * ~~Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations).~~ + +**2. Enemy Behavior - Formations & Attack Patterns:** + +* **Enemy Formations:** This is a core Galaga feature. + * ~~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).~~ **(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.~~ **(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).~~ **(DONE - Basic swoop towards center 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).~~ **(DONE - Added EnemyType enum and field)** + * ~~Assign different behaviors, point values, and maybe sprites to each type.~~ **(DONE - Behaviors, points & color based on type)** + +**3. Advanced Galaga Mechanics:** + +* **Boss Galaga & Capture Beam:** + * ~~Create a "Boss" enemy type.~~ **(DONE - Added to Enum, handled in matches)** + * Implement the tractor beam attack (visual effect, player capture logic). + * Player needs a `Captured` state. + * Logic for the Boss to return captured ships to the formation. +* **Dual Fighter (Rescuing Captured Ship):** + * Allow the player to shoot a Boss Galaga attempting to capture or one holding a captured ship. + * Implement the logic to free the captured ship. + * Implement the dual fighter mode (controlling two ships, firing two bullets). This involves significant changes to player control and shooting systems. +* **Challenging Stages:** + * Implement a special stage type (e.g., every few levels). + * Enemies fly in intricate patterns without shooting. + * Award bonus points for destroying all enemies in the stage. + +**4. Polish and User Interface:** + +* **Visuals:** + * Replace placeholder shapes with actual sprites using `SpriteBundle` or `SpriteSheetBundle`. Create distinct looks for the player, different enemy types, bullets. + * Add explosion effects/animations when enemies or the player are destroyed. + * Implement a scrolling starfield background for a more classic space feel. +* **Audio:** + * Integrate `bevy_audio`. + * Add sound effects for player shooting, enemy firing, explosions, player death, capturing, etc. + * Add background music that might change per stage or state (e.g., main gameplay vs. challenging stage). +* **UI:** + * Use `bevy_ui` to display the current Score, Lives remaining, and Stage number on screen during gameplay. + * Display High Score? + * Create a Start Menu state. + +Starting with the Core Gameplay Loop (Lives, Game Over, Score, basic Stages) and then moving onto Enemy Formations and Attack Patterns would likely provide the biggest steps towards a Galaga feel. diff --git a/src/components.rs b/src/components.rs index 6b02234..974f1fa 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use std::time::Duration; // Needed for Timer in Player // --- Components --- #[derive(Component)] @@ -28,6 +27,24 @@ pub struct Invincible { pub timer: Timer, } +// New component to mark a player as captured by a Boss enemy +#[derive(Component, Clone)] // Added Clone derive +pub struct Captured { + // Reference to the capturing boss entity + pub boss_entity: Entity, + // Timer for how long the player remains captured + pub timer: Timer, +} + +// New component for the tractor beam visual effect +#[derive(Component)] +pub struct TractorBeam { + pub target: Entity, // The entity being targeted (usually player) + pub timer: Timer, // How long the beam lasts + pub width: f32, // Visual width of the beam + pub active: bool, // Whether the beam is currently active +} + #[derive(Component)] pub struct FormationTarget { pub position: Vec3, @@ -36,16 +53,17 @@ pub struct FormationTarget { // 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 + 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) + CaptureBeam, // New pattern: Boss dives and attempts to capture the player with a tractor beam + // 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 + Entering, // Flying onto the screen towards formation target + InFormation, // Holding position in the formation Attacking(AttackPattern), // Diving towards the player using a specific pattern } @@ -54,4 +72,4 @@ pub struct EnemyBullet; // Game Over UI Component (might move to ui.rs later if more UI exists) #[derive(Component)] -pub struct GameOverUI; \ No newline at end of file +pub struct GameOverUI; diff --git a/src/constants.rs b/src/constants.rs index 2b5bd8e..4c3ec09 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,5 @@ use bevy::math::Vec2; +use bevy::prelude::*; // --- Constants --- pub const WINDOW_WIDTH: f32 = 600.0; @@ -31,3 +32,9 @@ pub const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) 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; + +// Tractor beam constants +pub const TRACTOR_BEAM_WIDTH: f32 = 20.0; +pub const TRACTOR_BEAM_DURATION: f32 = 3.0; +pub const TRACTOR_BEAM_COLOR: Color = Color::rgba(0.5, 0.0, 0.8, 0.6); +pub const CAPTURE_DURATION: f32 = 10.0; // How long the player stays captured diff --git a/src/enemy.rs b/src/enemy.rs index 5cacf51..a497e15 100644 --- a/src/enemy.rs +++ b/src/enemy.rs @@ -1,17 +1,15 @@ 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::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, TractorBeam, Captured}; +use crate::constants::{ // Only keeping used constants + ENEMY_BULLET_SIZE, ENEMY_SIZE, ENEMY_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH, + TRACTOR_BEAM_WIDTH, TRACTOR_BEAM_DURATION, TRACTOR_BEAM_COLOR, CAPTURE_DURATION, }; use crate::resources::{ - AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives, - PlayerRespawnTimer, StageConfigurations, // Make sure StageConfigurations is imported if not already + AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, + StageConfigurations, }; -use crate::game_state::AppState; pub fn spawn_enemies( mut commands: Commands, @@ -52,8 +50,15 @@ pub fn spawn_enemies( 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 enemy type - now with a chance to spawn Boss enemies + // Higher stages have a slightly higher boss chance + let boss_chance = 0.05 + (stage.number as f32 * 0.01).min(0.15); + let enemy_type = if fastrand::f32() < boss_chance { + println!("Spawning a Boss enemy!"); + EnemyType::Boss + } else { + EnemyType::Grunt + }; // Determine sprite color based on type let sprite_color = match enemy_type { @@ -115,6 +120,7 @@ pub fn move_enemies( mut commands: Commands, stage: Res<CurrentStage>, stage_configs: Res<StageConfigurations>, // Add stage configurations + has_beam_query: Query<&TractorBeam>, ) { // Get current stage config for speed multiplier let config_index = (stage.number as usize - 1) % stage_configs.stages.len(); @@ -132,6 +138,7 @@ pub fn move_enemies( if *state == EnemyState::Entering { let current_pos = transform.translation; let target_pos = target.position; + // Using target_pos which is already a Vec3, not a reference let direction = target_pos - current_pos; let distance = direction.length(); @@ -153,56 +160,121 @@ pub fn move_enemies( } // --- Handle Attacking Enemies --- - // Note: attack_speed calculated above using multiplier - for (entity, mut transform, state, _enemy) in attacking_query.iter_mut() { + for (entity, mut transform, state, enemy) in attacking_query.iter_mut() { + // Check what state the enemy is in + if let EnemyState::Attacking(attack_pattern) = state { + // Apply different movement based on enemy type + match enemy.enemy_type { + EnemyType::Grunt => { + // Basic enemies follow their attack pattern + match attack_pattern { + // ... existing patterns ... + AttackPattern::SwoopDive => { + // ... existing code ... + let vertical_movement = attack_speed * time.delta_seconds(); + let horizontal_speed_factor = 0.5; + let horizontal_movement = if transform.translation.x < 0.0 { + attack_speed * horizontal_speed_factor * time.delta_seconds() + } else if transform.translation.x > 0.0 { + -attack_speed * horizontal_speed_factor * time.delta_seconds() + } else { 0.0 }; - // 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(); + transform.translation.y -= vertical_movement; + transform.translation.x += horizontal_movement; - 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 }; + // 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 => { + transform.translation.y -= attack_speed * time.delta_seconds(); + } + AttackPattern::Kamikaze(target) => { + // Copy the target value rather than dereferencing + // since target should actually be a Vec3 in this context + let target_pos = *target; // Dereference here + let direction = target_pos - transform.translation; + let distance = direction.length(); + let kamikaze_threshold = attack_speed * time.delta_seconds() * 1.1; // Threshold to stop near target - 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; + if distance > kamikaze_threshold { + let move_delta = direction.normalize() * attack_speed * time.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 + } + } + // New CaptureBeam pattern - Bosses behave differently + AttackPattern::CaptureBeam => { + // For Grunt enemies, just do a direct dive (fallback) + transform.translation.y -= attack_speed * time.delta_seconds(); + } } } - 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 + EnemyType::Boss => { + // Boss has special behavior, especially for CaptureBeam + match attack_pattern { + AttackPattern::CaptureBeam => { + // Boss moves down to a position above the player area + let target_y = -WINDOW_HEIGHT / 4.0; // Position at lower quarter of the screen + + if transform.translation.y > target_y { + // Move down to position + transform.translation.y -= attack_speed * 0.8 * time.delta_seconds(); + } else { + // Once in position, stay there briefly before activating beam + // Check if this boss already has a TractorBeam component + if has_beam_query.get(entity).is_err() { + // Spawn tractor beam component on this boss + commands.entity(entity).insert(TractorBeam { + target: Entity::PLACEHOLDER, // Will be filled in by the boss_capture_attack + timer: Timer::new(Duration::from_secs_f32(TRACTOR_BEAM_DURATION), TimerMode::Once), + width: TRACTOR_BEAM_WIDTH, + active: false, + }); + } + } + } + AttackPattern::SwoopDive => { + // ... existing code for swoop dive ... + let center_x = 0.0; + let bottom_y = -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y; + + // First move towards center-bottom + let target = Vec3::new(center_x, bottom_y, 0.0); + // target is directly created as Vec3, not a reference + let direction = target - transform.translation; + + // Normalize and move + if direction.length() > 0.0 { + let normalized_dir = direction.normalize(); + transform.translation += normalized_dir * attack_speed * time.delta_seconds(); + } + } + AttackPattern::DirectDive => { + transform.translation.y -= attack_speed * time.delta_seconds(); + } + AttackPattern::Kamikaze(target) => { + // Convert the target to a value type + let target_pos = *target; // Dereference here + let direction = target_pos - transform.translation; + + // If very close to target, just move straight down + if direction.length() < 50.0 { + transform.translation.y -= attack_speed * time.delta_seconds(); + } else { + // Move toward target + let normalized_dir = direction.normalize(); + transform.translation += normalized_dir * attack_speed * time.delta_seconds(); + } + } } } - // 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 { @@ -262,13 +334,12 @@ pub fn check_formation_complete( } } -use crate::components::AttackPattern; // Import the new enum -use crate::components::Player; // Import Player for Kamikaze target +use crate::components::{AttackPattern, Player}; // Import the new enum and Player pub fn trigger_attack_dives( mut timer: ResMut<AttackDiveTimer>, time: Res<Time>, - mut enemy_query: Query<(Entity, &mut EnemyState), With<Enemy>>, // Renamed for clarity + mut enemy_query: Query<(Entity, &mut EnemyState, &Enemy)>, // Added Enemy component to check type formation_state: Res<FormationState>, stage: Res<CurrentStage>, // Need current stage stage_configs: Res<StageConfigurations>, // Need stage configs @@ -278,29 +349,42 @@ pub fn trigger_attack_dives( // 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 + let mut available_enemies: Vec<(Entity, EnemyType)> = Vec::new(); + for (entity, state, enemy) in enemy_query.iter() { + // Check the state correctly and store enemy type if matches!(state, EnemyState::InFormation) { - available_enemies.push(entity); + available_enemies.push((entity, enemy.enemy_type)); } } // 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]; + let (chosen_entity, enemy_type) = 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 + // Select an attack pattern based on enemy type + let mut selected_pattern = match enemy_type { + // For Boss enemies, occasionally use the CaptureBeam pattern + EnemyType::Boss => { + if fastrand::f32() < 0.4 { // 40% chance for Boss to use CaptureBeam + AttackPattern::CaptureBeam + } else { + // Otherwise use a random pattern from the stage config + let pattern_index = fastrand::usize(..current_config.attack_patterns.len()); + current_config.attack_patterns[pattern_index] + } + }, + // Regular enemies use patterns from the stage config + EnemyType::Grunt => { + let pattern_index = fastrand::usize(..current_config.attack_patterns.len()); + current_config.attack_patterns[pattern_index] + } + }; // If Kamikaze, get player position (if player exists) if let AttackPattern::Kamikaze(_) = selected_pattern { @@ -313,9 +397,8 @@ pub fn trigger_attack_dives( } } - // Get the chosen enemy's state mutably and change it - if let Ok((_, mut state)) = enemy_query.get_mut(chosen_entity) { + 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) @@ -365,4 +448,81 @@ pub fn enemy_shoot( // New run condition: Check if the formation is complete pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool { formation_state.formation_complete +} + +// New system to handle the tractor beam attack from Boss enemies +pub fn boss_capture_attack( + mut commands: Commands, + time: Res<Time>, + mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>, + player_query: Query<(Entity, &Transform), (With<Player>, Without<Captured>)>, // Only target non-captured players + has_beam_query: Query<&TractorBeam>, +) { + for (boss_entity, boss_transform, mut tractor_beam) in boss_query.iter_mut() { + // Tick the beam timer + tractor_beam.timer.tick(time.delta()); + + // If player exists and beam is not active yet, set player as target + if !tractor_beam.active && tractor_beam.target == Entity::PLACEHOLDER { + if let Ok((player_entity, _)) = player_query.get_single() { + tractor_beam.target = player_entity; + tractor_beam.active = true; + println!("Boss {:?} activated tractor beam targeting player!", boss_entity); + + // Create visual beam effect (using a simple sprite for now) + let beam_height = boss_transform.translation.y - (-WINDOW_HEIGHT / 2.0); // Height from boss to bottom of screen + + // Spawn the beam as a child of the boss + commands.entity(boss_entity).with_children(|parent| { + parent.spawn(SpriteBundle { + sprite: Sprite { + color: TRACTOR_BEAM_COLOR, + custom_size: Some(Vec2::new(tractor_beam.width, beam_height)), + ..default() + }, + transform: Transform::from_xyz(0.0, -beam_height/2.0, 0.0), + ..default() + }); + }); + } + } + + // If beam is active, check if player is in beam's path + if tractor_beam.active { + if let Ok((player_entity, player_transform)) = player_query.get_single() { + // Check if player is roughly under the boss + if (player_transform.translation.x - boss_transform.translation.x).abs() < tractor_beam.width / 2.0 { + // Player is in the beam! Capture them + println!("Player captured by boss {:?}!", boss_entity); + + // Add Captured component to player + commands.entity(player_entity).insert(Captured { + boss_entity, + timer: Timer::new(Duration::from_secs_f32(CAPTURE_DURATION), TimerMode::Once), + }); + + // Boss returns to formation with captured player + commands.entity(boss_entity).remove::<TractorBeam>(); + + // TODO: Implement logic for boss to return to formation + // For now, just despawn the boss to simplify + commands.entity(boss_entity).despawn_recursive(); + break; + } + } + } + + // If beam timer finishes and player wasn't captured, end the beam attack + if tractor_beam.timer.finished() { + println!("Boss {:?} tractor beam expired", boss_entity); + commands.entity(boss_entity).remove::<TractorBeam>(); + + // If we had a proper visual beam, we'd despawn it here + // For now, just make sure we clean up by despawning all children + commands.entity(boss_entity).despawn_descendants(); + + // For simplicity, after beam attack the boss flies off-screen + // In a more complete implementation, it might return to formation + } + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8a1ba75..e074663 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,11 +21,11 @@ use game_state::{ }; use player::{ check_player_enemy_collisions, manage_invincibility, move_player, player_shoot, - respawn_player, + respawn_player, handle_captured_player, }; use enemy::{ check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies, - trigger_attack_dives, + trigger_attack_dives, boss_capture_attack, }; use bullet::{ check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets, @@ -36,75 +36,67 @@ use systems::{player_exists, player_vulnerable, setup, should_respawn_player, up fn main() { 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.1))) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - title: "Galaga :: Stage: 1 Lives: 3 Score: 0".into(), // Initial title + title: "BGLGA".into(), resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(), + resizable: false, ..default() }), ..default() })) - // Add Resources - .insert_resource(PlayerLives { - count: STARTING_LIVES, + // Add states + .init_state::<AppState>() // Changed from add_state to init_state + // Initialize game resources + .insert_resource(EnemySpawnTimer { + timer: Timer::new(Duration::from_secs_f32(1.0), TimerMode::Once), }) + .insert_resource(PlayerLives { count: STARTING_LIVES }) .insert_resource(PlayerRespawnTimer { timer: Timer::new( Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), TimerMode::Once, ), }) - .insert_resource(EnemySpawnTimer { - timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating), - }) .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 - } + total_spawned_this_stage: 0, + next_slot_index: 0, }) .insert_resource(AttackDiveTimer { - timer: { - let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating); - timer.pause(); // Start paused - timer - }, + timer: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Once), }) - // Add Systems + .insert_resource(StageConfigurations::default()) // Use default stages for now + // Add startup systems .add_systems(Startup, setup) - // Systems running only when Playing + // Core game systems .add_systems( Update, ( - // Player systems - move_player, + update_window_title, + // Enemy and player systems + spawn_enemies, + move_player.run_if(player_exists), player_shoot.run_if(player_exists), check_player_enemy_collisions.run_if(player_vulnerable), respawn_player.run_if(should_respawn_player), manage_invincibility, + handle_captured_player, // New system for handling captured player // Bullet systems 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) + boss_capture_attack, // New system for boss tractor beam ) .run_if(in_state(AppState::Playing)), ) @@ -119,18 +111,9 @@ fn main() { .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) - .add_systems( - Update, - ( - update_window_title, // Keep title updated - // TODO: Add system to check for restart input in GameOver state - bevy::window::close_on_esc, // Allow closing anytime - ), - ) - // Systems for entering/exiting states + // UI and state management systems .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui) + .add_systems(OnExit(AppState::Playing), cleanup_game_entities) .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui) - .add_systems(OnExit(AppState::Playing), cleanup_game_entities) // Cleanup when leaving Playing .run(); } diff --git a/src/player.rs b/src/player.rs index 3554e8a..acb1b96 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use std::time::Duration; -use crate::components::{Bullet, Enemy, Invincible, Player}; +use crate::components::{Bullet, Enemy, Invincible, Player, Captured}; use crate::constants::{ BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE, PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH, @@ -42,7 +42,7 @@ pub fn spawn_player_ship(commands: &mut Commands) { pub fn move_player( keyboard_input: Res<ButtonInput<KeyCode>>, - mut query: Query<(&mut Transform, &Player)>, + mut query: Query<(&mut Transform, &Player), Without<Captured>>, // Don't move captured players with controls time: Res<Time>, ) { // Using get_single_mut handles the case where player might not exist yet (or was just destroyed) @@ -66,9 +66,94 @@ pub fn move_player( } } +// New system to handle captured player movement +pub fn handle_captured_player( + mut commands: Commands, + time: Res<Time>, + player_query: Query<(Entity, &Transform, &Captured)>, + mut player_mut_query: Query<(&mut Transform, &mut Captured)>, + enemy_query: Query<&Transform, With<Enemy>>, + mut lives: ResMut<PlayerLives>, + mut respawn_timer: ResMut<PlayerRespawnTimer>, + mut next_state: ResMut<NextState<AppState>>, +) { + // First, collect data from all captured players + let mut to_process = Vec::new(); + + for (entity, transform, captured) in player_query.iter() { + // Check if the boss exists + let boss_exists = enemy_query.get(captured.boss_entity).is_ok(); + let boss_pos = if boss_exists { + enemy_query.get(captured.boss_entity).map(|t| t.translation).ok() + } else { + None + }; + + // Create a copy of the timer to check if it would finish + let mut timer_copy = captured.timer.clone(); + timer_copy.tick(time.delta()); + + to_process.push((entity, transform.translation, boss_pos, timer_copy.finished())); + } + + // Now process each player separately + for (entity, current_pos, boss_pos_opt, timer_would_finish) in to_process { + if let Ok((mut transform, mut captured)) = player_mut_query.get_mut(entity) { + // Tick the real timer + captured.timer.tick(time.delta()); + + match boss_pos_opt { + Some(boss_pos) => { + // Boss exists, update player position + let target_pos = boss_pos - Vec3::new(0.0, PLAYER_SIZE.y + 10.0, 0.0); + transform.translation = current_pos.lerp(target_pos, 0.2); + }, + None => { + // Boss is gone, release player but lose a life + println!("Boss is gone, releasing captured player!"); + commands.entity(entity).remove::<Captured>(); + lose_life_and_respawn(&mut commands, &mut lives, &mut respawn_timer, &mut next_state, entity); + } + } + + // If capture duration expires, player escapes but loses a life + if timer_would_finish || captured.timer.finished() { + println!("Player escaped from capture after timer expired!"); + commands.entity(entity).remove::<Captured>(); + lose_life_and_respawn(&mut commands, &mut lives, &mut respawn_timer, &mut next_state, entity); + } + } + } +} + +// Helper function for player life loss and respawn logic +fn lose_life_and_respawn( + commands: &mut Commands, + lives: &mut ResMut<PlayerLives>, + respawn_timer: &mut ResMut<PlayerRespawnTimer>, + next_state: &mut ResMut<NextState<AppState>>, + player_entity: Entity, +) { + // Lose a life + lives.count = lives.count.saturating_sub(1); + println!("Lives remaining: {}", lives.count); + + // Destroy player + commands.entity(player_entity).despawn(); + + 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); + } +} + 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 query: Query<(&Transform, &mut Player), Without<Captured>>, // Only non-captured players can shoot mut commands: Commands, time: Res<Time>, ) { @@ -107,7 +192,7 @@ pub fn check_player_enemy_collisions( 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>)>, + player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>, Without<Captured>)>, // Don't check collisions for captured players enemy_query: Query<(Entity, &Transform), With<Enemy>>, ) { // This system only runs if player exists and is not invincible, due to run_if @@ -133,7 +218,7 @@ pub fn check_player_enemy_collisions( println!("Respawn timer started."); } else { println!("GAME OVER!"); - next_state.set(AppState::GameOver); // Transition to GameOver state + next_state.set(AppState::GameOver); // Updated for newer Bevy states API } // Important: Break after handling one collision per frame for the player break; diff --git a/src/resources.rs b/src/resources.rs index 7a21d4e..3483422 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use std::time::Duration; // Needed for Timer // --- Resources --- #[derive(Resource)] @@ -32,8 +31,10 @@ impl Default for FormationLayout { 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); + 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 { @@ -82,15 +83,22 @@ impl Default for StageConfigurations { 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)); + positions.push(Vec3::new( + angle.cos() * radius, + center_y + angle.sin() * radius, + 0.0, + )); + } + FormationLayout { + name: "Circle".to_string(), + positions, } - 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 + attack_dive_interval: 2.5, // Faster dives enemy_speed_multiplier: 1.2, // Faster enemies enemy_shoot_interval: ENEMY_SHOOT_INTERVAL * 0.8, // Faster shooting }; @@ -123,4 +131,4 @@ pub struct FormationState { #[derive(Resource)] pub struct AttackDiveTimer { pub timer: Timer, -} \ No newline at end of file +} diff --git a/src/systems.rs b/src/systems.rs index ce23cb1..d87a2a7 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use crate::components::{Invincible, Player}; -use crate::resources::{CurrentStage, FormationState, PlayerLives, Score}; +use crate::resources::{CurrentStage, PlayerLives, Score}; use crate::player::spawn_player_ship; // Import the helper function // --- Setup ---