split files

Signed-off-by: Harald Hoyer <harald@hoyer.xyz>
This commit is contained in:
Harald Hoyer 2025-04-11 10:44:23 +02:00
parent 3dbfb9dac1
commit 0b8527b955
3 changed files with 78 additions and 208 deletions

View file

@ -33,20 +33,11 @@ pub struct FormationTarget {
pub position: Vec3,
}
// Enum defining different ways an enemy can attack
#[derive(Component, Clone, Copy, PartialEq, Debug)]
pub enum AttackPattern {
SwoopDive, // Original pattern: dive towards center, then off screen
DirectDive, // Dive straight down
Kamikaze(Vec3), // Dive towards a specific target location (e.g., player's last known position) - Needs target Vec3
// Add more patterns later (e.g., FigureEight, Looping)
}
#[derive(Component, Clone, PartialEq, Debug)] // Added Debug derive
#[derive(Component, Clone, PartialEq)]
pub enum EnemyState {
Entering, // Flying onto the screen towards formation target
InFormation, // Holding position in the formation
Attacking(AttackPattern), // Diving towards the player using a specific pattern
Entering, // Flying onto the screen towards formation target
InFormation, // Holding position in the formation
Attacking, // Diving towards the player
}
#[derive(Component)]

View file

@ -2,69 +2,59 @@ use bevy::prelude::*;
use std::time::Duration;
use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget};
use crate::constants::{ // Added WINDOW_WIDTH
use crate::constants::{
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,
FORMATION_ENEMY_COUNT, FORMATION_X_SPACING, FORMATION_Y_SPACING, WINDOW_HEIGHT,
};
use crate::resources::{
AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, PlayerLives,
PlayerRespawnTimer, StageConfigurations, // Make sure StageConfigurations is imported if not already
PlayerRespawnTimer,
};
use crate::game_state::AppState;
use crate::game_state::AppState; // Needed for check_enemy_bullet_player_collisions
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
mut formation_state: ResMut<FormationState>, // Add formation state resource
) {
// 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
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;
let col = slot_index % FORMATION_COLS;
// 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;
}
// Calculate target position in formation
let target_x = (col as f32 - (FORMATION_COLS as f32 - 1.0) / 2.0) * FORMATION_X_SPACING;
let target_y = FORMATION_BASE_Y - (row as f32 * FORMATION_Y_SPACING);
let target_pos = Vec3::new(target_x, target_y, 0.0);
// Get target position from the stage's formation layout
let target_pos = current_config.formation_layout.positions[slot_index];
// Spawn position (still random at the top for now)
let spawn_x = (fastrand::f32() - 0.5) * (WINDOW_HEIGHT - ENEMY_SIZE.x); // Use WINDOW_HEIGHT for x spawn range? Should be WINDOW_WIDTH
let spawn_y = WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y / 2.0;
// 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)
// Determine enemy type (currently always Grunt)
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),
EnemyType::Grunt => Color::rgb(1.0, 0.2, 0.2), // Red for Grunts
EnemyType::Boss => Color::rgb(0.8, 0.2, 1.0), // Purple for Bosses
};
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: sprite_color,
color: sprite_color, // Use determined color
custom_size: Some(ENEMY_SIZE),
..default()
},
@ -72,32 +62,30 @@ pub fn spawn_enemies(
..default()
},
Enemy {
enemy_type,
// Use shoot interval from stage config
enemy_type, // Use the defined type
// Initialize cooldown, maybe slightly randomized later
shoot_cooldown: Timer::new(
Duration::from_secs_f32(current_config.enemy_shoot_interval),
Duration::from_secs_f32(ENEMY_SHOOT_INTERVAL),
TimerMode::Once,
),
},
FormationTarget { position: target_pos },
EnemyState::Entering,
FormationTarget {
position: target_pos,
},
EnemyState::Entering, // Start in Entering state
));
// Use stage_enemy_count for cycling index
formation_state.next_slot_index = (slot_index + 1) % stage_enemy_count;
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;
// 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 {
if formation_state.total_spawned_this_stage < FORMATION_ENEMY_COUNT {
timer.timer.reset();
} else {
println!(
"Full formation ({}) spawned for Stage {}",
current_config.formation_layout.name, stage.number
);
println!("Full formation spawned for Stage {}", stage.number);
}
}
}
@ -113,18 +101,11 @@ pub fn move_enemies(
>, // Query potential attackers
time: Res<Time>,
mut commands: Commands,
stage: Res<CurrentStage>,
stage_configs: Res<StageConfigurations>, // Add stage configurations
stage: Res<CurrentStage>, // Add stage resource for speed calculation
) {
// 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
// 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() {
@ -145,62 +126,49 @@ pub fn move_enemies(
entity
);
} else {
// Move towards target using base_speed
let move_delta = direction.normalize() * base_speed * time.delta_seconds();
// Move towards target
let move_delta = direction.normalize() * current_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 attack_speed = current_speed * 1.5; // Make them dive faster
for (entity, mut transform, state, enemy) in attacking_query.iter_mut() {
// Get state and enemy
// *** Explicitly check if the enemy is actually in the Attacking state ***
if *state == EnemyState::Attacking {
// --- Apply movement based on EnemyType ---
match enemy.enemy_type {
EnemyType::Grunt | EnemyType::Boss => { // Combine logic if it's the same
// Swooping Dive Logic
let delta_seconds = time.delta_seconds();
let vertical_movement = attack_speed * delta_seconds;
let horizontal_speed_factor = 0.5;
// Horizontal movement: Move towards the center (x=0)
let horizontal_speed_factor = 0.5; // Adjust this to control the swoop intensity
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 };
} else {
0.0 // No horizontal movement if exactly at center
};
// Apply movement
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) {
// Ensure enemy doesn't overshoot the center horizontally if moving towards it
if (transform.translation.x - horizontal_movement < 0.0
&& transform.translation.x > 0.0)
|| (transform.translation.x - horizontal_movement > 0.0
&& transform.translation.x < 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
} // Add cases for other enemy types later if different
} // Closes match enemy.enemy_type
} // Closes if *state == EnemyState::Attacking
@ -250,17 +218,11 @@ pub fn check_formation_complete(
}
}
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
mut query: Query<(Entity, &mut EnemyState), With<Enemy>>,
formation_state: Res<FormationState>, // Read formation state
) {
timer.timer.tick(time.delta());
@ -268,45 +230,22 @@ pub fn trigger_attack_dives(
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) {
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() && !current_config.attack_patterns.is_empty() {
if !available_enemies.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)
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
}
}
}
@ -316,21 +255,21 @@ 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
mut enemy_query: Query<(&Transform, &mut Enemy, &EnemyState), Without<FormationTarget>>,
) {
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
// Only shoot if attacking
if *state == EnemyState::Attacking {
enemy.shoot_cooldown.tick(time.delta());
if enemy.shoot_cooldown.finished() {
// println!("Enemy {:?} firing!", transform.translation); // Less verbose logging
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),
color: Color::rgb(1.0, 0.5, 0.5), // Different color for enemy bullets
custom_size: Some(ENEMY_BULLET_SIZE),
..default()
},

View file

@ -42,66 +42,6 @@ impl Default for FormationLayout {
}
}
}
// Configuration for a single stage
#[derive(Clone, Debug)]
pub struct StageConfig {
pub formation_layout: FormationLayout,
pub enemy_count: usize, // Allow overriding enemy count per stage
pub attack_patterns: Vec<crate::components::AttackPattern>, // Possible patterns for this stage
pub attack_dive_interval: f32, // Time between attack dives for this stage
pub enemy_speed_multiplier: f32, // Speed multiplier for this stage
pub enemy_shoot_interval: f32, // Shoot interval for this stage
}
// Resource to hold all stage configurations
#[derive(Resource, Debug, Clone)]
pub struct StageConfigurations {
pub stages: Vec<StageConfig>,
}
impl Default for StageConfigurations {
fn default() -> Self {
use crate::components::AttackPattern;
use crate::constants::*; // Import constants for default values
// Define configurations for a few stages
let stage1 = StageConfig {
formation_layout: FormationLayout::default(), // Use the default grid
enemy_count: FORMATION_ENEMY_COUNT,
attack_patterns: vec![AttackPattern::SwoopDive],
attack_dive_interval: 3.0,
enemy_speed_multiplier: 1.0,
enemy_shoot_interval: ENEMY_SHOOT_INTERVAL,
};
let stage2_layout = {
let mut positions = Vec::new();
let radius = WINDOW_WIDTH / 4.0;
let center_y = FORMATION_BASE_Y - 50.0;
let count = 16; // Example: Fewer enemies in a circle
for i in 0..count {
let angle = (i as f32 / count as f32) * 2.0 * std::f32::consts::PI;
positions.push(Vec3::new(angle.cos() * radius, center_y + angle.sin() * radius, 0.0));
}
FormationLayout { name: "Circle".to_string(), positions }
};
let stage2 = StageConfig {
formation_layout: stage2_layout,
enemy_count: 16,
attack_patterns: vec![AttackPattern::SwoopDive, AttackPattern::DirectDive], // Add direct dive
attack_dive_interval: 2.5, // Faster dives
enemy_speed_multiplier: 1.2, // Faster enemies
enemy_shoot_interval: ENEMY_SHOOT_INTERVAL * 0.8, // Faster shooting
};
// Add more stages here...
StageConfigurations {
stages: vec![stage1, stage2], // Add more stages as needed
}
}
}
#[derive(Resource)]
pub struct Score {
pub value: u32,