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

@ -27,6 +27,24 @@ pub struct Invincible {
pub timer: Timer, pub timer: Timer,
} }
// New component to mark a player as captured by a Boss enemy
#[derive(Component, Clone)] // Added Clone derive
pub struct Captured {
// Reference to the capturing boss entity
pub boss_entity: Entity,
// Timer for how long the player remains captured
pub timer: Timer,
}
// New component for the tractor beam visual effect
#[derive(Component)]
pub struct TractorBeam {
pub target: Entity, // The entity being targeted (usually player)
pub timer: Timer, // How long the beam lasts
pub width: f32, // Visual width of the beam
pub active: bool, // Whether the beam is currently active
}
#[derive(Component)] #[derive(Component)]
pub struct FormationTarget { pub struct FormationTarget {
pub position: Vec3, pub position: Vec3,
@ -38,7 +56,8 @@ pub enum AttackPattern {
SwoopDive, // Original pattern: dive towards center, then off screen SwoopDive, // Original pattern: dive towards center, then off screen
DirectDive, // Dive straight down DirectDive, // Dive straight down
Kamikaze(Vec3), // Dive towards a specific target location (e.g., player's last known position) - Needs target Vec3 Kamikaze(Vec3), // Dive towards a specific target location (e.g., player's last known position) - Needs target Vec3
// Add more patterns later (e.g., FigureEight, Looping) CaptureBeam, // New pattern: Boss dives and attempts to capture the player with a tractor beam
// Add more patterns later (e.g., FigureEight, Looping)
} }
#[derive(Component, Clone, PartialEq, Debug)] // Added Debug derive #[derive(Component, Clone, PartialEq, Debug)] // Added Debug derive

View file

@ -1,4 +1,5 @@
use bevy::math::Vec2; use bevy::math::Vec2;
use bevy::prelude::*;
// --- Constants --- // --- Constants ---
pub const WINDOW_WIDTH: f32 = 600.0; pub const WINDOW_WIDTH: f32 = 600.0;
@ -31,3 +32,9 @@ pub const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x)
pub const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5; pub const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5;
// 35.0 // 35.0
pub const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 = (ENEMY_BULLET_SIZE.x + PLAYER_SIZE.x) * 0.5; pub const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 = (ENEMY_BULLET_SIZE.x + PLAYER_SIZE.x) * 0.5;
// Tractor beam constants
pub const TRACTOR_BEAM_WIDTH: f32 = 20.0;
pub const TRACTOR_BEAM_DURATION: f32 = 3.0;
pub const TRACTOR_BEAM_COLOR: Color = Color::rgba(0.5, 0.0, 0.8, 0.6);
pub const CAPTURE_DURATION: f32 = 10.0; // How long the player stays captured

View file

@ -1,9 +1,10 @@
use bevy::prelude::*; use bevy::prelude::*;
use std::time::Duration; use std::time::Duration;
use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget}; use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, TractorBeam, Captured};
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,
}; };
use crate::resources::{ use crate::resources::{
AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState,
@ -49,8 +50,15 @@ pub fn spawn_enemies(
let spawn_x = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - ENEMY_SIZE.x); let spawn_x = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - ENEMY_SIZE.x);
let spawn_y = WINDOW_HEIGHT / 2.0 + ENEMY_SIZE.y / 2.0; // Spawn slightly above screen let spawn_y = WINDOW_HEIGHT / 2.0 + ENEMY_SIZE.y / 2.0; // Spawn slightly above screen
// Determine enemy type (can be randomized or based on stage config later) // Determine enemy type - now with a chance to spawn Boss enemies
let enemy_type = EnemyType::Grunt; // Higher stages have a slightly higher boss chance
let boss_chance = 0.05 + (stage.number as f32 * 0.01).min(0.15);
let enemy_type = if fastrand::f32() < boss_chance {
println!("Spawning a Boss enemy!");
EnemyType::Boss
} else {
EnemyType::Grunt
};
// Determine sprite color based on type // Determine sprite color based on type
let sprite_color = match enemy_type { let sprite_color = match enemy_type {
@ -112,6 +120,7 @@ pub fn move_enemies(
mut commands: Commands, mut commands: Commands,
stage: Res<CurrentStage>, stage: Res<CurrentStage>,
stage_configs: Res<StageConfigurations>, // Add stage configurations stage_configs: Res<StageConfigurations>, // Add stage configurations
has_beam_query: Query<&TractorBeam>,
) { ) {
// Get current stage config for speed multiplier // Get current stage config for speed multiplier
let config_index = (stage.number as usize - 1) % stage_configs.stages.len(); let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
@ -129,6 +138,7 @@ pub fn move_enemies(
if *state == EnemyState::Entering { if *state == EnemyState::Entering {
let current_pos = transform.translation; let current_pos = transform.translation;
let target_pos = target.position; let target_pos = target.position;
// Using target_pos which is already a Vec3, not a reference
let direction = target_pos - current_pos; let direction = target_pos - current_pos;
let distance = direction.length(); let distance = direction.length();
@ -150,56 +160,121 @@ pub fn move_enemies(
} }
// --- Handle Attacking Enemies --- // --- Handle Attacking Enemies ---
// Note: attack_speed calculated above using multiplier for (entity, mut transform, state, enemy) in attacking_query.iter_mut() {
for (entity, mut transform, state, _enemy) in attacking_query.iter_mut() { // Check what state the enemy is in
if let EnemyState::Attacking(attack_pattern) = state {
// Apply different movement based on enemy type
match enemy.enemy_type {
EnemyType::Grunt => {
// Basic enemies follow their attack pattern
match attack_pattern {
// ... existing patterns ...
AttackPattern::SwoopDive => {
// ... existing code ...
let vertical_movement = attack_speed * time.delta_seconds();
let horizontal_speed_factor = 0.5;
let horizontal_movement = if transform.translation.x < 0.0 {
attack_speed * horizontal_speed_factor * time.delta_seconds()
} else if transform.translation.x > 0.0 {
-attack_speed * horizontal_speed_factor * time.delta_seconds()
} else { 0.0 };
// Match on the specific attack pattern using matches! and then get the pattern transform.translation.y -= vertical_movement;
if matches!(state, EnemyState::Attacking(_)) { transform.translation.x += horizontal_movement;
if let EnemyState::Attacking(pattern) = state { // Get the pattern safely now
let delta_seconds = time.delta_seconds();
match pattern { // Prevent overshooting center
AttackPattern::SwoopDive => { if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) ||
// Original Swooping Dive Logic (transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) {
let vertical_movement = attack_speed * delta_seconds; transform.translation.x = 0.0;
let horizontal_speed_factor = 0.5; }
let horizontal_movement = if transform.translation.x < 0.0 { }
attack_speed * horizontal_speed_factor * delta_seconds AttackPattern::DirectDive => {
} else if transform.translation.x > 0.0 { transform.translation.y -= attack_speed * time.delta_seconds();
-attack_speed * horizontal_speed_factor * delta_seconds }
} else { 0.0 }; AttackPattern::Kamikaze(target) => {
// Copy the target value rather than dereferencing
// since target should actually be a Vec3 in this context
let target_pos = *target; // Dereference here
let direction = target_pos - transform.translation;
let distance = direction.length();
let kamikaze_threshold = attack_speed * time.delta_seconds() * 1.1; // Threshold to stop near target
transform.translation.y -= vertical_movement; if distance > kamikaze_threshold {
transform.translation.x += horizontal_movement; let move_delta = direction.normalize() * attack_speed * time.delta_seconds();
transform.translation += move_delta;
// Prevent overshooting center } else {
if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) || // Optionally stop or continue past target - for now, just stop moving towards it
(transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) { // Could also despawn here if desired upon reaching target
transform.translation.x = 0.0; }
}
// New CaptureBeam pattern - Bosses behave differently
AttackPattern::CaptureBeam => {
// For Grunt enemies, just do a direct dive (fallback)
transform.translation.y -= attack_speed * time.delta_seconds();
}
} }
} }
AttackPattern::DirectDive => { EnemyType::Boss => {
// Move straight down // Boss has special behavior, especially for CaptureBeam
transform.translation.y -= attack_speed * delta_seconds; match attack_pattern {
} AttackPattern::CaptureBeam => {
AttackPattern::Kamikaze(target_pos) => { // Boss moves down to a position above the player area
// Move towards the target position let target_y = -WINDOW_HEIGHT / 4.0; // Position at lower quarter of the screen
let direction = *target_pos - transform.translation;
let distance = direction.length();
let kamikaze_threshold = attack_speed * delta_seconds * 1.1; // Threshold to stop near target
if distance > kamikaze_threshold { if transform.translation.y > target_y {
let move_delta = direction.normalize() * attack_speed * delta_seconds; // Move down to position
transform.translation += move_delta; transform.translation.y -= attack_speed * 0.8 * time.delta_seconds();
} else { } else {
// Optionally stop or continue past target - for now, just stop moving towards it // Once in position, stay there briefly before activating beam
// Could also despawn here if desired upon reaching target // Check if this boss already has a TractorBeam component
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
timer: Timer::new(Duration::from_secs_f32(TRACTOR_BEAM_DURATION), TimerMode::Once),
width: TRACTOR_BEAM_WIDTH,
active: false,
});
}
}
}
AttackPattern::SwoopDive => {
// ... existing code for swoop dive ...
let center_x = 0.0;
let bottom_y = -WINDOW_HEIGHT / 2.0 - ENEMY_SIZE.y;
// First move towards center-bottom
let target = Vec3::new(center_x, bottom_y, 0.0);
// target is directly created as Vec3, not a reference
let direction = target - transform.translation;
// Normalize and move
if direction.length() > 0.0 {
let normalized_dir = direction.normalize();
transform.translation += normalized_dir * attack_speed * time.delta_seconds();
}
}
AttackPattern::DirectDive => {
transform.translation.y -= attack_speed * time.delta_seconds();
}
AttackPattern::Kamikaze(target) => {
// Convert the target to a value type
let target_pos = *target; // Dereference here
let direction = target_pos - transform.translation;
// If very close to target, just move straight down
if direction.length() < 50.0 {
transform.translation.y -= attack_speed * time.delta_seconds();
} else {
// Move toward target
let normalized_dir = direction.normalize();
transform.translation += normalized_dir * attack_speed * time.delta_seconds();
}
}
} }
} }
// Add cases for other patterns here }
} // Close inner if let }
} // Closes match enemy.enemy_type
} // Closes if *state == EnemyState::Attacking
// 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 {
@ -259,13 +334,12 @@ pub fn check_formation_complete(
} }
} }
use crate::components::AttackPattern; // Import the new enum use crate::components::{AttackPattern, Player}; // Import the new enum and Player
use crate::components::Player; // Import Player for Kamikaze target
pub fn trigger_attack_dives( pub fn trigger_attack_dives(
mut timer: ResMut<AttackDiveTimer>, mut timer: ResMut<AttackDiveTimer>,
time: Res<Time>, time: Res<Time>,
mut enemy_query: Query<(Entity, &mut EnemyState), With<Enemy>>, // Renamed for clarity mut enemy_query: Query<(Entity, &mut EnemyState, &Enemy)>, // Added Enemy component to check type
formation_state: Res<FormationState>, formation_state: Res<FormationState>,
stage: Res<CurrentStage>, // Need current stage stage: Res<CurrentStage>, // Need current stage
stage_configs: Res<StageConfigurations>, // Need stage configs stage_configs: Res<StageConfigurations>, // Need stage configs
@ -280,22 +354,37 @@ pub fn trigger_attack_dives(
let current_config = &stage_configs.stages[config_index]; let current_config = &stage_configs.stages[config_index];
// Find all enemies currently in formation // Find all enemies currently in formation
let mut available_enemies: Vec<Entity> = Vec::new(); let mut available_enemies: Vec<(Entity, EnemyType)> = Vec::new();
for (entity, state) in enemy_query.iter() { for (entity, state, enemy) in enemy_query.iter() {
// Check the state correctly // Check the state correctly and store enemy type
if matches!(state, EnemyState::InFormation) { if matches!(state, EnemyState::InFormation) {
available_enemies.push(entity); available_enemies.push((entity, enemy.enemy_type));
} }
} }
// If there are enemies available, pick one randomly // If there are enemies available, pick one randomly
if !available_enemies.is_empty() && !current_config.attack_patterns.is_empty() { if !available_enemies.is_empty() && !current_config.attack_patterns.is_empty() {
let random_index = fastrand::usize(..available_enemies.len()); let random_index = fastrand::usize(..available_enemies.len());
let chosen_entity = available_enemies[random_index]; let (chosen_entity, enemy_type) = available_enemies[random_index];
// Select a random attack pattern for this stage // Select an attack pattern based on enemy type
let pattern_index = fastrand::usize(..current_config.attack_patterns.len()); let mut selected_pattern = match enemy_type {
let mut selected_pattern = current_config.attack_patterns[pattern_index]; // Copy the pattern // For Boss enemies, occasionally use the CaptureBeam pattern
EnemyType::Boss => {
if fastrand::f32() < 0.4 { // 40% chance for Boss to use CaptureBeam
AttackPattern::CaptureBeam
} else {
// Otherwise use a random pattern from the stage config
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
current_config.attack_patterns[pattern_index]
}
},
// Regular enemies use patterns from the stage config
EnemyType::Grunt => {
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
current_config.attack_patterns[pattern_index]
}
};
// If Kamikaze, get player position (if player exists) // If Kamikaze, get player position (if player exists)
if let AttackPattern::Kamikaze(_) = selected_pattern { if let AttackPattern::Kamikaze(_) = selected_pattern {
@ -308,9 +397,8 @@ pub fn trigger_attack_dives(
} }
} }
// Get the chosen enemy's state mutably and change it // Get the chosen enemy's state mutably and change it
if let Ok((_, mut state)) = enemy_query.get_mut(chosen_entity) { if let Ok((_, mut state, _)) = enemy_query.get_mut(chosen_entity) {
println!("Enemy {:?} starting attack dive with pattern {:?}!", chosen_entity, selected_pattern); println!("Enemy {:?} starting attack dive with pattern {:?}!", chosen_entity, selected_pattern);
*state = EnemyState::Attacking(selected_pattern); // Set state with pattern *state = EnemyState::Attacking(selected_pattern); // Set state with pattern
// Timer duration is handled elsewhere (e.g., check_formation_complete) // Timer duration is handled elsewhere (e.g., check_formation_complete)
@ -361,3 +449,80 @@ pub fn enemy_shoot(
pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool { pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
formation_state.formation_complete formation_state.formation_complete
} }
// New system to handle the tractor beam attack from Boss enemies
pub fn boss_capture_attack(
mut commands: Commands,
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>,
) {
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 let Ok((player_entity, _)) = player_query.get_single() {
tractor_beam.target = player_entity;
tractor_beam.active = true;
println!("Boss {:?} activated tractor beam targeting player!", boss_entity);
// Create visual beam effect (using a simple sprite for now)
let beam_height = boss_transform.translation.y - (-WINDOW_HEIGHT / 2.0); // Height from boss to bottom of screen
// Spawn the beam as a child of the boss
commands.entity(boss_entity).with_children(|parent| {
parent.spawn(SpriteBundle {
sprite: Sprite {
color: TRACTOR_BEAM_COLOR,
custom_size: Some(Vec2::new(tractor_beam.width, beam_height)),
..default()
},
transform: Transform::from_xyz(0.0, -beam_height/2.0, 0.0),
..default()
});
});
}
}
// If beam is active, check if player is in beam's path
if tractor_beam.active {
if let Ok((player_entity, player_transform)) = player_query.get_single() {
// Check if player is roughly under the boss
if (player_transform.translation.x - boss_transform.translation.x).abs() < tractor_beam.width / 2.0 {
// Player is in the beam! Capture them
println!("Player captured by boss {:?}!", boss_entity);
// Add Captured component to player
commands.entity(player_entity).insert(Captured {
boss_entity,
timer: Timer::new(Duration::from_secs_f32(CAPTURE_DURATION), TimerMode::Once),
});
// 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();
break;
}
}
}
// If beam timer finishes and player wasn't captured, end the beam attack
if tractor_beam.timer.finished() {
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
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
}
}
}

View file

@ -21,11 +21,11 @@ use game_state::{
}; };
use player::{ use player::{
check_player_enemy_collisions, manage_invincibility, move_player, player_shoot, check_player_enemy_collisions, manage_invincibility, move_player, player_shoot,
respawn_player, respawn_player, handle_captured_player,
}; };
use enemy::{ use enemy::{
check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies, check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies,
trigger_attack_dives, trigger_attack_dives, boss_capture_attack,
}; };
use bullet::{ use bullet::{
check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets, check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets,
@ -36,75 +36,67 @@ use systems::{player_exists, player_vulnerable, setup, should_respawn_player, up
fn main() { fn main() {
App::new() App::new()
.init_state::<AppState>() // Initialize the AppState .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.1)))
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
title: "Galaga :: Stage: 1 Lives: 3 Score: 0".into(), // Initial title title: "BGLGA".into(),
resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(), resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
resizable: false,
..default() ..default()
}), }),
..default() ..default()
})) }))
// Add Resources // Add states
.insert_resource(PlayerLives { .init_state::<AppState>() // Changed from add_state to init_state
count: STARTING_LIVES, // Initialize game resources
.insert_resource(EnemySpawnTimer {
timer: Timer::new(Duration::from_secs_f32(1.0), TimerMode::Once),
}) })
.insert_resource(PlayerLives { count: STARTING_LIVES })
.insert_resource(PlayerRespawnTimer { .insert_resource(PlayerRespawnTimer {
timer: Timer::new( timer: Timer::new(
Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), Duration::from_secs_f32(PLAYER_RESPAWN_DELAY),
TimerMode::Once, TimerMode::Once,
), ),
}) })
.insert_resource(EnemySpawnTimer {
timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating),
})
.insert_resource(Score { value: 0 }) .insert_resource(Score { value: 0 })
.insert_resource(CurrentStage { .insert_resource(CurrentStage {
number: 1, number: 1,
waiting_for_clear: false, waiting_for_clear: false,
}) })
.insert_resource(FormationState { .insert_resource(FormationState {
next_slot_index: 0,
total_spawned_this_stage: 0,
formation_complete: false, formation_complete: false,
}) total_spawned_this_stage: 0,
.insert_resource(StageConfigurations::default()) // Add stage configurations next_slot_index: 0,
.insert_resource(AttackDiveTimer {
timer: { // Correctly assign the block expression to the timer field
let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating); // Default duration, will be overwritten by stage config
timer.pause(); // Start paused
timer
}
}) })
.insert_resource(AttackDiveTimer { .insert_resource(AttackDiveTimer {
timer: { timer: Timer::new(Duration::from_secs_f32(3.0), TimerMode::Once),
let mut timer = Timer::new(Duration::from_secs_f32(3.0), TimerMode::Repeating);
timer.pause(); // Start paused
timer
},
}) })
// Add Systems .insert_resource(StageConfigurations::default()) // Use default stages for now
// Add startup systems
.add_systems(Startup, setup) .add_systems(Startup, setup)
// Systems running only when Playing // Core game systems
.add_systems( .add_systems(
Update, Update,
( (
// Player systems update_window_title,
move_player, // Enemy and player systems
spawn_enemies,
move_player.run_if(player_exists),
player_shoot.run_if(player_exists), player_shoot.run_if(player_exists),
check_player_enemy_collisions.run_if(player_vulnerable), check_player_enemy_collisions.run_if(player_vulnerable),
respawn_player.run_if(should_respawn_player), respawn_player.run_if(should_respawn_player),
manage_invincibility, manage_invincibility,
handle_captured_player, // New system for handling captured player
// Bullet systems // Bullet systems
move_bullets, move_bullets,
check_bullet_collisions, check_bullet_collisions,
move_enemy_bullets, move_enemy_bullets,
check_enemy_bullet_player_collisions.run_if(player_vulnerable), check_enemy_bullet_player_collisions.run_if(player_vulnerable),
// Enemy systems // Enemy systems
spawn_enemies,
move_enemies, move_enemies,
enemy_shoot, // Consider run_if attacking state? (Handled internally for now) enemy_shoot, // Consider run_if attacking state? (Handled internally for now)
boss_capture_attack, // New system for boss tractor beam
) )
.run_if(in_state(AppState::Playing)), .run_if(in_state(AppState::Playing)),
) )
@ -119,18 +111,9 @@ fn main() {
.chain() // Ensure these run in order if needed, check_formation first .chain() // Ensure these run in order if needed, check_formation first
.run_if(in_state(AppState::Playing)), .run_if(in_state(AppState::Playing)),
) )
// Systems running regardless of state (or managing state transitions) // UI and state management systems
.add_systems(
Update,
(
update_window_title, // Keep title updated
// TODO: Add system to check for restart input in GameOver state
bevy::window::close_on_esc, // Allow closing anytime
),
)
// Systems for entering/exiting states
.add_systems(OnEnter(AppState::GameOver), setup_game_over_ui) .add_systems(OnEnter(AppState::GameOver), setup_game_over_ui)
.add_systems(OnExit(AppState::Playing), cleanup_game_entities)
.add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui) .add_systems(OnExit(AppState::GameOver), cleanup_game_over_ui)
.add_systems(OnExit(AppState::Playing), cleanup_game_entities) // Cleanup when leaving Playing
.run(); .run();
} }

View file

@ -1,7 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use std::time::Duration; use std::time::Duration;
use crate::components::{Bullet, Enemy, Invincible, Player}; use crate::components::{Bullet, Enemy, Invincible, Player, Captured};
use crate::constants::{ use crate::constants::{
BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE, BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE,
PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH, PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
@ -42,7 +42,7 @@ pub fn spawn_player_ship(commands: &mut Commands) {
pub fn move_player( pub fn move_player(
keyboard_input: Res<ButtonInput<KeyCode>>, 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>, time: Res<Time>,
) { ) {
// Using get_single_mut handles the case where player might not exist yet (or was just destroyed) // 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( pub fn player_shoot(
keyboard_input: Res<ButtonInput<KeyCode>>, 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, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
) { ) {
@ -107,7 +192,7 @@ pub fn check_player_enemy_collisions(
mut respawn_timer: ResMut<PlayerRespawnTimer>, mut respawn_timer: ResMut<PlayerRespawnTimer>,
mut next_state: ResMut<NextState<AppState>>, // Resource to change state mut next_state: ResMut<NextState<AppState>>, // Resource to change state
// Query player without Invincible component - relies on run_if condition too // 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>>, enemy_query: Query<(Entity, &Transform), With<Enemy>>,
) { ) {
// This system only runs if player exists and is not invincible, due to run_if // 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."); println!("Respawn timer started.");
} else { } else {
println!("GAME OVER!"); 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 // Important: Break after handling one collision per frame for the player
break; break;