Compare commits

..

No commits in common. "008f9cc24a69fed867bda84ec2af8d42d026565d" and "9fa4a14d5414922e13438190d2a992404bfc437b" have entirely different histories.

13 changed files with 201 additions and 472 deletions

1
.gitignore vendored
View file

@ -2,4 +2,3 @@
/.direnv
/logs
/.idea
result

11
.vscode/mcp.json vendored
View file

@ -1,11 +0,0 @@
{
"servers": {
"cratedocs": {
"type": "stdio",
"command": "/home/harald/.cargo/bin/cratedocs",
"args": [
"stdio"
]
}
}
}

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
.goosehints

View file

@ -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.

69
TODO.md
View file

@ -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.

View file

@ -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,
@ -56,7 +39,6 @@ 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
CaptureBeam, // New pattern: Boss dives and attempts to capture the player with a tractor beam
// Add more patterns later (e.g., FigureEight, Looping)
}

View file

@ -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

View file

@ -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<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();
@ -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,23 +153,23 @@ 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 ...
// 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 => {
// ... existing code ...
let vertical_movement = attack_speed * time.delta_seconds();
// 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 * time.delta_seconds()
attack_speed * horizontal_speed_factor * delta_seconds
} else if transform.translation.x > 0.0 {
-attack_speed * horizontal_speed_factor * time.delta_seconds()
-attack_speed * horizontal_speed_factor * delta_seconds
} else { 0.0 };
transform.translation.y -= vertical_movement;
@ -189,92 +182,27 @@ pub fn move_enemies(
}
}
AttackPattern::DirectDive => {
transform.translation.y -= attack_speed * time.delta_seconds();
// Move straight down
transform.translation.y -= attack_speed * 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;
AttackPattern::Kamikaze(target_pos) => {
// Move towards the target position
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
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 * time.delta_seconds();
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
}
}
// 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();
}
}
}
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 {
@ -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<AttackDiveTimer>,
time: Res<Time>,
mut enemy_query: Query<(Entity, &mut EnemyState, &Enemy)>, // Added Enemy component to check type
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
@ -349,42 +278,29 @@ 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, EnemyType)> = Vec::new();
for (entity, state, enemy) in enemy_query.iter() {
// Check the state correctly and store enemy type
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, enemy.enemy_type));
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, enemy_type) = available_enemies[random_index];
let chosen_entity = available_enemies[random_index];
// 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
// Select a random attack pattern for this stage
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]
}
};
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 {
@ -397,8 +313,9 @@ 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)
@ -449,80 +366,3 @@ pub fn enemy_shoot(
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
}
}
}

View file

@ -21,11 +21,11 @@ use game_state::{
};
use player::{
check_player_enemy_collisions, manage_invincibility, move_player, player_shoot,
respawn_player, handle_captured_player,
respawn_player,
};
use enemy::{
check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies,
trigger_attack_dives, boss_capture_attack,
trigger_attack_dives,
};
use bullet::{
check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets,
@ -36,67 +36,75 @@ use systems::{player_exists, player_vulnerable, setup, should_respawn_player, up
fn main() {
App::new()
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.1)))
.init_state::<AppState>() // Initialize the AppState
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BGLGA".into(),
title: "Galaga :: Stage: 1 Lives: 3 Score: 0".into(), // Initial title
resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
resizable: false,
..default()
}),
..default()
}))
// 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),
// Add Resources
.insert_resource(PlayerLives {
count: STARTING_LIVES,
})
.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 {
formation_complete: false,
total_spawned_this_stage: 0,
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: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Once),
timer: {
let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating);
timer.pause(); // Start paused
timer
},
})
.insert_resource(StageConfigurations::default()) // Use default stages for now
// Add startup systems
// Add Systems
.add_systems(Startup, setup)
// Core game systems
// Systems running only when Playing
.add_systems(
Update,
(
update_window_title,
// Enemy and player systems
spawn_enemies,
move_player.run_if(player_exists),
// Player systems
move_player,
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)),
)
@ -111,9 +119,18 @@ fn main() {
.chain() // Ensure these run in order if needed, check_formation first
.run_if(in_state(AppState::Playing)),
)
// UI and state management systems
// 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
.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();
}

View file

@ -1,7 +1,7 @@
use bevy::prelude::*;
use std::time::Duration;
use crate::components::{Bullet, Enemy, Invincible, Player, Captured};
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,
@ -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), Without<Captured>>, // Don't move captured players with controls
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)
@ -66,94 +66,9 @@ 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), Without<Captured>>, // Only non-captured players can shoot
mut query: Query<(&Transform, &mut Player)>, // Should only run if player exists due to run_if
mut commands: Commands,
time: Res<Time>,
) {
@ -192,7 +107,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>, Without<Captured>)>, // Don't check collisions for captured players
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
@ -218,7 +133,7 @@ pub fn check_player_enemy_collisions(
println!("Respawn timer started.");
} else {
println!("GAME OVER!");
next_state.set(AppState::GameOver); // Updated for newer Bevy states API
next_state.set(AppState::GameOver); // Transition to GameOver state
}
// Important: Break after handling one collision per frame for the player
break;

View file

@ -1,4 +1,5 @@
use bevy::prelude::*;
use std::time::Duration; // Needed for Timer
// --- Resources ---
#[derive(Resource)]
@ -31,10 +32,8 @@ 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 {
@ -83,16 +82,9 @@ 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,
));
}
FormationLayout {
name: "Circle".to_string(),
positions,
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,

View file

@ -1,7 +1,7 @@
use bevy::prelude::*;
use crate::components::{Invincible, Player};
use crate::resources::{CurrentStage, PlayerLives, Score};
use crate::resources::{CurrentStage, FormationState, PlayerLives, Score};
use crate::player::spawn_player_ship; // Import the helper function
// --- Setup ---