From 2ff561efb1c423e803bcb8eabdcb99173b6dd96a Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 6 May 2026 16:43:16 +0200 Subject: [PATCH] feat: add explosion animations on entity destruction Spawn expanding/fading orange explosion effects when enemies or the player are destroyed. Explosions scale from 15x15 to 50x50 over 0.4s while fading from full opacity to transparent, then auto-despawn. Integration points: - Enemy killed by player bullet (bullet.rs) - Player hit by enemy bullet (bullet.rs) - Player collides with enemy (player.rs) - both explode - Captured player released (player.rs) Refs: GAL-44 --- src/bullet.rs | 3 +++ src/components.rs | 5 +++++ src/constants.rs | 6 ++++++ src/main.rs | 4 +++- src/player.rs | 9 ++++++++ src/systems.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/bullet.rs b/src/bullet.rs index 2cd46a4..fcd3863 100644 --- a/src/bullet.rs +++ b/src/bullet.rs @@ -9,6 +9,7 @@ use crate::resources::{PlayerLives, PlayerRespawnTimer, Score}; use crate::game_state::AppState; use crate::components::Player; // Needed for check_enemy_bullet_player_collisions use crate::components::Invincible; // Needed for check_enemy_bullet_player_collisions +use crate::systems::spawn_explosion; // --- Player Bullet Systems --- @@ -41,6 +42,7 @@ pub fn check_bullet_collisions( if distance < BULLET_ENEMY_COLLISION_THRESHOLD { commands.entity(bullet_entity).despawn(); + spawn_explosion(&mut commands, enemy_transform.translation); commands.entity(enemy_entity).despawn(); // Increment score based on enemy type let points = match enemy.enemy_type { @@ -90,6 +92,7 @@ pub fn check_enemy_bullet_player_collisions( if distance < ENEMY_BULLET_PLAYER_COLLISION_THRESHOLD { println!("Player hit by enemy bullet!"); + spawn_explosion(&mut commands, player_transform.translation); commands.entity(bullet_entity).despawn(); // Despawn bullet lives.count = lives.count.saturating_sub(1); diff --git a/src/components.rs b/src/components.rs index fe5ff07..070f6c8 100644 --- a/src/components.rs +++ b/src/components.rs @@ -95,3 +95,8 @@ pub struct RestartMessage; pub struct Star { pub speed: f32, } + +#[derive(Component)] +pub struct Explosion { + pub timer: Timer, +} diff --git a/src/constants.rs b/src/constants.rs index c8e8a15..964e114 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -47,3 +47,9 @@ pub const STAR_MAX_SIZE: f32 = 3.0; pub const STAR_MIN_SPEED: f32 = 20.0; pub const STAR_MAX_SPEED: f32 = 100.0; pub const STAR_Z_DEPTH: f32 = -10.0; // Behind all game entities + +// Explosion constants +pub const EXPLOSION_DURATION: f32 = 0.4; +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_COLOR: Color = Color::rgba(1.0, 0.6, 0.1, 1.0); diff --git a/src/main.rs b/src/main.rs index af9585b..8523724 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ use bullet::{ move_enemy_bullets, }; use stage::check_stage_clear; -use systems::{player_exists, player_vulnerable, setup, should_respawn_player, update_window_title}; +use systems::{player_exists, player_vulnerable, setup, should_respawn_player, update_window_title, animate_explosion}; use starfield::scroll_starfield; fn main() { @@ -119,6 +119,8 @@ fn main() { ) // Starfield runs in all states .add_systems(Update, scroll_starfield) + // Explosion animation runs in all states + .add_systems(Update, animate_explosion) // UI and state management systems .add_systems(OnEnter(AppState::StartMenu), setup_start_menu_ui) .add_systems(OnExit(AppState::StartMenu), cleanup_start_menu_ui) diff --git a/src/player.rs b/src/player.rs index 2cc49e1..433a5a7 100644 --- a/src/player.rs +++ b/src/player.rs @@ -9,6 +9,7 @@ use crate::constants::{ }; 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) { @@ -116,6 +117,7 @@ pub fn handle_captured_player( &mut respawn_timer, &mut next_state, entity, + _player_pos, ); continue; // Skip the rest of processing for this player } @@ -131,6 +133,7 @@ pub fn handle_captured_player( &mut respawn_timer, &mut next_state, entity, + transform.translation, ); } } @@ -144,11 +147,15 @@ fn lose_life_and_respawn( respawn_timer: &mut ResMut, next_state: &mut ResMut>, 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(); @@ -218,11 +225,13 @@ pub fn check_player_enemy_collisions( 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 { diff --git a/src/systems.rs b/src/systems.rs index 421bd5d..69b5085 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,6 +1,7 @@ use bevy::prelude::*; -use crate::components::{Invincible, Player}; +use crate::components::{Explosion, Invincible, Player}; +use crate::constants::{EXPLOSION_BASE_SIZE, EXPLOSION_COLOR, EXPLOSION_DURATION, EXPLOSION_MAX_SIZE}; use crate::resources::{CurrentStage, PlayerLives, Score}; use crate::player::spawn_player_ship; use crate::starfield::spawn_starfield; @@ -44,4 +45,53 @@ pub fn update_window_title( stage.number, lives.count, score.value ); } +} + +// --- Explosion Systems --- + +pub fn spawn_explosion(commands: &mut Commands, position: Vec3) { + commands.spawn(( + SpriteBundle { + sprite: Sprite { + color: EXPLOSION_COLOR, + custom_size: Some(EXPLOSION_BASE_SIZE), + ..default() + }, + transform: Transform { + translation: position, + ..default() + }, + ..default() + }, + Explosion { + timer: Timer::new(std::time::Duration::from_secs_f32(EXPLOSION_DURATION), TimerMode::Once), + }, + )); +} + +pub fn animate_explosion( + mut commands: Commands, + time: Res