add game over

This commit is contained in:
Schrottkatze 2024-05-11 01:40:56 +02:00
parent e5f87736df
commit 7de16e410e
Signed by: schrottkatze
SSH key fingerprint: SHA256:hXb3t1vINBFCiDCmhRABHX5ocdbLiKyCdKI4HK2Rbbc
7 changed files with 268 additions and 26 deletions

View file

@ -1,13 +1,14 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
sprite::{MaterialMesh2dBundle, Mesh2dHandle}, sprite::{MaterialMesh2dBundle, Mesh2dHandle},
utils::HashSet, utils::{Duration, HashSet},
}; };
use bevy_rand::prelude::*; use bevy_rand::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use rand::Rng; use rand::Rng;
use crate::{ use crate::{
game_state::GameTimer,
player::{LifeChangeEvent, Player}, player::{LifeChangeEvent, Player},
scene::SceneObj, scene::SceneObj,
GameState, GameplaySet, METER, GameState, GameplaySet, METER,
@ -29,6 +30,7 @@ pub fn spawner_plugin(app: &mut App) {
.add_event::<CrateDropEvent>() .add_event::<CrateDropEvent>()
.add_event::<CrateCollision>() .add_event::<CrateCollision>()
.add_systems(OnEnter(GameState::InGame), add_timer.in_set(GameplaySet)) .add_systems(OnEnter(GameState::InGame), add_timer.in_set(GameplaySet))
.add_systems(OnExit(GameState::InGame), cleanup.in_set(GameplaySet))
.add_systems( .add_systems(
Update, Update,
( (
@ -37,13 +39,65 @@ pub fn spawner_plugin(app: &mut App) {
crate_collisions, crate_collisions,
delete_on_env_coll, delete_on_env_coll,
player_coll, player_coll,
update_timer_for_difficulty,
) )
.in_set(GameplaySet), .in_set(GameplaySet),
); );
} }
fn cleanup(
timer: Query<Entity, With<SpawnTimer>>,
crates: Query<Entity, With<DroppedCrate>>,
mut commands: Commands,
) {
commands.entity(timer.single()).despawn();
for e in &crates {
commands.entity(e).despawn();
}
}
fn add_timer(mut commands: Commands) { fn add_timer(mut commands: Commands) {
commands.spawn(SpawnTimer(Timer::from_seconds(2., TimerMode::Repeating))); commands.spawn(SpawnTimer(Timer::from_seconds(0.2, TimerMode::Repeating)));
}
fn update_timer_for_difficulty(
mut spawn_timer: Query<&mut SpawnTimer>,
mut game_timer: Query<&mut GameTimer>,
time: Res<Time>,
) {
let game_time = game_timer
.single_mut()
.game_time
.tick(time.delta())
.elapsed_secs() as i32;
println!("time: {game_time}s");
spawn_timer
.single_mut()
.0
.set_duration(Duration::from_millis(match game_time {
0..=9 => 2000,
10..=19 => 1000,
20..=29 => 500,
30..=39 => 400,
40..=49 => 350,
50..=59 => 300,
60..=69 => 250,
70..=79 => 200,
80..=89 => 175,
90..=99 => 150,
100..=109 => 125,
110..=119 => 100,
120..=129 => 90,
130..=139 => 80,
140..=149 => 70,
150..=159 => 60,
160..=169 => 50,
170..=179 => 25,
180.. => 10,
..=-1 => unreachable!(),
}));
} }
fn drop_crates( fn drop_crates(

121
src/game_over_menu.rs Normal file
View file

@ -0,0 +1,121 @@
use bevy::{app::AppExit, prelude::*};
use crate::{game_state::GameTimer, GameState};
pub fn game_over_menu_plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::GameOver),
game_over_menu_setup.in_set(GameOverMenuSet),
)
.add_systems(Update, button_action.in_set(GameOverMenuSet))
.add_systems(OnExit(GameState::GameOver), exit_menu);
}
#[derive(Component)]
struct GameOverMenu;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct GameOverMenuSet;
#[derive(Component)]
enum ButtonAction {
Retry,
MainMenu,
}
fn exit_menu(
to_despawn: Query<Entity, With<GameOverMenu>>,
game_timer: Query<Entity, With<GameTimer>>,
mut commands: Commands,
) {
commands.entity(game_timer.single()).despawn();
for e in &to_despawn {
commands.entity(e).despawn_recursive();
}
}
fn game_over_menu_setup(mut commands: Commands, timer: Query<&GameTimer>) {
commands
.spawn((
NodeBundle {
style: Style {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
..default()
},
GameOverMenu,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
format!(
"Your score: {:.0}",
timer.single().game_time.elapsed_secs() * 100.
),
TextStyle {
font_size: 96.,
..default()
},
));
let btn_style = Style {
width: Val::Px(300.0),
height: Val::Px(80.0),
margin: UiRect::all(Val::Px(20.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
};
let btn_txt_style = TextStyle {
font_size: 64.,
..default()
};
parent
.spawn((
ButtonBundle {
style: btn_style.clone(),
background_color: Color::rgb(0.3, 0.3, 0.3).into(),
..default()
},
ButtonAction::Retry,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section("Retry", btn_txt_style.clone()));
});
parent
.spawn((
ButtonBundle {
style: btn_style.clone(),
background_color: Color::rgb(0.3, 0.3, 0.3).into(),
..default()
},
ButtonAction::MainMenu,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section("Main Menu", btn_txt_style.clone()));
});
});
}
fn button_action(
interaction_query: Query<(&Interaction, &ButtonAction), (Changed<Interaction>, With<Button>)>,
mut ev_exit: EventWriter<AppExit>,
mut game_state: ResMut<NextState<GameState>>,
) {
for (interaction, action) in &interaction_query {
if *interaction == Interaction::Pressed {
match action {
ButtonAction::MainMenu => {
game_state.set(GameState::MainMenu);
}
ButtonAction::Retry => {
game_state.set(GameState::InGame);
}
}
}
}
}

View file

@ -1,25 +1,30 @@
use bevy::{prelude::*, time::Stopwatch}; use bevy::{prelude::*, time::Stopwatch};
use crate::GameplaySet; use crate::{GameState, GameplaySet};
#[derive(Component)] #[derive(Component)]
pub struct GameState { pub struct GameTimer {
game_time: Stopwatch, pub game_time: Stopwatch,
} }
#[derive(Component)] #[derive(Component)]
struct StateText; struct ScoreText;
pub fn state_and_ui_plugin(app: &mut App) { pub fn state_and_ui_plugin(app: &mut App) {
app.add_systems( app.add_systems(
OnEnter(crate::GameState::InGame), OnEnter(GameState::InGame),
(setup_gamestate, setup_ui).in_set(GameplaySet), (setup_gamestate, setup_ui).in_set(GameplaySet),
) )
.add_systems(Update, (update_time).in_set(GameplaySet)); .add_systems(Update, update_time.in_set(GameplaySet))
.add_systems(OnExit(GameState::InGame), cleanup_ui.in_set(GameplaySet));
}
fn cleanup_ui(e: Query<Entity, With<ScoreText>>, mut commands: Commands) {
commands.entity(e.single()).despawn()
} }
fn setup_gamestate(mut commands: Commands) { fn setup_gamestate(mut commands: Commands) {
commands.spawn(GameState { commands.spawn(GameTimer {
game_time: Stopwatch::new(), game_time: Stopwatch::new(),
}); });
} }
@ -36,13 +41,13 @@ fn setup_ui(mut commands: Commands) {
right: Val::Px(5.0), right: Val::Px(5.0),
..default() ..default()
}), }),
StateText, ScoreText,
)); ));
} }
fn update_time( fn update_time(
mut state_txt: Query<&mut Text, With<StateText>>, mut state_txt: Query<&mut Text, With<ScoreText>>,
mut game_state: Query<&mut GameState>, mut game_state: Query<&mut GameTimer>,
time: Res<Time>, time: Res<Time>,
) { ) {
let mut txt = state_txt.single_mut(); let mut txt = state_txt.single_mut();

View file

@ -5,8 +5,11 @@ use bevy::{
}; };
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use drops::spawner_plugin; use drops::spawner_plugin;
use game_over_menu::{game_over_menu_plugin, GameOverMenuSet};
use game_state::state_and_ui_plugin;
use main_menu::MainMenuSet; use main_menu::MainMenuSet;
use player::LifeChangeEvent; use player::{player_plugin, GameOverEvent, LifeChangeEvent};
use scene::scene_plugin;
const METER: f32 = 48.; const METER: f32 = 48.;
@ -17,6 +20,7 @@ struct GameplaySet;
enum GameState { enum GameState {
MainMenu, MainMenu,
InGame, InGame,
GameOver,
} }
fn main() { fn main() {
@ -32,25 +36,38 @@ fn main() {
.insert_resource(rapier_config) .insert_resource(rapier_config)
.insert_state(GameState::MainMenu) .insert_state(GameState::MainMenu)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(METER)) .add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(METER))
.add_plugins(RapierDebugRenderPlugin::default()) // .add_plugins(RapierDebugRenderPlugin::default())
.add_event::<GameOverEvent>()
.add_plugins(( .add_plugins((
main_menu_plugin, main_menu_plugin,
spawner_plugin, spawner_plugin,
game_state::state_and_ui_plugin, state_and_ui_plugin,
player::player_plugin, player_plugin,
scene_plugin,
game_over_menu_plugin,
)) ))
.add_systems(Startup, setup_cam) .add_systems(Startup, setup_cam)
.add_systems(OnEnter(GameState::InGame), scene::setup_scene) .add_systems(Update, handle_game_over.in_set(GameplaySet))
.configure_sets( .configure_sets(
Update, Update,
( (
GameplaySet.run_if(in_state(GameState::InGame)), GameplaySet.run_if(in_state(GameState::InGame)),
MainMenuSet.run_if(in_state(GameState::MainMenu)), MainMenuSet.run_if(in_state(GameState::MainMenu)),
GameOverMenuSet.run_if(in_state(GameState::GameOver)),
), ),
) )
.run() .run()
} }
fn handle_game_over(
mut ev_gameover: EventReader<GameOverEvent>,
mut game_state: ResMut<NextState<GameState>>,
) {
for ev in ev_gameover.read() {
game_state.set(GameState::GameOver)
}
}
fn setup_cam(mut commands: Commands) { fn setup_cam(mut commands: Commands) {
commands.spawn(Camera2dBundle { commands.spawn(Camera2dBundle {
transform: Transform::from_xyz(0., 4. * METER, 0.), transform: Transform::from_xyz(0., 4. * METER, 0.),
@ -59,6 +76,7 @@ fn setup_cam(mut commands: Commands) {
} }
mod drops; mod drops;
mod game_over_menu;
mod game_state; mod game_state;
mod main_menu; mod main_menu;
mod player; mod player;

View file

@ -1,5 +1,6 @@
use bevy::{app::AppExit, prelude::*}; use bevy::{app::AppExit, prelude::*};
use crate::game_over_menu::GameOverMenuSet;
use crate::GameState; use crate::GameState;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
@ -10,9 +11,12 @@ pub fn main_menu_plugin(app: &mut App) {
OnEnter(GameState::MainMenu), OnEnter(GameState::MainMenu),
setup_main_menu_ui.in_set(MainMenuSet), setup_main_menu_ui.in_set(MainMenuSet),
) )
.add_systems(Update, button_action.in_set(MainMenuSet))
.add_systems( .add_systems(
Update, Update,
(button_color_system, button_action).in_set(MainMenuSet), button_color_system
.in_set(MainMenuSet)
.in_set(GameOverMenuSet),
) )
.add_systems(OnExit(GameState::MainMenu), exit_menu.in_set(MainMenuSet)); .add_systems(OnExit(GameState::MainMenu), exit_menu.in_set(MainMenuSet));
} }
@ -97,7 +101,7 @@ fn exit_menu(to_despawn: Query<Entity, With<MainMenu>>, mut commands: Commands)
} }
} }
fn button_color_system( pub fn button_color_system(
mut interaction_query: Query< mut interaction_query: Query<
(&Interaction, &mut BackgroundColor), (&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>), (Changed<Interaction>, With<Button>),

View file

@ -20,6 +20,9 @@ pub enum LifeChangeEvent {
Lost, Lost,
} }
#[derive(Event)]
pub struct GameOverEvent;
const DEFAULT_LIVES: u8 = 5; const DEFAULT_LIVES: u8 = 5;
pub fn player_plugin(app: &mut App) { pub fn player_plugin(app: &mut App) {
@ -28,13 +31,17 @@ pub fn player_plugin(app: &mut App) {
OnEnter(GameState::InGame), OnEnter(GameState::InGame),
(add_player, add_lives_ui).in_set(GameplaySet), (add_player, add_lives_ui).in_set(GameplaySet),
) )
.add_systems(
OnExit(GameState::InGame),
cleanup_player.in_set(GameplaySet),
)
.add_systems( .add_systems(
Update, Update,
(move_player, player_ground_collision, update_lives).in_set(GameplaySet), (move_player, player_ground_collision, update_lives).in_set(GameplaySet),
); );
} }
pub fn add_player( fn add_player(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>, mut materials: ResMut<Assets<ColorMaterial>>,
@ -63,17 +70,33 @@ pub fn add_player(
.insert(KinematicCharacterController { ..default() }); .insert(KinematicCharacterController { ..default() });
} }
fn cleanup_player(
player: Query<Entity, With<Player>>,
lives_ui: Query<Entity, With<LivesUi>>,
mut commands: Commands,
) {
commands.entity(player.single()).despawn();
commands.entity(lives_ui.single()).despawn();
}
#[derive(Component)] #[derive(Component)]
struct LivesUi; struct LivesUi;
fn add_lives_ui(mut commands: Commands) { fn add_lives_ui(mut commands: Commands) {
commands.spawn(( commands.spawn((
TextBundle::from("<3 ".repeat(DEFAULT_LIVES.into())), TextBundle::from_section(
"<3 ".repeat(DEFAULT_LIVES.into()),
TextStyle {
font_size: 56.,
..default()
},
),
LivesUi, LivesUi,
)); ));
} }
fn update_lives( fn update_lives(
mut ev_lifechange: EventReader<LifeChangeEvent>, mut ev_lifechange: EventReader<LifeChangeEvent>,
mut ev_gameover: EventWriter<GameOverEvent>,
mut player: Query<&mut Player>, mut player: Query<&mut Player>,
mut ui: Query<&mut Text, With<LivesUi>>, mut ui: Query<&mut Text, With<LivesUi>>,
) { ) {
@ -85,11 +108,16 @@ fn update_lives(
LifeChangeEvent::Lost => p.lives -= 1, LifeChangeEvent::Lost => p.lives -= 1,
LifeChangeEvent::Gained => p.lives += 1, LifeChangeEvent::Gained => p.lives += 1,
} }
txt.sections[0].value = "<3 ".repeat(p.lives.into())
if p.lives == 0 {
ev_gameover.send(GameOverEvent);
} else {
txt.sections[0].value = "<3 ".repeat(p.lives.into());
}
} }
} }
pub fn player_ground_collision( fn player_ground_collision(
mut player: Query<(Entity, &mut Player)>, mut player: Query<(Entity, &mut Player)>,
scene_objs: Query<Entity, With<super::scene::SceneObj>>, scene_objs: Query<Entity, With<super::scene::SceneObj>>,
mut collision_events: EventReader<CollisionEvent>, mut collision_events: EventReader<CollisionEvent>,
@ -113,7 +141,7 @@ pub fn player_ground_collision(
} }
} }
pub fn move_player( fn move_player(
kb_input: Res<ButtonInput<KeyCode>>, kb_input: Res<ButtonInput<KeyCode>>,
mut query: Query<( mut query: Query<(
&mut Velocity, &mut Velocity,

View file

@ -4,11 +4,17 @@ use bevy::{
}; };
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use crate::METER; use crate::{GameState, METER};
#[derive(Component)] #[derive(Component)]
pub struct SceneObj; pub struct SceneObj;
pub fn setup_scene(
pub fn scene_plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::InGame), setup_scene)
.add_systems(OnExit(GameState::InGame), cleanup_scene);
}
fn setup_scene(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>, mut materials: ResMut<Assets<ColorMaterial>>,
@ -42,3 +48,9 @@ pub fn setup_scene(
)); ));
} }
} }
fn cleanup_scene(query: Query<Entity, With<SceneObj>>, mut commands: Commands) {
for e in &query {
commands.entity(e).despawn();
}
}