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
This commit is contained in:
parent
db061820b9
commit
2ff561efb1
6 changed files with 77 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -95,3 +95,8 @@ pub struct RestartMessage;
|
|||
pub struct Star {
|
||||
pub speed: f32,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Explosion {
|
||||
pub timer: Timer,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<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();
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -45,3 +46,52 @@ pub fn update_window_title(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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<Time>,
|
||||
mut query: Query<(Entity, &mut Explosion, &mut Sprite)>,
|
||||
) {
|
||||
for (entity, mut explosion, mut sprite) in query.iter_mut() {
|
||||
explosion.timer.tick(time.delta());
|
||||
|
||||
let progress = explosion.timer.elapsed_secs() / EXPLOSION_DURATION;
|
||||
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_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));
|
||||
|
||||
// Fade alpha from 1.0 to 0.0
|
||||
let alpha = 1.0 - clamped_progress;
|
||||
let base = EXPLOSION_COLOR;
|
||||
sprite.color = Color::rgba(base.r(), base.g(), base.b(), alpha);
|
||||
|
||||
if explosion.timer.finished() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue