chore(deps): upgrade Bevy 0.13 → 0.18

Three major versions of breaking changes:
- Bundles → Required Components (SpriteBundle, NodeBundle, ButtonBundle,
  TextBundle, Camera2dBundle removed; spawn tuples of components instead)
- Style merged into Node; TextStyle split into TextFont + TextColor
- Color API: rgb/rgba → srgb/srgba; Color::Rgba pattern matching replaced
  with .alpha()/.set_alpha(); .r()/.g()/.b() → .to_srgba().red/green/blue
- Time: delta_seconds() → delta_secs(); elapsed_seconds() → elapsed_secs()
- Query: get_single()/get_single_mut() → single()/single_mut() (now Result)
- Timer::finished() → Timer::is_finished()
- despawn_recursive() removed (despawn() is now recursive); despawn_descendants()
  removed — replaced with Children query iteration
- BorderColor(c) → BorderColor::all(c)
- WindowResolution: From<(f32,f32)> removed → cast to (u32,u32)
- flake.nix: added wayland to runtime libs (default-on in 0.18)

Tests pass (8/8), clippy clean, headless render verified showing start
menu, button, starfield, and player ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Harald Hoyer 2026-05-06 21:26:48 +02:00
parent 7a4305677b
commit b2b564f690
11 changed files with 2746 additions and 1028 deletions

3367
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,5 +4,5 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bevy = "0.13" bevy = "0.18"
fastrand = "2.0.1" fastrand = "2.0.1"

View file

@ -26,6 +26,7 @@
libxi libxi
libxkbcommon libxkbcommon
libxcb libxcb
wayland
vulkan-loader vulkan-loader
glfw glfw
]; ];

View file

@ -19,7 +19,7 @@ pub fn move_bullets(
mut commands: Commands, mut commands: Commands,
) { ) {
for (entity, mut transform) in query.iter_mut() { for (entity, mut transform) in query.iter_mut() {
transform.translation.y += BULLET_SPEED * time.delta_seconds(); transform.translation.y += BULLET_SPEED * time.delta_secs();
if transform.translation.y > WINDOW_HEIGHT / 2.0 + BULLET_SIZE.y { if transform.translation.y > WINDOW_HEIGHT / 2.0 + BULLET_SIZE.y {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
@ -65,7 +65,7 @@ pub fn move_enemy_bullets(
mut query: Query<(Entity, &mut Transform), With<EnemyBullet>>, mut query: Query<(Entity, &mut Transform), With<EnemyBullet>>,
) { ) {
for (entity, mut transform) in query.iter_mut() { for (entity, mut transform) in query.iter_mut() {
transform.translation.y -= ENEMY_BULLET_SPEED * time.delta_seconds(); transform.translation.y -= ENEMY_BULLET_SPEED * time.delta_secs();
// Despawn if off screen (bottom) // Despawn if off screen (bottom)
if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_BULLET_SIZE.y { if transform.translation.y < -WINDOW_HEIGHT / 2.0 - ENEMY_BULLET_SIZE.y {
@ -84,7 +84,7 @@ pub fn check_enemy_bullet_player_collisions(
player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>, player_query: Query<(Entity, &Transform), (With<Player>, Without<Invincible>)>,
enemy_bullet_query: Query<(Entity, &Transform), With<EnemyBullet>>, enemy_bullet_query: Query<(Entity, &Transform), With<EnemyBullet>>,
) { ) {
if let Ok((player_entity, player_transform)) = player_query.get_single() { if let Ok((player_entity, player_transform)) = player_query.single() {
for (bullet_entity, bullet_transform) in enemy_bullet_query.iter() { for (bullet_entity, bullet_transform) in enemy_bullet_query.iter() {
let distance = player_transform let distance = player_transform
.translation .translation

View file

@ -37,12 +37,12 @@ pub const ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD: f32 =
// Tractor beam constants // Tractor beam constants
pub const TRACTOR_BEAM_WIDTH: f32 = 20.0; pub const TRACTOR_BEAM_WIDTH: f32 = 20.0;
pub const TRACTOR_BEAM_DURATION: f32 = 3.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 TRACTOR_BEAM_COLOR: Color = Color::srgba(0.5, 0.0, 0.8, 0.6);
pub const CAPTURE_DURATION: f32 = 10.0; // How long the player stays captured pub const CAPTURE_DURATION: f32 = 10.0; // How long the player stays captured
// Tractor beam visual constants // Tractor beam visual constants
pub const BEAM_GLOW_WIDTH: f32 = 40.0; 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_GLOW_COLOR: Color = Color::srgba(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_CORE_COLOR: Color = Color::srgba(0.7, 0.2, 1.0, 0.7);
pub const BEAM_PULSE_FREQ: f32 = 3.0; pub const BEAM_PULSE_FREQ: f32 = 3.0;
pub const BEAM_PULSE_AMPLITUDE: f32 = 0.15; pub const BEAM_PULSE_AMPLITUDE: f32 = 0.15;
@ -58,4 +58,4 @@ pub const STAR_Z_DEPTH: f32 = -10.0; // Behind all game entities
pub const EXPLOSION_DURATION: f32 = 0.4; pub const EXPLOSION_DURATION: f32 = 0.4;
pub const EXPLOSION_BASE_SIZE: Vec2 = Vec2::new(15.0, 15.0); pub const EXPLOSION_BASE_SIZE: Vec2 = Vec2::new(15.0, 15.0);
pub const EXPLOSION_MAX_SIZE: Vec2 = Vec2::new(50.0, 50.0); pub const EXPLOSION_MAX_SIZE: Vec2 = Vec2::new(50.0, 50.0);
pub const EXPLOSION_COLOR: Color = Color::rgba(1.0, 0.6, 0.1, 1.0); pub const EXPLOSION_COLOR: Color = Color::srgba(1.0, 0.6, 0.1, 1.0);

View file

@ -78,20 +78,17 @@ pub fn spawn_enemies(
// Determine sprite color based on type // Determine sprite color based on type
let sprite_color = match enemy_type { let sprite_color = match enemy_type {
EnemyType::Grunt => Color::rgb(1.0, 0.2, 0.2), EnemyType::Grunt => Color::srgb(1.0, 0.2, 0.2),
EnemyType::Boss => Color::rgb(0.8, 0.2, 1.0), EnemyType::Boss => Color::srgb(0.8, 0.2, 1.0),
}; };
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: sprite_color,
color: sprite_color, custom_size: Some(ENEMY_SIZE),
custom_size: Some(ENEMY_SIZE),
..default()
},
transform: Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
..default() ..default()
}, },
Transform::from_translation(Vec3::new(spawn_x, spawn_y, 0.0)),
Enemy { Enemy {
enemy_type, enemy_type,
// Use shoot interval from stage config // Use shoot interval from stage config
@ -154,7 +151,7 @@ pub fn move_enemies(
// Calculate speeds for this frame // Calculate speeds for this frame
let base_speed = ENEMY_SPEED * speed_multiplier; let base_speed = ENEMY_SPEED * speed_multiplier;
let attack_speed = base_speed * 1.5; // Attackers are faster let attack_speed = base_speed * 1.5; // Attackers are faster
let arrival_threshold = base_speed * time.delta_seconds() * 1.1; // Threshold for reaching formation target let arrival_threshold = base_speed * time.delta_secs() * 1.1; // Threshold for reaching formation target
// --- Handle Entering Enemies --- // --- Handle Entering Enemies ---
for (entity, mut transform, target, mut state) in entering_query.iter_mut() { for (entity, mut transform, target, mut state) in entering_query.iter_mut() {
@ -183,7 +180,7 @@ pub fn move_enemies(
); );
} else { } else {
// Move towards target using base_speed // Move towards target using base_speed
let move_delta = direction.normalize() * base_speed * time.delta_seconds(); let move_delta = direction.normalize() * base_speed * time.delta_secs();
transform.translation += move_delta; transform.translation += move_delta;
} }
} }
@ -202,12 +199,12 @@ pub fn move_enemies(
// ... existing patterns ... // ... existing patterns ...
AttackPattern::SwoopDive => { AttackPattern::SwoopDive => {
// ... existing code ... // ... existing code ...
let vertical_movement = attack_speed * time.delta_seconds(); let vertical_movement = attack_speed * time.delta_secs();
let horizontal_speed_factor = 0.5; let horizontal_speed_factor = 0.5;
let horizontal_movement = if transform.translation.x < 0.0 { let horizontal_movement = if transform.translation.x < 0.0 {
attack_speed * horizontal_speed_factor * time.delta_seconds() attack_speed * horizontal_speed_factor * time.delta_secs()
} else if transform.translation.x > 0.0 { } else if transform.translation.x > 0.0 {
-attack_speed * horizontal_speed_factor * time.delta_seconds() -attack_speed * horizontal_speed_factor * time.delta_secs()
} else { } else {
0.0 0.0
}; };
@ -225,7 +222,7 @@ pub fn move_enemies(
} }
} }
AttackPattern::DirectDive => { AttackPattern::DirectDive => {
transform.translation.y -= attack_speed * time.delta_seconds(); transform.translation.y -= attack_speed * time.delta_secs();
} }
AttackPattern::Kamikaze(target) => { AttackPattern::Kamikaze(target) => {
// Copy the target value rather than dereferencing // Copy the target value rather than dereferencing
@ -233,11 +230,11 @@ pub fn move_enemies(
let target_pos = *target; // Dereference here let target_pos = *target; // Dereference here
let direction = target_pos - transform.translation; let direction = target_pos - transform.translation;
let distance = direction.length(); let distance = direction.length();
let kamikaze_threshold = attack_speed * time.delta_seconds() * 1.1; // Threshold to stop near target let kamikaze_threshold = attack_speed * time.delta_secs() * 1.1; // Threshold to stop near target
if distance > kamikaze_threshold { if distance > kamikaze_threshold {
let move_delta = let move_delta =
direction.normalize() * attack_speed * time.delta_seconds(); direction.normalize() * attack_speed * time.delta_secs();
transform.translation += move_delta; transform.translation += move_delta;
} else { } else {
// Optionally stop or continue past target - for now, just stop moving towards it // Optionally stop or continue past target - for now, just stop moving towards it
@ -247,7 +244,7 @@ pub fn move_enemies(
// New CaptureBeam pattern - Bosses behave differently // New CaptureBeam pattern - Bosses behave differently
AttackPattern::CaptureBeam => { AttackPattern::CaptureBeam => {
// For Grunt enemies, just do a direct dive (fallback) // For Grunt enemies, just do a direct dive (fallback)
transform.translation.y -= attack_speed * time.delta_seconds(); transform.translation.y -= attack_speed * time.delta_secs();
} }
} }
} }
@ -261,7 +258,7 @@ pub fn move_enemies(
if transform.translation.y > target_y { if transform.translation.y > target_y {
// Move down to position // Move down to position
transform.translation.y -= transform.translation.y -=
attack_speed * 0.8 * time.delta_seconds(); attack_speed * 0.8 * time.delta_secs();
} else { } else {
// Once in position, stay there briefly before activating beam // Once in position, stay there briefly before activating beam
// Check if this boss already has a TractorBeam component // Check if this boss already has a TractorBeam component
@ -293,11 +290,11 @@ pub fn move_enemies(
if direction.length() > 0.0 { if direction.length() > 0.0 {
let normalized_dir = direction.normalize(); let normalized_dir = direction.normalize();
transform.translation += transform.translation +=
normalized_dir * attack_speed * time.delta_seconds(); normalized_dir * attack_speed * time.delta_secs();
} }
} }
AttackPattern::DirectDive => { AttackPattern::DirectDive => {
transform.translation.y -= attack_speed * time.delta_seconds(); transform.translation.y -= attack_speed * time.delta_secs();
} }
AttackPattern::Kamikaze(target) => { AttackPattern::Kamikaze(target) => {
// Convert the target to a value type // Convert the target to a value type
@ -306,12 +303,12 @@ pub fn move_enemies(
// If very close to target, just move straight down // If very close to target, just move straight down
if direction.length() < 50.0 { if direction.length() < 50.0 {
transform.translation.y -= attack_speed * time.delta_seconds(); transform.translation.y -= attack_speed * time.delta_secs();
} else { } else {
// Move toward target // Move toward target
let normalized_dir = direction.normalize(); let normalized_dir = direction.normalize();
transform.translation += transform.translation +=
normalized_dir * attack_speed * time.delta_seconds(); normalized_dir * attack_speed * time.delta_secs();
} }
} }
} }
@ -334,7 +331,7 @@ pub fn move_enemies(
} else { } else {
// Move towards formation position // Move towards formation position
let move_delta = let move_delta =
direction.normalize() * return_speed * time.delta_seconds(); direction.normalize() * return_speed * time.delta_secs();
transform.translation += move_delta; transform.translation += move_delta;
} }
} }
@ -463,7 +460,7 @@ pub fn trigger_attack_dives(
// If Kamikaze, get player position (if player exists) // If Kamikaze, get player position (if player exists)
if let AttackPattern::Kamikaze(_) = selected_pattern { if let AttackPattern::Kamikaze(_) = selected_pattern {
if let Ok(player_transform) = player_query.get_single() { if let Ok(player_transform) = player_query.single() {
selected_pattern = AttackPattern::Kamikaze(player_transform.translation); selected_pattern = AttackPattern::Kamikaze(player_transform.translation);
} else { } else {
// Fallback if player doesn't exist (e.g., just died) // Fallback if player doesn't exist (e.g., just died)
@ -496,21 +493,18 @@ pub fn enemy_shoot(
if matches!(state, EnemyState::Attacking(_)) { if matches!(state, EnemyState::Attacking(_)) {
// Use matches! macro // Use matches! macro
enemy.shoot_cooldown.tick(time.delta()); enemy.shoot_cooldown.tick(time.delta());
if enemy.shoot_cooldown.finished() { if enemy.shoot_cooldown.is_finished() {
// println!("Enemy {:?} firing!", transform.translation); // Less verbose logging // println!("Enemy {:?} firing!", transform.translation); // Less verbose logging
let bullet_start_pos = transform.translation let bullet_start_pos = transform.translation
- Vec3::Y * (ENEMY_SIZE.y / 2.0 + ENEMY_BULLET_SIZE.y / 2.0); - Vec3::Y * (ENEMY_SIZE.y / 2.0 + ENEMY_BULLET_SIZE.y / 2.0);
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: Color::srgb(1.0, 0.5, 0.5),
color: Color::rgb(1.0, 0.5, 0.5), custom_size: Some(ENEMY_BULLET_SIZE),
custom_size: Some(ENEMY_BULLET_SIZE),
..default()
},
transform: Transform::from_translation(bullet_start_pos),
..default() ..default()
}, },
Transform::from_translation(bullet_start_pos),
EnemyBullet, EnemyBullet,
)); ));
@ -546,6 +540,7 @@ pub fn boss_capture_attack(
mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>, mut boss_query: Query<(Entity, &Transform, &mut TractorBeam)>,
player_query: Query<(Entity, &Transform), (With<Player>, Without<Captured>)>, // Only target non-captured players player_query: Query<(Entity, &Transform), (With<Player>, Without<Captured>)>, // Only target non-captured players
enemy_query: Query<&EnemyState, With<Enemy>>, enemy_query: Query<&EnemyState, With<Enemy>>,
children_query: Query<&Children>,
) { ) {
for (boss_entity, boss_transform, mut tractor_beam) in boss_query.iter_mut() { for (boss_entity, boss_transform, mut tractor_beam) in boss_query.iter_mut() {
// Tick the beam timer // Tick the beam timer
@ -553,7 +548,7 @@ pub fn boss_capture_attack(
// If player exists and beam is not active yet, set player as target // If player exists and beam is not active yet, set player as target
if !tractor_beam.active && tractor_beam.target.is_none() { if !tractor_beam.active && tractor_beam.target.is_none() {
if let Ok((player_entity, _)) = player_query.get_single() { if let Ok((player_entity, _)) = player_query.single() {
tractor_beam.target = Some(player_entity); tractor_beam.target = Some(player_entity);
tractor_beam.active = true; tractor_beam.active = true;
println!( println!(
@ -566,33 +561,27 @@ pub fn boss_capture_attack(
commands.entity(boss_entity).with_children(|parent| { commands.entity(boss_entity).with_children(|parent| {
// Outer glow layer (wide, dim) // Outer glow layer (wide, dim)
parent.spawn(( parent.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: BEAM_GLOW_COLOR,
color: BEAM_GLOW_COLOR, custom_size: Some(Vec2::new(BEAM_GLOW_WIDTH, beam_height)),
custom_size: Some(Vec2::new(BEAM_GLOW_WIDTH, beam_height)),
..default()
},
transform: Transform::from_xyz(0.0, -beam_height / 2.0, -0.5),
..default() ..default()
}, },
Transform::from_xyz(0.0, -beam_height / 2.0, -0.5),
TractorBeamSprite, TractorBeamSprite,
)); ));
// Inner core layer (narrow, bright, shorter) // Inner core layer (narrow, bright, shorter)
let core_height = beam_height * 0.6; let core_height = beam_height * 0.6;
parent.spawn(( parent.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: BEAM_CORE_COLOR,
color: BEAM_CORE_COLOR, custom_size: Some(Vec2::new(TRACTOR_BEAM_WIDTH, core_height)),
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() ..default()
}, },
Transform::from_xyz(
0.0,
-core_height / 2.0 - beam_height * 0.2,
-0.3,
),
TractorBeamSprite, TractorBeamSprite,
)); ));
}); });
@ -601,7 +590,7 @@ pub fn boss_capture_attack(
// If beam is active, check if player is in beam's path // If beam is active, check if player is in beam's path
if tractor_beam.active { if tractor_beam.active {
if let Ok((player_entity, player_transform)) = player_query.get_single() { if let Ok((player_entity, player_transform)) = player_query.single() {
// Check if player is roughly under the boss // Check if player is roughly under the boss
if (player_transform.translation.x - boss_transform.translation.x).abs() if (player_transform.translation.x - boss_transform.translation.x).abs()
< tractor_beam.width / 2.0 < tractor_beam.width / 2.0
@ -622,14 +611,16 @@ pub fn boss_capture_attack(
commands.entity(boss_entity).remove::<TractorBeam>(); commands.entity(boss_entity).remove::<TractorBeam>();
// Change boss state to returning with captive // Change boss state to returning with captive
if let Ok(enemy) = enemy_query.get(boss_entity) { if let Ok(EnemyState::Attacking(_)) = enemy_query.get(boss_entity) {
if let EnemyState::Attacking(_) = enemy { commands
commands .entity(boss_entity)
.entity(boss_entity) .insert(EnemyState::ReturningWithCaptive);
.insert(EnemyState::ReturningWithCaptive);
// Clean up the beam visual // Clean up the beam visual
commands.entity(boss_entity).despawn_descendants(); if let Ok(children) = children_query.get(boss_entity) {
for child in children.iter() {
commands.entity(child).despawn();
}
} }
} }
break; break;
@ -638,20 +629,22 @@ pub fn boss_capture_attack(
} }
// If beam timer finishes and player wasn't captured, end the beam attack // If beam timer finishes and player wasn't captured, end the beam attack
if tractor_beam.timer.finished() { if tractor_beam.timer.is_finished() {
println!("Boss {:?} tractor beam expired", boss_entity); println!("Boss {:?} tractor beam expired", boss_entity);
commands.entity(boss_entity).remove::<TractorBeam>(); commands.entity(boss_entity).remove::<TractorBeam>();
// Clean up the beam visual // Clean up the beam visual
commands.entity(boss_entity).despawn_descendants(); if let Ok(children) = children_query.get(boss_entity) {
for child in children.iter() {
commands.entity(child).despawn();
}
}
// Boss returns to formation after failed capture // Boss returns to formation after failed capture
if let Ok(enemy_state) = enemy_query.get(boss_entity) { if let Ok(EnemyState::Attacking(_)) = enemy_query.get(boss_entity) {
if let EnemyState::Attacking(_) = enemy_state { commands
commands .entity(boss_entity)
.entity(boss_entity) .insert(EnemyState::ReturningWithCaptive);
.insert(EnemyState::ReturningWithCaptive);
}
} }
} }
} }
@ -666,33 +659,20 @@ pub fn update_tractor_beam_visual(
) { ) {
for (boss_entity, boss_transform, _tractor_beam) in boss_query.iter() { for (boss_entity, boss_transform, _tractor_beam) in boss_query.iter() {
let current_beam_height = calculate_beam_height(boss_transform.translation.y); let current_beam_height = calculate_beam_height(boss_transform.translation.y);
let time_secs = time.elapsed_seconds(); let time_secs = time.elapsed_secs();
if let Ok(child_entities) = children.get(boss_entity) { if let Ok(child_entities) = children.get(boss_entity) {
for child in child_entities { for child in child_entities {
if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) { if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) {
// Update color pulse // Update color pulse
if let Color::Rgba { let base_alpha = sprite.color.alpha();
red, let new_alpha = beam_pulse_alpha(
green, base_alpha,
blue, time_secs,
mut alpha, BEAM_PULSE_FREQ,
} = sprite.color BEAM_PULSE_AMPLITUDE,
{ );
let base_alpha = alpha; sprite.color.set_alpha(new_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 // Update size and position
let current_width = sprite.custom_size.map(|s| s.x).unwrap_or(0.0); let current_width = sprite.custom_size.map(|s| s.x).unwrap_or(0.0);

View file

@ -16,39 +16,35 @@ pub enum AppState {
pub fn setup_game_over_ui(mut commands: Commands) { pub fn setup_game_over_ui(mut commands: Commands) {
println!("Entering GameOver state. Setting up UI."); println!("Entering GameOver state. Setting up UI.");
commands.spawn(( commands.spawn((
TextBundle::from_section( Text::new("GAME OVER"),
"GAME OVER", TextFont {
TextStyle { font_size: 100.0,
font_size: 100.0, ..default()
color: Color::WHITE, },
..default() TextColor(Color::WHITE),
}, Node {
)
.with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
align_self: AlignSelf::Center, align_self: AlignSelf::Center,
justify_self: JustifySelf::Center, justify_self: JustifySelf::Center,
top: Val::Percent(40.0), top: Val::Percent(40.0),
..default() ..default()
}), },
GameOverUI, GameOverUI,
)); ));
commands.spawn(( commands.spawn((
TextBundle::from_section( Text::new("Press R to Restart"),
"Press R to Restart", TextFont {
TextStyle { font_size: 32.0,
font_size: 32.0, ..default()
color: Color::WHITE, },
..default() TextColor(Color::WHITE),
}, Node {
)
.with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
align_self: AlignSelf::Center, align_self: AlignSelf::Center,
justify_self: JustifySelf::Center, justify_self: JustifySelf::Center,
top: Val::Percent(55.0), top: Val::Percent(55.0),
..default() ..default()
}), },
RestartMessage, RestartMessage,
)); ));
} }
@ -60,10 +56,10 @@ pub fn cleanup_game_over_ui(
) { ) {
println!("Exiting GameOver state. Cleaning up UI."); println!("Exiting GameOver state. Cleaning up UI.");
for entity in query.iter() { for entity in query.iter() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
for entity in restart_query.iter() { for entity in restart_query.iter() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
} }
@ -76,8 +72,6 @@ pub fn cleanup_game_entities(
enemy_bullet_query: Query<Entity, With<crate::components::EnemyBullet>>, enemy_bullet_query: Query<Entity, With<crate::components::EnemyBullet>>,
enemy_query: Query<Entity, With<Enemy>>, enemy_query: Query<Entity, With<Enemy>>,
restart_message_query: Query<Entity, With<crate::components::RestartMessage>>, restart_message_query: Query<Entity, With<crate::components::RestartMessage>>,
// Optionally despawn player too, or handle separately if needed for restart
// player_query: Query<Entity, With<Player>>,
) { ) {
println!("Exiting Playing state. Cleaning up game entities."); println!("Exiting Playing state. Cleaning up game entities.");
for entity in bullet_query.iter() { for entity in bullet_query.iter() {
@ -92,9 +86,6 @@ pub fn cleanup_game_entities(
for entity in restart_message_query.iter() { for entity in restart_message_query.iter() {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
} }
// for entity in player_query.iter() {
// commands.entity(entity).despawn();
// }
} }
// --- Start Menu UI --- // --- Start Menu UI ---
@ -102,65 +93,57 @@ pub fn cleanup_game_entities(
pub fn setup_start_menu_ui(mut commands: Commands) { pub fn setup_start_menu_ui(mut commands: Commands) {
println!("Entering StartMenu state. Setting up UI."); println!("Entering StartMenu state. Setting up UI.");
// Root UI container
commands commands
.spawn(( .spawn((
NodeBundle { Node {
style: Style { width: Val::Percent(100.0),
width: Val::Percent(100.0), height: Val::Percent(100.0),
height: Val::Percent(100.0), align_items: AlignItems::Center,
align_items: AlignItems::Center, justify_content: JustifyContent::Center,
justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column,
flex_direction: FlexDirection::Column,
..default()
},
..default() ..default()
}, },
StartMenuUI, StartMenuUI,
)) ))
.with_children(|parent| { .with_children(|parent| {
// Title // Title
parent.spawn( parent.spawn((
TextBundle::from_section( Text::new("BGLGA"),
"BGLGA", TextFont {
TextStyle { font_size: 120.0,
font_size: 120.0, ..default()
color: Color::WHITE, },
..default() TextColor(Color::WHITE),
}, Node {
)
.with_style(Style {
margin: UiRect::bottom(Val::Px(50.0)), margin: UiRect::bottom(Val::Px(50.0)),
..default() ..default()
}), },
); ));
// Start Game Button // Start Game Button
parent parent
.spawn(( .spawn((
ButtonBundle { Button,
style: Style { Node {
width: Val::Px(250.0), width: Val::Px(250.0),
height: Val::Px(80.0), height: Val::Px(80.0),
border: UiRect::all(Val::Px(2.0)), border: UiRect::all(Val::Px(2.0)),
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default()
},
border_color: BorderColor(Color::WHITE),
background_color: BackgroundColor(Color::rgb(0.1, 0.1, 0.5)),
..default() ..default()
}, },
BorderColor::all(Color::WHITE),
BackgroundColor(Color::srgb(0.1, 0.1, 0.5)),
StartButton, StartButton,
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle::from_section( parent.spawn((
"Start Game", Text::new("Start Game"),
TextStyle { TextFont {
font_size: 40.0, font_size: 40.0,
color: Color::WHITE,
..default() ..default()
}, },
TextColor(Color::WHITE),
)); ));
}); });
}); });
@ -169,7 +152,7 @@ pub fn setup_start_menu_ui(mut commands: Commands) {
pub fn cleanup_start_menu_ui(mut commands: Commands, query: Query<Entity, With<StartMenuUI>>) { pub fn cleanup_start_menu_ui(mut commands: Commands, query: Query<Entity, With<StartMenuUI>>) {
println!("Exiting StartMenu state. Cleaning up UI."); println!("Exiting StartMenu state. Cleaning up UI.");
for entity in query.iter() { for entity in query.iter() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
} }
@ -187,10 +170,10 @@ pub fn start_menu_button_system(
app_state.set(AppState::Playing); app_state.set(AppState::Playing);
} }
Interaction::Hovered => { Interaction::Hovered => {
color.0 = Color::rgb(0.2, 0.2, 0.7); color.0 = Color::srgb(0.2, 0.2, 0.7);
} }
Interaction::None => { Interaction::None => {
color.0 = Color::rgb(0.1, 0.1, 0.5); color.0 = Color::srgb(0.1, 0.1, 0.5);
} }
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use bevy::prelude::*; use bevy::prelude::*;
use std::time::Duration; use std::time::Duration;
@ -42,11 +44,11 @@ use starfield::scroll_starfield;
pub fn run() { pub fn run() {
App::new() App::new()
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.1))) .insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.1)))
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
title: "BGLGA".into(), title: "BGLGA".into(),
resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(), resolution: (WINDOW_WIDTH as u32, WINDOW_HEIGHT as u32).into(),
resizable: false, resizable: false,
..default() ..default()
}), }),

View file

@ -14,19 +14,16 @@ use crate::systems::spawn_explosion;
// Helper to spawn player (used in setup and respawn) // Helper to spawn player (used in setup and respawn)
pub fn spawn_player_ship(commands: &mut Commands) { pub fn spawn_player_ship(commands: &mut Commands) {
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: Color::srgb(0.0, 0.5, 1.0),
color: Color::rgb(0.0, 0.5, 1.0), custom_size: Some(PLAYER_SIZE),
custom_size: Some(PLAYER_SIZE),
..default()
},
transform: Transform::from_translation(Vec3::new(
0.0,
-WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0,
0.0,
)),
..default() ..default()
}, },
Transform::from_translation(Vec3::new(
0.0,
-WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0,
0.0,
)),
Player { Player {
speed: PLAYER_SPEED, speed: PLAYER_SPEED,
shoot_cooldown: Timer::new(Duration::from_secs_f32(0.3), TimerMode::Once), shoot_cooldown: Timer::new(Duration::from_secs_f32(0.3), TimerMode::Once),
@ -48,7 +45,7 @@ pub fn move_player(
time: Res<Time>, time: Res<Time>,
) { ) {
// Using get_single_mut handles the case where player might not exist yet (or was just destroyed) // Using get_single_mut handles the case where player might not exist yet (or was just destroyed)
if let Ok((mut transform, player)) = query.get_single_mut() { if let Ok((mut transform, player)) = query.single_mut() {
let mut direction = 0.0; let mut direction = 0.0;
if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) { if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
@ -59,7 +56,7 @@ pub fn move_player(
direction += 1.0; direction += 1.0;
} }
transform.translation.x += direction * player.speed * time.delta_seconds(); transform.translation.x += direction * player.speed * time.delta_secs();
let half_player_width = PLAYER_SIZE.x / 2.0; let half_player_width = PLAYER_SIZE.x / 2.0;
transform.translation.x = transform.translation.x.clamp( transform.translation.x = transform.translation.x.clamp(
-WINDOW_WIDTH / 2.0 + half_player_width, -WINDOW_WIDTH / 2.0 + half_player_width,
@ -124,7 +121,7 @@ pub fn handle_captured_player(
} }
// If capture duration expires, player escapes but loses a life // If capture duration expires, player escapes but loses a life
if captured.timer.finished() { if captured.timer.is_finished() {
println!("Player escaped from capture after timer expired!"); println!("Player escaped from capture after timer expired!");
commands.entity(entity).remove::<Captured>(); commands.entity(entity).remove::<Captured>();
lose_life_and_respawn( lose_life_and_respawn(
@ -175,12 +172,12 @@ pub fn player_shoot(
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
) { ) {
if let Ok((player_transform, mut player)) = query.get_single_mut() { if let Ok((player_transform, mut player)) = query.single_mut() {
player.shoot_cooldown.tick(time.delta()); player.shoot_cooldown.tick(time.delta());
if (keyboard_input.just_pressed(KeyCode::Space) if (keyboard_input.just_pressed(KeyCode::Space)
|| keyboard_input.just_pressed(KeyCode::ArrowUp)) || keyboard_input.just_pressed(KeyCode::ArrowUp))
&& player.shoot_cooldown.finished() && player.shoot_cooldown.is_finished()
{ {
player.shoot_cooldown.reset(); player.shoot_cooldown.reset();
@ -188,15 +185,12 @@ pub fn player_shoot(
+ Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0); + Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: Color::srgb(1.0, 1.0, 1.0),
color: Color::rgb(1.0, 1.0, 1.0), custom_size: Some(BULLET_SIZE),
custom_size: Some(BULLET_SIZE),
..default()
},
transform: Transform::from_translation(bullet_start_pos),
..default() ..default()
}, },
Transform::from_translation(bullet_start_pos),
Bullet, Bullet,
)); ));
} }
@ -217,7 +211,7 @@ pub fn check_player_enemy_collisions(
enemy_query: Query<(Entity, &Transform), With<Enemy>>, enemy_query: Query<(Entity, &Transform), With<Enemy>>,
) { ) {
// This system only runs if player exists and is not invincible, due to run_if // This system only runs if player exists and is not invincible, due to run_if
if let Ok((player_entity, player_transform)) = player_query.get_single() { if let Ok((player_entity, player_transform)) = player_query.single() {
for (enemy_entity, enemy_transform) in enemy_query.iter() { for (enemy_entity, enemy_transform) in enemy_query.iter() {
let distance = player_transform let distance = player_transform
.translation .translation
@ -285,7 +279,7 @@ pub fn manage_invincibility(
}; };
} }
if invincible.timer.finished() { if invincible.timer.is_finished() {
println!("Invincibility finished."); println!("Invincibility finished.");
commands.entity(entity).remove::<Invincible>(); commands.entity(entity).remove::<Invincible>();
// Ensure player is visible when invincibility ends // Ensure player is visible when invincibility ends

View file

@ -13,26 +13,23 @@ pub fn spawn_starfield(commands: &mut Commands) {
let brightness = fastrand::f32() * 0.5 + 0.5; // 0.5 to 1.0 let brightness = fastrand::f32() * 0.5 + 0.5; // 0.5 to 1.0
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: Color::srgb(brightness, brightness, brightness),
color: Color::rgb(brightness, brightness, brightness), custom_size: Some(Vec2::new(size, size)),
custom_size: Some(Vec2::new(size, size)),
..default()
},
transform: Transform::from_translation(Vec3::new(
fastrand::f32() * WINDOW_WIDTH - WINDOW_WIDTH / 2.0,
fastrand::f32() * WINDOW_HEIGHT - WINDOW_HEIGHT / 2.0,
STAR_Z_DEPTH,
)),
..default() ..default()
}, },
Transform::from_translation(Vec3::new(
fastrand::f32() * WINDOW_WIDTH - WINDOW_WIDTH / 2.0,
fastrand::f32() * WINDOW_HEIGHT - WINDOW_HEIGHT / 2.0,
STAR_Z_DEPTH,
)),
Star { speed }, Star { speed },
)); ));
} }
} }
pub fn scroll_starfield(mut star_query: Query<(&mut Transform, &Star)>, time: Res<Time>) { pub fn scroll_starfield(mut star_query: Query<(&mut Transform, &Star)>, time: Res<Time>) {
let dt = time.delta_seconds(); let dt = time.delta_secs();
let half_height = WINDOW_HEIGHT / 2.0; let half_height = WINDOW_HEIGHT / 2.0;
let half_width = WINDOW_WIDTH / 2.0; let half_width = WINDOW_WIDTH / 2.0;

View file

@ -8,7 +8,7 @@ use crate::starfield::spawn_starfield;
// --- Setup --- // --- Setup ---
pub fn setup(mut commands: Commands) { pub fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2d);
spawn_starfield(&mut commands); spawn_starfield(&mut commands);
spawn_player_ship(&mut commands); spawn_player_ship(&mut commands);
} }
@ -26,24 +26,22 @@ pub fn should_respawn_player(lives: Res<PlayerLives>, player_query: Query<&Playe
player_query.is_empty() && lives.count > 0 player_query.is_empty() && lives.count > 0
} }
// Moved is_formation_complete to enemy.rs as it's closely related to enemy logic
// --- General Systems --- // --- General Systems ---
// Update Window Title with Lives, Score, and Stage // Update Window Title with Lives, Score, and Stage
pub fn update_window_title( pub fn update_window_title(
lives: Res<PlayerLives>, lives: Res<PlayerLives>,
score: Res<Score>, score: Res<Score>,
stage: Res<CurrentStage>, // Add CurrentStage resource stage: Res<CurrentStage>,
mut windows: Query<&mut Window>, mut windows: Query<&mut Window>,
) { ) {
// Update if lives, score, or stage changed
if lives.is_changed() || score.is_changed() || stage.is_changed() { if lives.is_changed() || score.is_changed() || stage.is_changed() {
let mut window = windows.single_mut(); if let Ok(mut window) = windows.single_mut() {
window.title = format!( window.title = format!(
"Galaga :: Stage: {} Lives: {} Score: {}", "Galaga :: Stage: {} Lives: {} Score: {}",
stage.number, lives.count, score.value stage.number, lives.count, score.value
); );
}
} }
} }
@ -51,18 +49,12 @@ pub fn update_window_title(
pub fn spawn_explosion(commands: &mut Commands, position: Vec3) { pub fn spawn_explosion(commands: &mut Commands, position: Vec3) {
commands.spawn(( commands.spawn((
SpriteBundle { Sprite {
sprite: Sprite { color: EXPLOSION_COLOR,
color: EXPLOSION_COLOR, custom_size: Some(EXPLOSION_BASE_SIZE),
custom_size: Some(EXPLOSION_BASE_SIZE),
..default()
},
transform: Transform {
translation: position,
..default()
},
..default() ..default()
}, },
Transform::from_translation(position),
Explosion { Explosion {
timer: Timer::new(std::time::Duration::from_secs_f32(EXPLOSION_DURATION), TimerMode::Once), timer: Timer::new(std::time::Duration::from_secs_f32(EXPLOSION_DURATION), TimerMode::Once),
}, },
@ -80,18 +72,16 @@ pub fn animate_explosion(
let progress = explosion.timer.elapsed_secs() / EXPLOSION_DURATION; let progress = explosion.timer.elapsed_secs() / EXPLOSION_DURATION;
let clamped_progress = progress.min(1.0); let clamped_progress = progress.min(1.0);
// Scale from base to max size
let scale_x = EXPLOSION_BASE_SIZE.x + (EXPLOSION_MAX_SIZE.x - EXPLOSION_BASE_SIZE.x) * clamped_progress; let scale_x = EXPLOSION_BASE_SIZE.x + (EXPLOSION_MAX_SIZE.x - EXPLOSION_BASE_SIZE.x) * clamped_progress;
let scale_y = EXPLOSION_BASE_SIZE.y + (EXPLOSION_MAX_SIZE.y - EXPLOSION_BASE_SIZE.y) * clamped_progress; let scale_y = EXPLOSION_BASE_SIZE.y + (EXPLOSION_MAX_SIZE.y - EXPLOSION_BASE_SIZE.y) * clamped_progress;
sprite.custom_size = Some(Vec2::new(scale_x, scale_y)); sprite.custom_size = Some(Vec2::new(scale_x, scale_y));
// Fade alpha from 1.0 to 0.0
let alpha = 1.0 - clamped_progress; let alpha = 1.0 - clamped_progress;
let base = EXPLOSION_COLOR; let base = EXPLOSION_COLOR.to_srgba();
sprite.color = Color::rgba(base.r(), base.g(), base.b(), alpha); sprite.color = Color::srgba(base.red, base.green, base.blue, alpha);
if explosion.timer.finished() { if explosion.timer.is_finished() {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
} }
} }
} }