Add Lissajous and cubic Bezier path types for enemies on special stages (every 3rd level). Path-following enemies spawn with ScriptedPath marker and FollowingPath state, move along parametric curves, and despawn when their path ends off-screen. Key changes: - New flight_paths module with pure path evaluation (Lissajous + Bezier) - ScriptedPath marker component and FollowingPath EnemyState variant - flight_patterns field on StageConfigurations for composable path data - Special stage spawn branch in spawn_enemies() with stagger delays - Path-following movement in move_enemies() with delay gating and despawn - Formation complete early return for special stages - Without<ScriptedPath> filter on attacking_query to prevent double-processing - 13 new unit tests for path evaluation and data contracts Refs: GAL-40
171 lines
4.7 KiB
Rust
171 lines
4.7 KiB
Rust
use bevy::prelude::*;
|
|
|
|
use crate::components::{
|
|
Bullet, Enemy, EnemyBullet, GameOverUI, RestartMessage, StartButton, StartMenuUI,
|
|
};
|
|
use crate::resources::RestartPressed;
|
|
|
|
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
|
|
pub enum AppState {
|
|
#[default]
|
|
StartMenu,
|
|
Playing,
|
|
GameOver,
|
|
}
|
|
|
|
const BUTTON_IDLE: Color = Color::srgb(0.1, 0.1, 0.5);
|
|
const BUTTON_HOVER: Color = Color::srgb(0.2, 0.2, 0.7);
|
|
|
|
// --- Game Over UI ---
|
|
|
|
pub fn setup_game_over_ui(mut commands: Commands) {
|
|
commands.spawn((
|
|
Text::new("GAME OVER"),
|
|
TextFont {
|
|
font_size: 100.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::WHITE),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
align_self: AlignSelf::Center,
|
|
justify_self: JustifySelf::Center,
|
|
top: Val::Percent(40.0),
|
|
..default()
|
|
},
|
|
GameOverUI,
|
|
));
|
|
commands.spawn((
|
|
Text::new("Press R to Restart"),
|
|
TextFont {
|
|
font_size: 32.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::WHITE),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
align_self: AlignSelf::Center,
|
|
justify_self: JustifySelf::Center,
|
|
top: Val::Percent(55.0),
|
|
..default()
|
|
},
|
|
RestartMessage,
|
|
));
|
|
}
|
|
|
|
pub fn cleanup_game_over_ui(
|
|
mut commands: Commands,
|
|
query: Query<Entity, Or<(With<GameOverUI>, With<RestartMessage>)>>,
|
|
) {
|
|
for entity in &query {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|
|
|
|
// --- Cleanup on leaving Playing ---
|
|
|
|
pub fn cleanup_game_entities(
|
|
mut commands: Commands,
|
|
query: Query<Entity, Or<(With<Bullet>, With<EnemyBullet>, With<Enemy>)>>,
|
|
) {
|
|
for entity in &query {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|
|
|
|
// --- Start Menu UI ---
|
|
|
|
pub fn setup_start_menu_ui(mut commands: Commands) {
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
flex_direction: FlexDirection::Column,
|
|
..default()
|
|
},
|
|
StartMenuUI,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
Text::new("BGLGA"),
|
|
TextFont {
|
|
font_size: 120.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::WHITE),
|
|
Node {
|
|
margin: UiRect::bottom(Val::Px(50.0)),
|
|
..default()
|
|
},
|
|
));
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
Node {
|
|
width: Val::Px(250.0),
|
|
height: Val::Px(80.0),
|
|
border: UiRect::all(Val::Px(2.0)),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BorderColor::all(Color::WHITE),
|
|
BackgroundColor(BUTTON_IDLE),
|
|
StartButton,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
Text::new("Start Game"),
|
|
TextFont {
|
|
font_size: 40.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::WHITE),
|
|
));
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn cleanup_start_menu_ui(mut commands: Commands, query: Query<Entity, With<StartMenuUI>>) {
|
|
for entity in &query {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|
|
|
|
pub fn start_menu_button_system(
|
|
mut interactions: Query<
|
|
(&Interaction, &mut BackgroundColor),
|
|
(Changed<Interaction>, With<StartButton>),
|
|
>,
|
|
mut next_state: ResMut<NextState<AppState>>,
|
|
) {
|
|
for (interaction, mut bg) in &mut interactions {
|
|
match *interaction {
|
|
Interaction::Pressed => next_state.set(AppState::Playing),
|
|
Interaction::Hovered => bg.0 = BUTTON_HOVER,
|
|
Interaction::None => bg.0 = BUTTON_IDLE,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_restart_input(
|
|
keyboard: Res<ButtonInput<KeyCode>>,
|
|
mut restart: ResMut<RestartPressed>,
|
|
) {
|
|
if keyboard.just_pressed(KeyCode::KeyR) {
|
|
restart.pressed = true;
|
|
}
|
|
}
|
|
|
|
pub fn restart_game_system(
|
|
mut next_state: ResMut<NextState<AppState>>,
|
|
mut restart: ResMut<RestartPressed>,
|
|
) {
|
|
if restart.pressed {
|
|
restart.pressed = false;
|
|
next_state.set(AppState::Playing);
|
|
}
|
|
}
|