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
This commit is contained in:
parent
db061820b9
commit
52b0919d3f
4 changed files with 375 additions and 162 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
289
src/enemy.rs
289
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,15 +43,17 @@ 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());
|
||||
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;
|
||||
|
|
@ -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<Enemy>, With<FormationTarget>),
|
||||
>,
|
||||
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<Enemy>, Without<FormationTarget>),
|
||||
>, // Query potential attackers
|
||||
time: Res<Time>,
|
||||
|
|
@ -149,7 +173,9 @@ pub fn move_enemies(
|
|||
*state = EnemyState::InFormation; // Change state
|
||||
|
||||
// Store the original formation position for potential return
|
||||
commands.entity(entity).insert(OriginalFormationPosition { position: target_pos });
|
||||
commands.entity(entity).insert(OriginalFormationPosition {
|
||||
position: target_pos,
|
||||
});
|
||||
|
||||
println!(
|
||||
"Enemy {:?} reached formation target and is now InFormation.",
|
||||
|
|
@ -182,14 +208,19 @@ pub fn move_enemies(
|
|||
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 };
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +236,8 @@ pub fn move_enemies(
|
|||
let kamikaze_threshold = attack_speed * time.delta_seconds() * 1.1; // Threshold to stop near target
|
||||
|
||||
if distance > kamikaze_threshold {
|
||||
let move_delta = direction.normalize() * attack_speed * time.delta_seconds();
|
||||
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
|
||||
|
|
@ -228,7 +260,8 @@ pub fn move_enemies(
|
|||
|
||||
if transform.translation.y > target_y {
|
||||
// Move down to position
|
||||
transform.translation.y -= attack_speed * 0.8 * time.delta_seconds();
|
||||
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
|
||||
|
|
@ -236,7 +269,10 @@ pub fn move_enemies(
|
|||
// Spawn tractor beam component on this boss
|
||||
commands.entity(entity).insert(TractorBeam {
|
||||
target: None, // Will be filled in by the boss_capture_attack
|
||||
timer: Timer::new(Duration::from_secs_f32(TRACTOR_BEAM_DURATION), TimerMode::Once),
|
||||
timer: Timer::new(
|
||||
Duration::from_secs_f32(TRACTOR_BEAM_DURATION),
|
||||
TimerMode::Once,
|
||||
),
|
||||
width: TRACTOR_BEAM_WIDTH,
|
||||
active: false,
|
||||
});
|
||||
|
|
@ -256,7 +292,8 @@ pub fn move_enemies(
|
|||
// Normalize and move
|
||||
if direction.length() > 0.0 {
|
||||
let normalized_dir = direction.normalize();
|
||||
transform.translation += normalized_dir * attack_speed * time.delta_seconds();
|
||||
transform.translation +=
|
||||
normalized_dir * attack_speed * time.delta_seconds();
|
||||
}
|
||||
}
|
||||
AttackPattern::DirectDive => {
|
||||
|
|
@ -273,7 +310,8 @@ pub fn move_enemies(
|
|||
} else {
|
||||
// Move toward target
|
||||
let normalized_dir = direction.normalize();
|
||||
transform.translation += normalized_dir * attack_speed * time.delta_seconds();
|
||||
transform.translation +=
|
||||
normalized_dir * attack_speed * time.delta_seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -287,14 +325,16 @@ pub fn move_enemies(
|
|||
let distance = direction.length();
|
||||
let return_speed = ENEMY_SPEED * speed_multiplier * 0.7; // Slightly slower when carrying captive
|
||||
|
||||
if distance < 5.0 { // Close enough to formation position
|
||||
if distance < 5.0 {
|
||||
// Close enough to formation position
|
||||
// Return to formation
|
||||
transform.translation = original_pos.position;
|
||||
*state = EnemyState::InFormation;
|
||||
println!("Boss {:?} returned to formation with captive!", entity);
|
||||
} else {
|
||||
// Move towards formation position
|
||||
let move_delta = direction.normalize() * return_speed * time.delta_seconds();
|
||||
let move_delta =
|
||||
direction.normalize() * return_speed * time.delta_seconds();
|
||||
transform.translation += move_delta;
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +394,10 @@ pub fn check_formation_complete(
|
|||
attack_dive_timer.timer.set_duration(dive_interval);
|
||||
attack_dive_timer.timer.reset();
|
||||
attack_dive_timer.timer.unpause();
|
||||
println!("Attack timer set to {:?} duration, unpaused and reset.", dive_interval);
|
||||
println!(
|
||||
"Attack timer set to {:?} duration, unpaused and reset.",
|
||||
dive_interval
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -397,16 +440,20 @@ pub fn trigger_attack_dives(
|
|||
let mut selected_pattern = match enemy_type {
|
||||
// For Boss enemies, occasionally use the CaptureBeam pattern
|
||||
EnemyType::Boss => {
|
||||
if fastrand::f32() < 0.8 { // 80% chance for Boss to use CaptureBeam (increased for testing)
|
||||
if fastrand::f32() < 0.8 {
|
||||
// 80% chance for Boss to use CaptureBeam (increased for testing)
|
||||
println!("Boss {:?} selected CaptureBeam attack!", chosen_entity);
|
||||
AttackPattern::CaptureBeam
|
||||
} else {
|
||||
// Otherwise use a random pattern from the stage config
|
||||
let pattern_index = fastrand::usize(..current_config.attack_patterns.len());
|
||||
println!("Boss {:?} selected {:?} attack!", chosen_entity, current_config.attack_patterns[pattern_index]);
|
||||
println!(
|
||||
"Boss {:?} selected {:?} attack!",
|
||||
chosen_entity, current_config.attack_patterns[pattern_index]
|
||||
);
|
||||
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());
|
||||
|
|
@ -427,7 +474,10 @@ 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) {
|
||||
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
|
||||
// Timer duration is handled elsewhere (e.g., check_formation_complete)
|
||||
}
|
||||
|
|
@ -443,7 +493,8 @@ pub fn enemy_shoot(
|
|||
) {
|
||||
for (transform, mut enemy, state) in enemy_query.iter_mut() {
|
||||
// Only shoot if in any Attacking state (pattern doesn't matter for shooting)
|
||||
if matches!(state, EnemyState::Attacking(_)) { // Use matches! macro
|
||||
if matches!(state, EnemyState::Attacking(_)) {
|
||||
// Use matches! macro
|
||||
enemy.shoot_cooldown.tick(time.delta());
|
||||
if enemy.shoot_cooldown.finished() {
|
||||
// println!("Enemy {:?} firing!", transform.translation); // Less verbose logging
|
||||
|
|
@ -478,6 +529,16 @@ pub fn is_formation_complete(formation_state: Res<FormationState>) -> bool {
|
|||
formation_state.formation_complete
|
||||
}
|
||||
|
||||
/// Calculate the beam height from the boss's Y position to the bottom of the screen.
|
||||
pub(crate) fn calculate_beam_height(boss_y: f32) -> f32 {
|
||||
(boss_y - (-WINDOW_HEIGHT / 2.0)).max(0.0)
|
||||
}
|
||||
|
||||
/// Calculate the pulsing alpha value for a beam layer.
|
||||
pub(crate) fn beam_pulse_alpha(base_alpha: f32, time_secs: f32, freq: f32, amplitude: f32) -> f32 {
|
||||
(base_alpha + (time_secs * freq).sin() * amplitude).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
// New system to handle the tractor beam attack from Boss enemies
|
||||
pub fn boss_capture_attack(
|
||||
mut commands: Commands,
|
||||
|
|
@ -495,22 +556,45 @@ pub fn boss_capture_attack(
|
|||
if let Ok((player_entity, _)) = player_query.get_single() {
|
||||
tractor_beam.target = Some(player_entity);
|
||||
tractor_beam.active = true;
|
||||
println!("Boss {:?} activated tractor beam targeting player!", boss_entity);
|
||||
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
|
||||
let beam_height = calculate_beam_height(boss_transform.translation.y);
|
||||
|
||||
// Spawn the beam as a child of the boss
|
||||
commands.entity(boss_entity).with_children(|parent| {
|
||||
parent.spawn(SpriteBundle {
|
||||
// Outer glow layer (wide, dim)
|
||||
parent.spawn((
|
||||
SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: TRACTOR_BEAM_COLOR,
|
||||
custom_size: Some(Vec2::new(tractor_beam.width, beam_height)),
|
||||
color: BEAM_GLOW_COLOR,
|
||||
custom_size: Some(Vec2::new(BEAM_GLOW_WIDTH, beam_height)),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.0, -beam_height/2.0, 0.0),
|
||||
transform: Transform::from_xyz(0.0, -beam_height / 2.0, -0.5),
|
||||
..default()
|
||||
});
|
||||
},
|
||||
TractorBeamSprite,
|
||||
));
|
||||
// Inner core layer (narrow, bright, shorter)
|
||||
let core_height = beam_height * 0.6;
|
||||
parent.spawn((
|
||||
SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: BEAM_CORE_COLOR,
|
||||
custom_size: Some(Vec2::new(TRACTOR_BEAM_WIDTH, core_height)),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(
|
||||
0.0,
|
||||
-core_height / 2.0 - beam_height * 0.2,
|
||||
-0.3,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
TractorBeamSprite,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -519,14 +603,19 @@ pub fn boss_capture_attack(
|
|||
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 {
|
||||
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),
|
||||
timer: Timer::new(
|
||||
Duration::from_secs_f32(CAPTURE_DURATION),
|
||||
TimerMode::Once,
|
||||
),
|
||||
});
|
||||
|
||||
// Boss returns to formation with captured player
|
||||
|
|
@ -535,7 +624,9 @@ pub fn boss_capture_attack(
|
|||
// Change boss state to returning with captive
|
||||
if let Ok(enemy) = enemy_query.get(boss_entity) {
|
||||
if let EnemyState::Attacking(_) = enemy {
|
||||
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
|
||||
commands
|
||||
.entity(boss_entity)
|
||||
.insert(EnemyState::ReturningWithCaptive);
|
||||
|
||||
// Clean up the beam visual
|
||||
commands.entity(boss_entity).despawn_descendants();
|
||||
|
|
@ -557,9 +648,121 @@ pub fn boss_capture_attack(
|
|||
// Boss returns to formation after failed capture
|
||||
if let Ok(enemy_state) = enemy_query.get(boss_entity) {
|
||||
if let EnemyState::Attacking(_) = enemy_state {
|
||||
commands.entity(boss_entity).insert(EnemyState::ReturningWithCaptive);
|
||||
commands
|
||||
.entity(boss_entity)
|
||||
.insert(EnemyState::ReturningWithCaptive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update tractor beam visual: pulse opacity and update beam height.
|
||||
pub fn update_tractor_beam_visual(
|
||||
time: Res<Time>,
|
||||
boss_query: Query<(Entity, &Transform, &TractorBeam)>,
|
||||
mut sprite_query: Query<(&mut Sprite, &mut Transform), With<TractorBeamSprite>>,
|
||||
children: Query<&Children>,
|
||||
) {
|
||||
for (boss_entity, boss_transform, _tractor_beam) in boss_query.iter() {
|
||||
let current_beam_height = calculate_beam_height(boss_transform.translation.y);
|
||||
let time_secs = time.elapsed_seconds();
|
||||
|
||||
if let Ok(child_entities) = children.get(boss_entity) {
|
||||
for child in child_entities {
|
||||
if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) {
|
||||
// Update color pulse
|
||||
if let Color::Rgba {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
mut alpha,
|
||||
} = sprite.color
|
||||
{
|
||||
let base_alpha = alpha;
|
||||
alpha = beam_pulse_alpha(
|
||||
base_alpha,
|
||||
time_secs,
|
||||
BEAM_PULSE_FREQ,
|
||||
BEAM_PULSE_AMPLITUDE,
|
||||
);
|
||||
sprite.color = Color::Rgba {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
};
|
||||
}
|
||||
|
||||
// Update size and position
|
||||
let current_width = sprite.custom_size.map(|s| s.x).unwrap_or(0.0);
|
||||
if current_width == BEAM_GLOW_WIDTH {
|
||||
let new_height = current_beam_height;
|
||||
sprite.custom_size = Some(Vec2::new(current_width, new_height));
|
||||
transform.translation.y = -new_height / 2.0;
|
||||
} else if current_width == TRACTOR_BEAM_WIDTH {
|
||||
let core_height = current_beam_height * 0.6;
|
||||
sprite.custom_size = Some(Vec2::new(current_width, core_height));
|
||||
transform.translation.y = -core_height / 2.0 - current_beam_height * 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod beam_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn beam_height_at_screen_center() {
|
||||
assert_eq!(calculate_beam_height(0.0), WINDOW_HEIGHT / 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beam_height_at_boss_position() {
|
||||
assert_eq!(calculate_beam_height(-200.0), 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beam_height_clamped_at_bottom() {
|
||||
assert_eq!(calculate_beam_height(-WINDOW_HEIGHT / 2.0), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_at_zero_returns_base() {
|
||||
assert_eq!(beam_pulse_alpha(0.6, 0.0, 3.0, 0.15), 0.6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_at_peak() {
|
||||
// sin(pi/2) = 1, so time = (pi/2) / freq
|
||||
let t = std::f32::consts::FRAC_PI_2 / 3.0;
|
||||
let result = beam_pulse_alpha(0.5, t, 3.0, 0.15);
|
||||
assert!((result - 0.65).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_at_trough() {
|
||||
// sin(3pi/2) = -1, so time = (3pi/2) / freq = pi/2
|
||||
let t = std::f32::consts::FRAC_PI_2;
|
||||
let result = beam_pulse_alpha(0.5, t, 3.0, 0.15);
|
||||
assert!((result - 0.35).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_clamped_to_zero() {
|
||||
// sin(3pi/2) = -1, so time = (3pi/2) / freq
|
||||
let t = 3.0 * std::f32::consts::FRAC_PI_2 / 3.0;
|
||||
let result = beam_pulse_alpha(0.05, t, 3.0, 1.0);
|
||||
assert_eq!(result, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_clamped_to_one() {
|
||||
let t = std::f32::consts::FRAC_PI_2 / 3.0;
|
||||
let result = beam_pulse_alpha(0.95, t, 3.0, 1.0);
|
||||
assert_eq!(result, 1.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use player::{
|
|||
};
|
||||
use enemy::{
|
||||
check_formation_complete, enemy_shoot, is_formation_complete, move_enemies, spawn_enemies,
|
||||
trigger_attack_dives, boss_capture_attack,
|
||||
trigger_attack_dives, boss_capture_attack, update_tractor_beam_visual,
|
||||
};
|
||||
use bullet::{
|
||||
check_bullet_collisions, check_enemy_bullet_player_collisions, move_bullets,
|
||||
|
|
@ -103,6 +103,7 @@ fn main() {
|
|||
move_enemies,
|
||||
enemy_shoot, // Consider run_if attacking state? (Handled internally for now)
|
||||
boss_capture_attack, // New system for boss tractor beam
|
||||
update_tractor_beam_visual,
|
||||
)
|
||||
.run_if(in_state(AppState::Playing)),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue