use bevy::{prelude::*, utils::RandomState}; use crate::{deck::Card, menu::UiMessage, setup::AnimationStore, view::ViewState}; pub struct PlayPlugin; impl Plugin for PlayPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_observer(serve_cards) .add_systems(OnEnter(ViewState::Play), deal_cards) .add_systems(Update, deal_cards.run_if(set_added)) .add_systems(Update, delayed_animation); } } /// Marker for cards which the player has selected #[derive(Component)] pub(crate) struct Selected; /// Where in the deck a card is #[derive(Component)] pub(crate) struct DeckOrder(pub u8); /// Where on the board/table/play-space a card is #[derive(Component)] pub(crate) struct PlayLocation { pub x: u8, pub y: u8, } /// Which captured set a card is associated with #[derive(Component)] pub(crate) struct SetNumber(pub u8); /// Observer system for adding/removing selected components when a card is clicked pub(crate) fn toggle_selected( trigger: Trigger>, mut commands: Commands, selections: Query<&Selected>, ) { commands.trigger(UiMessage("".into())); let e = trigger.entity(); if selections.contains(e) { commands.entity(e).remove::(); } else { commands.entity(e).insert(Selected); } } pub(crate) fn play_selected_animation( trigger: Trigger, mut query: Query<&mut AnimationPlayer>, store: Res, mut commands: Commands, ) { let (g, ai) = store.store.get("rotate".into()).unwrap(); commands.entity(trigger.entity()).insert(g.clone()); query .get_mut(trigger.entity()) .unwrap() .play(ai.clone()) .repeat(); } pub(crate) fn stop_selected_animation( trigger: Trigger, mut query: Query<(&mut Transform, &mut AnimationPlayer)>, ) { let (mut t, mut ap) = query.get_mut(trigger.entity()).unwrap(); ap.stop_all(); t.rotation = Quat::default(); } /// Check a set when the "Set" button is clicked pub(crate) fn check_set( _trigger: Trigger>, query: Query<(Entity, &Card, &PlayLocation, &Parent), With>, mut transforms: Query<&mut Transform>, sets: Query<&SetNumber>, mut commands: Commands, animation_store: Res, ) { let mut cards = query.iter(); if cards.len() == 3 { let ((_, a, _, _), (_, b, _, _), (_, c, _, _)) = ( cards.next().unwrap(), cards.next().unwrap(), cards.next().unwrap(), ); match is_set((a, b, c)) { Ok(()) => { query.iter().for_each(|(entity, _, play_location, parent)| { let (graph_handle, animation_index) = animation_store .store .get(&format!( "{:?}->discard", (play_location.x, play_location.y) )) .unwrap(); let set_number = sets.iter().len() as u8 / 3 + 1; // Adjust Z-location for proper discard animation transforms.get_mut(parent.get()).unwrap().translation.z = set_number as f32; commands .entity(entity) .remove::() .remove::() .insert(DelayedAnimation { delay: Timer::from_seconds(0.1, TimerMode::Once), graph: graph_handle.clone(), animation_index: animation_index.clone(), }) .insert(SetNumber(set_number)) .insert(PickingBehavior::IGNORE); }); commands.trigger(UiMessage("Yipee!".into())); } Err(invalid_set_err) => { commands.trigger(UiMessage(format!("{}", invalid_set_err))); } } } else if cards.len() > 3 { commands.trigger(UiMessage("Too many cards!".into())); } else if cards.len() < 3 { commands.trigger(UiMessage("Not enough cards!".into())); } } struct InvalidSetErr { color: bool, number: bool, pattern: bool, shape: bool, } impl std::fmt::Display for InvalidSetErr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let mut issues = vec![]; if !self.color { issues.push("Color is an issue..."); } if !self.number { issues.push("Number is an issue..."); } if !self.pattern { issues.push("Pattern is an issue..."); } if !self.shape { issues.push("Shapes is an issue..."); } write!(f, "{}", issues.join("\n")) } } /// Helper function to determine if three cards constitute a set fn is_set((a, b, c): (&Card, &Card, &Card)) -> Result<(), InvalidSetErr> { let color = { ((a.color == b.color) && (b.color == c.color) && (c.color == a.color)) || ((a.color != b.color) && (b.color != c.color) && (c.color != a.color)) }; let number = { ((a.number == b.number) && (b.number == c.number) && (c.number == a.number)) || ((a.number != b.number) && (b.number != c.number) && (c.number != a.number)) }; let pattern = { ((a.pattern == b.pattern) && (b.pattern == c.pattern) && (c.pattern == a.pattern)) || ((a.pattern != b.pattern) && (b.pattern != c.pattern) && (c.pattern != a.pattern)) }; let shape = { ((a.shape == b.shape) && (b.shape == c.shape) && (c.shape == a.shape)) || ((a.shape != b.shape) && (b.shape != c.shape) && (c.shape != a.shape)) }; if color && number && pattern && shape { Ok(()) } else { Err(InvalidSetErr { color, number, shape, pattern, }) } } #[derive(Event, Clone)] struct ServeCards; /// When requested, fill in empty spots on the board with cards fn serve_cards( _trigger: Trigger, in_deck: Query<(Entity, &Card, &DeckOrder)>, spots: Query<&PlayLocation>, mut commands: Commands, ) { info!( "Serving cards from deck ({} cards left)", in_deck.iter().len() ); let mut n = in_deck.iter().len().saturating_sub(1); // Iterate over every x, y play location for this_x in 0..=3 { for this_y in 0..=3 { // If this spot does not have a card if spots .iter() .find(|PlayLocation { x, y }| *x == (this_x as u8) && *y == (this_y as u8)) .is_none() { // If we have more than 9 cards on the board let candidate = in_deck .iter() .find(|(_entity, _deck_card, order)| order.0 == n as u8); n = n.saturating_sub(1); // If that search was fruitful, pull the card from the deck and play it if let Some((e, _, _)) = candidate { commands .entity(e) .remove::() .insert(PlayLocation { x: this_x, y: this_y, }); } } } } } /// Trigger dealing cards when the game starts fn deal_cards(mut commands: Commands) { commands.trigger(ServeCards); } fn set_added(query: Query>) -> bool { !query.is_empty() } #[derive(Component)] struct DelayedAnimation { graph: AnimationGraphHandle, animation_index: AnimationNodeIndex, delay: Timer, } #[derive(Event, Clone)] pub(crate) struct AnimationComplete; /// When a card is added to the board, place it on the screen pub(crate) fn place_card( trigger: Trigger, mut query: Query<(Entity, &PlayLocation, &mut Visibility)>, animation_store: Res, mut commands: Commands, random_state: Local, ) { let (entity, play_location, mut visibility) = query.get_mut(trigger.entity()).unwrap(); let (graph_handle, animation_index) = animation_store .store .get(&format!("deck->{:?}", (play_location.x, play_location.y))) .unwrap(); let delay = ((random_state.hash_one(entity) % 9) as f32 / 10.0) + 0.1; commands.entity(entity).insert(DelayedAnimation { animation_index: animation_index.clone(), delay: Timer::from_seconds(delay, TimerMode::Once), graph: graph_handle.clone(), }); // Set it to visible *visibility = Visibility::Inherited; } fn delayed_animation( mut query: Query<(Entity, &mut DelayedAnimation, &mut AnimationPlayer)>, mut commands: Commands, time: Res