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,9 +1,10 @@
use bevy::prelude::*;
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
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::{
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_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)
let enemy_type = EnemyType::Grunt;
// 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 enemy_type = if fastrand::f32() < boss_chance {
println!("Spawning a Boss enemy!");
EnemyType::Boss
} else {
EnemyType::Grunt
};
// Determine sprite color based on type
let sprite_color = match enemy_type {
@ -112,6 +120,7 @@ pub fn move_enemies(
mut commands: Commands,
stage: Res<CurrentStage>,
stage_configs: Res<StageConfigurations>, // Add stage configurations
has_beam_query: Query<&TractorBeam>,
) {
// Get current stage config for speed multiplier
let config_index = (stage.number as usize - 1) % stage_configs.stages.len();
@ -129,6 +138,7 @@ pub fn move_enemies(
if *state == EnemyState::Entering {
let current_pos = transform.translation;
let target_pos = target.position;
// Using target_pos which is already a Vec3, not a reference
let direction = target_pos - current_pos;
let distance = direction.length();
@ -150,56 +160,121 @@ pub fn move_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
if matches!(state, EnemyState::Attacking(_)) {
if let EnemyState::Attacking(pattern) = state { // Get the pattern safely now
let delta_seconds = time.delta_seconds();
transform.translation.y -= vertical_movement;
transform.translation.x += horizontal_movement;
match pattern {
AttackPattern::SwoopDive => {
// Original Swooping Dive Logic
let vertical_movement = attack_speed * delta_seconds;
let horizontal_speed_factor = 0.5;
let horizontal_movement = if transform.translation.x < 0.0 {
attack_speed * horizontal_speed_factor * delta_seconds
} else if transform.translation.x > 0.0 {
-attack_speed * horizontal_speed_factor * delta_seconds
} else { 0.0 };
// Prevent overshooting center
if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) ||
(transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) {
transform.translation.x = 0.0;
}
}
AttackPattern::DirectDive => {
transform.translation.y -= attack_speed * time.delta_seconds();
}
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;
transform.translation.x += horizontal_movement;
// Prevent overshooting center
if (transform.translation.x > 0.0 && transform.translation.x + horizontal_movement < 0.0) ||
(transform.translation.x < 0.0 && transform.translation.x + horizontal_movement > 0.0) {
transform.translation.x = 0.0;
if distance > kamikaze_threshold {
let move_delta = direction.normalize() * attack_speed * time.delta_seconds();
transform.translation += move_delta;
} else {
// Optionally stop or continue past target - for now, just stop moving towards it
// Could also despawn here if desired upon reaching target
}
}
// 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 => {
// Move straight down
transform.translation.y -= attack_speed * delta_seconds;
}
AttackPattern::Kamikaze(target_pos) => {
// Move towards the target position
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 {
let move_delta = direction.normalize() * attack_speed * delta_seconds;
transform.translation += move_delta;
} else {
// Optionally stop or continue past target - for now, just stop moving towards it
// Could also despawn here if desired upon reaching target
EnemyType::Boss => {
// Boss has special behavior, especially for CaptureBeam
match attack_pattern {
AttackPattern::CaptureBeam => {
// Boss moves down to a position above the player area
let target_y = -WINDOW_HEIGHT / 4.0; // Position at lower quarter of the screen
if transform.translation.y > target_y {
// Move down to position
transform.translation.y -= attack_speed * 0.8 * time.delta_seconds();
} else {
// Once in position, stay there briefly before activating beam
// 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)
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::Player; // Import Player for Kamikaze target
use crate::components::{AttackPattern, Player}; // Import the new enum and Player
pub fn trigger_attack_dives(
mut timer: ResMut<AttackDiveTimer>,
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>,
stage: Res<CurrentStage>, // Need current stage
stage_configs: Res<StageConfigurations>, // Need stage configs
@ -280,22 +354,37 @@ pub fn trigger_attack_dives(
let current_config = &stage_configs.stages[config_index];
// Find all enemies currently in formation
let mut available_enemies: Vec<Entity> = Vec::new();
for (entity, state) in enemy_query.iter() {
// Check the state correctly
let mut available_enemies: Vec<(Entity, EnemyType)> = Vec::new();
for (entity, state, enemy) in enemy_query.iter() {
// Check the state correctly and store enemy type
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 !available_enemies.is_empty() && !current_config.attack_patterns.is_empty() {
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
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
let mut selected_pattern = current_config.attack_patterns[pattern_index]; // Copy the pattern
// Select an attack pattern based on enemy type
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
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 let AttackPattern::Kamikaze(_) = selected_pattern {
@ -308,9 +397,8 @@ pub fn trigger_attack_dives(
}
}
// 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);
*state = EnemyState::Attacking(selected_pattern); // Set state with pattern
// Timer duration is handled elsewhere (e.g., check_formation_complete)
@ -360,4 +448,81 @@ pub fn enemy_shoot(
// New run condition: Check if the formation is complete
pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
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
}
}
}