You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
12 KiB
Rust
368 lines
12 KiB
Rust
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::<ServeCards>()
|
|
.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<Pointer<Click>>,
|
|
mut commands: Commands,
|
|
selections: Query<&Selected>,
|
|
) {
|
|
commands.trigger(UiMessage("".into()));
|
|
let e = trigger.entity();
|
|
if selections.contains(e) {
|
|
commands.entity(e).remove::<Selected>();
|
|
} else {
|
|
commands.entity(e).insert(Selected);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn play_selected_animation(
|
|
trigger: Trigger<OnAdd, Selected>,
|
|
mut query: Query<&mut AnimationPlayer>,
|
|
store: Res<AnimationStore>,
|
|
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<OnRemove, Selected>,
|
|
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<Pointer<Click>>,
|
|
query: Query<(Entity, &Card, &PlayLocation, &Parent), With<Selected>>,
|
|
mut transforms: Query<&mut Transform>,
|
|
sets: Query<&SetNumber>,
|
|
mut commands: Commands,
|
|
animation_store: Res<AnimationStore>,
|
|
) {
|
|
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::<PlayLocation>()
|
|
.remove::<Selected>()
|
|
.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<ServeCards>,
|
|
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::<DeckOrder>()
|
|
.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<Entity, Added<SetNumber>>) -> 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<OnAdd, PlayLocation>,
|
|
mut query: Query<(Entity, &PlayLocation, &mut Visibility)>,
|
|
animation_store: Res<AnimationStore>,
|
|
mut commands: Commands,
|
|
random_state: Local<RandomState>,
|
|
) {
|
|
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<Time>,
|
|
) {
|
|
query
|
|
.iter_mut()
|
|
.for_each(|(entity, mut delayed_animation, mut animation_player)| {
|
|
delayed_animation.delay.tick(time.delta());
|
|
if delayed_animation.delay.just_finished() {
|
|
animation_player.play(delayed_animation.animation_index);
|
|
commands
|
|
.entity(entity)
|
|
.insert(delayed_animation.graph.clone())
|
|
.remove::<DelayedAnimation>();
|
|
}
|
|
});
|
|
}
|
|
|
|
pub(crate) fn card_placement(PlayLocation { x, y }: &PlayLocation) -> Vec3 {
|
|
let card_size = [100.0, 160.0];
|
|
let offset = Vec2::new(card_size[0] * 4.0, card_size[1] * 4.0) / 2.5;
|
|
Vec3::new(
|
|
((*x as f32) * card_size[0]) - offset.x,
|
|
((*y as f32) * card_size[1]) - offset.y,
|
|
0.0,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn check_for_sets(
|
|
_trigger: Trigger<Pointer<Click>>,
|
|
cards: Query<(Entity, &Card), With<PlayLocation>>,
|
|
selected: Query<Entity, With<Selected>>,
|
|
mut commands: Commands,
|
|
) {
|
|
let candidate = cards
|
|
// Iterate over all combinations of cards on the board
|
|
.iter_combinations()
|
|
// Only find combinations including currently selected cards
|
|
.filter(|[(ea, _ca), (eb, _cb), (ec, _cc)]| {
|
|
// If selected is empty, proceed
|
|
match selected.iter().len() {
|
|
0 => true,
|
|
1..=3 => {
|
|
let a = selected.contains(*ea);
|
|
let b = selected.contains(*eb);
|
|
let c = selected.contains(*ec);
|
|
match selected.iter().len() {
|
|
1 => a || b || c,
|
|
2 => a && b || b && c || a && c,
|
|
3 => a && b && c,
|
|
_ => panic!("WTF?"),
|
|
}
|
|
}
|
|
_ => false,
|
|
}
|
|
})
|
|
// return the first valid set
|
|
.find_map(|[(ea, ca), (eb, cb), (ec, cc)]| {
|
|
if is_set((ca, cb, cc)).is_ok() {
|
|
info!("\n\t{}\n\t{}\n\t{}", ca, cb, cc);
|
|
Some((ea, eb, ec))
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some((a, b, c)) = candidate {
|
|
// Message for adding
|
|
match selected.iter().len() {
|
|
0 => commands.trigger(UiMessage("First one's free!".into())),
|
|
1 => commands.trigger(UiMessage("This one is tricky!".into())),
|
|
2 => commands.trigger(UiMessage("Threes company!".into())),
|
|
_ => commands.trigger(UiMessage("I don't feel so good...".into())),
|
|
};
|
|
// Add the selected component to the next entity
|
|
if !selected.contains(a) {
|
|
commands.entity(a).insert(Selected);
|
|
} else if !selected.contains(b) {
|
|
commands.entity(b).insert(Selected);
|
|
} else if !selected.contains(c) {
|
|
commands.entity(c).insert(Selected);
|
|
} else {
|
|
commands.trigger(UiMessage("Lock it in!".into()));
|
|
}
|
|
} else {
|
|
commands.trigger(UiMessage("I'm stumped!".into()));
|
|
}
|
|
}
|