1use bevy::{dev_tools::states::*, input::keyboard::Key, prelude::*};
20
21use ui::*;
22
23#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
25enum AppState {
26 #[default]
27 Menu,
28 InGame {
33 paused: bool,
34 turbo: bool,
35 },
36}
37
38#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
40enum TutorialState {
41 #[default]
42 Active,
43 Inactive,
44}
45
46#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
51struct InGame;
52
53impl ComputedStates for InGame {
54 type SourceStates = AppState;
56
57 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
60 fn compute(sources: AppState) -> Option<Self> {
62 match sources {
65 AppState::InGame { .. } => Some(Self),
67 _ => None,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
82struct TurboMode;
83
84impl ComputedStates for TurboMode {
85 type SourceStates = AppState;
86 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
87
88 fn compute(sources: AppState) -> Option<Self> {
89 match sources {
90 AppState::InGame { turbo: true, .. } => Some(Self),
91 _ => None,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
107enum IsPaused {
108 NotPaused,
109 Paused,
110}
111
112impl ComputedStates for IsPaused {
113 type SourceStates = AppState;
114 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
115
116 fn compute(sources: AppState) -> Option<Self> {
117 match sources {
119 AppState::InGame { paused: true, .. } => Some(Self::Paused),
120 AppState::InGame { paused: false, .. } => Some(Self::NotPaused),
121 _ => None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
133enum Tutorial {
134 MovementInstructions,
135 PauseInstructions,
136}
137
138impl ComputedStates for Tutorial {
139 type SourceStates = (TutorialState, InGame, Option<IsPaused>);
151
152 fn compute(
156 (tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option<IsPaused>),
157 ) -> Option<Self> {
158 if !matches!(tutorial_state, TutorialState::Active) {
160 return None;
161 }
162
163 match is_paused? {
166 IsPaused::NotPaused => Some(Tutorial::MovementInstructions),
167 IsPaused::Paused => Some(Tutorial::PauseInstructions),
168 }
169 }
170}
171
172fn main() {
173 App::new()
175 .add_plugins(DefaultPlugins)
176 .init_state::<AppState>()
177 .init_state::<TutorialState>()
178 .add_computed_state::<InGame>()
180 .add_computed_state::<IsPaused>()
181 .add_computed_state::<TurboMode>()
182 .add_computed_state::<Tutorial>()
183 .add_systems(Startup, setup)
186 .add_systems(OnEnter(AppState::Menu), setup_menu)
187 .add_systems(Update, menu.run_if(in_state(AppState::Menu)))
188 .add_systems(OnExit(AppState::Menu), cleanup_menu)
189 .add_systems(OnEnter(InGame), setup_game)
192 .add_systems(
195 Update,
196 (toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)),
197 )
198 .add_systems(
200 Update,
201 (toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)),
202 )
203 .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
205 .add_systems(OnEnter(TurboMode), setup_turbo_text)
206 .add_systems(
207 OnEnter(Tutorial::MovementInstructions),
208 movement_instructions,
209 )
210 .add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
211 .add_systems(
212 Update,
213 (
214 log_transitions::<AppState>,
215 log_transitions::<TutorialState>,
216 ),
217 )
218 .run();
219}
220
221fn menu(
222 mut next_state: ResMut<NextState<AppState>>,
223 tutorial_state: Res<State<TutorialState>>,
224 mut next_tutorial: ResMut<NextState<TutorialState>>,
225 mut interaction_query: Query<
226 (&Interaction, &mut BackgroundColor, &MenuButton),
227 (Changed<Interaction>, With<Button>),
228 >,
229) {
230 for (interaction, mut color, menu_button) in &mut interaction_query {
231 match *interaction {
232 Interaction::Pressed => {
233 *color = if menu_button == &MenuButton::Tutorial
234 && tutorial_state.get() == &TutorialState::Active
235 {
236 PRESSED_ACTIVE_BUTTON.into()
237 } else {
238 PRESSED_BUTTON.into()
239 };
240
241 match menu_button {
242 MenuButton::Play => next_state.set(AppState::InGame {
243 paused: false,
244 turbo: false,
245 }),
246 MenuButton::Tutorial => next_tutorial.set(match tutorial_state.get() {
247 TutorialState::Active => TutorialState::Inactive,
248 TutorialState::Inactive => TutorialState::Active,
249 }),
250 };
251 }
252 Interaction::Hovered => {
253 if menu_button == &MenuButton::Tutorial
254 && tutorial_state.get() == &TutorialState::Active
255 {
256 *color = HOVERED_ACTIVE_BUTTON.into();
257 } else {
258 *color = HOVERED_BUTTON.into();
259 }
260 }
261 Interaction::None => {
262 if menu_button == &MenuButton::Tutorial
263 && tutorial_state.get() == &TutorialState::Active
264 {
265 *color = ACTIVE_BUTTON.into();
266 } else {
267 *color = NORMAL_BUTTON.into();
268 }
269 }
270 }
271 }
272}
273
274fn toggle_pause(
275 input: Res<ButtonInput<KeyCode>>,
276 current_state: Res<State<AppState>>,
277 mut next_state: ResMut<NextState<AppState>>,
278) {
279 if input.just_pressed(KeyCode::Space)
280 && let AppState::InGame { paused, turbo } = current_state.get()
281 {
282 next_state.set(AppState::InGame {
283 paused: !*paused,
284 turbo: *turbo,
285 });
286 }
287}
288
289fn toggle_turbo(
290 input: Res<ButtonInput<Key>>,
291 current_state: Res<State<AppState>>,
292 mut next_state: ResMut<NextState<AppState>>,
293) {
294 if input.just_pressed(Key::Character("t".into()))
295 && let AppState::InGame { paused, turbo } = current_state.get()
296 {
297 next_state.set(AppState::InGame {
298 paused: *paused,
299 turbo: !*turbo,
300 });
301 }
302}
303
304fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
305 if input.just_pressed(KeyCode::Escape) {
306 next_state.set(AppState::Menu);
307 }
308}
309
310mod ui {
311 use crate::*;
312
313 #[derive(Resource)]
314 pub struct MenuData {
315 pub root_entity: Entity,
316 }
317
318 #[derive(Component, PartialEq, Eq)]
319 pub enum MenuButton {
320 Play,
321 Tutorial,
322 }
323
324 pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
325 pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
326 pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
327
328 pub const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15);
329 pub const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25);
330 pub const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35);
331
332 pub fn setup(mut commands: Commands) {
333 commands.spawn(Camera2d);
334 }
335
336 pub fn setup_menu(mut commands: Commands, tutorial_state: Res<State<TutorialState>>) {
337 let button_entity = commands
338 .spawn((
339 Node {
340 width: percent(100),
342 height: percent(100),
343 justify_content: JustifyContent::Center,
344 align_items: AlignItems::Center,
345 flex_direction: FlexDirection::Column,
346 row_gap: px(10),
347 ..default()
348 },
349 children![
350 (
351 Button,
352 Node {
353 width: px(200),
354 height: px(65),
355 justify_content: JustifyContent::Center,
357 align_items: AlignItems::Center,
359 ..default()
360 },
361 BackgroundColor(NORMAL_BUTTON),
362 MenuButton::Play,
363 children![(
364 Text::new("Play"),
365 TextFont {
366 font_size: FontSize::Px(33.0),
367 ..default()
368 },
369 TextColor(Color::srgb(0.9, 0.9, 0.9)),
370 )],
371 ),
372 (
373 Button,
374 Node {
375 width: px(200),
376 height: px(65),
377 justify_content: JustifyContent::Center,
379 align_items: AlignItems::Center,
381 ..default()
382 },
383 BackgroundColor(match tutorial_state.get() {
384 TutorialState::Active => ACTIVE_BUTTON,
385 TutorialState::Inactive => NORMAL_BUTTON,
386 }),
387 MenuButton::Tutorial,
388 children![(
389 Text::new("Tutorial"),
390 TextFont {
391 font_size: FontSize::Px(33.0),
392 ..default()
393 },
394 TextColor(Color::srgb(0.9, 0.9, 0.9)),
395 )]
396 ),
397 ],
398 ))
399 .id();
400 commands.insert_resource(MenuData {
401 root_entity: button_entity,
402 });
403 }
404
405 pub fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
406 commands.entity(menu_data.root_entity).despawn();
407 }
408
409 pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
410 commands.spawn((
411 DespawnOnExit(InGame),
412 Sprite::from_image(asset_server.load("branding/icon.png")),
413 ));
414 }
415
416 const SPEED: f32 = 100.0;
417 const TURBO_SPEED: f32 = 300.0;
418
419 pub fn movement(
420 time: Res<Time>,
421 input: Res<ButtonInput<KeyCode>>,
422 turbo: Option<Res<State<TurboMode>>>,
423 mut query: Query<&mut Transform, With<Sprite>>,
424 ) {
425 for mut transform in &mut query {
426 let mut direction = Vec3::ZERO;
427 if input.pressed(KeyCode::ArrowLeft) {
428 direction.x -= 1.0;
429 }
430 if input.pressed(KeyCode::ArrowRight) {
431 direction.x += 1.0;
432 }
433 if input.pressed(KeyCode::ArrowUp) {
434 direction.y += 1.0;
435 }
436 if input.pressed(KeyCode::ArrowDown) {
437 direction.y -= 1.0;
438 }
439
440 if direction != Vec3::ZERO {
441 transform.translation += direction.normalize()
442 * if turbo.is_some() { TURBO_SPEED } else { SPEED }
443 * time.delta_secs();
444 }
445 }
446 }
447
448 pub fn setup_paused_screen(mut commands: Commands) {
449 info!("Printing Pause");
450 commands.spawn((
451 DespawnOnExit(IsPaused::Paused),
452 Node {
453 width: percent(100),
455 height: percent(100),
456 justify_content: JustifyContent::Center,
457 align_items: AlignItems::Center,
458 flex_direction: FlexDirection::Column,
459 row_gap: px(10),
460 position_type: PositionType::Absolute,
461 ..default()
462 },
463 children![(
464 Node {
465 width: px(400),
466 height: px(400),
467 justify_content: JustifyContent::Center,
469 align_items: AlignItems::Center,
471 ..default()
472 },
473 BackgroundColor(NORMAL_BUTTON),
474 MenuButton::Play,
475 children![(
476 Text::new("Paused"),
477 TextFont {
478 font_size: FontSize::Px(33.0),
479 ..default()
480 },
481 TextColor(Color::srgb(0.9, 0.9, 0.9)),
482 )],
483 ),],
484 ));
485 }
486
487 pub fn setup_turbo_text(mut commands: Commands) {
488 commands.spawn((
489 DespawnOnExit(TurboMode),
490 Node {
491 width: percent(100),
493 height: percent(100),
494 justify_content: JustifyContent::Start,
495 align_items: AlignItems::Center,
496 flex_direction: FlexDirection::Column,
497 row_gap: px(10),
498 position_type: PositionType::Absolute,
499 ..default()
500 },
501 children![(
502 Text::new("TURBO MODE"),
503 TextFont {
504 font_size: FontSize::Px(33.0),
505 ..default()
506 },
507 TextColor(Color::srgb(0.9, 0.3, 0.1)),
508 )],
509 ));
510 }
511
512 pub fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
513 for mut sprite in &mut query {
514 let new_color = LinearRgba {
515 blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
516 ..LinearRgba::from(sprite.color)
517 };
518
519 sprite.color = new_color.into();
520 }
521 }
522
523 pub fn movement_instructions(mut commands: Commands) {
524 commands.spawn((
525 DespawnOnExit(Tutorial::MovementInstructions),
526 Node {
527 width: percent(100),
529 height: percent(100),
530 justify_content: JustifyContent::End,
531 align_items: AlignItems::Center,
532 flex_direction: FlexDirection::Column,
533 row_gap: px(10),
534 position_type: PositionType::Absolute,
535 ..default()
536 },
537 children![
538 (
539 Text::new("Move the bevy logo with the arrow keys"),
540 TextFont {
541 font_size: FontSize::Px(33.0),
542 ..default()
543 },
544 TextColor(Color::srgb(0.3, 0.3, 0.7)),
545 ),
546 (
547 Text::new("Press T to enter TURBO MODE"),
548 TextFont {
549 font_size: FontSize::Px(33.0),
550 ..default()
551 },
552 TextColor(Color::srgb(0.3, 0.3, 0.7)),
553 ),
554 (
555 Text::new("Press SPACE to pause"),
556 TextFont {
557 font_size: FontSize::Px(33.0),
558 ..default()
559 },
560 TextColor(Color::srgb(0.3, 0.3, 0.7)),
561 ),
562 (
563 Text::new("Press ESCAPE to return to the menu"),
564 TextFont {
565 font_size: FontSize::Px(33.0),
566 ..default()
567 },
568 TextColor(Color::srgb(0.3, 0.3, 0.7)),
569 ),
570 ],
571 ));
572 }
573
574 pub fn pause_instructions(mut commands: Commands) {
575 commands.spawn((
576 DespawnOnExit(Tutorial::PauseInstructions),
577 Node {
578 width: percent(100),
580 height: percent(100),
581 justify_content: JustifyContent::End,
582 align_items: AlignItems::Center,
583 flex_direction: FlexDirection::Column,
584 row_gap: px(10),
585 position_type: PositionType::Absolute,
586 ..default()
587 },
588 children![
589 (
590 Text::new("Press SPACE to resume"),
591 TextFont {
592 font_size: FontSize::Px(33.0),
593 ..default()
594 },
595 TextColor(Color::srgb(0.3, 0.3, 0.7)),
596 ),
597 (
598 Text::new("Press ESCAPE to return to the menu"),
599 TextFont {
600 font_size: FontSize::Px(33.0),
601 ..default()
602 },
603 TextColor(Color::srgb(0.3, 0.3, 0.7)),
604 ),
605 ],
606 ));
607 }
608}