1use bevy::{
20 dev_tools::states::*,
21 input::keyboard::Key,
22 picking::hover::Hovered,
23 prelude::*,
24 ui::{Checked, Pressed},
25 ui_widgets::{checkbox_self_update, Activate, ActivateOnPress, Button, Checkbox, ValueChange},
26};
27
28use ui::*;
29
30#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
32enum AppState {
33 #[default]
34 Menu,
35 InGame {
40 paused: bool,
41 turbo: bool,
42 },
43}
44
45#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
47enum TutorialState {
48 #[default]
49 Active,
50 Inactive,
51}
52
53#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
58struct InGame;
59
60impl ComputedStates for InGame {
61 type SourceStates = AppState;
63
64 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
67 fn compute(sources: AppState) -> Option<Self> {
69 match sources {
72 AppState::InGame { .. } => Some(Self),
74 _ => None,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
89struct TurboMode;
90
91impl ComputedStates for TurboMode {
92 type SourceStates = AppState;
93 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
94
95 fn compute(sources: AppState) -> Option<Self> {
96 match sources {
97 AppState::InGame { turbo: true, .. } => Some(Self),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
114enum IsPaused {
115 NotPaused,
116 Paused,
117}
118
119impl ComputedStates for IsPaused {
120 type SourceStates = AppState;
121 const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
122
123 fn compute(sources: AppState) -> Option<Self> {
124 match sources {
126 AppState::InGame { paused: true, .. } => Some(Self::Paused),
127 AppState::InGame { paused: false, .. } => Some(Self::NotPaused),
128 _ => None,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
140enum Tutorial {
141 MovementInstructions,
142 PauseInstructions,
143}
144
145impl ComputedStates for Tutorial {
146 type SourceStates = (TutorialState, InGame, Option<IsPaused>);
158
159 fn compute(
163 (tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option<IsPaused>),
164 ) -> Option<Self> {
165 if !matches!(tutorial_state, TutorialState::Active) {
167 return None;
168 }
169
170 match is_paused? {
173 IsPaused::NotPaused => Some(Tutorial::MovementInstructions),
174 IsPaused::Paused => Some(Tutorial::PauseInstructions),
175 }
176 }
177}
178
179fn main() {
180 App::new()
182 .add_plugins(DefaultPlugins)
183 .init_state::<AppState>()
184 .init_state::<TutorialState>()
185 .add_computed_state::<InGame>()
187 .add_computed_state::<IsPaused>()
188 .add_computed_state::<TurboMode>()
189 .add_computed_state::<Tutorial>()
190 .add_systems(Startup, setup)
193 .add_systems(OnEnter(AppState::Menu), setup_menu)
194 .add_systems(Update, menu_styling.run_if(in_state(AppState::Menu)))
195 .add_observer(menu_activate.run_if(in_state(AppState::Menu)))
196 .add_observer(menu_tutorial_checked.run_if(in_state(AppState::Menu)))
197 .add_observer(checkbox_self_update)
198 .add_systems(OnExit(AppState::Menu), cleanup_menu)
199 .add_systems(OnEnter(InGame), setup_game)
202 .add_systems(
205 Update,
206 (toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)),
207 )
208 .add_systems(
210 Update,
211 (toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)),
212 )
213 .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
215 .add_systems(OnEnter(TurboMode), setup_turbo_text)
216 .add_systems(
217 OnEnter(Tutorial::MovementInstructions),
218 movement_instructions,
219 )
220 .add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
221 .add_systems(
222 Update,
223 (
224 log_transitions::<AppState>,
225 log_transitions::<TutorialState>,
226 ),
227 )
228 .run();
229}
230
231fn menu_activate(_event: On<Activate>, mut next_state: ResMut<NextState<AppState>>) {
233 next_state.set(AppState::InGame {
235 paused: false,
236 turbo: false,
237 });
238}
239
240fn menu_tutorial_checked(
244 event: On<ValueChange<bool>>,
245 mut next_tutorial: ResMut<NextState<TutorialState>>,
246) {
247 next_tutorial.set(if event.value {
248 TutorialState::Active
249 } else {
250 TutorialState::Inactive
251 });
252}
253
254fn menu_styling(
256 mut widget_query: Query<
257 (
258 &Hovered,
259 Option<&Pressed>,
260 &mut BackgroundColor,
261 Has<Checkbox>,
262 ),
263 Or<(With<Button>, With<Checkbox>)>,
264 >,
265 tutorial_state: Res<State<TutorialState>>,
266) {
267 for (hovered, pressed, mut color, is_tutorial_checkbox) in &mut widget_query {
268 if pressed.is_some() {
269 *color = if is_tutorial_checkbox && tutorial_state.get() == &TutorialState::Active {
270 PRESSED_ACTIVE_BUTTON.into()
271 } else {
272 PRESSED_BUTTON.into()
273 };
274 } else if hovered.get() {
275 if is_tutorial_checkbox && tutorial_state.get() == &TutorialState::Active {
276 *color = HOVERED_ACTIVE_BUTTON.into();
277 } else {
278 *color = HOVERED_BUTTON.into();
279 }
280 } else {
281 if is_tutorial_checkbox && tutorial_state.get() == &TutorialState::Active {
282 *color = ACTIVE_BUTTON.into();
283 } else {
284 *color = NORMAL_BUTTON.into();
285 }
286 }
287 }
288}
289
290fn toggle_pause(
291 input: Res<ButtonInput<KeyCode>>,
292 current_state: Res<State<AppState>>,
293 mut next_state: ResMut<NextState<AppState>>,
294) {
295 if input.just_pressed(KeyCode::Space)
296 && let AppState::InGame { paused, turbo } = current_state.get()
297 {
298 next_state.set(AppState::InGame {
299 paused: !*paused,
300 turbo: *turbo,
301 });
302 }
303}
304
305fn toggle_turbo(
306 input: Res<ButtonInput<Key>>,
307 current_state: Res<State<AppState>>,
308 mut next_state: ResMut<NextState<AppState>>,
309) {
310 if input.just_pressed(Key::Character("t".into()))
311 && let AppState::InGame { paused, turbo } = current_state.get()
312 {
313 next_state.set(AppState::InGame {
314 paused: *paused,
315 turbo: !*turbo,
316 });
317 }
318}
319
320fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
321 if input.just_pressed(KeyCode::Escape) {
322 next_state.set(AppState::Menu);
323 }
324}
325
326mod ui {
327 use crate::*;
328
329 #[derive(Resource)]
330 pub struct MenuData {
331 pub root_entity: Entity,
332 }
333
334 #[derive(Component, PartialEq, Eq)]
335 pub enum MenuButton {
336 Play,
337 Tutorial,
338 }
339
340 pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
341 pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
342 pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
343
344 pub const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15);
345 pub const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25);
346 pub const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35);
347
348 pub fn setup(mut commands: Commands) {
349 commands.spawn(Camera2d);
350 }
351
352 pub fn setup_menu(mut commands: Commands, tutorial_state: Res<State<TutorialState>>) {
353 let button_entity = commands
354 .spawn((
355 Node {
356 width: percent(100),
358 height: percent(100),
359 justify_content: JustifyContent::Center,
360 align_items: AlignItems::Center,
361 flex_direction: FlexDirection::Column,
362 row_gap: px(10),
363 ..default()
364 },
365 children![(
366 Button,
367 Hovered::default(),
368 ActivateOnPress,
369 Node {
370 width: px(200),
371 height: px(65),
372 justify_content: JustifyContent::Center,
374 align_items: AlignItems::Center,
376 ..default()
377 },
378 BackgroundColor(NORMAL_BUTTON),
379 MenuButton::Play,
380 children![(
381 Text::new("Play"),
382 TextFont {
383 font_size: FontSize::Px(33.0),
384 ..default()
385 },
386 TextColor(Color::srgb(0.9, 0.9, 0.9)),
387 )],
388 ),],
389 ))
390 .id();
391
392 let mut tutorial_checkbox = commands.spawn((
393 Checkbox,
394 Hovered::default(),
395 ActivateOnPress,
396 Node {
397 width: px(200),
398 height: px(65),
399 justify_content: JustifyContent::Center,
401 align_items: AlignItems::Center,
403 ..default()
404 },
405 BackgroundColor(match tutorial_state.get() {
406 TutorialState::Active => ACTIVE_BUTTON,
407 TutorialState::Inactive => NORMAL_BUTTON,
408 }),
409 MenuButton::Tutorial,
410 children![(
411 Text::new("Tutorial"),
412 TextFont {
413 font_size: FontSize::Px(33.0),
414 ..default()
415 },
416 TextColor(Color::srgb(0.9, 0.9, 0.9)),
417 )],
418 ));
419 match tutorial_state.get() {
420 TutorialState::Active => {
421 tutorial_checkbox.insert(Checked);
422 }
423 TutorialState::Inactive => {}
424 };
425 let tutorial_checkbox_id = tutorial_checkbox.id();
426 commands
427 .entity(button_entity)
428 .add_child(tutorial_checkbox_id);
429
430 commands.insert_resource(MenuData {
431 root_entity: button_entity,
432 });
433 }
434
435 pub fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
436 commands.entity(menu_data.root_entity).despawn();
437 }
438
439 pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
440 commands.spawn((
441 DespawnOnExit(InGame),
442 Sprite::from_image(asset_server.load("branding/icon.png")),
443 ));
444 }
445
446 const SPEED: f32 = 100.0;
447 const TURBO_SPEED: f32 = 300.0;
448
449 pub fn movement(
450 time: Res<Time>,
451 input: Res<ButtonInput<KeyCode>>,
452 turbo: Option<Res<State<TurboMode>>>,
453 mut query: Query<&mut Transform, With<Sprite>>,
454 ) {
455 for mut transform in &mut query {
456 let mut direction = Vec3::ZERO;
457 if input.pressed(KeyCode::ArrowLeft) {
458 direction.x -= 1.0;
459 }
460 if input.pressed(KeyCode::ArrowRight) {
461 direction.x += 1.0;
462 }
463 if input.pressed(KeyCode::ArrowUp) {
464 direction.y += 1.0;
465 }
466 if input.pressed(KeyCode::ArrowDown) {
467 direction.y -= 1.0;
468 }
469
470 if direction != Vec3::ZERO {
471 transform.translation += direction.normalize()
472 * if turbo.is_some() { TURBO_SPEED } else { SPEED }
473 * time.delta_secs();
474 }
475 }
476 }
477
478 pub fn setup_paused_screen(mut commands: Commands) {
479 info!("Printing Pause");
480 commands.spawn((
481 DespawnOnExit(IsPaused::Paused),
482 Node {
483 width: percent(100),
485 height: percent(100),
486 justify_content: JustifyContent::Center,
487 align_items: AlignItems::Center,
488 flex_direction: FlexDirection::Column,
489 row_gap: px(10),
490 position_type: PositionType::Absolute,
491 ..default()
492 },
493 children![(
494 Node {
495 width: px(400),
496 height: px(400),
497 justify_content: JustifyContent::Center,
499 align_items: AlignItems::Center,
501 ..default()
502 },
503 BackgroundColor(NORMAL_BUTTON),
504 MenuButton::Play,
505 children![(
506 Text::new("Paused"),
507 TextFont {
508 font_size: FontSize::Px(33.0),
509 ..default()
510 },
511 TextColor(Color::srgb(0.9, 0.9, 0.9)),
512 )],
513 ),],
514 ));
515 }
516
517 pub fn setup_turbo_text(mut commands: Commands) {
518 commands.spawn((
519 DespawnOnExit(TurboMode),
520 Node {
521 width: percent(100),
523 height: percent(100),
524 justify_content: JustifyContent::Start,
525 align_items: AlignItems::Center,
526 flex_direction: FlexDirection::Column,
527 row_gap: px(10),
528 position_type: PositionType::Absolute,
529 ..default()
530 },
531 children![(
532 Text::new("TURBO MODE"),
533 TextFont {
534 font_size: FontSize::Px(33.0),
535 ..default()
536 },
537 TextColor(Color::srgb(0.9, 0.3, 0.1)),
538 )],
539 ));
540 }
541
542 pub fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
543 for mut sprite in &mut query {
544 let new_color = LinearRgba {
545 blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
546 ..LinearRgba::from(sprite.color)
547 };
548
549 sprite.color = new_color.into();
550 }
551 }
552
553 pub fn movement_instructions(mut commands: Commands) {
554 commands.spawn((
555 DespawnOnExit(Tutorial::MovementInstructions),
556 Node {
557 width: percent(100),
559 height: percent(100),
560 justify_content: JustifyContent::End,
561 align_items: AlignItems::Center,
562 flex_direction: FlexDirection::Column,
563 row_gap: px(10),
564 position_type: PositionType::Absolute,
565 ..default()
566 },
567 children![
568 (
569 Text::new("Move the bevy logo with the arrow keys"),
570 TextFont {
571 font_size: FontSize::Px(33.0),
572 ..default()
573 },
574 TextColor(Color::srgb(0.3, 0.3, 0.7)),
575 ),
576 (
577 Text::new("Press T to enter TURBO MODE"),
578 TextFont {
579 font_size: FontSize::Px(33.0),
580 ..default()
581 },
582 TextColor(Color::srgb(0.3, 0.3, 0.7)),
583 ),
584 (
585 Text::new("Press SPACE to pause"),
586 TextFont {
587 font_size: FontSize::Px(33.0),
588 ..default()
589 },
590 TextColor(Color::srgb(0.3, 0.3, 0.7)),
591 ),
592 (
593 Text::new("Press ESCAPE to return to the menu"),
594 TextFont {
595 font_size: FontSize::Px(33.0),
596 ..default()
597 },
598 TextColor(Color::srgb(0.3, 0.3, 0.7)),
599 ),
600 ],
601 ));
602 }
603
604 pub fn pause_instructions(mut commands: Commands) {
605 commands.spawn((
606 DespawnOnExit(Tutorial::PauseInstructions),
607 Node {
608 width: percent(100),
610 height: percent(100),
611 justify_content: JustifyContent::End,
612 align_items: AlignItems::Center,
613 flex_direction: FlexDirection::Column,
614 row_gap: px(10),
615 position_type: PositionType::Absolute,
616 ..default()
617 },
618 children![
619 (
620 Text::new("Press SPACE to resume"),
621 TextFont {
622 font_size: FontSize::Px(33.0),
623 ..default()
624 },
625 TextColor(Color::srgb(0.3, 0.3, 0.7)),
626 ),
627 (
628 Text::new("Press ESCAPE to return to the menu"),
629 TextFont {
630 font_size: FontSize::Px(33.0),
631 ..default()
632 },
633 TextColor(Color::srgb(0.3, 0.3, 0.7)),
634 ),
635 ],
636 ));
637 }
638}