Compare commits
10 commits
9fa4a14d54
...
008f9cc24a
Author | SHA1 | Date | |
---|---|---|---|
|
008f9cc24a | ||
|
1d1b927007 | ||
|
037924a4f4 | ||
|
9aad8dd130 | ||
|
4727c370d2 | ||
|
4a64e12945 | ||
|
3c92823cb6 | ||
|
3520d5122b | ||
|
54d1fd66d2 | ||
|
c61a28de29 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
/.direnv
|
||||
/logs
|
||||
/.idea
|
||||
result
|
||||
|
|
11
.vscode/mcp.json
vendored
Normal file
11
.vscode/mcp.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"servers": {
|
||||
"cratedocs": {
|
||||
"type": "stdio",
|
||||
"command": "/home/harald/.cargo/bin/cratedocs",
|
||||
"args": [
|
||||
"stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
70
README.md
70
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.
|
||||
|
|
69
TODO.md
Normal file
69
TODO.md
Normal file
|
@ -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.
|
|
@ -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,
|
||||
|
@ -39,6 +56,7 @@ 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
256
src/enemy.rs
256
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,23 +160,23 @@ 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() {
|
||||
|
||||
// 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 {
|
||||
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 => {
|
||||
// Original Swooping Dive Logic
|
||||
let vertical_movement = attack_speed * delta_seconds;
|
||||
// ... 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 * delta_seconds
|
||||
attack_speed * horizontal_speed_factor * time.delta_seconds()
|
||||
} else if transform.translation.x > 0.0 {
|
||||
-attack_speed * horizontal_speed_factor * delta_seconds
|
||||
-attack_speed * horizontal_speed_factor * time.delta_seconds()
|
||||
} else { 0.0 };
|
||||
|
||||
transform.translation.y -= vertical_movement;
|
||||
|
@ -182,27 +189,92 @@ pub fn move_enemies(
|
|||
}
|
||||
}
|
||||
AttackPattern::DirectDive => {
|
||||
// Move straight down
|
||||
transform.translation.y -= attack_speed * delta_seconds;
|
||||
transform.translation.y -= attack_speed * time.delta_seconds();
|
||||
}
|
||||
AttackPattern::Kamikaze(target_pos) => {
|
||||
// Move towards the target position
|
||||
let direction = *target_pos - transform.translation;
|
||||
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 * delta_seconds * 1.1; // Threshold to stop near target
|
||||
let kamikaze_threshold = attack_speed * time.delta_seconds() * 1.1; // Threshold to stop near target
|
||||
|
||||
if distance > kamikaze_threshold {
|
||||
let move_delta = direction.normalize() * attack_speed * delta_seconds;
|
||||
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
|
||||
}
|
||||
}
|
||||
// Add cases for other patterns here
|
||||
} // Close inner if let
|
||||
} // Closes match enemy.enemy_type
|
||||
} // Closes if *state == EnemyState::Attacking
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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());
|
||||
let mut selected_pattern = current_config.attack_patterns[pattern_index]; // Copy the pattern
|
||||
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)
|
||||
|
@ -366,3 +449,80 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
67
src/main.rs
67
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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,9 +83,16 @@ 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,
|
||||
|
|
|
@ -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 ---
|
||||
|
|
Loading…
Reference in a new issue