diff --git a/src/components.rs b/src/components.rs index c7fb877..c7a9e4c 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,5 +1,7 @@ use bevy::prelude::*; +use crate::flight_paths::PathType; + #[derive(Component)] pub struct Player { pub speed: f32, @@ -69,6 +71,12 @@ pub enum EnemyState { InFormation, Attacking(AttackPattern), ReturningWithCaptive, + FollowingPath { + path: PathType, + t: f32, + delay: f32, + elapsed: f32, + }, } #[derive(Component)] @@ -95,3 +103,7 @@ pub struct Star { pub struct Explosion { pub timer: Timer, } + +/// Marks an enemy as following a scripted flight path. +#[derive(Component)] +pub struct ScriptedPath; diff --git a/src/enemy.rs b/src/enemy.rs index 20f59c8..82ca360 100644 --- a/src/enemy.rs +++ b/src/enemy.rs @@ -3,16 +3,17 @@ use std::time::Duration; use crate::components::{ AttackPattern, Captured, Enemy, EnemyBullet, EnemyState, EnemyType, FormationTarget, - OriginalFormationPosition, Player, TractorBeam, TractorBeamSprite, + OriginalFormationPosition, Player, ScriptedPath, TractorBeam, TractorBeamSprite, }; use crate::constants::{ BEAM_CORE_COLOR, BEAM_GLOW_COLOR, BEAM_GLOW_WIDTH, BEAM_PULSE_AMPLITUDE, BEAM_PULSE_FREQ, CAPTURE_DURATION, ENEMY_BULLET_SIZE, ENEMY_SIZE, ENEMY_SPEED, TRACTOR_BEAM_DURATION, TRACTOR_BEAM_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH, }; +use crate::flight_paths; use crate::resources::{ - AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, StageConfigurations, - is_special_stage, + is_special_stage, AttackDiveTimer, CurrentStage, EnemySpawnTimer, FormationState, + StageConfigurations, }; const BOSS_BASE_CHANCE: f32 = 0.30; @@ -45,6 +46,72 @@ pub fn spawn_enemies( return; } + // Special stage: path-following enemies + if is_special_stage(stage.number) { + let flight_patterns = &stage_configs.flight_patterns; + if flight_patterns.is_empty() { + // Fall through to normal formation spawn + } else { + let slot = formation_state.next_slot_index; + if slot >= config.enemy_count { + return; + } + let pattern = &flight_patterns[slot % flight_patterns.len()]; + let spawn_pos = flight_paths::evaluate(pattern, 0.0); + let delay = slot as f32 * 0.15; + + let boss_chance = BOSS_BASE_CHANCE + + (stage.number as f32 * BOSS_CHANCE_PER_STAGE).min(BOSS_CHANCE_BONUS_CAP); + let enemy_type = if fastrand::f32() < boss_chance { + EnemyType::Boss + } else { + EnemyType::Grunt + }; + + let color = match enemy_type { + EnemyType::Grunt => Color::srgb(1.0, 0.2, 0.2), + EnemyType::Boss => Color::srgb(0.8, 0.2, 1.0), + }; + + commands.spawn(( + Sprite { + color, + custom_size: Some(ENEMY_SIZE), + ..default() + }, + Transform::from_translation(spawn_pos), + Enemy { + enemy_type, + shoot_cooldown: Timer::new( + Duration::from_secs_f32(config.enemy_shoot_interval), + TimerMode::Once, + ), + }, + ScriptedPath, + EnemyState::FollowingPath { + path: pattern.clone(), + t: 0.0, + delay, + elapsed: 0.0, + }, + )); + + formation_state.next_slot_index = (slot + 1) % config.enemy_count; + formation_state.total_spawned_this_stage += 1; + stage.waiting_for_clear = true; + + if formation_state.total_spawned_this_stage < config.enemy_count { + timer.timer.reset(); + } else { + info!( + "Stage {} special stage formation fully spawned", + stage.number + ); + } + return; // Skip normal spawn logic + } + } + let slot = formation_state.next_slot_index; if slot >= config.formation_layout.positions.len() { warn!( @@ -64,8 +131,8 @@ pub fn spawn_enemies( 0.0, ); - let boss_chance = BOSS_BASE_CHANCE - + (stage.number as f32 * BOSS_CHANCE_PER_STAGE).min(BOSS_CHANCE_BONUS_CAP); + let boss_chance = + BOSS_BASE_CHANCE + (stage.number as f32 * BOSS_CHANCE_PER_STAGE).min(BOSS_CHANCE_BONUS_CAP); let enemy_type = if fastrand::f32() < boss_chance { EnemyType::Boss } else { @@ -111,11 +178,13 @@ pub fn spawn_enemies( } } +#[allow(clippy::too_many_arguments)] pub fn move_enemies( mut entering_query: Query< (Entity, &mut Transform, &FormationTarget, &mut EnemyState), (With, With), >, + mut path_query: Query<(Entity, &mut Transform, &mut EnemyState), With>, mut attacking_query: Query< ( Entity, @@ -124,7 +193,7 @@ pub fn move_enemies( &Enemy, Option<&OriginalFormationPosition>, ), - (With, Without), + (With, Without, Without), >, time: Res