iii
Signed-off-by: Harald Hoyer <harald@hoyer.xyz>
This commit is contained in:
parent
ef055fe3c5
commit
3dbfb9dac1
11 changed files with 1085 additions and 867 deletions
356
src/enemy.rs
Normal file
356
src/enemy.rs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
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::resources::{
|
||||
AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives,
|
||||
PlayerRespawnTimer, StageConfigurations, // Make sure StageConfigurations is imported if not already
|
||||
};
|
||||
use crate::game_state::AppState;
|
||||
|
||||
pub fn spawn_enemies(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut timer: ResMut<EnemySpawnTimer>,
|
||||
mut stage: ResMut<CurrentStage>,
|
||||
mut formation_state: ResMut<FormationState>,
|
||||
stage_configs: Res<StageConfigurations>, // Use imported name
|
||||
) {
|
||||
// Get current stage config, looping if stage number exceeds defined configs
|
||||
let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
|
||||
let current_config = &stage_configs.stages[config_index];
|
||||
let stage_enemy_count = current_config.enemy_count;
|
||||
|
||||
// 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 just finished this frame
|
||||
if formation_state.total_spawned_this_stage < stage_enemy_count
|
||||
&& timer.timer.just_finished()
|
||||
{
|
||||
let slot_index = formation_state.next_slot_index;
|
||||
|
||||
// Ensure slot_index is within bounds of the formation layout
|
||||
if slot_index >= current_config.formation_layout.positions.len() {
|
||||
println!("Warning: slot_index {} out of bounds for formation layout '{}' (size {})",
|
||||
slot_index, current_config.formation_layout.name, current_config.formation_layout.positions.len());
|
||||
// Optionally, reset the timer and skip spawning this frame, or handle differently
|
||||
timer.timer.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get target position from the stage's formation layout
|
||||
let target_pos = current_config.formation_layout.positions[slot_index];
|
||||
|
||||
// Spawn position (random at the top) - Corrected to use WINDOW_WIDTH
|
||||
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 sprite color based on type
|
||||
let sprite_color = match enemy_type {
|
||||
EnemyType::Grunt => Color::rgb(1.0, 0.2, 0.2),
|
||||
EnemyType::Boss => Color::rgb(0.8, 0.2, 1.0),
|
||||
};
|
||||
|
||||
commands.spawn((
|
||||
SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: sprite_color,
|
||||
custom_size: Some(ENEMY_SIZE),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
|
||||
..default()
|
||||
},
|
||||
Enemy {
|
||||
enemy_type,
|
||||
// Use shoot interval from stage config
|
||||
shoot_cooldown: Timer::new(
|
||||
Duration::from_secs_f32(current_config.enemy_shoot_interval),
|
||||
TimerMode::Once,
|
||||
),
|
||||
},
|
||||
FormationTarget { position: target_pos },
|
||||
EnemyState::Entering,
|
||||
));
|
||||
|
||||
// Use stage_enemy_count for cycling index
|
||||
formation_state.next_slot_index = (slot_index + 1) % stage_enemy_count;
|
||||
formation_state.total_spawned_this_stage += 1;
|
||||
|
||||
// Mark that we are now waiting for enemies to be cleared
|
||||
stage.waiting_for_clear = true;
|
||||
|
||||
// Reset timer only if we are still spawning more enemies for the formation
|
||||
if formation_state.total_spawned_this_stage < stage_enemy_count {
|
||||
timer.timer.reset();
|
||||
} else {
|
||||
println!(
|
||||
"Full formation ({}) spawned for Stage {}",
|
||||
current_config.formation_layout.name, stage.number
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_enemies(
|
||||
mut entering_query: Query<
|
||||
(Entity, &mut Transform, &FormationTarget, &mut EnemyState),
|
||||
(With<Enemy>, With<FormationTarget>),
|
||||
>,
|
||||
mut attacking_query: Query<
|
||||
(Entity, &mut Transform, &EnemyState, &Enemy), // Add &Enemy here
|
||||
(With<Enemy>, Without<FormationTarget>),
|
||||
>, // Query potential attackers
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
stage: Res<CurrentStage>,
|
||||
stage_configs: Res<StageConfigurations>, // Add stage configurations
|
||||
) {
|
||||
// Get current stage config for speed multiplier
|
||||
let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
|
||||
let current_config = &stage_configs.stages[config_index];
|
||||
let speed_multiplier = current_config.enemy_speed_multiplier;
|
||||
|
||||
// Calculate speeds for this frame
|
||||
let base_speed = ENEMY_SPEED * speed_multiplier;
|
||||
let attack_speed = base_speed * 1.5; // Attackers are faster
|
||||
let arrival_threshold = base_speed * time.delta_seconds() * 1.1; // Threshold for reaching formation target
|
||||
|
||||
// --- 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 component
|
||||
*state = EnemyState::InFormation; // Change state
|
||||
println!(
|
||||
"Enemy {:?} reached formation target and is now InFormation.",
|
||||
entity
|
||||
);
|
||||
} else {
|
||||
// Move towards target using base_speed
|
||||
let move_delta = direction.normalize() * base_speed * time.delta_seconds();
|
||||
transform.translation += move_delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 {
|
||||
AttackPattern::SwoopDive => {
|
||||
// Original Swooping Dive Logic
|
||||
let vertical_movement = attack_speed * delta_seconds;
|
||||
let horizontal_speed_factor = 0.5;
|
||||
let horizontal_movement = if transform.translation.x < 0.0 {
|
||||
attack_speed * horizontal_speed_factor * delta_seconds
|
||||
} else if transform.translation.x > 0.0 {
|
||||
-attack_speed * horizontal_speed_factor * delta_seconds
|
||||
} else { 0.0 };
|
||||
|
||||
transform.translation.y -= vertical_movement;
|
||||
transform.translation.x += horizontal_movement;
|
||||
|
||||
// Prevent overshooting center
|
||||
if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) ||
|
||||
(transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) {
|
||||
transform.translation.x = 0.0;
|
||||
}
|
||||
}
|
||||
AttackPattern::DirectDive => {
|
||||
// Move straight down
|
||||
transform.translation.y -= attack_speed * delta_seconds;
|
||||
}
|
||||
AttackPattern::Kamikaze(target_pos) => {
|
||||
// Move towards the target position
|
||||
let direction = *target_pos - transform.translation;
|
||||
let distance = direction.length();
|
||||
let kamikaze_threshold = attack_speed * delta_seconds * 1.1; // Threshold to stop near target
|
||||
|
||||
if distance > kamikaze_threshold {
|
||||
let move_delta = direction.normalize() * attack_speed * 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
|
||||
|
||||
// Despawn if off screen (This should be inside the loop)
|
||||
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
|
||||
println!(
|
||||
"Despawning enemy {:?} that went off screen.", // Generic message as it could be InFormation or Attacking
|
||||
entity
|
||||
);
|
||||
commands.entity(entity).despawn();
|
||||
// TODO: Later, attacking enemies might return to formation or loop
|
||||
}
|
||||
} // Closes for loop
|
||||
}
|
||||
|
||||
// System to check if all spawned enemies have reached their formation position
|
||||
pub 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
|
||||
attack_dive_timer.timer.unpause(); // Start the timer now
|
||||
println!("Formation complete! Attack timer unpaused and reset.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::components::AttackPattern; // Import the new enum
|
||||
use crate::components::Player; // Import Player for Kamikaze target
|
||||
|
||||
pub fn trigger_attack_dives(
|
||||
mut timer: ResMut<AttackDiveTimer>,
|
||||
time: Res<Time>,
|
||||
mut enemy_query: Query<(Entity, &mut EnemyState), With<Enemy>>, // Renamed for clarity
|
||||
formation_state: Res<FormationState>,
|
||||
stage: Res<CurrentStage>, // Need current stage
|
||||
stage_configs: Res<StageConfigurations>, // Need stage configs
|
||||
player_query: Query<&Transform, With<Player>>, // Need player position for Kamikaze
|
||||
) {
|
||||
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();
|
||||
// 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
|
||||
if matches!(state, EnemyState::InFormation) {
|
||||
available_enemies.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// Select a random attack pattern for this stage
|
||||
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
|
||||
let mut selected_pattern = current_config.attack_patterns[pattern_index]; // Copy the pattern
|
||||
|
||||
// If Kamikaze, get player position (if player exists)
|
||||
if let AttackPattern::Kamikaze(_) = selected_pattern {
|
||||
if let Ok(player_transform) = player_query.get_single() {
|
||||
selected_pattern = AttackPattern::Kamikaze(player_transform.translation);
|
||||
} else {
|
||||
// Fallback if player doesn't exist (e.g., just died)
|
||||
selected_pattern = AttackPattern::DirectDive; // Or SwoopDive
|
||||
println!("Kamikaze target not found, falling back to DirectDive");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the chosen enemy's state mutably and change it
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub 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>>, // Query remains the same
|
||||
) {
|
||||
for (transform, mut enemy, state) in enemy_query.iter_mut() {
|
||||
// Only shoot if in any Attacking state (pattern doesn't matter for shooting)
|
||||
if matches!(state, EnemyState::Attacking(_)) { // Use matches! macro
|
||||
enemy.shoot_cooldown.tick(time.delta());
|
||||
if enemy.shoot_cooldown.finished() {
|
||||
// println!("Enemy {:?} firing!", transform.translation); // Less verbose logging
|
||||
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),
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check collisions between enemy bullets and the player
|
||||
// Moved to player.rs as it affects player state directly
|
||||
|
||||
// New run condition: Check if the formation is complete
|
||||
pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
|
||||
formation_state.formation_complete
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue