From 008f9cc24a69fed867bda84ec2af8d42d026565d Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 16 Apr 2025 08:41:17 +0200 Subject: [PATCH] feat: add Captured and TractorBeam components, enhance enemy behavior with capture mechanics --- src/components.rs | 21 +++- src/constants.rs | 7 ++ src/enemy.rs | 283 ++++++++++++++++++++++++++++++++++++---------- src/main.rs | 67 ++++------- src/player.rs | 95 +++++++++++++++- 5 files changed, 366 insertions(+), 107 deletions(-) diff --git a/src/components.rs b/src/components.rs index adc5021..974f1fa 100644 --- a/src/components.rs +++ b/src/components.rs @@ -27,6 +27,24 @@ pub struct Invincible { 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)] pub struct FormationTarget { pub position: Vec3, @@ -38,7 +56,8 @@ pub enum AttackPattern { SwoopDive, // Original pattern: dive towards center, then off screen DirectDive, // Dive straight down 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 diff --git a/src/constants.rs b/src/constants.rs index 2b5bd8e..4c3ec09 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,5 @@ use bevy::math::Vec2; +use bevy::prelude::*; // --- Constants --- 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; // 35.0 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 diff --git a/src/enemy.rs b/src/enemy.rs index 8cda903..a497e15 100644 --- a/src/enemy.rs +++ b/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, stage_configs: Res, // 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, time: Res