bglga/src/game_state.rs
Harald Hoyer a0863b290c feat(gameplay): add scripted flight paths for special stage enemies
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
2026-05-07 19:54:18 +02:00

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);
}
}