1use bevy::prelude::*;
6
7const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
8
9#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
11enum GameState {
12 #[default]
13 Splash,
14 Menu,
15 Game,
16}
17
18#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
20enum DisplayQuality {
21 Low,
22 Medium,
23 High,
24}
25
26#[derive(Component)]
27struct Setting<T>(T);
28
29#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
31struct Volume(u32);
32
33fn main() {
34 App::new()
35 .add_plugins(DefaultPlugins)
36 .insert_resource(DisplayQuality::Medium)
38 .insert_resource(Volume(7))
39 .init_state::<GameState>()
41 .add_systems(Startup, setup)
42 .add_plugins((splash::splash_plugin, menu::menu_plugin, game::game_plugin))
44 .run();
45}
46
47fn setup(mut commands: Commands) {
48 commands.spawn(Camera2d);
49}
50
51mod splash {
52 use bevy::prelude::*;
53
54 use super::GameState;
55
56 pub fn splash_plugin(app: &mut App) {
58 app
60 .add_systems(OnEnter(GameState::Splash), splash_setup)
62 .add_systems(Update, countdown.run_if(in_state(GameState::Splash)));
64 }
65
66 #[derive(Component)]
68 struct OnSplashScreen;
69
70 #[derive(Resource, Deref, DerefMut)]
72 struct SplashTimer(Timer);
73
74 fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
75 let icon = asset_server.load("branding/icon.png");
76 commands.spawn((
78 DespawnOnExit(GameState::Splash),
80 Node {
81 align_items: AlignItems::Center,
82 justify_content: JustifyContent::Center,
83 width: percent(100),
84 height: percent(100),
85 ..default()
86 },
87 OnSplashScreen,
88 children![(
89 ImageNode::new(icon),
90 Node {
91 width: px(200),
93 ..default()
94 },
95 )],
96 ));
97 commands.insert_resource(SplashTimer(Timer::from_seconds(1.0, TimerMode::Once)));
99 }
100
101 fn countdown(
103 mut game_state: ResMut<NextState<GameState>>,
104 time: Res<Time>,
105 mut timer: ResMut<SplashTimer>,
106 ) {
107 if timer.tick(time.delta()).is_finished() {
108 game_state.set(GameState::Menu);
109 }
110 }
111}
112
113mod game {
114 use bevy::{
115 color::palettes::basic::{BLUE, LIME},
116 prelude::*,
117 };
118
119 use super::{DisplayQuality, GameState, Volume, TEXT_COLOR};
120
121 pub fn game_plugin(app: &mut App) {
124 app.add_systems(OnEnter(GameState::Game), game_setup)
125 .add_systems(Update, game.run_if(in_state(GameState::Game)));
126 }
127
128 #[derive(Component)]
130 struct OnGameScreen;
131
132 #[derive(Resource, Deref, DerefMut)]
133 struct GameTimer(Timer);
134
135 fn game_setup(
136 mut commands: Commands,
137 display_quality: Res<DisplayQuality>,
138 volume: Res<Volume>,
139 ) {
140 commands.spawn((
141 DespawnOnExit(GameState::Game),
142 Node {
143 width: percent(100),
144 height: percent(100),
145 align_items: AlignItems::Center,
147 justify_content: JustifyContent::Center,
148 ..default()
149 },
150 OnGameScreen,
151 children![(
152 Node {
153 flex_direction: FlexDirection::Column,
155 align_items: AlignItems::Center,
159 ..default()
160 },
161 BackgroundColor(Color::BLACK),
162 children![
163 (
164 Text::new("Will be back to the menu shortly..."),
165 TextFont {
166 font_size: FontSize::Px(67.0),
167 ..default()
168 },
169 TextColor(TEXT_COLOR),
170 Node {
171 margin: UiRect::all(px(50)),
172 ..default()
173 },
174 ),
175 (
176 Text::default(),
177 Node {
178 margin: UiRect::all(px(50)),
179 ..default()
180 },
181 children![
182 (
183 TextSpan(format!("quality: {:?}", *display_quality)),
184 TextFont {
185 font_size: FontSize::Px(50.0),
186 ..default()
187 },
188 TextColor(BLUE.into()),
189 ),
190 (
191 TextSpan::new(" - "),
192 TextFont {
193 font_size: FontSize::Px(50.0),
194 ..default()
195 },
196 TextColor(TEXT_COLOR),
197 ),
198 (
199 TextSpan(format!("volume: {:?}", *volume)),
200 TextFont {
201 font_size: FontSize::Px(50.0),
202 ..default()
203 },
204 TextColor(LIME.into()),
205 ),
206 ]
207 ),
208 ]
209 )],
210 ));
211 commands.insert_resource(GameTimer(Timer::from_seconds(5.0, TimerMode::Once)));
213 }
214
215 fn game(
217 time: Res<Time>,
218 mut game_state: ResMut<NextState<GameState>>,
219 mut timer: ResMut<GameTimer>,
220 ) {
221 if timer.tick(time.delta()).is_finished() {
222 game_state.set(GameState::Menu);
223 }
224 }
225}
226
227mod menu {
228 use bevy::{
229 app::AppExit,
230 color::palettes::css::CRIMSON,
231 ecs::component::Mutable,
232 ecs::spawn::{SpawnIter, SpawnWith},
233 prelude::*,
234 };
235
236 use super::{DisplayQuality, GameState, Setting, Volume, TEXT_COLOR};
237
238 pub fn menu_plugin(app: &mut App) {
243 app
244 .init_state::<MenuState>()
248 .add_systems(OnEnter(GameState::Menu), menu_setup)
249 .add_systems(OnEnter(MenuState::Main), main_menu_setup)
251 .add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
253 .add_systems(
255 OnEnter(MenuState::SettingsDisplay),
256 display_settings_menu_setup,
257 )
258 .add_systems(
259 Update,
260 (setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)),),
261 )
262 .add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
264 .add_systems(
265 Update,
266 setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
267 )
268 .add_systems(
270 Update,
271 (menu_action, button_system).run_if(in_state(GameState::Menu)),
272 );
273 }
274
275 #[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
277 enum MenuState {
278 Main,
279 Settings,
280 SettingsDisplay,
281 SettingsSound,
282 #[default]
283 Disabled,
284 }
285
286 #[derive(Component)]
288 struct OnMainMenuScreen;
289
290 #[derive(Component)]
292 struct OnSettingsMenuScreen;
293
294 #[derive(Component)]
296 struct OnDisplaySettingsMenuScreen;
297
298 #[derive(Component)]
300 struct OnSoundSettingsMenuScreen;
301
302 const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
303 const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
304 const HOVERED_PRESSED_BUTTON: Color = Color::srgb(0.25, 0.65, 0.25);
305 const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
306
307 #[derive(Component)]
309 struct SelectedOption;
310
311 #[derive(Component)]
313 enum MenuButtonAction {
314 Play,
315 Settings,
316 SettingsDisplay,
317 SettingsSound,
318 BackToMainMenu,
319 BackToSettings,
320 Quit,
321 }
322
323 fn button_system(
325 mut interaction_query: Query<
326 (&Interaction, &mut BackgroundColor, Option<&SelectedOption>),
327 (Changed<Interaction>, With<Button>),
328 >,
329 ) {
330 for (interaction, mut background_color, selected) in &mut interaction_query {
331 *background_color = match (*interaction, selected) {
332 (Interaction::Pressed, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
333 (Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
334 (Interaction::Hovered, None) => HOVERED_BUTTON.into(),
335 (Interaction::None, None) => NORMAL_BUTTON.into(),
336 }
337 }
338 }
339
340 fn setting_button<T: Resource<Mutability = Mutable> + Component + PartialEq + Copy>(
343 interaction_query: Query<
344 (&Interaction, &Setting<T>, Entity),
345 (Changed<Interaction>, With<Button>),
346 >,
347 selected_query: Single<(Entity, &mut BackgroundColor), With<SelectedOption>>,
348 mut commands: Commands,
349 mut setting: ResMut<T>,
350 ) {
351 let (previous_button, mut previous_button_color) = selected_query.into_inner();
352 for (interaction, button_setting, entity) in &interaction_query {
353 if *interaction == Interaction::Pressed && *setting != button_setting.0 {
354 *previous_button_color = NORMAL_BUTTON.into();
355 commands.entity(previous_button).remove::<SelectedOption>();
356 commands.entity(entity).insert(SelectedOption);
357 *setting = button_setting.0;
358 }
359 }
360 }
361
362 fn menu_setup(mut menu_state: ResMut<NextState<MenuState>>) {
363 menu_state.set(MenuState::Main);
364 }
365
366 fn main_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
367 let button_node = Node {
369 width: px(300),
370 height: px(65),
371 margin: UiRect::all(px(20)),
372 justify_content: JustifyContent::Center,
373 align_items: AlignItems::Center,
374 ..default()
375 };
376 let button_icon_node = Node {
377 width: px(30),
378 position_type: PositionType::Absolute,
380 left: px(10),
382 ..default()
383 };
384 let button_text_font = TextFont {
385 font_size: FontSize::Px(33.0),
386 ..default()
387 };
388
389 let right_icon = asset_server.load("textures/Game Icons/right.png");
390 let wrench_icon = asset_server.load("textures/Game Icons/wrench.png");
391 let exit_icon = asset_server.load("textures/Game Icons/exitRight.png");
392
393 commands.spawn((
394 DespawnOnExit(MenuState::Main),
395 Node {
396 width: percent(100),
397 height: percent(100),
398 align_items: AlignItems::Center,
399 justify_content: JustifyContent::Center,
400 ..default()
401 },
402 OnMainMenuScreen,
403 children![(
404 Node {
405 flex_direction: FlexDirection::Column,
406 align_items: AlignItems::Center,
407 ..default()
408 },
409 BackgroundColor(CRIMSON.into()),
410 children![
411 (
413 Text::new("Bevy Game Menu UI"),
414 TextFont {
415 font_size: FontSize::Px(67.0),
416 ..default()
417 },
418 TextColor(TEXT_COLOR),
419 Node {
420 margin: UiRect::all(px(50)),
421 ..default()
422 },
423 ),
424 (
429 Button,
430 button_node.clone(),
431 BackgroundColor(NORMAL_BUTTON),
432 MenuButtonAction::Play,
433 children![
434 (ImageNode::new(right_icon), button_icon_node.clone()),
435 (
436 Text::new("New Game"),
437 button_text_font.clone(),
438 TextColor(TEXT_COLOR),
439 ),
440 ]
441 ),
442 (
443 Button,
444 button_node.clone(),
445 BackgroundColor(NORMAL_BUTTON),
446 MenuButtonAction::Settings,
447 children![
448 (ImageNode::new(wrench_icon), button_icon_node.clone()),
449 (
450 Text::new("Settings"),
451 button_text_font.clone(),
452 TextColor(TEXT_COLOR),
453 ),
454 ]
455 ),
456 (
457 Button,
458 button_node,
459 BackgroundColor(NORMAL_BUTTON),
460 MenuButtonAction::Quit,
461 children![
462 (ImageNode::new(exit_icon), button_icon_node),
463 (Text::new("Quit"), button_text_font, TextColor(TEXT_COLOR),),
464 ]
465 ),
466 ]
467 )],
468 ));
469 }
470
471 fn settings_menu_setup(mut commands: Commands) {
472 let button_node = Node {
473 width: px(200),
474 height: px(65),
475 margin: UiRect::all(px(20)),
476 justify_content: JustifyContent::Center,
477 align_items: AlignItems::Center,
478 ..default()
479 };
480
481 let button_text_style = (
482 TextFont {
483 font_size: FontSize::Px(33.0),
484 ..default()
485 },
486 TextColor(TEXT_COLOR),
487 );
488
489 commands.spawn((
490 DespawnOnExit(MenuState::Settings),
491 Node {
492 width: percent(100),
493 height: percent(100),
494 align_items: AlignItems::Center,
495 justify_content: JustifyContent::Center,
496 ..default()
497 },
498 OnSettingsMenuScreen,
499 children![(
500 Node {
501 flex_direction: FlexDirection::Column,
502 align_items: AlignItems::Center,
503 ..default()
504 },
505 BackgroundColor(CRIMSON.into()),
506 Children::spawn(SpawnIter(
507 [
508 (MenuButtonAction::SettingsDisplay, "Display"),
509 (MenuButtonAction::SettingsSound, "Sound"),
510 (MenuButtonAction::BackToMainMenu, "Back"),
511 ]
512 .into_iter()
513 .map(move |(action, text)| {
514 (
515 Button,
516 button_node.clone(),
517 BackgroundColor(NORMAL_BUTTON),
518 action,
519 children![(Text::new(text), button_text_style.clone())],
520 )
521 })
522 ))
523 )],
524 ));
525 }
526
527 fn display_settings_menu_setup(mut commands: Commands, display_quality: Res<DisplayQuality>) {
528 fn button_node() -> Node {
529 Node {
530 width: px(200),
531 height: px(65),
532 margin: UiRect::all(px(20)),
533 justify_content: JustifyContent::Center,
534 align_items: AlignItems::Center,
535 ..default()
536 }
537 }
538 fn button_text_style() -> impl Bundle {
539 (
540 TextFont {
541 font_size: FontSize::Px(33.0),
542 ..default()
543 },
544 TextColor(TEXT_COLOR),
545 )
546 }
547
548 let display_quality = *display_quality;
549 commands.spawn((
550 DespawnOnExit(MenuState::SettingsDisplay),
551 Node {
552 width: percent(100),
553 height: percent(100),
554 align_items: AlignItems::Center,
555 justify_content: JustifyContent::Center,
556 ..default()
557 },
558 OnDisplaySettingsMenuScreen,
559 children![(
560 Node {
561 flex_direction: FlexDirection::Column,
562 align_items: AlignItems::Center,
563 ..default()
564 },
565 BackgroundColor(CRIMSON.into()),
566 children![
567 (
570 Node {
571 align_items: AlignItems::Center,
572 ..default()
573 },
574 BackgroundColor(CRIMSON.into()),
575 Children::spawn((
576 Spawn((Text::new("Display Quality"), button_text_style())),
578 SpawnWith(move |parent: &mut ChildSpawner| {
579 for quality_setting in [
580 DisplayQuality::Low,
581 DisplayQuality::Medium,
582 DisplayQuality::High,
583 ] {
584 let mut entity = parent.spawn((
585 Button,
586 Node {
587 width: px(150),
588 height: px(65),
589 ..button_node()
590 },
591 BackgroundColor(NORMAL_BUTTON),
592 Setting(quality_setting),
593 children![(
594 Text::new(format!("{quality_setting:?}")),
595 button_text_style(),
596 )],
597 ));
598 if display_quality == quality_setting {
599 entity.insert(SelectedOption);
600 }
601 }
602 })
603 ))
604 ),
605 (
607 Button,
608 button_node(),
609 BackgroundColor(NORMAL_BUTTON),
610 MenuButtonAction::BackToSettings,
611 children![(Text::new("Back"), button_text_style())]
612 )
613 ]
614 )],
615 ));
616 }
617
618 fn sound_settings_menu_setup(mut commands: Commands, volume: Res<Volume>) {
619 let button_node = Node {
620 width: px(200),
621 height: px(65),
622 margin: UiRect::all(px(20)),
623 justify_content: JustifyContent::Center,
624 align_items: AlignItems::Center,
625 ..default()
626 };
627 let button_text_style = (
628 TextFont {
629 font_size: FontSize::Px(33.0),
630 ..default()
631 },
632 TextColor(TEXT_COLOR),
633 );
634
635 let volume = *volume;
636 let button_node_clone = button_node.clone();
637 commands.spawn((
638 DespawnOnExit(MenuState::SettingsSound),
639 Node {
640 width: percent(100),
641 height: percent(100),
642 align_items: AlignItems::Center,
643 justify_content: JustifyContent::Center,
644 ..default()
645 },
646 OnSoundSettingsMenuScreen,
647 children![(
648 Node {
649 flex_direction: FlexDirection::Column,
650 align_items: AlignItems::Center,
651 ..default()
652 },
653 BackgroundColor(CRIMSON.into()),
654 children![
655 (
656 Node {
657 align_items: AlignItems::Center,
658 ..default()
659 },
660 BackgroundColor(CRIMSON.into()),
661 Children::spawn((
662 Spawn((Text::new("Volume"), button_text_style.clone())),
663 SpawnWith(move |parent: &mut ChildSpawner| {
664 for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
665 let mut entity = parent.spawn((
666 Button,
667 Node {
668 width: px(30),
669 height: px(65),
670 ..button_node_clone.clone()
671 },
672 BackgroundColor(NORMAL_BUTTON),
673 Setting(Volume(volume_setting)),
674 ));
675 if volume == Volume(volume_setting) {
676 entity.insert(SelectedOption);
677 }
678 }
679 })
680 ))
681 ),
682 (
683 Button,
684 button_node,
685 BackgroundColor(NORMAL_BUTTON),
686 MenuButtonAction::BackToSettings,
687 children![(Text::new("Back"), button_text_style)]
688 )
689 ]
690 )],
691 ));
692 }
693
694 fn menu_action(
695 interaction_query: Query<
696 (&Interaction, &MenuButtonAction),
697 (Changed<Interaction>, With<Button>),
698 >,
699 mut app_exit_writer: MessageWriter<AppExit>,
700 mut menu_state: ResMut<NextState<MenuState>>,
701 mut game_state: ResMut<NextState<GameState>>,
702 ) {
703 for (interaction, menu_button_action) in &interaction_query {
704 if *interaction == Interaction::Pressed {
705 match menu_button_action {
706 MenuButtonAction::Quit => {
707 app_exit_writer.write(AppExit::Success);
708 }
709 MenuButtonAction::Play => {
710 game_state.set(GameState::Game);
711 menu_state.set(MenuState::Disabled);
712 }
713 MenuButtonAction::Settings => menu_state.set(MenuState::Settings),
714 MenuButtonAction::SettingsDisplay => {
715 menu_state.set(MenuState::SettingsDisplay);
716 }
717 MenuButtonAction::SettingsSound => {
718 menu_state.set(MenuState::SettingsSound);
719 }
720 MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main),
721 MenuButtonAction::BackToSettings => {
722 menu_state.set(MenuState::Settings);
723 }
724 }
725 }
726 }
727 }
728}