diff --git a/README.md b/README.md index a373a22..4fae563 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,73 @@ nix develop --command bash -c "cargo build" 2. Clone the repository. 3. Navigate to the project directory. 4. Run the game using the command: `nix develop --command bash -c "cargo run"` + +# TODO + +**1. Core Gameplay Loop & State Management:** + +* **Player Lives:** + * Add a `PlayerLives` resource (e.g., starting with 3). + * Modify `check_player_enemy_collisions`: Instead of just printing, decrement the lives count. + * Implement player destruction (despawn the player sprite) and respawn logic (maybe after a short delay, with temporary invincibility). +* **Game Over State:** + * Use Bevy's `States` (e.g., `Playing`, `GameOver`). + * Transition to `GameOver` when `PlayerLives` reaches zero. + * In the `GameOver` state: stop enemy spawning, stop player controls, display a "Game Over" message (using `bevy_ui`), potentially offer a restart option. +* **Scoring:** + * Add a `Score` resource. + * Increment the score in `check_bullet_collisions` when an enemy is hit. + * Consider different point values for different enemy types or hitting enemies during dives later. +* **Levels/Stages:** + * Add a `CurrentStage` resource. + * Define criteria for clearing a stage (e.g., destroying all enemies in a wave/formation). + * Implement logic to advance to the next stage, potentially increasing difficulty (enemy speed, firing rate, different formations). + +**2. Enemy Behavior - Formations & Attack Patterns:** + +* **Enemy Formations:** This is a core Galaga feature. + * Define target positions for the enemy formation on screen. + * Give enemies an `Entering` state/component: They fly onto the screen following predefined paths (curves, waypoints). + * Give enemies a `Formation` state/component: Once they reach their target position, they stop and hold formation. +* **Enemy Attack Dives:** + * Give enemies an `Attacking` state/component. + * Periodically trigger enemies in the formation to switch to the `Attacking` state. + * Define attack paths (swooping dives towards the player area). + * Make enemies fire bullets (downwards or towards the player) during their dives. + * After an attack dive, enemies could return to their formation position or fly off-screen. +* **Enemy Variety:** + * Introduce different types of enemies (e.g., using different components or an enum). + * Assign different behaviors, point values, and maybe sprites to each type. + +**3. Advanced Galaga Mechanics:** + +* **Boss Galaga & Capture Beam:** + * Create a "Boss" enemy type. + * Implement the tractor beam attack (visual effect, player capture logic). + * Player needs a `Captured` state. + * Logic for the Boss to return captured ships to the formation. +* **Dual Fighter (Rescuing Captured Ship):** + * Allow the player to shoot a Boss Galaga attempting to capture or one holding a captured ship. + * Implement the logic to free the captured ship. + * Implement the dual fighter mode (controlling two ships, firing two bullets). This involves significant changes to player control and shooting systems. +* **Challenging Stages:** + * Implement a special stage type (e.g., every few levels). + * Enemies fly in intricate patterns without shooting. + * Award bonus points for destroying all enemies in the stage. + +**4. Polish and User Interface:** + +* **Visuals:** + * Replace placeholder shapes with actual sprites using `SpriteBundle` or `SpriteSheetBundle`. Create distinct looks for the player, different enemy types, bullets. + * Add explosion effects/animations when enemies or the player are destroyed. + * Implement a scrolling starfield background for a more classic space feel. +* **Audio:** + * Integrate `bevy_audio`. + * Add sound effects for player shooting, enemy firing, explosions, player death, capturing, etc. + * Add background music that might change per stage or state (e.g., main gameplay vs. challenging stage). +* **UI:** + * Use `bevy_ui` to display the current Score, Lives remaining, and Stage number on screen during gameplay. + * Display High Score? + * Create a Start Menu state. + +Starting with the Core Gameplay Loop (Lives, Game Over, Score, basic Stages) and then moving onto Enemy Formations and Attack Patterns would likely provide the biggest steps towards a Galaga feel. diff --git a/src/main.rs b/src/main.rs index 277a791..09f0bf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,25 @@ use bevy::prelude::*; use std::time::Duration; +// --- Constants --- const WINDOW_WIDTH: f32 = 600.0; const WINDOW_HEIGHT: f32 = 800.0; const PLAYER_SPEED: f32 = 300.0; const BULLET_SPEED: f32 = 500.0; const ENEMY_SPEED: f32 = 100.0; +const PLAYER_SIZE: Vec2 = Vec2::new(30.0, 30.0); +const ENEMY_SIZE: Vec2 = Vec2::new(40.0, 40.0); +const BULLET_SIZE: Vec2 = Vec2::new(5.0, 15.0); +const STARTING_LIVES: u32 = 3; +const PLAYER_RESPAWN_DELAY: f32 = 2.0; +const PLAYER_INVINCIBILITY_DURATION: f32 = 2.0; + +// Collision thresholds +const BULLET_ENEMY_COLLISION_THRESHOLD: f32 = (BULLET_SIZE.x + ENEMY_SIZE.x) * 0.5; // 22.5 +const PLAYER_ENEMY_COLLISION_THRESHOLD: f32 = (PLAYER_SIZE.x + ENEMY_SIZE.x) * 0.5; // 35.0 + +// --- Components --- #[derive(Component)] struct Player { speed: f32, @@ -19,63 +32,121 @@ struct Bullet; #[derive(Component)] struct Enemy; +#[derive(Component)] +struct Invincible { + timer: Timer, +} + +// --- Resources --- #[derive(Resource)] struct EnemySpawnTimer { timer: Timer, } +#[derive(Resource)] +struct PlayerLives { + count: u32, +} + +#[derive(Resource)] +struct PlayerRespawnTimer { + timer: Timer, +} + + fn main() { App::new() .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - title: "Galaga".into(), + title: "Galaga :: Lives: 3".into(), // Initial title resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(), ..default() }), ..default() })) - .add_systems(Startup, setup) - .add_systems(Update, ( - move_player, - player_shoot, - move_bullets, - move_enemies, - check_bullet_collisions, - spawn_enemies, - )) + // Add Resources + .insert_resource(PlayerLives { count: STARTING_LIVES }) + .insert_resource(PlayerRespawnTimer { + // Start paused and finished + timer: Timer::new(Duration::from_secs_f32(PLAYER_RESPAWN_DELAY), TimerMode::Once) + }) .insert_resource(EnemySpawnTimer { timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating), }) + // Add Systems + .add_systems(Startup, setup) + .add_systems(Update, ( + move_player, + player_shoot.run_if(player_exists), // Only shoot if player exists + move_bullets, + move_enemies, + spawn_enemies, + check_bullet_collisions, + check_player_enemy_collisions.run_if(player_vulnerable), // Only check if player exists and is not invincible + respawn_player.run_if(should_respawn_player), // Conditionally run respawn + manage_invincibility, // Handle invincibility timer and blinking + update_window_title, // Show lives in window title + // (Game Over check would go here later) + )) .run(); } +// --- Run Conditions --- +fn player_exists(query: Query<&Player>) -> bool { + !query.is_empty() +} + +fn player_vulnerable(query: Query<&Player, Without>) -> bool { + !query.is_empty() +} + +fn should_respawn_player( + lives: Res, + player_query: Query<&Player>, +) -> bool { + player_query.is_empty() && lives.count > 0 +} + + +// --- Systems --- + fn setup(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); + spawn_player_ship(&mut commands); // Use helper function +} - // Spawn player - commands.spawn(( +// Helper to spawn player (used in setup and respawn) +fn spawn_player_ship(commands: &mut Commands) { + commands.spawn(( SpriteBundle { sprite: Sprite { color: Color::rgb(0.0, 0.5, 1.0), - custom_size: Some(Vec2::new(30.0, 30.0)), + custom_size: Some(PLAYER_SIZE), ..default() }, - transform: Transform::from_translation(Vec3::new(0.0, -300.0, 0.0)), + transform: Transform::from_translation(Vec3::new(0.0, -WINDOW_HEIGHT / 2.0 + PLAYER_SIZE.y / 2.0 + 20.0, 0.0)), ..default() }, Player { speed: PLAYER_SPEED, - shoot_cooldown: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Once), + 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!"); } + fn move_player( keyboard_input: Res>, mut query: Query<(&mut Transform, &Player)>, time: Res