From 52b0919d3f62dc4534b35db3732551c8f5508e06 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 6 May 2026 19:35:16 +0200 Subject: [PATCH] feat: improve tractor beam visual with 2-layer glow and pulse animation Replace the single static rectangle with a 2-layer beam (outer glow + inner core) and sinusoidal opacity pulse. Add per-frame beam height tracking to follow boss position. Include 8 unit tests for pure math functions (beam height calculation, pulse alpha). Refs: GAL-33 --- src/components.rs | 3 + src/constants.rs | 6 + src/enemy.rs | 525 ++++++++++++++++++++++++++++++++-------------- src/main.rs | 3 +- 4 files changed, 375 insertions(+), 162 deletions(-) diff --git a/src/components.rs b/src/components.rs index fe5ff07..03d5473 100644 --- a/src/components.rs +++ b/src/components.rs @@ -45,6 +45,9 @@ pub struct TractorBeam { pub active: bool, // Whether the beam is currently active } +#[derive(Component)] +pub struct TractorBeamSprite; + #[derive(Component)] pub struct FormationTarget { pub position: Vec3, diff --git a/src/constants.rs b/src/constants.rs index c8e8a15..ff5fb51 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -39,6 +39,12 @@ 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 +// Tractor beam visual constants +pub const BEAM_GLOW_WIDTH: f32 = 40.0; +pub const BEAM_GLOW_COLOR: Color = Color::rgba(0.3, 0.0, 0.5, 0.25); +pub const BEAM_CORE_COLOR: Color = Color::rgba(0.7, 0.2, 1.0, 0.7); +pub const BEAM_PULSE_FREQ: f32 = 3.0; +pub const BEAM_PULSE_AMPLITUDE: f32 = 0.15; // Starfield constants pub const STAR_COUNT: usize = 150; diff --git a/src/enemy.rs b/src/enemy.rs index 8433a7b..e172487 100644 --- a/src/enemy.rs +++ b/src/enemy.rs @@ -1,14 +1,28 @@ use bevy::prelude::*; use std::time::Duration; -use crate::components::{Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, TractorBeam, Captured, OriginalFormationPosition}; -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::components::{ + Captured, Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, + OriginalFormationPosition, TractorBeam, TractorBeamSprite, +}; +use crate::constants::{ + BEAM_CORE_COLOR, + BEAM_GLOW_COLOR, + BEAM_GLOW_WIDTH, + BEAM_PULSE_AMPLITUDE, + BEAM_PULSE_FREQ, + CAPTURE_DURATION, + // Only keeping used constants + ENEMY_BULLET_SIZE, + ENEMY_SIZE, + ENEMY_SPEED, + TRACTOR_BEAM_DURATION, + TRACTOR_BEAM_WIDTH, + WINDOW_HEIGHT, + WINDOW_WIDTH, }; use crate::resources::{ - AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, - StageConfigurations, + AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, StageConfigurations, }; pub fn spawn_enemies( @@ -29,18 +43,20 @@ pub fn spawn_enemies( // Only spawn if we haven't spawned the full formation for this stage yet // AND the timer just finished this frame - if formation_state.total_spawned_this_stage < stage_enemy_count - && timer.timer.just_finished() - { + if formation_state.total_spawned_this_stage < stage_enemy_count && timer.timer.just_finished() { let slot_index = formation_state.next_slot_index; // Ensure slot_index is within bounds of the formation layout if slot_index >= current_config.formation_layout.positions.len() { - println!("Warning: slot_index {} out of bounds for formation layout '{}' (size {})", - slot_index, current_config.formation_layout.name, current_config.formation_layout.positions.len()); - // Optionally, reset the timer and skip spawning this frame, or handle differently - timer.timer.reset(); - return; + println!( + "Warning: slot_index {} out of bounds for formation layout '{}' (size {})", + slot_index, + current_config.formation_layout.name, + current_config.formation_layout.positions.len() + ); + // Optionally, reset the timer and skip spawning this frame, or handle differently + timer.timer.reset(); + return; } // Get target position from the stage's formation layout @@ -84,7 +100,9 @@ pub fn spawn_enemies( TimerMode::Once, ), }, - FormationTarget { position: target_pos }, + FormationTarget { + position: target_pos, + }, EnemyState::Entering, )); @@ -113,7 +131,13 @@ pub fn move_enemies( (With, With), >, mut attacking_query: Query< - (Entity, &mut Transform, &mut EnemyState, &Enemy, Option<&OriginalFormationPosition>), // Add mutable state and original position + ( + Entity, + &mut Transform, + &mut EnemyState, + &Enemy, + Option<&OriginalFormationPosition>, + ), // Add mutable state and original position (With, Without), >, // Query potential attackers time: Res