use bevy::{dev_tools::states::*, prelude::*};
use ui::*;
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
enum AppState {
#[default]
Menu,
InGame {
paused: bool,
turbo: bool,
},
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
enum TutorialState {
#[default]
Active,
Inactive,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
struct InGame;
impl ComputedStates for InGame {
type SourceStates = AppState;
fn compute(sources: AppState) -> Option<Self> {
match sources {
AppState::InGame { .. } => Some(Self),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
struct TurboMode;
impl ComputedStates for TurboMode {
type SourceStates = AppState;
fn compute(sources: AppState) -> Option<Self> {
match sources {
AppState::InGame { turbo: true, .. } => Some(Self),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum IsPaused {
NotPaused,
Paused,
}
impl ComputedStates for IsPaused {
type SourceStates = AppState;
fn compute(sources: AppState) -> Option<Self> {
match sources {
AppState::InGame { paused: true, .. } => Some(Self::Paused),
AppState::InGame { paused: false, .. } => Some(Self::NotPaused),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum Tutorial {
MovementInstructions,
PauseInstructions,
}
impl ComputedStates for Tutorial {
type SourceStates = (TutorialState, InGame, Option<IsPaused>);
fn compute(
(tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option<IsPaused>),
) -> Option<Self> {
if !matches!(tutorial_state, TutorialState::Active) {
return None;
}
match is_paused? {
IsPaused::NotPaused => Some(Tutorial::MovementInstructions),
IsPaused::Paused => Some(Tutorial::PauseInstructions),
}
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<AppState>()
.init_state::<TutorialState>()
.add_computed_state::<InGame>()
.add_computed_state::<IsPaused>()
.add_computed_state::<TurboMode>()
.add_computed_state::<Tutorial>()
.add_systems(Startup, setup)
.add_systems(OnEnter(AppState::Menu), setup_menu)
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
.add_systems(OnExit(AppState::Menu), cleanup_menu)
.add_systems(OnEnter(InGame), setup_game)
.add_systems(
Update,
(toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)),
)
.add_systems(
Update,
(toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)),
)
.add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
.add_systems(OnEnter(TurboMode), setup_turbo_text)
.add_systems(
OnEnter(Tutorial::MovementInstructions),
movement_instructions,
)
.add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
.add_systems(
Update,
(
log_transitions::<AppState>,
log_transitions::<TutorialState>,
),
)
.run();
}
fn menu(
mut next_state: ResMut<NextState<AppState>>,
tutorial_state: Res<State<TutorialState>>,
mut next_tutorial: ResMut<NextState<TutorialState>>,
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor, &MenuButton),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color, menu_button) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
*color = if menu_button == &MenuButton::Tutorial
&& tutorial_state.get() == &TutorialState::Active
{
PRESSED_ACTIVE_BUTTON.into()
} else {
PRESSED_BUTTON.into()
};
match menu_button {
MenuButton::Play => next_state.set(AppState::InGame {
paused: false,
turbo: false,
}),
MenuButton::Tutorial => next_tutorial.set(match tutorial_state.get() {
TutorialState::Active => TutorialState::Inactive,
TutorialState::Inactive => TutorialState::Active,
}),
};
}
Interaction::Hovered => {
if menu_button == &MenuButton::Tutorial
&& tutorial_state.get() == &TutorialState::Active
{
*color = HOVERED_ACTIVE_BUTTON.into();
} else {
*color = HOVERED_BUTTON.into();
}
}
Interaction::None => {
if menu_button == &MenuButton::Tutorial
&& tutorial_state.get() == &TutorialState::Active
{
*color = ACTIVE_BUTTON.into();
} else {
*color = NORMAL_BUTTON.into();
}
}
}
}
}
fn toggle_pause(
input: Res<ButtonInput<KeyCode>>,
current_state: Res<State<AppState>>,
mut next_state: ResMut<NextState<AppState>>,
) {
if input.just_pressed(KeyCode::Space)
&& let AppState::InGame { paused, turbo } = current_state.get()
{
next_state.set(AppState::InGame {
paused: !*paused,
turbo: *turbo,
});
}
}
fn toggle_turbo(
input: Res<ButtonInput<KeyCode>>,
current_state: Res<State<AppState>>,
mut next_state: ResMut<NextState<AppState>>,
) {
if input.just_pressed(KeyCode::KeyT)
&& let AppState::InGame { paused, turbo } = current_state.get()
{
next_state.set(AppState::InGame {
paused: *paused,
turbo: !*turbo,
});
}
}
fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
if input.just_pressed(KeyCode::Escape) {
next_state.set(AppState::Menu);
}
}
mod ui {
use crate::*;
#[derive(Resource)]
pub struct MenuData {
pub root_entity: Entity,
}
#[derive(Component, PartialEq, Eq)]
pub enum MenuButton {
Play,
Tutorial,
}
pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
pub const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15);
pub const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25);
pub const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35);
pub fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
}
pub fn setup_menu(mut commands: Commands, tutorial_state: Res<State<TutorialState>>) {
let button_entity = commands
.spawn((
Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
row_gap: px(10),
..default()
},
children![
(
Button,
Node {
width: px(200),
height: px(65),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(NORMAL_BUTTON),
MenuButton::Play,
children![(
Text::new("Play"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)],
),
(
Button,
Node {
width: px(200),
height: px(65),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(match tutorial_state.get() {
TutorialState::Active => ACTIVE_BUTTON,
TutorialState::Inactive => NORMAL_BUTTON,
}),
MenuButton::Tutorial,
children![(
Text::new("Tutorial"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)]
),
],
))
.id();
commands.insert_resource(MenuData {
root_entity: button_entity,
});
}
pub fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
commands.entity(menu_data.root_entity).despawn();
}
pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DespawnOnExit(InGame),
Sprite::from_image(asset_server.load("branding/icon.png")),
));
}
const SPEED: f32 = 100.0;
const TURBO_SPEED: f32 = 300.0;
pub fn movement(
time: Res<Time>,
input: Res<ButtonInput<KeyCode>>,
turbo: Option<Res<State<TurboMode>>>,
mut query: Query<&mut Transform, With<Sprite>>,
) {
for mut transform in &mut query {
let mut direction = Vec3::ZERO;
if input.pressed(KeyCode::ArrowLeft) {
direction.x -= 1.0;
}
if input.pressed(KeyCode::ArrowRight) {
direction.x += 1.0;
}
if input.pressed(KeyCode::ArrowUp) {
direction.y += 1.0;
}
if input.pressed(KeyCode::ArrowDown) {
direction.y -= 1.0;
}
if direction != Vec3::ZERO {
transform.translation += direction.normalize()
* if turbo.is_some() { TURBO_SPEED } else { SPEED }
* time.delta_secs();
}
}
}
pub fn setup_paused_screen(mut commands: Commands) {
info!("Printing Pause");
commands.spawn((
DespawnOnExit(IsPaused::Paused),
Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
row_gap: px(10),
position_type: PositionType::Absolute,
..default()
},
children![(
Node {
width: px(400),
height: px(400),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(NORMAL_BUTTON),
MenuButton::Play,
children![(
Text::new("Paused"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)],
),],
));
}
pub fn setup_turbo_text(mut commands: Commands) {
commands.spawn((
DespawnOnExit(TurboMode),
Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::Start,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
row_gap: px(10),
position_type: PositionType::Absolute,
..default()
},
children![(
Text::new("TURBO MODE"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.3, 0.1)),
)],
));
}
pub fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
for mut sprite in &mut query {
let new_color = LinearRgba {
blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
..LinearRgba::from(sprite.color)
};
sprite.color = new_color.into();
}
}
pub fn movement_instructions(mut commands: Commands) {
commands.spawn((
DespawnOnExit(Tutorial::MovementInstructions),
Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::End,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
row_gap: px(10),
position_type: PositionType::Absolute,
..default()
},
children![
(
Text::new("Move the bevy logo with the arrow keys"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
(
Text::new("Press T to enter TURBO MODE"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
(
Text::new("Press SPACE to pause"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
(
Text::new("Press ESCAPE to return to the menu"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
],
));
}
pub fn pause_instructions(mut commands: Commands) {
commands.spawn((
DespawnOnExit(Tutorial::PauseInstructions),
Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::End,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
row_gap: px(10),
position_type: PositionType::Absolute,
..default()
},
children![
(
Text::new("Press SPACE to resume"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
(
Text::new("Press ESCAPE to return to the menu"),
TextFont {
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.3, 0.3, 0.7)),
),
],
));
}
}