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:
parent
d27d27bb5a
commit
efef8df102
5 changed files with 97 additions and 60 deletions
1
CLAUDE.md
Symbolic link
1
CLAUDE.md
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
AGENT.md
|
||||||
1
GEMINI.md
Symbolic link
1
GEMINI.md
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
CLAUDE.md
|
||||||
|
|
@ -39,7 +39,7 @@ pub struct Captured {
|
||||||
// New component for the tractor beam visual effect
|
// New component for the tractor beam visual effect
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct TractorBeam {
|
pub struct TractorBeam {
|
||||||
pub target: Entity, // The entity being targeted (usually player)
|
pub target: Option<Entity>, // The entity being targeted (usually player)
|
||||||
pub timer: Timer, // How long the beam lasts
|
pub timer: Timer, // How long the beam lasts
|
||||||
pub width: f32, // Visual width of the beam
|
pub width: f32, // Visual width of the beam
|
||||||
pub active: bool, // Whether the beam is currently active
|
pub active: bool, // Whether the beam is currently active
|
||||||
|
|
@ -50,6 +50,12 @@ pub struct FormationTarget {
|
||||||
pub position: Vec3,
|
pub position: Vec3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Component to store the original formation position for enemies that need to return
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct OriginalFormationPosition {
|
||||||
|
pub position: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
// Enum defining different ways an enemy can attack
|
// Enum defining different ways an enemy can attack
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Debug)]
|
#[derive(Component, Clone, Copy, PartialEq, Debug)]
|
||||||
pub enum AttackPattern {
|
pub enum AttackPattern {
|
||||||
|
|
@ -65,6 +71,7 @@ pub enum EnemyState {
|
||||||
Entering, // Flying onto the screen towards formation target
|
Entering, // Flying onto the screen towards formation target
|
||||||
InFormation, // Holding position in the formation
|
InFormation, // Holding position in the formation
|
||||||
Attacking(AttackPattern), // Diving towards the player using a specific pattern
|
Attacking(AttackPattern), // Diving towards the player using a specific pattern
|
||||||
|
ReturningWithCaptive, // Boss returning to formation with captured player
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
|
|
|
||||||
73
src/enemy.rs
73
src/enemy.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use std::time::Duration;
|
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
|
use crate::constants::{ // Only keeping used constants
|
||||||
ENEMY_BULLET_SIZE, ENEMY_SIZE, ENEMY_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
|
ENEMY_BULLET_SIZE, ENEMY_SIZE, ENEMY_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
|
||||||
TRACTOR_BEAM_WIDTH, TRACTOR_BEAM_DURATION, TRACTOR_BEAM_COLOR, CAPTURE_DURATION,
|
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
|
// Determine enemy type - now with a chance to spawn Boss enemies
|
||||||
// Higher stages have a slightly higher boss chance
|
// 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 {
|
let enemy_type = if fastrand::f32() < boss_chance {
|
||||||
println!("Spawning a Boss enemy!");
|
println!("Spawning a Boss enemy!");
|
||||||
EnemyType::Boss
|
EnemyType::Boss
|
||||||
|
|
@ -113,7 +113,7 @@ pub fn move_enemies(
|
||||||
(With<Enemy>, With<FormationTarget>),
|
(With<Enemy>, With<FormationTarget>),
|
||||||
>,
|
>,
|
||||||
mut attacking_query: Query<
|
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>),
|
(With<Enemy>, Without<FormationTarget>),
|
||||||
>, // Query potential attackers
|
>, // Query potential attackers
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
|
|
@ -147,6 +147,10 @@ pub fn move_enemies(
|
||||||
transform.translation = target_pos;
|
transform.translation = target_pos;
|
||||||
commands.entity(entity).remove::<FormationTarget>(); // Remove target component
|
commands.entity(entity).remove::<FormationTarget>(); // Remove target component
|
||||||
*state = EnemyState::InFormation; // Change state
|
*state = EnemyState::InFormation; // Change state
|
||||||
|
|
||||||
|
// Store the original formation position for potential return
|
||||||
|
commands.entity(entity).insert(OriginalFormationPosition { position: target_pos });
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Enemy {:?} reached formation target and is now InFormation.",
|
"Enemy {:?} reached formation target and is now InFormation.",
|
||||||
entity
|
entity
|
||||||
|
|
@ -159,10 +163,11 @@ pub fn move_enemies(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Handle Attacking Enemies ---
|
// --- Handle Attacking and Returning Enemies ---
|
||||||
for (entity, mut transform, state, enemy) in attacking_query.iter_mut() {
|
for (entity, mut transform, mut state, enemy, original_pos) in attacking_query.iter_mut() {
|
||||||
// Check what state the enemy is in
|
// 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
|
// Apply different movement based on enemy type
|
||||||
match enemy.enemy_type {
|
match enemy.enemy_type {
|
||||||
EnemyType::Grunt => {
|
EnemyType::Grunt => {
|
||||||
|
|
@ -230,7 +235,7 @@ pub fn move_enemies(
|
||||||
if has_beam_query.get(entity).is_err() {
|
if has_beam_query.get(entity).is_err() {
|
||||||
// Spawn tractor beam component on this boss
|
// Spawn tractor beam component on this boss
|
||||||
commands.entity(entity).insert(TractorBeam {
|
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),
|
timer: Timer::new(Duration::from_secs_f32(TRACTOR_BEAM_DURATION), TimerMode::Once),
|
||||||
width: TRACTOR_BEAM_WIDTH,
|
width: TRACTOR_BEAM_WIDTH,
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -275,6 +280,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)
|
// Despawn if off screen (This should be inside the loop)
|
||||||
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
|
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y {
|
||||||
|
|
@ -371,11 +397,13 @@ pub fn trigger_attack_dives(
|
||||||
let mut selected_pattern = match enemy_type {
|
let mut selected_pattern = match enemy_type {
|
||||||
// For Boss enemies, occasionally use the CaptureBeam pattern
|
// For Boss enemies, occasionally use the CaptureBeam pattern
|
||||||
EnemyType::Boss => {
|
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
|
AttackPattern::CaptureBeam
|
||||||
} else {
|
} else {
|
||||||
// Otherwise use a random pattern from the stage config
|
// Otherwise use a random pattern from the stage config
|
||||||
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
|
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]
|
current_config.attack_patterns[pattern_index]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -456,16 +484,16 @@ pub fn boss_capture_attack(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>,
|
mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>,
|
||||||
player_query: Query<(Entity, &Transform), (With<Player>, Without<Captured>)>, // Only target non-captured players
|
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() {
|
for (boss_entity, boss_transform, mut tractor_beam) in boss_query.iter_mut() {
|
||||||
// Tick the beam timer
|
// Tick the beam timer
|
||||||
tractor_beam.timer.tick(time.delta());
|
tractor_beam.timer.tick(time.delta());
|
||||||
|
|
||||||
// If player exists and beam is not active yet, set player as target
|
// 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() {
|
if let Ok((player_entity, _)) = player_query.get_single() {
|
||||||
tractor_beam.target = player_entity;
|
tractor_beam.target = Some(player_entity);
|
||||||
tractor_beam.active = true;
|
tractor_beam.active = true;
|
||||||
println!("Boss {:?} activated tractor beam targeting player!", boss_entity);
|
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
|
// Boss returns to formation with captured player
|
||||||
commands.entity(boss_entity).remove::<TractorBeam>();
|
commands.entity(boss_entity).remove::<TractorBeam>();
|
||||||
|
|
||||||
// TODO: Implement logic for boss to return to formation
|
// Change boss state to returning with captive
|
||||||
// For now, just despawn the boss to simplify
|
if let Ok(enemy) = enemy_query.get(boss_entity) {
|
||||||
commands.entity(boss_entity).despawn_recursive();
|
if let EnemyState::Attacking(_) = enemy {
|
||||||
|
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
|
||||||
|
|
||||||
|
// Clean up the beam visual
|
||||||
|
commands.entity(boss_entity).despawn_descendants();
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -517,12 +551,15 @@ pub fn boss_capture_attack(
|
||||||
println!("Boss {:?} tractor beam expired", boss_entity);
|
println!("Boss {:?} tractor beam expired", boss_entity);
|
||||||
commands.entity(boss_entity).remove::<TractorBeam>();
|
commands.entity(boss_entity).remove::<TractorBeam>();
|
||||||
|
|
||||||
// If we had a proper visual beam, we'd despawn it here
|
// Clean up the beam visual
|
||||||
// For now, just make sure we clean up by despawning all children
|
|
||||||
commands.entity(boss_entity).despawn_descendants();
|
commands.entity(boss_entity).despawn_descendants();
|
||||||
|
|
||||||
// For simplicity, after beam attack the boss flies off-screen
|
// Boss returns to formation after failed capture
|
||||||
// In a more complete implementation, it might return to formation
|
if let Ok(enemy_state) = enemy_query.get(boss_entity) {
|
||||||
|
if let EnemyState::Attacking(_) = enemy_state {
|
||||||
|
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy::ecs::system::ParamSet;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::components::{Bullet, Captured, Enemy, Invincible, Player};
|
use crate::components::{Bullet, Captured, Enemy, Invincible, Player};
|
||||||
|
|
@ -70,51 +71,40 @@ pub fn move_player(
|
||||||
pub fn handle_captured_player(
|
pub fn handle_captured_player(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
player_query: Query<(Entity, &Transform, &Captured)>,
|
mut set: ParamSet<(
|
||||||
mut player_mut_query: Query<(&mut Transform, &mut Captured)>,
|
Query<(Entity, &mut Transform, &mut Captured)>,
|
||||||
enemy_query: Query<&Transform, With<Enemy>>,
|
Query<&Transform, (With<Enemy>, Without<Player>)>,
|
||||||
|
)>,
|
||||||
mut lives: ResMut<PlayerLives>,
|
mut lives: ResMut<PlayerLives>,
|
||||||
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
) {
|
) {
|
||||||
// First, collect data from all captured players
|
// First, collect data about captured players and their bosses
|
||||||
let mut to_process = Vec::new();
|
let mut captured_data = Vec::new();
|
||||||
|
|
||||||
for (entity, transform, captured) in player_query.iter() {
|
// Get player data
|
||||||
// Check if the boss exists
|
for (entity, transform, captured) in set.p0().iter() {
|
||||||
let boss_exists = enemy_query.get(captured.boss_entity).is_ok();
|
captured_data.push((entity, transform.translation, captured.boss_entity, captured.timer.clone()));
|
||||||
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
|
// Process each captured player
|
||||||
for (entity, current_pos, boss_pos_opt, timer_would_finish) in to_process {
|
for (player_entity, _player_pos, boss_entity, mut timer) in captured_data {
|
||||||
if let Ok((mut transform, mut captured)) = player_mut_query.get_mut(entity) {
|
// Check if the boss exists and get its position
|
||||||
// Tick the real timer
|
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());
|
captured.timer.tick(time.delta());
|
||||||
|
|
||||||
match boss_pos_opt {
|
match boss_pos {
|
||||||
Some(boss_pos) => {
|
Some(boss_pos) => {
|
||||||
// Boss exists, update player position
|
// Boss exists, update player position
|
||||||
let target_pos = boss_pos - Vec3::new(0.0, PLAYER_SIZE.y + 10.0, 0.0);
|
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 => {
|
None => {
|
||||||
// Boss is gone, release player but lose a life
|
// Boss is gone, release player but lose a life
|
||||||
|
|
@ -127,11 +117,12 @@ pub fn handle_captured_player(
|
||||||
&mut next_state,
|
&mut next_state,
|
||||||
entity,
|
entity,
|
||||||
);
|
);
|
||||||
|
continue; // Skip the rest of processing for this player
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If capture duration expires, player escapes but loses a life
|
// 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!");
|
println!("Player escaped from capture after timer expired!");
|
||||||
commands.entity(entity).remove::<Captured>();
|
commands.entity(entity).remove::<Captured>();
|
||||||
lose_life_and_respawn(
|
lose_life_and_respawn(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue