feat: add Captured and TractorBeam components, enhance enemy behavior with capture mechanics

This commit is contained in:
Harald Hoyer 2025-04-16 08:41:17 +02:00
parent 1d1b927007
commit 008f9cc24a
5 changed files with 366 additions and 107 deletions

View file

@ -1,7 +1,7 @@
use bevy::prelude::*;
use std::time::Duration;
use crate::components::{Bullet, Enemy, Invincible, Player};
use crate::components::{Bullet, Enemy, Invincible, Player, Captured};
use crate::constants::{
BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE,
PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
@ -42,7 +42,7 @@ pub fn spawn_player_ship(commands: &mut Commands) {
pub fn move_player(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut Transform, &Player)>,
mut query: Query<(&mut Transform, &Player), Without<Captured>>, // Don't move captured players with controls
time: Res<Time>,
) {
// Using get_single_mut handles the case where player might not exist yet (or was just destroyed)
@ -66,9 +66,94 @@ pub fn move_player(
}
}
// New system to handle captured player movement
pub fn handle_captured_player(
mut commands: Commands,
time: Res<Time>,
player_query: Query<(Entity, &Transform, &Captured)>,
mut player_mut_query: Query<(&mut Transform, &mut Captured)>,
enemy_query: Query<&Transform, With<Enemy>>,
mut lives: ResMut<PlayerLives>,
mut respawn_timer: ResMut<PlayerRespawnTimer>,
mut next_state: ResMut<NextState<AppState>>,
) {
// First, collect data from all captured players
let mut to_process = Vec::new();
for (entity, transform, captured) in player_query.iter() {
// Check if the boss exists
let boss_exists = enemy_query.get(captured.boss_entity).is_ok();
let boss_pos = if boss_exists {
enemy_query.get(captured.boss_entity).map(|t| t.translation).ok()
} else {
None
};
// Create a copy of the timer to check if it would finish
let mut timer_copy = captured.timer.clone();
timer_copy.tick(time.delta());
to_process.push((entity, transform.translation, boss_pos, timer_copy.finished()));
}
// Now process each player separately
for (entity, current_pos, boss_pos_opt, timer_would_finish) in to_process {
if let Ok((mut transform, mut captured)) = player_mut_query.get_mut(entity) {
// Tick the real timer
captured.timer.tick(time.delta());
match boss_pos_opt {
Some(boss_pos) => {
// Boss exists, update player position
let target_pos = boss_pos - Vec3::new(0.0, PLAYER_SIZE.y + 10.0, 0.0);
transform.translation = current_pos.lerp(target_pos, 0.2);
},
None => {
// Boss is gone, release player but lose a life
println!("Boss is gone, releasing captured player!");
commands.entity(entity).remove::<Captured>();
lose_life_and_respawn(&mut commands, &mut lives, &mut respawn_timer, &mut next_state, entity);
}
}
// If capture duration expires, player escapes but loses a life
if timer_would_finish || captured.timer.finished() {
println!("Player escaped from capture after timer expired!");
commands.entity(entity).remove::<Captured>();
lose_life_and_respawn(&mut commands, &mut lives, &mut respawn_timer, &mut next_state, entity);
}
}
}
}
// Helper function for player life loss and respawn logic
fn lose_life_and_respawn(
commands: &mut Commands,
lives: &mut ResMut<PlayerLives>,
respawn_timer: &mut ResMut<PlayerRespawnTimer>,
next_state: &mut ResMut<NextState<AppState>>,
player_entity: Entity,
) {
// Lose a life
lives.count = lives.count.saturating_sub(1);
println!("Lives remaining: {}", lives.count);
// Destroy player
commands.entity(player_entity).despawn();
if lives.count > 0 {
respawn_timer.timer.reset();
respawn_timer.timer.unpause();
println!("Respawn timer started.");
} else {
println!("GAME OVER!");
next_state.set(AppState::GameOver);
}
}
pub fn player_shoot(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&Transform, &mut Player)>, // Should only run if player exists due to run_if
mut query: Query<(&Transform, &mut Player), Without<Captured>>, // Only non-captured players can shoot
mut commands: Commands,
time: Res<Time>,
) {
@ -107,7 +192,7 @@ pub fn check_player_enemy_collisions(
mut respawn_timer: ResMut<PlayerRespawnTimer>,
mut next_state: ResMut<NextState<AppState>>, // Resource to change state
// Query player without Invincible component - relies on run_if condition too
player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>,
player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>, Without<Captured>)>, // Don't check collisions for captured players
enemy_query: Query<(Entity, &Transform), With<Enemy>>,
) {
// This system only runs if player exists and is not invincible, due to run_if
@ -133,7 +218,7 @@ pub fn check_player_enemy_collisions(
println!("Respawn timer started.");
} else {
println!("GAME OVER!");
next_state.set(AppState::GameOver); // Transition to GameOver state
next_state.set(AppState::GameOver); // Updated for newer Bevy states API
}
// Important: Break after handling one collision per frame for the player
break;