diff --git a/.gitignore b/.gitignore index 2768870..7e17b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ /.direnv /logs /.idea -result diff --git a/AGENT.md b/.goosehints similarity index 100% rename from AGENT.md rename to .goosehints diff --git a/.vscode/mcp.json b/.vscode/mcp.json deleted file mode 100644 index 588f96f..0000000 --- a/.vscode/mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "servers": { - "cratedocs": { - "type": "stdio", - "command": "/home/harald/.cargo/bin/cratedocs", - "args": [ - "stdio" - ] - } - } -} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..2145a5c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +.goosehints \ No newline at end of file diff --git a/README.md b/README.md index 869b4fa..8b33cbf 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,73 @@ 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 deleted file mode 100644 index a41b72a..0000000 --- a/TODO.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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 974f1fa..6b02234 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use std::time::Duration; // Needed for Timer in Player // --- Components --- #[derive(Component)] @@ -27,24 +28,6 @@ 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, @@ -53,17 +36,16 @@ 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 - CaptureBeam, // New pattern: Boss dives and attempts to capture the player with a tractor beam - // Add more patterns later (e.g., FigureEight, Looping) + // 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 } @@ -72,4 +54,4 @@ pub struct EnemyBullet; // Game Over UI Component (might move to ui.rs later if more UI exists) #[derive(Component)] -pub struct GameOverUI; +pub struct GameOverUI; \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs index 4c3ec09..2b5bd8e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,5 +1,4 @@ use bevy::math::Vec2; -use bevy::prelude::*; // --- Constants --- pub const WINDOW_WIDTH: f32 = 600.0; @@ -32,9 +31,3 @@ 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 a497e15..5cacf51 100644 --- a/src/enemy.rs +++ b/src/enemy.rs @@ -1,15 +1,17 @@ use bevy::prelude::*; use std::time::Duration; -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::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, - StageConfigurations, + 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, @@ -50,15 +52,8 @@ 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 - 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 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 { @@ -120,7 +115,6 @@ pub fn move_enemies( mut commands: Commands, stage: Res, stage_configs: Res, // 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(); @@ -138,7 +132,6 @@ 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(); @@ -160,121 +153,56 @@ pub fn move_enemies( } // --- Handle Attacking Enemies --- - 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 }; + // Note: attack_speed calculated above using multiplier + for (entity, mut transform, state, _enemy) in attacking_query.iter_mut() { - transform.translation.y -= vertical_movement; - transform.translation.x += horizontal_movement; + // 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(); - // 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 + 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 }; - 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(); - } + 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; } } - 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(); - } - } + 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 { @@ -334,12 +262,13 @@ pub fn check_formation_complete( } } -use crate::components::{AttackPattern, Player}; // Import the new enum and Player +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, time: Res