feat: Implement enemy attack dive mechanics and shooting behavior
This commit is contained in:
parent
4256b0046c
commit
66fd1e8b1b
10
README.md
10
README.md
|
@ -63,11 +63,11 @@ nix develop --command bash -c "cargo build"
|
|||
* ~~Give enemies an `Entering` state/component: They fly onto the screen following predefined paths (curves, waypoints).~~ **(DONE - Basic linear path implemented)**
|
||||
* Give enemies a `Formation` state/component: Once they reach their target position, they stop and hold formation. **(DONE - Stop implemented by removing FormationTarget)**
|
||||
* **Enemy Attack Dives:**
|
||||
* Give enemies an `Attacking` state/component.
|
||||
* Periodically trigger enemies in the formation to switch to the `Attacking` state.
|
||||
* Define attack paths (swooping dives towards the player area).
|
||||
* Make enemies fire bullets (downwards or towards the player) during their dives.
|
||||
* After an attack dive, enemies could return to their formation position or fly off-screen.
|
||||
* ~~Give enemies an `Attacking` state/component.~~ **(DONE)**
|
||||
* ~~Periodically trigger enemies in the formation to switch to the `Attacking` state.~~ **(DONE - Random selection after formation complete)**
|
||||
* Define attack paths (swooping dives towards the player area). **(Basic downward dive implemented)**
|
||||
* ~~Make enemies fire bullets (downwards or towards the player) during their dives.~~ **(DONE - Downward)**
|
||||
* After an attack dive, enemies could return to their formation position or fly off-screen. **(Fly off-screen implemented)**
|
||||
* **Enemy Variety:**
|
||||
* Introduce different types of enemies (e.g., using different components or an enum).
|
||||
* Assign different behaviors, point values, and maybe sprites to each type.
|
||||
|
|
388
src/main.rs
388
src/main.rs
|
@ -15,7 +15,10 @@ const BULLET_SPEED: f32 = 500.0;
|
|||
const ENEMY_SPEED: f32 = 100.0;
|
||||
const PLAYER_SIZE: Vec2 = Vec2::new(30.0, 30.0);
|
||||
const ENEMY_SIZE: Vec2 = Vec2::new(40.0, 40.0);
|
||||
const BULLET_SIZE: Vec2 = Vec2::new(5.0, 15.0);
|
||||
const BULLET_SIZE: Vec2 = Vec2::new(5.0, 15.0); // Player bullet
|
||||
const ENEMY_BULLET_SIZE: Vec2 = Vec2::new(8.0, 8.0); // Enemy bullet
|
||||
const ENEMY_BULLET_SPEED: f32 = 300.0;
|
||||
const ENEMY_SHOOT_INTERVAL: f32 = 1.5; // Seconds between shots for an attacking enemy
|
||||
|
||||
// Formation constants
|
||||
const FORMATION_ROWS: usize = 4;
|
||||
|
@ -31,6 +34,7 @@ const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0;
|
|||
// Collision thresholds
|
||||
const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) * 0.5; // 22.5
|
||||
const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5; // 35.0
|
||||
const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 = (ENEMY_BULLET_SIZE.x + PLAYER_SIZE.x) * 0.5; // ~19.0
|
||||
|
||||
// --- Components ---
|
||||
#[derive(Component)]
|
||||
|
@ -43,7 +47,9 @@ struct Player {
|
|||
struct Bullet;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Enemy;
|
||||
struct Enemy {
|
||||
shoot_cooldown: Timer,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Invincible {
|
||||
|
@ -59,8 +65,11 @@ struct FormationTarget {
|
|||
enum EnemyState {
|
||||
Entering, // Flying onto the screen towards formation target
|
||||
InFormation, // Holding position in the formation
|
||||
Attacking, // Diving towards the player (to be implemented)
|
||||
Attacking, // Diving towards the player
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct EnemyBullet;
|
||||
// --- Resources ---
|
||||
#[derive(Resource)]
|
||||
struct EnemySpawnTimer {
|
||||
|
@ -91,6 +100,7 @@ struct CurrentStage {
|
|||
struct FormationState {
|
||||
next_slot_index: usize,
|
||||
total_spawned_this_stage: usize,
|
||||
formation_complete: bool, // Flag to indicate if all enemies are in position
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
|
@ -180,6 +190,7 @@ fn check_stage_clear(world: &mut World) {
|
|||
if let Some(mut formation_state) = world.get_resource_mut::<FormationState>() {
|
||||
formation_state.next_slot_index = 0;
|
||||
formation_state.total_spawned_this_stage = 0;
|
||||
formation_state.formation_complete = false; // Reset flag for new stage
|
||||
}
|
||||
if let Some(mut spawn_timer) = world.get_resource_mut::<EnemySpawnTimer>() {
|
||||
spawn_timer.timer.reset();
|
||||
|
@ -187,6 +198,173 @@ fn check_stage_clear(world: &mut World) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Enemy Attack Logic ---
|
||||
|
||||
fn trigger_attack_dives(
|
||||
mut timer: ResMut<AttackDiveTimer>,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(Entity, &mut EnemyState), With<Enemy>>,
|
||||
formation_state: Res<FormationState>, // Read formation state
|
||||
) {
|
||||
timer.timer.tick(time.delta());
|
||||
|
||||
// Tick the timer regardless
|
||||
timer.timer.tick(time.delta());
|
||||
|
||||
// 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();
|
||||
for (entity, state) in query.iter() {
|
||||
if *state == EnemyState::InFormation {
|
||||
available_enemies.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are enemies available, pick one randomly
|
||||
if !available_enemies.is_empty() {
|
||||
let random_index = fastrand::usize(..available_enemies.len());
|
||||
let chosen_entity = available_enemies[random_index];
|
||||
|
||||
// Get the chosen enemy's state mutably and change it
|
||||
if let Ok((_, mut state)) = query.get_mut(chosen_entity) {
|
||||
println!("Enemy {:?} starting attack dive!", chosen_entity);
|
||||
*state = EnemyState::Attacking;
|
||||
// Timer will automatically repeat due to TimerMode::Repeating,
|
||||
// no need to reset here unless we wanted a different interval after the first dive.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enemy_shoot(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
// Query attacking enemies: need their transform and mutable Enemy component for the timer
|
||||
mut enemy_query: Query<(&Transform, &mut Enemy, &EnemyState), Without<FormationTarget>>,
|
||||
) {
|
||||
for (transform, mut enemy, state) in enemy_query.iter_mut() {
|
||||
// Only shoot if attacking
|
||||
if *state == EnemyState::Attacking {
|
||||
enemy.shoot_cooldown.tick(time.delta());
|
||||
if enemy.shoot_cooldown.finished() {
|
||||
println!("Enemy {:?} firing!", transform.translation); // Placeholder for entity ID if needed
|
||||
let bullet_start_pos = transform.translation
|
||||
- Vec3::Y * (ENEMY_SIZE.y / 2.0 + ENEMY_BULLET_SIZE.y / 2.0);
|
||||
|
||||
commands.spawn((
|
||||
SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: Color::rgb(1.0, 0.5, 0.5), // Different color for enemy bullets
|
||||
custom_size: Some(ENEMY_BULLET_SIZE),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_translation(bullet_start_pos),
|
||||
..default()
|
||||
},
|
||||
EnemyBullet,
|
||||
));
|
||||
|
||||
// Reset the timer for the next shot
|
||||
enemy.shoot_cooldown.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_enemy_bullets(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(Entity, &mut Transform), With<EnemyBullet>>,
|
||||
) {
|
||||
for (entity, mut transform) in query.iter_mut() {
|
||||
transform.translation.y -= ENEMY_BULLET_SPEED * time.delta_seconds();
|
||||
|
||||
// Despawn if off screen (bottom)
|
||||
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_BULLET_SIZE.y {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System to check if all spawned enemies have reached their formation position
|
||||
fn check_formation_complete(
|
||||
mut formation_state: ResMut<FormationState>,
|
||||
enemy_query: Query<&EnemyState, With<Enemy>>, // Query states directly
|
||||
mut attack_dive_timer: ResMut<AttackDiveTimer>, // Add timer to reset it
|
||||
) {
|
||||
// Only run the check if the formation isn't already marked as complete
|
||||
if !formation_state.formation_complete {
|
||||
// Check if all enemies for the stage have been spawned
|
||||
if formation_state.total_spawned_this_stage == FORMATION_ENEMY_COUNT {
|
||||
// Check if any enemies are still in the Entering state
|
||||
let mut any_entering = false;
|
||||
for state in enemy_query.iter() {
|
||||
if *state == EnemyState::Entering {
|
||||
any_entering = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all spawned and none are entering, formation is complete
|
||||
if !any_entering {
|
||||
println!(
|
||||
"Formation complete! Resetting attack timer. (Spawned={})",
|
||||
formation_state.total_spawned_this_stage
|
||||
);
|
||||
formation_state.formation_complete = true;
|
||||
attack_dive_timer.timer.reset(); // Reset the dive timer to ensure a delay
|
||||
}
|
||||
// else { // Optional log
|
||||
// println!("Checking formation complete: Still entering...");
|
||||
// }
|
||||
}
|
||||
// else { // Optional log
|
||||
// println!("Checking formation complete: Not all spawned yet ({}/{})", formation_state.total_spawned_this_stage, FORMATION_ENEMY_COUNT);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// Check collisions between enemy bullets and the player
|
||||
fn check_enemy_bullet_player_collisions(
|
||||
mut commands: Commands,
|
||||
mut lives: ResMut<PlayerLives>,
|
||||
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
// Player query matching the run_if condition
|
||||
player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>,
|
||||
enemy_bullet_query: Query<(Entity, &Transform), With<EnemyBullet>>,
|
||||
) {
|
||||
if let Ok((player_entity, player_transform)) = player_query.get_single() {
|
||||
for (bullet_entity, bullet_transform) in enemy_bullet_query.iter() {
|
||||
let distance = player_transform
|
||||
.translation
|
||||
.distance(bullet_transform.translation);
|
||||
|
||||
if distance < ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD {
|
||||
println!("Player hit by enemy bullet!");
|
||||
commands.entity(bullet_entity).despawn(); // Despawn bullet
|
||||
|
||||
lives.count = lives.count.saturating_sub(1);
|
||||
println!("Lives remaining: {}", lives.count);
|
||||
|
||||
commands.entity(player_entity).despawn(); // Despawn player
|
||||
|
||||
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);
|
||||
}
|
||||
// Break because the player can only be hit once per frame
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new() // Start App builder
|
||||
.init_state::<AppState>() // Initialize the AppState *after* App::new()
|
||||
|
@ -200,22 +378,38 @@ fn main() {
|
|||
..default()
|
||||
}))
|
||||
// Add Resources
|
||||
.insert_resource(PlayerLives { count: STARTING_LIVES })
|
||||
.insert_resource(PlayerLives {
|
||||
count: STARTING_LIVES,
|
||||
})
|
||||
.insert_resource(PlayerRespawnTimer {
|
||||
// Start paused and finished
|
||||
timer: Timer::new(Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), TimerMode::Once)
|
||||
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 }) // Initialize score
|
||||
.insert_resource(CurrentStage { number: 1, waiting_for_clear: false }) // Initialize stage and flag
|
||||
.insert_resource(FormationState { next_slot_index: 0, total_spawned_this_stage: 0 }) // Initialize formation state
|
||||
.insert_resource(AttackDiveTimer { timer: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating)}) // Initialize attack timer (e.g., every 3 seconds)
|
||||
.insert_resource(CurrentStage {
|
||||
number: 1,
|
||||
waiting_for_clear: false,
|
||||
}) // Initialize stage and flag
|
||||
.insert_resource(FormationState {
|
||||
next_slot_index: 0,
|
||||
total_spawned_this_stage: 0,
|
||||
formation_complete: false,
|
||||
}) // Initialize formation state with flag
|
||||
.insert_resource(AttackDiveTimer {
|
||||
timer: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating),
|
||||
}) // Initialize attack timer (e.g., every 3 seconds)
|
||||
// Add Systems
|
||||
.add_systems(Startup, setup)
|
||||
// Systems running only when Playing
|
||||
.add_systems(Update, (
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
move_player,
|
||||
player_shoot.run_if(player_exists),
|
||||
move_bullets,
|
||||
|
@ -225,17 +419,38 @@ fn main() {
|
|||
check_player_enemy_collisions.run_if(player_vulnerable),
|
||||
respawn_player.run_if(should_respawn_player),
|
||||
manage_invincibility,
|
||||
trigger_attack_dives, // Add system to trigger dives
|
||||
// check_formation_complete and trigger_attack_dives added separately below
|
||||
enemy_shoot, // Add enemy shooting system
|
||||
move_enemy_bullets,
|
||||
check_enemy_bullet_player_collisions.run_if(player_vulnerable), // Add enemy bullet collision check
|
||||
// check_stage_clear is now added separately below using .pipe(system)
|
||||
// Game Over check is now implicit in check_player_enemy_collisions
|
||||
).run_if(in_state(AppState::Playing)))
|
||||
.add_systems(Update, check_stage_clear.run_if(in_state(AppState::Playing))) // Add world-based system directly
|
||||
)
|
||||
.run_if(in_state(AppState::Playing)),
|
||||
)
|
||||
// Add formation check, dive trigger (with run condition), and stage clear separately
|
||||
.add_systems(
|
||||
Update,
|
||||
check_formation_complete.run_if(in_state(AppState::Playing)),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
trigger_attack_dives
|
||||
.run_if(in_state(AppState::Playing).and_then(is_formation_complete)),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
check_stage_clear.run_if(in_state(AppState::Playing)),
|
||||
) // check_stage_clear uses world access
|
||||
// Systems running regardless of state (or managing state transitions)
|
||||
.add_systems(Update, (
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_window_title, // Keep title updated
|
||||
// Add system to check for restart input in GameOver state later
|
||||
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::GameOver), cleanup_game_over_ui)
|
||||
|
@ -252,13 +467,14 @@ fn player_vulnerable(query: Query<&Player, Without<Invincible>>) -> bool {
|
|||
!query.is_empty()
|
||||
}
|
||||
|
||||
fn should_respawn_player(
|
||||
lives: Res<PlayerLives>,
|
||||
player_query: Query<&Player>,
|
||||
) -> bool {
|
||||
fn should_respawn_player(lives: Res<PlayerLives>, player_query: Query<&Player>) -> bool {
|
||||
player_query.is_empty() && lives.count > 0
|
||||
}
|
||||
|
||||
// New run condition: Check if the formation is complete
|
||||
fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
|
||||
formation_state.formation_complete
|
||||
}
|
||||
|
||||
// --- Systems ---
|
||||
|
||||
|
@ -276,7 +492,11 @@ fn spawn_player_ship(commands: &mut Commands) {
|
|||
custom_size: Some(PLAYER_SIZE),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_translation(Vec3::new(0.0, -WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0, 0.0)),
|
||||
transform: Transform::from_translation(Vec3::new(
|
||||
0.0,
|
||||
-WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0,
|
||||
0.0,
|
||||
)),
|
||||
..default()
|
||||
},
|
||||
Player {
|
||||
|
@ -285,13 +505,15 @@ fn spawn_player_ship(commands: &mut Commands) {
|
|||
},
|
||||
// Player starts invincible for a short time
|
||||
Invincible {
|
||||
timer: Timer::new(Duration::from_secs_f32(PLAYER_INVINCIBILITY_DURATION), TimerMode::Once)
|
||||
}
|
||||
timer: Timer::new(
|
||||
Duration::from_secs_f32(PLAYER_INVINCIBILITY_DURATION),
|
||||
TimerMode::Once,
|
||||
),
|
||||
},
|
||||
));
|
||||
println!("Player spawned!");
|
||||
}
|
||||
|
||||
|
||||
fn move_player(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut query: Query<(&mut Transform, &Player)>,
|
||||
|
@ -311,7 +533,10 @@ fn move_player(
|
|||
|
||||
transform.translation.x += direction * player.speed * time.delta_seconds();
|
||||
let half_player_width = PLAYER_SIZE.x / 2.0;
|
||||
transform.translation.x = transform.translation.x.clamp(-WINDOW_WIDTH / 2.0 + half_player_width, WINDOW_WIDTH / 2.0 - half_player_width);
|
||||
transform.translation.x = transform.translation.x.clamp(
|
||||
-WINDOW_WIDTH / 2.0 + half_player_width,
|
||||
WINDOW_WIDTH / 2.0 - half_player_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,11 +549,14 @@ fn player_shoot(
|
|||
if let Ok((player_transform, mut player)) = query.get_single_mut() {
|
||||
player.shoot_cooldown.tick(time.delta());
|
||||
|
||||
if (keyboard_input.just_pressed(KeyCode::Space) || keyboard_input.just_pressed(KeyCode::ArrowUp))
|
||||
&& player.shoot_cooldown.finished() {
|
||||
if (keyboard_input.just_pressed(KeyCode::Space)
|
||||
|| keyboard_input.just_pressed(KeyCode::ArrowUp))
|
||||
&& player.shoot_cooldown.finished()
|
||||
{
|
||||
player.shoot_cooldown.reset();
|
||||
|
||||
let bullet_start_pos = player_transform.translation + Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
|
||||
let bullet_start_pos = player_transform.translation
|
||||
+ Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
|
||||
|
||||
commands.spawn((
|
||||
SpriteBundle {
|
||||
|
@ -372,7 +600,9 @@ fn spawn_enemies(
|
|||
|
||||
// Only spawn if we haven't spawned the full formation for this stage yet
|
||||
// AND the timer just finished this frame
|
||||
if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT && timer.timer.just_finished() {
|
||||
if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT
|
||||
&& timer.timer.just_finished()
|
||||
{
|
||||
// Calculate grid position
|
||||
let slot_index = formation_state.next_slot_index;
|
||||
let row = slot_index / FORMATION_COLS;
|
||||
|
@ -397,8 +627,16 @@ fn spawn_enemies(
|
|||
transform: Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
|
||||
..default()
|
||||
},
|
||||
Enemy,
|
||||
FormationTarget { position: target_pos },
|
||||
Enemy {
|
||||
// Initialize cooldown, maybe slightly randomized later
|
||||
shoot_cooldown: Timer::new(
|
||||
Duration::from_secs_f32(ENEMY_SHOOT_INTERVAL),
|
||||
TimerMode::Once,
|
||||
),
|
||||
},
|
||||
FormationTarget {
|
||||
position: target_pos,
|
||||
},
|
||||
EnemyState::Entering, // Start in Entering state
|
||||
));
|
||||
|
||||
|
@ -419,20 +657,26 @@ fn spawn_enemies(
|
|||
|
||||
fn move_enemies(
|
||||
// Query now includes FormationTarget
|
||||
// Query now includes EnemyState, but FormationTarget is optional
|
||||
// We need separate queries or logic paths for different states
|
||||
mut entering_query: Query<(Entity, &mut Transform, &FormationTarget, &mut EnemyState), (With<Enemy>, With<FormationTarget>)>,
|
||||
mut attacking_query: Query<(Entity, &mut Transform), (With<Enemy>, Without<FormationTarget>, With<EnemyState>)>, // Query attacking enemies separately
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
stage: Res<CurrentStage>, // Add stage resource for speed calculation
|
||||
// Query now includes EnemyState, but FormationTarget is optional
|
||||
// We need separate queries or logic paths for different states
|
||||
mut entering_query: Query<
|
||||
(Entity, &mut Transform, &FormationTarget, &mut EnemyState),
|
||||
(With<Enemy>, With<FormationTarget>),
|
||||
>,
|
||||
mut attacking_query: Query<
|
||||
(Entity, &mut Transform),
|
||||
(With<Enemy>, Without<FormationTarget>, With<EnemyState>),
|
||||
>, // Query attacking enemies separately
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
stage: Res<CurrentStage>, // Add stage resource for speed calculation
|
||||
) {
|
||||
// Increase speed based on stage number
|
||||
let current_speed = ENEMY_SPEED + (stage.number - 1) as f32 * 10.0;
|
||||
let arrival_threshold = current_speed * time.delta_seconds() * 1.1; // Slightly more than one frame's movement
|
||||
|
||||
// --- Handle Entering Enemies ---
|
||||
for (entity, mut transform, target, mut state) in entering_query.iter_mut() {
|
||||
// --- Handle Entering Enemies ---
|
||||
for (entity, mut transform, target, mut state) in entering_query.iter_mut() {
|
||||
// Ensure we only process Entering state here, though query filters it mostly
|
||||
if *state == EnemyState::Entering {
|
||||
let current_pos = transform.translation;
|
||||
|
@ -445,18 +689,21 @@ for (entity, mut transform, target, mut state) in entering_query.iter_mut() {
|
|||
transform.translation = target_pos;
|
||||
commands.entity(entity).remove::<FormationTarget>(); // Remove target component
|
||||
*state = EnemyState::InFormation; // Change state
|
||||
println!("Enemy {:?} reached formation target and is now InFormation.", entity);
|
||||
println!(
|
||||
"Enemy {:?} reached formation target and is now InFormation.",
|
||||
entity
|
||||
);
|
||||
} else {
|
||||
// Move towards target
|
||||
let move_delta = direction.normalize() * current_speed * time.delta_seconds();
|
||||
transform.translation += move_delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Handle Attacking Enemies ---
|
||||
let attack_speed = current_speed * 1.5; // Make them dive faster
|
||||
for (entity, mut transform) in attacking_query.iter_mut() {
|
||||
// --- Handle Attacking Enemies ---
|
||||
let attack_speed = current_speed * 1.5; // Make them dive faster
|
||||
for (entity, mut transform) in attacking_query.iter_mut() {
|
||||
// Check state again in case the query picks up something unintended
|
||||
// (Though With<EnemyState> and Without<FormationTarget> should be sufficient)
|
||||
// We need a way to get the state here if we want to be super safe,
|
||||
|
@ -468,11 +715,14 @@ for (entity, mut transform) in attacking_query.iter_mut() {
|
|||
|
||||
// Despawn if off screen
|
||||
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
|
||||
println!("Despawning attacking enemy {:?} that went off screen.", entity);
|
||||
println!(
|
||||
"Despawning attacking enemy {:?} that went off screen.",
|
||||
entity
|
||||
);
|
||||
commands.entity(entity).despawn();
|
||||
// TODO: Later, attacking enemies might return to formation or loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bullet_collisions(
|
||||
|
@ -483,7 +733,9 @@ fn check_bullet_collisions(
|
|||
) {
|
||||
for (bullet_entity, bullet_transform) in bullet_query.iter() {
|
||||
for (enemy_entity, enemy_transform) in enemy_query.iter() {
|
||||
let distance = bullet_transform.translation.distance(enemy_transform.translation);
|
||||
let distance = bullet_transform
|
||||
.translation
|
||||
.distance(enemy_transform.translation);
|
||||
|
||||
if distance < BULLET_ENEMY_COLLISION_THRESHOLD {
|
||||
commands.entity(bullet_entity).despawn();
|
||||
|
@ -509,7 +761,9 @@ fn check_player_enemy_collisions(
|
|||
// This system only runs if player exists and is not invincible, due to run_if
|
||||
if let Ok((player_entity, player_transform)) = player_query.get_single() {
|
||||
for (enemy_entity, enemy_transform) in enemy_query.iter() {
|
||||
let distance = player_transform.translation.distance(enemy_transform.translation);
|
||||
let distance = player_transform
|
||||
.translation
|
||||
.distance(enemy_transform.translation);
|
||||
|
||||
if distance < PLAYER_ENEMY_COLLISION_THRESHOLD {
|
||||
println!("Player hit!");
|
||||
|
@ -571,7 +825,6 @@ fn manage_invincibility(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
if invincible.timer.finished() {
|
||||
println!("Invincibility finished.");
|
||||
commands.entity(entity).remove::<Invincible>();
|
||||
|
@ -580,40 +833,6 @@ fn manage_invincibility(
|
|||
**vis = Visibility::Visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Enemy Attack Logic ---
|
||||
|
||||
fn trigger_attack_dives(
|
||||
mut timer: ResMut<AttackDiveTimer>,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(Entity, &mut EnemyState), With<Enemy>>, // Query enemies and their states
|
||||
) {
|
||||
timer.timer.tick(time.delta());
|
||||
|
||||
if timer.timer.just_finished() {
|
||||
// Find all enemies currently in formation
|
||||
let mut available_enemies: Vec<Entity> = Vec::new();
|
||||
for (entity, state) in query.iter() {
|
||||
if *state == EnemyState::InFormation {
|
||||
available_enemies.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are enemies available, pick one randomly
|
||||
if !available_enemies.is_empty() {
|
||||
let random_index = fastrand::usize(..available_enemies.len());
|
||||
let chosen_entity = available_enemies[random_index];
|
||||
|
||||
// Get the chosen enemy's state mutably and change it
|
||||
if let Ok((_, mut state)) = query.get_mut(chosen_entity) {
|
||||
println!("Enemy {:?} starting attack dive!", chosen_entity);
|
||||
*state = EnemyState::Attacking;
|
||||
// Timer will automatically repeat due to TimerMode::Repeating
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -628,6 +847,9 @@ fn update_window_title(
|
|||
// Update if lives, score, or stage changed
|
||||
if lives.is_changed() || score.is_changed() || stage.is_changed() {
|
||||
let mut window = windows.single_mut();
|
||||
window.title = format!("Galaga :: Stage: {} Lives: {} Score: {}", stage.number, lives.count, score.value);
|
||||
window.title = format!(
|
||||
"Galaga :: Stage: {} Lives: {} Score: {}",
|
||||
stage.number, lives.count, score.value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue