feat: Implement boss tractor beam and player capture

This commit introduces a new mechanic where boss enemies can use a tractor beam to capture the player.

Key changes:
- Bosses can now fire a tractor beam that targets the player.
- If the player is caught in the beam for a certain duration, they are "captured".
- Captured players are carried by the boss as it returns to its formation.
- If the boss is destroyed while carrying a player, the player is freed.
- If the player is captured, they lose a life and respawn.
- Refactored player and enemy systems to handle the new capture logic and states.
- Added GEMINI.md and CLAUDE.md to track assistant configurations.
This commit is contained in:
Harald Hoyer 2025-06-26 09:46:49 +02:00
parent d27d27bb5a
commit efef8df102
5 changed files with 97 additions and 60 deletions

View file

@ -1,4 +1,5 @@
use bevy::prelude::*;
use bevy::ecs::system::ParamSet;
use std::time::Duration;
use crate::components::{Bullet, Captured, Enemy, Invincible, Player};
@ -70,51 +71,40 @@ pub fn move_player(
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 set: ParamSet<(
Query<(Entity, &mut Transform, &mut Captured)>,
Query<&Transform, (With<Enemy>, Without<Player>)>,
)>,
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(),
));
// First, collect data about captured players and their bosses
let mut captured_data = Vec::new();
// Get player data
for (entity, transform, captured) in set.p0().iter() {
captured_data.push((entity, transform.translation, captured.boss_entity, captured.timer.clone()));
}
// 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
// Process each captured player
for (player_entity, _player_pos, boss_entity, mut timer) in captured_data {
// Check if the boss exists and get its position
let boss_pos = set.p1().get(boss_entity).map(|t| t.translation).ok();
// Tick the timer
timer.tick(time.delta());
// Update the player
if let Ok((entity, mut transform, mut captured)) = set.p0().get_mut(player_entity) {
// Update the actual timer
captured.timer.tick(time.delta());
match boss_pos_opt {
match boss_pos {
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);
transform.translation = transform.translation.lerp(target_pos, 0.2);
}
None => {
// Boss is gone, release player but lose a life
@ -127,11 +117,12 @@ pub fn handle_captured_player(
&mut next_state,
entity,
);
continue; // Skip the rest of processing for this player
}
}
// If capture duration expires, player escapes but loses a life
if timer_would_finish || captured.timer.finished() {
if captured.timer.finished() {
println!("Player escaped from capture after timer expired!");
commands.entity(entity).remove::<Captured>();
lose_life_and_respawn(