diff --git a/art/fonts.aseprite b/art/fonts.aseprite index 7afb1a1..c125b4a 100644 Binary files a/art/fonts.aseprite and b/art/fonts.aseprite differ diff --git a/art/shield.aseprite b/art/shield.aseprite new file mode 100644 index 0000000..fa4c378 Binary files /dev/null and b/art/shield.aseprite differ diff --git a/assets/font.ttf b/assets/font.ttf index cdff95e..14b5bc4 100644 Binary files a/assets/font.ttf and b/assets/font.ttf differ diff --git a/assets/sprites/shield-empty.png b/assets/sprites/shield-empty.png new file mode 100644 index 0000000..0858c19 Binary files /dev/null and b/assets/sprites/shield-empty.png differ diff --git a/assets/sprites/shield-low.png b/assets/sprites/shield-low.png new file mode 100644 index 0000000..99376bd Binary files /dev/null and b/assets/sprites/shield-low.png differ diff --git a/assets/sprites/shield.png b/assets/sprites/shield.png new file mode 100644 index 0000000..7486736 Binary files /dev/null and b/assets/sprites/shield.png differ diff --git a/src/audio/mod.rs b/src/audio/mod.rs index c6fe9b5..18f5f43 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -175,7 +175,6 @@ fn fade_out( audio.set_volume( current_volume.fade_towards(Volume::Linear(0.0), time.delta_secs() / fade_out.duration), ); - println!("fading out: {:?}", audio.volume().to_linear()); if audio.volume().to_linear() <= 0.15 { commands.entity(entity).despawn(); } diff --git a/src/screens/game.rs b/src/screens/game.rs index 4f56cef..20b15c4 100644 --- a/src/screens/game.rs +++ b/src/screens/game.rs @@ -88,6 +88,7 @@ pub(super) fn plugin(app: &mut App) { update_projectiles, projectile_asteroid_collision, update_explosions, + shield_regen, ) .in_set(GameSystems::Play) .run_if(in_state(Screen::Game)), @@ -113,6 +114,9 @@ pub(super) fn plugin(app: &mut App) { #[derive(Component)] struct Thruster; +#[derive(Component)] +struct Shield; + fn spawn_player(mut commands: Commands, asset_server: Res) { let default_speed = asset_server.load("sprites/ships/ship3-default.png"); commands.spawn(( @@ -120,6 +124,9 @@ fn spawn_player(mut commands: Commands, asset_server: Res) { Player { can_shoot: true, timer: Timer::from_seconds(0.15, TimerMode::Once), + shield: 2, + shield_regen: Timer::from_seconds(5.0, TimerMode::Once), + shield_hit_cooldown: Timer::from_seconds(0.5, TimerMode::Once), }, Sprite { image: asset_server.load("sprites/ships/ship3.png"), @@ -136,15 +143,25 @@ fn spawn_player(mut commands: Commands, asset_server: Res) { ..default() }, PIXEL_PERFECT_LAYERS, - children![( - Name::new("Player Ship Thruster"), - Thruster, - Sprite { - image: default_speed.clone(), - custom_size: Some(Vec2::splat(32.0)), - ..default() - } - )], + children![ + ( + Name::new("Player Ship Thruster"), + Thruster, + Sprite { + image: default_speed.clone(), + custom_size: Some(Vec2::splat(32.0)), + ..default() + } + ), + ( + Name::new("Player Shield"), + Shield, + Sprite { + image: asset_server.load("sprites/shield.png"), + ..default() + } + ) + ], )); } @@ -185,6 +202,9 @@ fn enter_player( pub struct Player { pub can_shoot: bool, pub timer: Timer, + pub shield: u8, + pub shield_regen: Timer, + pub shield_hit_cooldown: Timer, } #[derive(Component)] @@ -216,7 +236,6 @@ fn stopwatch( mut game_stats: ResMut, mut end_game: ResMut, ) { - println!("time played: {:?}", game_stats.time_played); if game_stats.time_played > 300.0 { game_stats.time_played = 300.0; end_game.0 = true; @@ -454,26 +473,37 @@ fn spawn_asteroid( fn collide_with_asteroid_check( mut commands: Commands, - player: Query<&Transform, With>, + mut player: Single<(&Transform, &mut Player)>, asteroids: Query<&Transform, With>, mut time: ResMut>, mut state: ResMut>, asset_server: Res, mut texture_atlases: ResMut>, + mut shield: Single<&mut Sprite, With>, ) { - let player = player.single(); - if player.is_err() { - return; - } - let player_transform = player.unwrap(); + let player_transform = player.0; + let player = &mut player.1; let hitbox_size = 8.0; for asteroid_transform in asteroids.iter() { let distance = player_transform .translation .distance(asteroid_transform.translation); - if distance < (hitbox_size / 2.0) + (30.0 / 2.0) { - time.pause(); - state.set(GameState::GameOver); + if distance < (hitbox_size / 2.0) + (30.0 / 2.0) && player.shield_hit_cooldown.is_finished() + { + if player.shield == 0 { + time.pause(); + state.set(GameState::GameOver); + } else { + player.shield -= 1; + player.shield_regen.reset(); + player.shield_hit_cooldown.reset(); + if player.shield == 0 { + shield.image = asset_server.load("sprites/shield-empty.png"); + } else if player.shield == 1 { + shield.image = asset_server.load("sprites/shield-low.png"); + } + } + commands.trigger(PlaySfxEvent { sfx: SoundEffect::Explosion, }); @@ -501,6 +531,29 @@ fn collide_with_asteroid_check( } } +fn shield_regen( + time: Res>, + mut player: Single<&mut Player>, + mut shield: Single<&mut Sprite, With>, + asset_server: Res, +) { + player.shield_regen.tick(time.delta()); + player.shield_hit_cooldown.tick(time.delta()); + if player.shield_regen.just_finished() { + player.shield_regen.reset(); + player.shield += 1; + if player.shield > 2 { + player.shield = 2; + } + match player.shield { + 0 => shield.image = asset_server.load("sprites/shield-empty.png"), + 1 => shield.image = asset_server.load("sprites/shield-low.png"), + 2 => shield.image = asset_server.load("sprites/shield.png"), + _ => {} + } + } +} + #[derive(Component)] struct Explosion; @@ -581,7 +634,7 @@ fn restart_game( mut pause_state: ResMut>, mut game_state: ResMut>, mut has_entered: ResMut, - mut player: Query<&mut Transform, With>, + mut player: Query<(&mut Transform, &mut Player)>, asteroids: Query>, projectiles: Query>, mut end_game: ResMut, @@ -597,8 +650,11 @@ fn restart_game( end_game.0 = false; pause_state.set(PauseState::NotPaused); - let mut player_transform = player.single_mut().unwrap(); + let (mut player_transform, mut player) = player.single_mut().unwrap(); player_transform.translation = Vec3::new(0.0, -280.0, 0.0); + player.shield = 2; + player.shield_hit_cooldown.reset(); + player.shield_regen.reset(); let mut planet_transform = planet.single_mut().unwrap(); planet_transform.translation = Vec3::new(0.0, 400.0, -11.0); diff --git a/src/screens/howtoplay.rs b/src/screens/howtoplay.rs new file mode 100644 index 0000000..5e23c3f --- /dev/null +++ b/src/screens/howtoplay.rs @@ -0,0 +1,153 @@ +use bevy::{prelude::*, text::FontSmoothing}; +use leafwing_input_manager::prelude::ActionState; + +use crate::{PIXEL_PERFECT_LAYERS, ScaleFactor, input::Action, screens::Screen}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems(OnEnter(Screen::HowToPlay), spawn_howtoplay); + app.add_systems( + Update, + (howtoplay_input, howtoplay_button).run_if(in_state(Screen::HowToPlay)), + ); +} + +fn spawn_howtoplay( + mut commands: Commands, + scale_factor: Res, + asset_server: Res, +) { + let scale = scale_factor.0; + let font_handle = asset_server.load("font.ttf"); + commands.spawn(( + Name::new("HowToPlay container"), + Node { + width: Val::Percent(80.0), + height: Val::Percent(80.0), + margin: UiRect::all(Val::Auto), + padding: UiRect::all(Val::Px(8.0 * scale)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + ..default() + }, + DespawnOnExit(Screen::HowToPlay), + PIXEL_PERFECT_LAYERS, + children![ + ( + Name::new("HowToPlay"), + Text::new("HOW TO PLAY"), + TextFont { + font_size: 16.0 * scale, + font: font_handle.clone(), + font_smoothing: FontSmoothing::None, + ..default() + }, + Node { + position_type: PositionType::Absolute, + top: Val::Px(0.0), + margin: UiRect { + left: Val::Auto, + right: Val::Auto, + ..default() + }, + ..default() + } + ), + ( + Text::new("MOVE WITH WASD / ARROWS / ANALOG STICK / D PAD"), + TextFont { + font_size: 12.0 * scale, + font: font_handle.clone(), + font_smoothing: FontSmoothing::None, + ..default() + }, + Node { + padding: UiRect { + top: Val::Px(8.0 * scale), + bottom: Val::Px(8.0 * scale), + ..default() + }, + ..default() + }, + ), + ( + Text::new("SHOOT WITH SPACE / BOTTOM BUTTON ON GAMEPAD CLUSTER"), + TextFont { + font_size: 12.0 * scale, + font: font_handle.clone(), + font_smoothing: FontSmoothing::None, + ..default() + }, + Node { + padding: UiRect { + top: Val::Px(8.0 * scale), + bottom: Val::Px(8.0 * scale), + ..default() + }, + ..default() + }, + ), + ( + Text::new("PAUSE WITH ESCAPE / GAMEPAD START / GAMEPAD SELECT"), + TextFont { + font_size: 12.0 * scale, + font: font_handle.clone(), + font_smoothing: FontSmoothing::None, + ..default() + }, + Node { + padding: UiRect { + top: Val::Px(8.0 * scale), + bottom: Val::Px(8.0 * scale), + ..default() + }, + ..default() + }, + ), + ( + Name::new("Back to menu button"), + Button, + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(0.0), + margin: UiRect { + left: Val::Auto, + right: Val::Auto, + ..default() + }, + ..default() + }, + children![( + Name::new("Back to menu text"), + Text::new("BACK TO MENU"), + TextFont { + font_size: 16.0 * scale, + font: font_handle.clone(), + font_smoothing: FontSmoothing::None, + ..default() + } + )] + ), + ], + )); +} + +fn howtoplay_button( + interaction_query: Query<&Interaction, Changed>, + mut screen: ResMut>, +) { + for interaction in interaction_query.iter() { + if *interaction == Interaction::Pressed { + screen.set(Screen::Title) + } + } +} + +fn howtoplay_input( + action_state: Single<&ActionState>, + mut screen: ResMut>, +) { + if action_state.just_pressed(&Action::Select) { + screen.set(Screen::Title); + } +} diff --git a/src/screens/mod.rs b/src/screens/mod.rs index 97d301f..3765177 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -4,6 +4,7 @@ mod credits; mod endgame; mod game; mod gameover; +mod howtoplay; mod pause; mod splash; mod title; @@ -19,6 +20,7 @@ pub(super) fn plugin(app: &mut App) { pause::plugin, gameover::plugin, endgame::plugin, + howtoplay::plugin, )); } @@ -26,6 +28,7 @@ pub(super) fn plugin(app: &mut App) { pub enum Screen { #[default] Splash, + HowToPlay, Title, Game, Credits, diff --git a/src/screens/title.rs b/src/screens/title.rs index 48f94e1..60a09d3 100644 --- a/src/screens/title.rs +++ b/src/screens/title.rs @@ -20,6 +20,7 @@ pub(super) fn plugin(app: &mut App) { #[derive(Component)] enum MenuButton { + HowToPlay, NewGame, // Settings, Credits, @@ -68,9 +69,16 @@ fn spawn_title_screen( } let font_handle: Handle = asset_server.load("font.ttf"); + let how_to_play = title_menu_button( + "HOW TO PLAY", + 0, + MenuButton::HowToPlay, + font_handle.clone(), + scale, + ); let new_game = title_menu_button( "NEW GAME", - 0, + 1, MenuButton::NewGame, font_handle.clone(), scale, @@ -84,18 +92,18 @@ fn spawn_title_screen( // ); let credits = title_menu_button( "CREDITS", - 1, + 2, MenuButton::Credits, font_handle.clone(), scale, ); #[cfg(target_arch = "wasm32")] - let menu_buttons = children![new_game, credits]; + let menu_buttons = children![how_to_play, new_game, credits]; #[cfg(not(target_arch = "wasm32"))] - let quit = title_menu_button("QUIT", 2, MenuButton::Quit, font_handle.clone(), scale); + let quit = title_menu_button("QUIT", 3, MenuButton::Quit, font_handle.clone(), scale); #[cfg(not(target_arch = "wasm32"))] - let menu_buttons = children![new_game, credits, quit]; + let menu_buttons = children![how_to_play, new_game, credits, quit]; commands.spawn(( Node { @@ -134,9 +142,9 @@ fn input_system( let action_state = input_query.single().unwrap(); #[cfg(not(target_arch = "wasm32"))] - let max_index = 2; + let max_index = 3; #[cfg(target_arch = "wasm32")] - let max_index = 1; + let max_index = 2; if action_state.just_pressed(&Action::Up) { if active_index.0 == 0 { @@ -165,13 +173,16 @@ fn input_system( if action_state.just_pressed(&Action::Select) { match active_index.0 { 0 => { + screen_state.set(Screen::HowToPlay); + } + 1 => { screen_state.set(Screen::Game); commands.trigger(RestartGame); } - 1 => { + 2 => { screen_state.set(Screen::Credits); } - 2 => { + 3 => { message_writer.write(AppExit::Success); } _ => {} @@ -189,6 +200,9 @@ fn button_system( for (interaction, menu_button, children) in interaction_query { match *interaction { Interaction::Pressed => match menu_button { + MenuButton::HowToPlay => { + screen_state.set(Screen::HowToPlay); + } MenuButton::NewGame => { screen_state.set(Screen::Game); commands.trigger(RestartGame); @@ -207,13 +221,14 @@ fn button_system( .entity(*children.first().unwrap()) .insert(TextColor(WHITE)); match menu_button { - MenuButton::NewGame => active_index.0 = 0, - MenuButton::Credits => active_index.0 = 1, + MenuButton::HowToPlay => active_index.0 = 0, + MenuButton::NewGame => active_index.0 = 1, + MenuButton::Credits => active_index.0 = 2, // MenuButton::Settings => { // active_index.0 = 1; // } #[cfg(not(target_arch = "wasm32"))] - MenuButton::Quit => active_index.0 = 2, + MenuButton::Quit => active_index.0 = 3, } } Interaction::None => {