feat: add Captured and TractorBeam components, enhance enemy behavior with capture mechanics
This commit is contained in:
parent
1d1b927007
commit
008f9cc24a
5 changed files with 366 additions and 107 deletions
283
src/enemy.rs
283
src/enemy.rs
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue