feat: Implement enemy formation behavior and attack dive mechanics

This commit is contained in:
Harald Hoyer 2025-04-05 00:26:22 +02:00
parent fe5579727f
commit 4256b0046c
2 changed files with 114 additions and 43 deletions

View file

@ -59,9 +59,9 @@ nix develop --command bash -c "cargo build"
**2. Enemy Behavior - Formations & Attack Patterns:** **2. Enemy Behavior - Formations & Attack Patterns:**
* **Enemy Formations:** This is a core Galaga feature. * **Enemy Formations:** This is a core Galaga feature.
* Define target positions for the enemy formation on screen. * ~~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). * ~~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. * 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:** * **Enemy Attack Dives:**
* Give enemies an `Attacking` state/component. * Give enemies an `Attacking` state/component.
* Periodically trigger enemies in the formation to switch to the `Attacking` state. * Periodically trigger enemies in the formation to switch to the `Attacking` state.

View file

@ -54,6 +54,13 @@ struct Invincible {
struct FormationTarget { struct FormationTarget {
position: Vec3, position: Vec3,
} }
#[derive(Component, Clone, PartialEq)]
enum EnemyState {
Entering, // Flying onto the screen towards formation target
InFormation, // Holding position in the formation
Attacking, // Diving towards the player (to be implemented)
}
// --- Resources --- // --- Resources ---
#[derive(Resource)] #[derive(Resource)]
struct EnemySpawnTimer { struct EnemySpawnTimer {
@ -85,6 +92,11 @@ struct FormationState {
next_slot_index: usize, next_slot_index: usize,
total_spawned_this_stage: usize, total_spawned_this_stage: usize,
} }
#[derive(Resource)]
struct AttackDiveTimer {
timer: Timer,
}
// --- Game Over UI --- // --- Game Over UI ---
#[derive(Component)] #[derive(Component)]
@ -199,6 +211,7 @@ fn main() {
.insert_resource(Score { value: 0 }) // Initialize score .insert_resource(Score { value: 0 }) // Initialize score
.insert_resource(CurrentStage { number: 1, waiting_for_clear: false }) // Initialize stage and flag .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(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)
// Add Systems // Add Systems
.add_systems(Startup, setup) .add_systems(Startup, setup)
// Systems running only when Playing // Systems running only when Playing
@ -212,6 +225,7 @@ fn main() {
check_player_enemy_collisions.run_if(player_vulnerable), check_player_enemy_collisions.run_if(player_vulnerable),
respawn_player.run_if(should_respawn_player), respawn_player.run_if(should_respawn_player),
manage_invincibility, manage_invincibility,
trigger_attack_dives, // Add system to trigger dives
// check_stage_clear is now added separately below using .pipe(system) // check_stage_clear is now added separately below using .pipe(system)
// Game Over check is now implicit in check_player_enemy_collisions // Game Over check is now implicit in check_player_enemy_collisions
).run_if(in_state(AppState::Playing))) ).run_if(in_state(AppState::Playing)))
@ -353,11 +367,12 @@ fn spawn_enemies(
mut stage: ResMut<CurrentStage>, mut stage: ResMut<CurrentStage>,
mut formation_state: ResMut<FormationState>, // Add formation state resource mut formation_state: ResMut<FormationState>, // Add formation state resource
) { ) {
// Tick the timer every frame
timer.timer.tick(time.delta()); timer.timer.tick(time.delta());
// Only spawn if we haven't spawned the full formation for this stage yet // Only spawn if we haven't spawned the full formation for this stage yet
// AND the timer is finished // AND the timer just finished this frame
if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT && timer.timer.tick(time.delta()).just_finished() { if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT && timer.timer.just_finished() {
// Calculate grid position // Calculate grid position
let slot_index = formation_state.next_slot_index; let slot_index = formation_state.next_slot_index;
let row = slot_index / FORMATION_COLS; let row = slot_index / FORMATION_COLS;
@ -383,7 +398,8 @@ fn spawn_enemies(
..default() ..default()
}, },
Enemy, Enemy,
FormationTarget { position: target_pos }, // Add the target position FormationTarget { position: target_pos },
EnemyState::Entering, // Start in Entering state
)); ));
formation_state.next_slot_index = (slot_index + 1) % FORMATION_ENEMY_COUNT; // Cycle through slots if needed, though we stop spawning formation_state.next_slot_index = (slot_index + 1) % FORMATION_ENEMY_COUNT; // Cycle through slots if needed, though we stop spawning
@ -403,16 +419,22 @@ fn spawn_enemies(
fn move_enemies( fn move_enemies(
// Query now includes FormationTarget // Query now includes FormationTarget
mut query: Query<(Entity, &mut Transform, &FormationTarget), With<Enemy>>, // Query now includes EnemyState, but FormationTarget is optional
time: Res<Time>, // We need separate queries or logic paths for different states
mut commands: Commands, mut entering_query: Query<(Entity, &mut Transform, &FormationTarget, &mut EnemyState), (With<Enemy>, With<FormationTarget>)>,
stage: Res<CurrentStage>, // Add stage resource for speed calculation 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 // Increase speed based on stage number
let current_speed = ENEMY_SPEED + (stage.number - 1) as f32 * 10.0; 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 let arrival_threshold = current_speed * time.delta_seconds() * 1.1; // Slightly more than one frame's movement
for (entity, mut transform, target) in 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; let current_pos = transform.translation;
let target_pos = target.position; let target_pos = target.position;
let direction = target_pos - current_pos; let direction = target_pos - current_pos;
@ -421,21 +443,36 @@ fn move_enemies(
if distance < arrival_threshold { if distance < arrival_threshold {
// Arrived at target // Arrived at target
transform.translation = target_pos; transform.translation = target_pos;
commands.entity(entity).remove::<FormationTarget>(); // Remove target so it stops moving commands.entity(entity).remove::<FormationTarget>(); // Remove target component
// TODO: Add an InFormation component/state later for attack dives *state = EnemyState::InFormation; // Change state
println!("Enemy {} reached formation target.", entity.index()); println!("Enemy {:?} reached formation target and is now InFormation.", entity);
} else { } else {
// Move towards target // Move towards target
let move_delta = direction.normalize() * current_speed * time.delta_seconds(); let move_delta = direction.normalize() * current_speed * time.delta_seconds();
transform.translation += move_delta; transform.translation += move_delta;
} }
}
}
// Still despawn if somehow it goes way off screen (shouldn't happen with target seeking) // --- Handle Attacking Enemies ---
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y * 2.0 { let attack_speed = current_speed * 1.5; // Make them dive faster
println!("Despawning enemy {} that went off screen.", entity.index()); 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,
// maybe query state separately or adjust the query structure.
// For now, assume this query correctly gets Attacking enemies.
// Move straight down
transform.translation.y -= attack_speed * time.delta_seconds();
// Despawn if off screen
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
println!("Despawning attacking enemy {:?} that went off screen.", entity);
commands.entity(entity).despawn(); commands.entity(entity).despawn();
// TODO: Later, attacking enemies might return to formation or loop
} }
} }
} }
fn check_bullet_collisions( fn check_bullet_collisions(
@ -543,6 +580,40 @@ fn manage_invincibility(
**vis = Visibility::Visible; **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
}
}
}
}
} }
} }