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:**
* **Enemy Formations:** This is a core Galaga feature.
* Define target positions for the enemy formation on screen.
* Give enemies an `Entering` state/component: They fly onto the screen following predefined paths (curves, waypoints).
* Give enemies a `Formation` state/component: Once they reach their target position, they stop and hold formation.
* ~~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.
* Periodically trigger enemies in the formation to switch to the `Attacking` state.

View file

@ -52,7 +52,14 @@ struct Invincible {
#[derive(Component)]
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 ---
#[derive(Resource)]
@ -83,7 +90,12 @@ struct CurrentStage {
#[derive(Resource)]
struct FormationState {
next_slot_index: usize,
total_spawned_this_stage: usize,
total_spawned_this_stage: usize,
}
#[derive(Resource)]
struct AttackDiveTimer {
timer: Timer,
}
// --- Game Over UI ---
@ -198,8 +210,9 @@ fn main() {
})
.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
// Add Systems
.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(Startup, setup)
// Systems running only when Playing
.add_systems(Update, (
@ -211,8 +224,9 @@ fn main() {
check_bullet_collisions,
check_player_enemy_collisions.run_if(player_vulnerable),
respawn_player.run_if(should_respawn_player),
manage_invincibility,
// check_stage_clear is now added separately below using .pipe(system)
manage_invincibility,
trigger_attack_dives, // Add system to trigger dives
// 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
@ -353,11 +367,12 @@ fn spawn_enemies(
mut stage: ResMut<CurrentStage>,
mut formation_state: ResMut<FormationState>, // Add formation state resource
) {
timer.timer.tick(time.delta());
// Tick the timer every frame
timer.timer.tick(time.delta());
// Only spawn if we haven't spawned the full formation for this stage yet
// AND the timer is finished
if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT && timer.timer.tick(time.delta()).just_finished() {
// AND the timer just finished this frame
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;
@ -382,9 +397,10 @@ fn spawn_enemies(
transform: Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
..default()
},
Enemy,
FormationTarget { position: target_pos }, // Add the target position
));
Enemy,
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.total_spawned_this_stage += 1;
@ -403,39 +419,60 @@ fn spawn_enemies(
fn move_enemies(
// Query now includes FormationTarget
mut query: Query<(Entity, &mut Transform, &FormationTarget), With<Enemy>>,
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
for (entity, mut transform, target) in query.iter_mut() {
let current_pos = transform.translation;
let target_pos = target.position;
let direction = target_pos - current_pos;
let distance = direction.length();
// --- 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 target_pos = target.position;
let direction = target_pos - current_pos;
let distance = direction.length();
if distance < arrival_threshold {
// Arrived at target
transform.translation = target_pos;
commands.entity(entity).remove::<FormationTarget>(); // Remove target so it stops moving
// TODO: Add an InFormation component/state later for attack dives
println!("Enemy {} reached formation target.", entity.index());
} else {
// Move towards target
let move_delta = direction.normalize() * current_speed * time.delta_seconds();
transform.translation += move_delta;
}
if distance < arrival_threshold {
// Arrived at target
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);
} else {
// Move towards target
let move_delta = direction.normalize() * current_speed * time.delta_seconds();
transform.translation += move_delta;
}
}
}
// Still despawn if somehow it goes way off screen (shouldn't happen with target seeking)
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y * 2.0 {
println!("Despawning enemy {} that went off screen.", entity.index());
commands.entity(entity).despawn();
}
}
// --- 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,
// 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();
// TODO: Later, attacking enemies might return to formation or loop
}
}
}
fn check_bullet_collisions(
@ -541,8 +578,42 @@ fn manage_invincibility(
// Ensure player is visible when invincibility ends
if let Some(ref mut vis) = visibility {
**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
}
}
}
}
}
}