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>
291 lines
10 KiB
Rust
291 lines
10 KiB
Rust
use bevy::prelude::*;
|
|
use bevy::ecs::system::ParamSet;
|
|
use std::time::Duration;
|
|
|
|
use crate::components::{Bullet, Captured, Enemy, Invincible, Player};
|
|
use crate::constants::{
|
|
BULLET_SIZE, PLAYER_ENEMY_COLLISION_THRESHOLD, PLAYER_INVINCIBILITY_DURATION, PLAYER_SIZE,
|
|
PLAYER_SPEED, WINDOW_HEIGHT, WINDOW_WIDTH,
|
|
};
|
|
use crate::game_state::AppState;
|
|
use crate::resources::{PlayerLives, PlayerRespawnTimer};
|
|
use crate::systems::spawn_explosion;
|
|
|
|
// Helper to spawn player (used in setup and respawn)
|
|
pub fn spawn_player_ship(commands: &mut Commands) {
|
|
commands.spawn((
|
|
Sprite {
|
|
color: Color::srgb(0.0, 0.5, 1.0),
|
|
custom_size: Some(PLAYER_SIZE),
|
|
..default()
|
|
},
|
|
Transform::from_translation(Vec3::new(
|
|
0.0,
|
|
-WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0,
|
|
0.0,
|
|
)),
|
|
Player {
|
|
speed: PLAYER_SPEED,
|
|
shoot_cooldown: Timer::new(Duration::from_secs_f32(0.3), TimerMode::Once),
|
|
},
|
|
// Player starts invincible for a short time
|
|
Invincible {
|
|
timer: Timer::new(
|
|
Duration::from_secs_f32(PLAYER_INVINCIBILITY_DURATION),
|
|
TimerMode::Once,
|
|
),
|
|
},
|
|
));
|
|
println!("Player spawned!");
|
|
}
|
|
|
|
pub fn move_player(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut query: Query<(&mut Transform, &Player), Without<Captured>>, // Don't move captured players with controls
|
|
time: Res<Time>,
|
|
) {
|
|
// Using get_single_mut handles the case where player might not exist yet (or was just destroyed)
|
|
if let Ok((mut transform, player)) = query.single_mut() {
|
|
let mut direction = 0.0;
|
|
|
|
if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
|
|
direction -= 1.0;
|
|
}
|
|
|
|
if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
|
|
direction += 1.0;
|
|
}
|
|
|
|
transform.translation.x += direction * player.speed * time.delta_secs();
|
|
let half_player_width = PLAYER_SIZE.x / 2.0;
|
|
transform.translation.x = transform.translation.x.clamp(
|
|
-WINDOW_WIDTH / 2.0 + half_player_width,
|
|
WINDOW_WIDTH / 2.0 - half_player_width,
|
|
);
|
|
}
|
|
}
|
|
|
|
// New system to handle captured player movement
|
|
pub fn handle_captured_player(
|
|
mut commands: Commands,
|
|
time: Res<Time>,
|
|
mut set: ParamSet<(
|
|
Query<(Entity, &mut Transform, &mut Captured)>,
|
|
Query<&Transform, (With<Enemy>, Without<Player>)>,
|
|
)>,
|
|
mut lives: ResMut<PlayerLives>,
|
|
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
|
mut next_state: ResMut<NextState<AppState>>,
|
|
) {
|
|
// First, collect data about captured players and their bosses
|
|
let mut captured_data = Vec::new();
|
|
|
|
// Get player data
|
|
for (entity, transform, captured) in set.p0().iter() {
|
|
captured_data.push((entity, transform.translation, captured.boss_entity, captured.timer.clone()));
|
|
}
|
|
|
|
// Process each captured player
|
|
for (player_entity, _player_pos, boss_entity, mut timer) in captured_data {
|
|
// Check if the boss exists and get its position
|
|
let boss_pos = set.p1().get(boss_entity).map(|t| t.translation).ok();
|
|
|
|
// Tick the timer
|
|
timer.tick(time.delta());
|
|
|
|
// Update the player
|
|
if let Ok((entity, mut transform, mut captured)) = set.p0().get_mut(player_entity) {
|
|
// Update the actual timer
|
|
captured.timer.tick(time.delta());
|
|
|
|
match boss_pos {
|
|
Some(boss_pos) => {
|
|
// Boss exists, update player position
|
|
let target_pos = boss_pos - Vec3::new(0.0, PLAYER_SIZE.y + 10.0, 0.0);
|
|
transform.translation = transform.translation.lerp(target_pos, 0.2);
|
|
}
|
|
None => {
|
|
// Boss is gone, release player but lose a life
|
|
println!("Boss is gone, releasing captured player!");
|
|
commands.entity(entity).remove::<Captured>();
|
|
lose_life_and_respawn(
|
|
&mut commands,
|
|
&mut lives,
|
|
&mut respawn_timer,
|
|
&mut next_state,
|
|
entity,
|
|
_player_pos,
|
|
);
|
|
continue; // Skip the rest of processing for this player
|
|
}
|
|
}
|
|
|
|
// If capture duration expires, player escapes but loses a life
|
|
if captured.timer.is_finished() {
|
|
println!("Player escaped from capture after timer expired!");
|
|
commands.entity(entity).remove::<Captured>();
|
|
lose_life_and_respawn(
|
|
&mut commands,
|
|
&mut lives,
|
|
&mut respawn_timer,
|
|
&mut next_state,
|
|
entity,
|
|
transform.translation,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function for player life loss and respawn logic
|
|
fn lose_life_and_respawn(
|
|
commands: &mut Commands,
|
|
lives: &mut ResMut<PlayerLives>,
|
|
respawn_timer: &mut ResMut<PlayerRespawnTimer>,
|
|
next_state: &mut ResMut<NextState<AppState>>,
|
|
player_entity: Entity,
|
|
player_position: Vec3,
|
|
) {
|
|
// Lose a life
|
|
lives.count = lives.count.saturating_sub(1);
|
|
println!("Lives remaining: {}", lives.count);
|
|
|
|
// Spawn explosion at player position before destroying
|
|
spawn_explosion(commands, player_position);
|
|
|
|
// Destroy player
|
|
commands.entity(player_entity).despawn();
|
|
|
|
if lives.count > 0 {
|
|
respawn_timer.timer.reset();
|
|
respawn_timer.timer.unpause();
|
|
println!("Respawn timer started.");
|
|
} else {
|
|
println!("GAME OVER!");
|
|
next_state.set(AppState::GameOver);
|
|
}
|
|
}
|
|
|
|
pub fn player_shoot(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut query: Query<(&Transform, &mut Player), Without<Captured>>, // Only non-captured players can shoot
|
|
mut commands: Commands,
|
|
time: Res<Time>,
|
|
) {
|
|
if let Ok((player_transform, mut player)) = query.single_mut() {
|
|
player.shoot_cooldown.tick(time.delta());
|
|
|
|
if (keyboard_input.just_pressed(KeyCode::Space)
|
|
|| keyboard_input.just_pressed(KeyCode::ArrowUp))
|
|
&& player.shoot_cooldown.is_finished()
|
|
{
|
|
player.shoot_cooldown.reset();
|
|
|
|
let bullet_start_pos = player_transform.translation
|
|
+ Vec3::Y * (PLAYER_SIZE.y / 2.0 + BULLET_SIZE.y / 2.0);
|
|
|
|
commands.spawn((
|
|
Sprite {
|
|
color: Color::srgb(1.0, 1.0, 1.0),
|
|
custom_size: Some(BULLET_SIZE),
|
|
..default()
|
|
},
|
|
Transform::from_translation(bullet_start_pos),
|
|
Bullet,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Modified Collision Check for Player vs Enemy
|
|
pub fn check_player_enemy_collisions(
|
|
mut commands: Commands,
|
|
mut lives: ResMut<PlayerLives>,
|
|
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
|
mut next_state: ResMut<NextState<AppState>>, // Resource to change state
|
|
// Query player without Invincible component - relies on run_if condition too
|
|
player_query: Query<
|
|
(Entity, &Transform),
|
|
(With<Player>, Without<Invincible>, Without<Captured>),
|
|
>, // Don't check collisions for captured players
|
|
enemy_query: Query<(Entity, &Transform), With<Enemy>>,
|
|
) {
|
|
// This system only runs if player exists and is not invincible, due to run_if
|
|
if let Ok((player_entity, player_transform)) = player_query.single() {
|
|
for (enemy_entity, enemy_transform) in enemy_query.iter() {
|
|
let distance = player_transform
|
|
.translation
|
|
.distance(enemy_transform.translation);
|
|
|
|
if distance < PLAYER_ENEMY_COLLISION_THRESHOLD {
|
|
println!("Player hit by enemy!");
|
|
spawn_explosion(&mut commands, enemy_transform.translation);
|
|
commands.entity(enemy_entity).despawn(); // Despawn enemy
|
|
|
|
lives.count = lives.count.saturating_sub(1); // Decrement lives safely
|
|
println!("Lives remaining: {}", lives.count);
|
|
|
|
spawn_explosion(&mut commands, player_transform.translation);
|
|
commands.entity(player_entity).despawn(); // Despawn player
|
|
|
|
if lives.count > 0 {
|
|
// Start the respawn timer
|
|
respawn_timer.timer.reset();
|
|
respawn_timer.timer.unpause();
|
|
println!("Respawn timer started.");
|
|
} else {
|
|
println!("GAME OVER!");
|
|
next_state.set(AppState::GameOver); // Updated for newer Bevy states API
|
|
}
|
|
// Important: Break after handling one collision per frame for the player
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New System: Respawn Player
|
|
pub fn respawn_player(
|
|
mut commands: Commands,
|
|
time: Res<Time>,
|
|
mut respawn_timer: ResMut<PlayerRespawnTimer>,
|
|
// No player query needed here due to run_if condition
|
|
) {
|
|
// Tick the timer only if it's actually running
|
|
if respawn_timer.timer.tick(time.delta()).just_finished() {
|
|
println!("Respawn timer finished. Spawning player.");
|
|
spawn_player_ship(&mut commands);
|
|
respawn_timer.timer.pause(); // Pause timer until next death
|
|
}
|
|
}
|
|
|
|
// New System: Manage Invincibility
|
|
pub fn manage_invincibility(
|
|
mut commands: Commands,
|
|
time: Res<Time>,
|
|
mut query: Query<(Entity, &mut Invincible, Option<&mut Visibility>), With<Player>>,
|
|
) {
|
|
for (entity, mut invincible, mut visibility) in query.iter_mut() {
|
|
invincible.timer.tick(time.delta());
|
|
|
|
// Blinking effect (optional)
|
|
if let Some(ref mut vis) = visibility {
|
|
// Blink roughly 5 times per second
|
|
let elapsed_secs = invincible.timer.elapsed_secs();
|
|
**vis = if (elapsed_secs * 10.0).floor() % 2.0 == 0.0 {
|
|
Visibility::Visible
|
|
} else {
|
|
Visibility::Hidden
|
|
};
|
|
}
|
|
|
|
if invincible.timer.is_finished() {
|
|
println!("Invincibility finished.");
|
|
commands.entity(entity).remove::<Invincible>();
|
|
// Ensure player is visible when invincibility ends
|
|
if let Some(ref mut vis) = visibility {
|
|
**vis = Visibility::Visible;
|
|
}
|
|
}
|
|
}
|
|
}
|