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,7 +1,7 @@
use bevy::prelude::*;
use std::time::Duration;
use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, TractorBeam, Captured};
use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, TractorBeam, Captured, OriginalFormationPosition};
use crate::constants::{ // Only keeping used constants
ENEMY_BULLET_SIZE, ENEMY_SIZE, ENEMY_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
TRACTOR_BEAM_WIDTH, TRACTOR_BEAM_DURATION, TRACTOR_BEAM_COLOR, CAPTURE_DURATION,
@ -52,7 +52,7 @@ pub fn spawn_enemies(
// Determine enemy type - now with a chance to spawn Boss enemies
// Higher stages have a slightly higher boss chance
let boss_chance = 0.05 + (stage.number as f32 * 0.01).min(0.15);
let boss_chance = 0.30 + (stage.number as f32 * 0.05).min(0.50); // Increased for testing
let enemy_type = if fastrand::f32() < boss_chance {
println!("Spawning a Boss enemy!");
EnemyType::Boss
@ -113,7 +113,7 @@ pub fn move_enemies(
(With<Enemy>, With<FormationTarget>),
>,
mut attacking_query: Query<
(Entity, &mut Transform, &EnemyState, &Enemy), // Add &Enemy here
(Entity, &mut Transform, &mut EnemyState, &Enemy, Option<&OriginalFormationPosition>), // Add mutable state and original position
(With<Enemy>, Without<FormationTarget>),
>, // Query potential attackers
time: Res<Time>,
@ -147,6 +147,10 @@ pub fn move_enemies(
transform.translation = target_pos;
commands.entity(entity).remove::<FormationTarget>(); // Remove target component
*state = EnemyState::InFormation; // Change state
// Store the original formation position for potential return
commands.entity(entity).insert(OriginalFormationPosition { position: target_pos });
println!(
"Enemy {:?} reached formation target and is now InFormation.",
entity
@ -159,10 +163,11 @@ pub fn move_enemies(
}
}
// --- Handle Attacking Enemies ---
for (entity, mut transform, state, enemy) in attacking_query.iter_mut() {
// --- Handle Attacking and Returning Enemies ---
for (entity, mut transform, mut state, enemy, original_pos) in attacking_query.iter_mut() {
// Check what state the enemy is in
if let EnemyState::Attacking(attack_pattern) = state {
match state.as_ref() {
EnemyState::Attacking(attack_pattern) => {
// Apply different movement based on enemy type
match enemy.enemy_type {
EnemyType::Grunt => {
@ -230,7 +235,7 @@ pub fn move_enemies(
if has_beam_query.get(entity).is_err() {
// Spawn tractor beam component on this boss
commands.entity(entity).insert(TractorBeam {
target: Entity::PLACEHOLDER, // Will be filled in by the boss_capture_attack
target: None, // Will be filled in by the boss_capture_attack
timer: Timer::new(Duration::from_secs_f32(TRACTOR_BEAM_DURATION), TimerMode::Once),
width: TRACTOR_BEAM_WIDTH,
active: false,
@ -274,6 +279,27 @@ pub fn move_enemies(
}
}
}
}
EnemyState::ReturningWithCaptive => {
// Boss returning to formation with captured player
if let Some(original_pos) = original_pos {
let direction = original_pos.position - transform.translation;
let distance = direction.length();
let return_speed = ENEMY_SPEED * speed_multiplier * 0.7; // Slightly slower when carrying captive
if distance < 5.0 { // Close enough to formation position
// Return to formation
transform.translation = original_pos.position;
*state = EnemyState::InFormation;
println!("Boss {:?} returned to formation with captive!", entity);
} else {
// Move towards formation position
let move_delta = direction.normalize() * return_speed * time.delta_seconds();
transform.translation += move_delta;
}
}
}
_ => {} // Handle other states if needed
}
// Despawn if off screen (This should be inside the loop)
@ -371,11 +397,13 @@ pub fn trigger_attack_dives(
let mut selected_pattern = match enemy_type {
// For Boss enemies, occasionally use the CaptureBeam pattern
EnemyType::Boss => {
if fastrand::f32() < 0.4 { // 40% chance for Boss to use CaptureBeam
if fastrand::f32() < 0.8 { // 80% chance for Boss to use CaptureBeam (increased for testing)
println!("Boss {:?} selected CaptureBeam attack!", chosen_entity);
AttackPattern::CaptureBeam
} else {
// Otherwise use a random pattern from the stage config
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
println!("Boss {:?} selected {:?} attack!", chosen_entity, current_config.attack_patterns[pattern_index]);
current_config.attack_patterns[pattern_index]
}
},
@ -456,16 +484,16 @@ pub fn boss_capture_attack(
time: Res<Time>,
mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>,
player_query: Query<(Entity, &Transform), (With<Player>, Without<Captured>)>, // Only target non-captured players
has_beam_query: Query<&TractorBeam>,
enemy_query: Query<&EnemyState, With<Enemy>>,
) {
for (boss_entity, boss_transform, mut tractor_beam) in boss_query.iter_mut() {
// Tick the beam timer
tractor_beam.timer.tick(time.delta());
// If player exists and beam is not active yet, set player as target
if !tractor_beam.active && tractor_beam.target == Entity::PLACEHOLDER {
if !tractor_beam.active && tractor_beam.target.is_none() {
if let Ok((player_entity, _)) = player_query.get_single() {
tractor_beam.target = player_entity;
tractor_beam.target = Some(player_entity);
tractor_beam.active = true;
println!("Boss {:?} activated tractor beam targeting player!", boss_entity);
@ -504,9 +532,15 @@ pub fn boss_capture_attack(
// Boss returns to formation with captured player
commands.entity(boss_entity).remove::<TractorBeam>();
// TODO: Implement logic for boss to return to formation
// For now, just despawn the boss to simplify
commands.entity(boss_entity).despawn_recursive();
// Change boss state to returning with captive
if let Ok(enemy) = enemy_query.get(boss_entity) {
if let EnemyState::Attacking(_) = enemy {
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
// Clean up the beam visual
commands.entity(boss_entity).despawn_descendants();
}
}
break;
}
}
@ -517,12 +551,15 @@ pub fn boss_capture_attack(
println!("Boss {:?} tractor beam expired", boss_entity);
commands.entity(boss_entity).remove::<TractorBeam>();
// If we had a proper visual beam, we'd despawn it here
// For now, just make sure we clean up by despawning all children
// Clean up the beam visual
commands.entity(boss_entity).despawn_descendants();
// For simplicity, after beam attack the boss flies off-screen
// In a more complete implementation, it might return to formation
// Boss returns to formation after failed capture
if let Ok(enemy_state) = enemy_query.get(boss_entity) {
if let EnemyState::Attacking(_) = enemy_state {
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
}
}
}
}
}