// Note: this is more of a "ui" than a "menu" module use bevy::{ color::palettes::css::{BLACK, WHITE}, prelude::*, }; use crate::{ audio, boot, deck::{Card, Deck, ItemColor, ItemNumber, ItemPattern, ItemShape}, play::{check_for_sets, check_set, SetNumber}, view::{button_set_state, ViewState}, GameState, }; /// Game Menu pub struct MenuPlugin; impl Plugin for MenuPlugin { fn build(&self, app: &mut App) { app.add_observer(set_set_count) .add_observer(ui_alert) .add_systems( Startup, ( setup, setup_play, setup_about, setup_how_to_play.after(boot::load), setup_deck, setup_sets, setup_alert, ), ); } } fn button_builder(node: Node) -> (Button, BackgroundColor, Node, BorderColor) { ( Button, BackgroundColor(BLACK.with_alpha(0.9).into()), Node { padding: UiRect::all(Val::Px(10.)), margin: UiRect::top(Val::Px(25.)), border: UiRect::all(Val::Px(1.0)), ..node }, BorderColor(WHITE.into()), ) } fn setup(mut commands: Commands, server: Res) { commands .spawn(( ViewState::Menu, Node { position_type: PositionType::Absolute, justify_content: JustifyContent::Center, align_items: AlignItems::Center, width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, )) .with_children(|parent| { parent .spawn(Node { width: Val::Percent(100.0), flex_direction: FlexDirection::Column, align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }) .with_children(|parent| { parent.spawn(( ImageNode { image: server.load("logo.png"), ..default() }, Node { height: Val::Px(300.0), ..default() }, )); parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("Play".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(button_set_state(ViewState::Play)); parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("New Game".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(button_set_state(GameState::NewGame)); parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("About".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(button_set_state(ViewState::About)); parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("How to Play".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(button_set_state(ViewState::HowToPlay)); #[cfg(not(target_arch = "wasm32"))] parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("Quit".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(quit_button); }); }); } #[derive(Component)] pub(crate) struct SetCounter; fn setup_play(mut commands: Commands, server: Res) { commands .spawn(( ViewState::Play, Node { width: Val::Percent(100.0), justify_content: JustifyContent::SpaceBetween, ..default() }, )) .with_children(|parent| { parent.spawn(Node::default()).with_children(|parent| { parent .spawn(( Button, BackgroundColor(BLACK.with_alpha(0.9).into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), height: Val::Px(40.0), ..default() }, BorderColor(WHITE.into()), GlobalZIndex(1), )) .with_children(|parent| { parent.spawn(ImageNode { image: server.load("kenney_game-icons/PNG/White/1x/home.png"), ..default() }); }) .observe(button_hover_on) .observe(button_hover_off) .observe(button_set_state(ViewState::Menu)); parent .spawn(( Button, BackgroundColor(BLACK.with_alpha(0.9).into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), height: Val::Px(40.0), ..default() }, BorderColor(WHITE.into()), GlobalZIndex(1), )) .with_children(|parent| { parent.spawn(( ImageNode { image: server.load("kenney_game-icons/PNG/White/1x/musicOn.png"), ..default() }, audio::MusicIcon, )); }) .observe(button_hover_on) .observe(button_hover_off) .observe(audio::toggle_music); }); parent .spawn(( Button, BackgroundColor(BLACK.with_alpha(0.9).into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(10.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, BorderColor(WHITE.into()), GlobalZIndex(1), )) .with_children(|parent| { parent.spawn(Text("Set!".to_string())); }) .observe(button_hover_on) .observe(button_hover_off) .observe(check_set); parent .spawn(( Button, BackgroundColor(BLACK.with_alpha(0.9).into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), height: Val::Px(40.0), ..default() }, BorderColor(WHITE.into()), GlobalZIndex(1), )) .with_children(|parent| { parent.spawn(ImageNode { image: server.load("kenney_game-icons/PNG/White/1x/question.png"), ..default() }); }) .observe(button_hover_on) .observe(button_hover_off) .observe(check_for_sets); }); commands .spawn(( Node { position_type: PositionType::Absolute, bottom: Val::Px(0.0), width: Val::Percent(100.0), justify_content: JustifyContent::End, ..default() }, ViewState::Play, )) .with_children(|parent| { parent .spawn(( Node { padding: UiRect::all(Val::Px(10.0)), margin: UiRect::all(Val::Px(10.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, BackgroundColor(Color::BLACK.with_alpha(0.9).into()), BorderColor(WHITE.into()), )) .with_children(|parent| { parent.spawn(Text("Sets:".into())); parent.spawn((Text("##".into()), SetCounter)); }); }); } fn set_set_count( trigger: Trigger, mut query: Query<&mut Text, With>, set_number: Query<&SetNumber>, ) { query.single_mut().0 = format!("{}", set_number.get(trigger.entity()).unwrap().0); } fn setup_about(mut commands: Commands) { commands .spawn(( ViewState::About, Node { position_type: PositionType::Absolute, width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..default() }, )) .with_children(|parent| { parent .spawn(button_builder(Node { position_type: PositionType::Absolute, top: Val::Px(0.0), left: Val::Px(0.0), ..default() })) .with_children(|parent| { parent.spawn(Text("Menu".to_string())); }) .observe(button_set_state(ViewState::Menu)); parent .spawn(( Node { width: Val::Px(500.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..default() }, BackgroundColor(BLACK.with_alpha(0.9).into()), )) .with_children(|parent| { parent.spawn(( Text("About the Game".into()), TextFont::default().with_font_size(30.0), )); parent.spawn(( Text("The real-time card game SET! originally designed by Marsha Falco and published by Set Enterprises.".into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), ..default() }, )); parent.spawn(( Text("This videogame version was developed by Elijah Voigt.".into()), Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), ..default() }, )); }); }); } fn setup_how_to_play(mut commands: Commands, deck: Res) { commands .spawn(( ViewState::HowToPlay, Node { position_type: PositionType::Absolute, width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..default() }, )) .with_children(|parent| { parent .spawn(button_builder(Node { position_type: PositionType::Absolute, top: Val::Px(0.0), left: Val::Px(0.0), ..default() })) .with_children(|parent| { parent.spawn(Text("Menu".to_string())); }) .observe(button_set_state(ViewState::Menu)); parent.spawn((Node { width: Val::Px(500.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..default() }, BackgroundColor(BLACK.with_alpha(0.9).into()), )).with_children(|parent| { parent.spawn((Text("How to Play".into()), TextFont::default().with_font_size(30.0))); let most_matching = ( Card { color: ItemColor::Red, shape: ItemShape::Squiggle, number: ItemNumber::One, pattern: ItemPattern::Solid, }, Card { color: ItemColor::Red, shape: ItemShape::Squiggle, number: ItemNumber::Two, pattern: ItemPattern::Solid, }, Card { color: ItemColor::Red, shape: ItemShape::Squiggle, number: ItemNumber::Three, pattern: ItemPattern::Solid, } ); let most_not_matching = ( Card { color: ItemColor::Green, shape: ItemShape::Oval, number: ItemNumber::Three, pattern: ItemPattern::Solid, }, Card { color: ItemColor::Purple, shape: ItemShape::Squiggle, number: ItemNumber::Three, pattern: ItemPattern::Striped, }, Card { color: ItemColor::Red, shape: ItemShape::Diamond, number: ItemNumber::Three, pattern: ItemPattern::Open, } ); let not_valid = ( Card { color: ItemColor::Purple, shape: ItemShape::Oval, number: ItemNumber::Three, pattern: ItemPattern::Striped, }, Card { color: ItemColor::Purple, shape: ItemShape::Oval, number: ItemNumber::Two, pattern: ItemPattern::Striped, }, Card { color: ItemColor::Purple, shape: ItemShape::Oval, number: ItemNumber::Three, pattern: ItemPattern::Open, } ); [ ("Find groups of 3 cards", None), ("All cards in a set must either match or differ in their traits: Color, Number, Shading, and Shape.", None), ("For example all agree on Color, Shading, and Shape but not Number...", Some(most_matching)), ("...or all agree on Number but not Color, Shading, and Shape...", Some(most_not_matching)), ("...but never partial agreement...", Some(not_valid)), ("When in doubt, click the 'Help!' button to get an assist.", None), ] .iter() .for_each(|(t, example)| { parent.spawn(( Node { max_width: Val::Percent(100.0), flex_direction: FlexDirection::Row, align_items: AlignItems::Center, ..default() }, )).with_children(|parent| { parent.spawn(( Node { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), ..default() }, Text(t.to_string()), )); parent.spawn(Node { ..default() }).with_children(|parent| { if let Some((a, b, c)) = example { [a, b, c].iter().for_each(|x| { let sprite = deck.cards.get(*x).unwrap(); parent.spawn(( Node { height: Val::Px(100.0), ..default() }, ImageNode { image: sprite.image.clone(), texture_atlas: sprite.texture_atlas.clone(), ..default() } )); }); }; }); }); }); }); }); } fn setup_deck(mut commands: Commands) { commands .spawn((ViewState::Deck, Node::default())) .with_children(|parent| { parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("Back".to_string())); }) .observe(button_set_state(ViewState::Play)); }); } fn setup_sets(mut commands: Commands) { commands .spawn((ViewState::Sets, Node::default())) .with_children(|parent| { parent .spawn(button_builder(Node::default())) .with_children(|parent| { parent.spawn(Text("Back".to_string())); }) .observe(button_set_state(ViewState::Play)); }); } fn quit_button(_trigger: Trigger>, mut exit_event: EventWriter) { exit_event.send(AppExit::Success); } fn button_hover_on(trigger: Trigger>, mut query: Query<&mut BackgroundColor>) { let mut background_color = query.get_mut(trigger.entity()).unwrap(); background_color.0.set_alpha(1.0); } fn button_hover_off(trigger: Trigger>, mut query: Query<&mut BackgroundColor>) { let mut background_color = query.get_mut(trigger.entity()).unwrap(); background_color.0.set_alpha(0.9); } #[derive(Component)] struct Alert; #[derive(Event)] pub(crate) struct UiMessage(pub String); fn setup_alert(mut commands: Commands) { commands .spawn(( Node { position_type: PositionType::Absolute, bottom: Val::Px(0.0), width: Val::Percent(100.0), justify_content: JustifyContent::Start, ..default() }, ViewState::Play, )) .with_children(|parent| { parent .spawn(( Node { padding: UiRect::all(Val::Px(10.0)), margin: UiRect::all(Val::Px(10.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, BackgroundColor(Color::BLACK.with_alpha(0.9).into()), BorderColor(WHITE.into()), )) .with_children(|parent| { parent.spawn((Text::default(), Alert)); }); }); } fn ui_alert(trigger: Trigger, mut query: Query<&mut Text, With>) { query.single_mut().0 = trigger.event().0.clone(); }