feat: Add enemy type differentiation and adjust movement logic for Grunt and Boss enemies

This commit is contained in:
Harald Hoyer 2025-04-05 19:21:09 +02:00
parent e8dceaca27
commit ef055fe3c5
3 changed files with 92 additions and 38 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target
/.direnv
/logs
.idea/workspace.xml
/.idea

View file

@ -69,13 +69,13 @@ nix develop --command bash -c "cargo build"
* ~~Make enemies fire bullets (downwards or towards the player) during their dives.~~ **(DONE - Downward)**
* After an attack dive, enemies could return to their formation position or fly off-screen. **(Fly off-screen implemented)**
* **Enemy Variety:**
* Introduce different types of enemies (e.g., using different components or an enum).
* Assign different behaviors, point values, and maybe sprites to each type.
* ~~Introduce different types of enemies (e.g., using different components or an enum).~~ **(DONE - Added EnemyType enum and field)**
* ~~Assign different behaviors, point values, and maybe sprites to each type.~~ **(DONE - Behaviors, points & color based on type)**
**3. Advanced Galaga Mechanics:**
* **Boss Galaga & Capture Beam:**
* Create a "Boss" enemy type.
* ~~Create a "Boss" enemy type.~~ **(DONE - Added to Enum, handled in matches)**
* Implement the tractor beam attack (visual effect, player capture logic).
* Player needs a `Captured` state.
* Logic for the Boss to return captured ships to the formation.

View file

@ -48,6 +48,7 @@ struct Bullet;
#[derive(Component)]
struct Enemy {
enemy_type: EnemyType, // Add type field
shoot_cooldown: Timer,
}
@ -70,6 +71,12 @@ enum EnemyState {
#[derive(Component)]
struct EnemyBullet;
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)] // Added derive for common traits
pub enum EnemyType {
Grunt,
Boss, // Added Boss type
}
// --- Resources ---
#[derive(Resource)]
struct EnemySpawnTimer {
@ -625,10 +632,19 @@ fn spawn_enemies(
let spawn_x = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - ENEMY_SIZE.x);
let spawn_y = WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y / 2.0;
// 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), // Red for Grunts
EnemyType::Boss => Color::rgb(0.8, 0.2, 1.0), // Purple for Bosses
};
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::rgb(1.0, 0.2, 0.2),
color: sprite_color, // Use determined color
custom_size: Some(ENEMY_SIZE),
..default()
},
@ -636,6 +652,7 @@ fn spawn_enemies(
..default()
},
Enemy {
enemy_type, // Use the defined type
// Initialize cooldown, maybe slightly randomized later
shoot_cooldown: Timer::new(
Duration::from_secs_f32(ENEMY_SHOOT_INTERVAL),
@ -672,8 +689,8 @@ fn move_enemies(
(With<Enemy>, With<FormationTarget>),
>,
mut attacking_query: Query<
(Entity, &mut Transform, &EnemyState), // Add &EnemyState here
(With<Enemy>, Without<FormationTarget>), // Keep With<EnemyState> filter if desired, but we check explicitly below
(Entity, &mut Transform, &EnemyState, &Enemy), // Add &Enemy here
(With<Enemy>, Without<FormationTarget>),
>, // Query potential attackers
time: Res<Time>,
mut commands: Commands,
@ -711,56 +728,88 @@ fn move_enemies(
// --- Handle Attacking Enemies ---
let attack_speed = current_speed * 1.5; // Make them dive faster
for (entity, mut transform, state) in attacking_query.iter_mut() { // Get state from query
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 {
// --- Swooping Dive Logic ---
let delta_seconds = time.delta_seconds();
let vertical_movement = attack_speed * delta_seconds;
// --- Apply movement based on EnemyType ---
match enemy.enemy_type {
EnemyType::Grunt => {
// Grunt: Swooping Dive Logic
let delta_seconds = time.delta_seconds();
let vertical_movement = attack_speed * delta_seconds;
// 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 // No horizontal movement if exactly at center
};
// 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 // No horizontal movement if exactly at center
};
// Apply movement
transform.translation.y -= vertical_movement;
transform.translation.x += horizontal_movement;
// Apply movement
transform.translation.y -= vertical_movement;
transform.translation.x += horizontal_movement;
// 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;
}
}
// Enemies that are InFormation but Without<FormationTarget> will now be ignored by this movement logic.
// 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;
}
} // Add cases for other enemy types later
EnemyType::Boss => {
// Boss: Same Swooping Dive Logic for now
let delta_seconds = time.delta_seconds();
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;
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;
}
}
} // Closes match enemy.enemy_type
} // Closes if *state == EnemyState::Attacking
// Despawn if off screen
// Enemies that are InFormation but Without<FormationTarget> will now be ignored by the movement logic above.
// Despawn if off screen (This should be inside the loop)
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
println!(
"Despawning attacking enemy {:?} that went off screen.",
"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
}
fn check_bullet_collisions(
mut commands: Commands,
bullet_query: Query<(Entity, &Transform), With<Bullet>>,
enemy_query: Query<(Entity, &Transform), With<Enemy>>,
mut score: ResMut<Score>, // Add Score resource
enemy_query: Query<(Entity, &Transform, &Enemy), With<Enemy>>, // Fetch Enemy component too
mut score: ResMut<Score>, // Add Score resource
) {
for (bullet_entity, bullet_transform) in bullet_query.iter() {
for (enemy_entity, enemy_transform) in enemy_query.iter() {
for (enemy_entity, enemy_transform, enemy) in enemy_query.iter() {
// Get Enemy component
let distance = bullet_transform
.translation
.distance(enemy_transform.translation);
@ -768,7 +817,12 @@ fn check_bullet_collisions(
if distance < BULLET_ENEMY_COLLISION_THRESHOLD {
commands.entity(bullet_entity).despawn();
commands.entity(enemy_entity).despawn();
score.value += 100; // Increment score
// Increment score based on enemy type
let points = match enemy.enemy_type {
EnemyType::Grunt => 100,
EnemyType::Boss => 100, // Same points as Grunt for now
};
score.value += points;
println!("Enemy hit! Score: {}", score.value); // Log score update
break;
}