feat: Implement enemy formation behavior and attack dive mechanics
This commit is contained in:
parent
fe5579727f
commit
4256b0046c
|
@ -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.
|
||||
|
|
101
src/main.rs
101
src/main.rs
|
@ -54,6 +54,13 @@ struct Invincible {
|
|||
struct FormationTarget {
|
||||
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)]
|
||||
struct EnemySpawnTimer {
|
||||
|
@ -85,6 +92,11 @@ struct FormationState {
|
|||
next_slot_index: usize,
|
||||
total_spawned_this_stage: usize,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct AttackDiveTimer {
|
||||
timer: Timer,
|
||||
}
|
||||
// --- Game Over UI ---
|
||||
|
||||
#[derive(Component)]
|
||||
|
@ -199,6 +211,7 @@ 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
|
||||
.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
|
||||
|
@ -212,6 +225,7 @@ 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_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)))
|
||||
|
@ -353,11 +367,12 @@ fn spawn_enemies(
|
|||
mut stage: ResMut<CurrentStage>,
|
||||
mut formation_state: ResMut<FormationState>, // Add formation state resource
|
||||
) {
|
||||
// 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;
|
||||
|
@ -383,7 +398,8 @@ fn spawn_enemies(
|
|||
..default()
|
||||
},
|
||||
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
|
||||
|
@ -403,16 +419,22 @@ 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() {
|
||||
// --- 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;
|
||||
|
@ -421,21 +443,36 @@ fn move_enemies(
|
|||
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());
|
||||
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());
|
||||
// --- 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(
|
||||
|
@ -543,6 +580,40 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue