feat: Add enemy type differentiation and adjust movement logic for Grunt and Boss enemies
This commit is contained in:
parent
e8dceaca27
commit
ef055fe3c5
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
/target
|
||||
/.direnv
|
||||
/logs
|
||||
.idea/workspace.xml
|
||||
/.idea
|
||||
|
|
|
@ -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.
|
||||
|
|
122
src/main.rs
122
src/main.rs
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue