/**
* Copyright 2019 Heroic Labs and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DemoGame.Scripts.Gameplay.Cards;
using DemoGame.Scripts.Menus;
using DemoGame.Scripts.Session;
using Nakama;
using Nakama.TinyJson;
using UnityEngine;
using UnityEngine.UI;
namespace DemoGame.Scripts.Gameplay.Decks
{
///
/// Menu responsible for building users deck.
/// Players can view their cards and upgrade them to next level.
///
public class DeckBuildingMenu : Menu
{
#region Fields
#region Card Lists
///
/// Reference to the starting deck card list.
/// This deck is given to the played upon first authentication.
///
[SerializeField] private CardList _defaultDeck = null;
#endregion
#region Card Prefabs
///
/// Prefab of a single card holder.
///
[SerializeField] private CardSlotUI _slotPrefab = null;
///
/// Prefab of card row.
///
[SerializeField] private GameObject _slotRowPrefab = null;
#endregion
#region Visuals
///
/// The number of cards in a deck.
///
[SerializeField] private int _deckSize = 6;
///
/// The number of cards displayed per row.
///
[SerializeField] private int _cardsPerRow = 6;
///
/// Panel containing all cards currently added to user's deck.
///
[SerializeField] private GridLayoutGroup _deckPanel = null;
///
/// When user is trying to swap cards, this backround image will flicker to visualize
/// awailable options.
///
[SerializeField] private Image _deckPanelBackground = null;
///
/// Panel containing all unused cards owned by the user.
///
[SerializeField] private VerticalLayoutGroup _ownedPanel = null;
///
/// Textfield displaying current funds.
///
[SerializeField] private Text _fundsText = null;
#endregion
#region Card Management
///
/// Buy a random card with gems.
///
[SerializeField] private Button _buyRandomCardButton = null;
///
/// Instantly get free gems.
///
///
/// This button is for demo purposes only.
///
[SerializeField] private Button _getFreeGemsButton = null;
///
/// Debug button used to remove all owned cards and replace this
/// deck with .
///
[SerializeField] private Button _clearCardsButton = null;
#endregion
///
/// Storage used to write and read the deck content from Nakama server.
///
[SerializeField] private DeckStorage _deckStorage = null;
///
/// Panel displaying the info of selected card.
///
[SerializeField] private CardInfoSidePanel _cardInfoSidePanel = null;
///
/// List of all card holders in deck.
///
private List _deckCardsDisplays = new List();
///
/// List of all card stacks in unused panel.
///
private List _ownedCardsDisplays = new List();
///
/// Reference to users deck.
///
private Deck _deck = null;
///
/// Card slot being currently selected.
///
private CardSlotUI _selectedSlot = null;
///
/// If value is not null, user has selected this card to be put
/// in the deck and is choosing a card from the deck to replace it with.
///
private Card _usedCard = null;
///
/// The ammount of currency user own.
///
private int _funds = 0;
#endregion
#region Mono
///
/// Sets buttons listeners and invokes on connection with Nakama.
///
private void Awake()
{
_buyRandomCardButton.onClick.AddListener(GetRandomCard);
_clearCardsButton.onClick.AddListener(ClearCards);
_getFreeGemsButton.onClick.AddListener(AddFreeGems);
base.SetBackButtonHandler(MenuManager.Instance.HideTopMenu);
if (NakamaSessionManager.Instance.IsConnected == false)
{
NakamaSessionManager.Instance.OnConnectionSuccess += Init;
}
else
{
Init();
}
}
#endregion
#region Methods
///
/// Fills the deck panel and unused cards panel with cards retrieved from the server.
///
private async void Init()
{
NakamaSessionManager.Instance.OnConnectionSuccess -= Init;
_deck = await _deckStorage.LoadDataAsync("deck");
if (_deck == null)
{
_deck = GenerateDefaultDeck(_defaultDeck);
await _deckStorage.StoreDataAsync(_deck);
}
_cardInfoSidePanel.SetDeck(_deck);
// Create a number of CardMenuUI equal to _deckSize and initialize them with user cards.
for (int i = _deckSize - 1; i >= 0; i--)
{
CardSlotUI cardDisplay = Instantiate(_slotPrefab, _deckPanel.transform, false);
cardDisplay.Init(OnCardSelected);
_deckCardsDisplays.Add(cardDisplay);
if (i < _deck.usedCards.Count)
{
cardDisplay.SetCard(_deck.usedCards[i]);
}
else
{
cardDisplay.SetCard(null);
}
}
// Update the unused cards panel UI
RefreshUnusedCards(_deck.unusedCards);
// Refresh gems counter
await UpdateFundsCounterAsync();
// Select first slot if no card is selected
if (_selectedSlot == null)
{
OnCardSelected(_deckCardsDisplays[0]);
}
}
///
/// Generates a default deck based on the list of cards stored in .
///
private Deck GenerateDefaultDeck(CardList defaultDeck)
{
Deck deck = new Deck();
deck.deckName = "Default Deck";
deck.unusedCards = new List();
deck.usedCards = new List();
foreach (CardInfo info in defaultDeck.CardInfos)
{
Card card = new Card();
card.cardType = info.CardType;
card.level = 1;
if (deck.usedCards.Count < _deckSize)
{
deck.usedCards.Add(card);
card.isUsed = true;
}
else
{
deck.unusedCards.Add(card);
card.isUsed = false;
}
}
return deck;
}
///
/// Updates the cards displayed by each in user's deck.
///
private void RefreshUsedCards(List deckCards)
{
for (int i = _deckSize - 1; i >= 0; --i)
{
CardSlotUI cardDisplay = _deckCardsDisplays[i];
if (i > _deckSize - deckCards.Count - 1)
{
cardDisplay.SetCard(deckCards[_deckSize - i - 1]);
}
else
{
cardDisplay.SetCard(null);
}
}
}
///
/// Updates the cards displayed by each .
///
private void RefreshUnusedCards(List cards)
{
List unusedCards = new List();
List unusedCardCounts = new List();
// Sort the list of cards by their type and level
Comparison comparison = new Comparison(CompareCard);
cards.Sort(comparison);
// Group all cards by their type and level
foreach (Card card in cards)
{
int index = unusedCards.FindIndex(x => x.IsCopy(card));
if (index == -1)
{
unusedCards.Add(card);
unusedCardCounts.Add(1);
}
else
{
unusedCardCounts[index] += 1;
}
}
// Determine the row count
int rowCount = 1;
if (unusedCards.Count > 0)
{
rowCount = (int)Mathf.Ceil((float)unusedCards.Count / (float)_cardsPerRow);
}
// Add a number of rows to fit all owned cards
while (rowCount * _cardsPerRow > _ownedCardsDisplays.Count)
{
GameObject cardsRow = Instantiate(_slotRowPrefab, _ownedPanel.transform, false);
for (int i = 0; i < _cardsPerRow; i++)
{
CardSlotStackUI cardDisplay = cardsRow.transform.GetChild(i).GetComponent();
cardDisplay.SetCard(null);
cardDisplay.Init(OnCardSelected);
_ownedCardsDisplays.Add(cardDisplay);
}
}
// Remove empty rows
while (rowCount * _cardsPerRow < _ownedCardsDisplays.Count)
{
for (int i = 0; i < _cardsPerRow; i++)
{
CardSlotStackUI cardDisplay = _ownedCardsDisplays[_ownedCardsDisplays.Count - 1];
_ownedCardsDisplays.RemoveAt(_ownedCardsDisplays.Count - 1);
Destroy(cardDisplay.gameObject);
}
Transform lastRow = _ownedPanel.transform.GetChild(_ownedPanel.transform.childCount - 1);
Destroy(lastRow.gameObject);
}
// If there are less cards than the maximum card count in all existing rows,
// set the cards of all unused slots to null
if (rowCount * _cardsPerRow > unusedCards.Count)
{
for (int i = rowCount * _cardsPerRow - 1; i >= unusedCards.Count; i--)
{
_ownedCardsDisplays[i].SetCard(null);
}
}
// Set the card references
for (int i = 0; i < unusedCards.Count; i++)
{
Card card = unusedCards[i];
int count = unusedCardCounts[i];
_ownedCardsDisplays[i].SetCard(card, count);
}
}
///
/// Method used to sort a list of cards by their type and level.
/// Initially, cards are sorted by enum.
/// If two cards have the same card type, they are sorted by their level.
///
private int CompareCard(Card card1, Card card2)
{
// Sorting by the type
if (card1.cardType < card2.cardType)
{
return -1;
}
else if (card1.cardType > card2.cardType)
{
return 1;
}
else
{
// Both cards have the same type
// Sorting by the level
if (card1.level < card2.level)
{
return -1;
}
else if (card1.level > card2.level)
{
return 1;
}
else
{
return 0;
}
}
}
///
/// A card has been selected by the user.
/// Shows the displaying selected card's info.
///
private async void OnCardSelected(CardSlotUI slot)
{
if (slot == null)
{
// Cannot unselect card
return;
}
else
{
ChangeSelection(slot);
if (slot.Card != null && slot.Card.isUsed == true)
{
// Selected card from deck
if (_usedCard != null)
{
// Card replacement has been already started
// Replacing _usedCard with selected card
bool good = await ReplaceCardAsync(slot.Card, _usedCard);
if (good == true)
{
EndReplaceCard();
OnCardSelected(slot);
}
}
else
{
_cardInfoSidePanel.SetUsedCard(slot.Card, OnMerge, _funds >= 50);
}
}
else
{
// Selected card from unused card list
_cardInfoSidePanel.SetUnusedCard(slot.Card, BegniReplaceCard, OnMerge);
if (_usedCard != null)
{
// Card replacement has been started but user didn't select any card
// from deck. Terminating card replacement
EndReplaceCard();
}
}
}
}
///
/// Changes color of selected slot background.
///
///
private void ChangeSelection(CardSlotUI slot)
{
if (_selectedSlot != null)
{
_selectedSlot.Unselect();
}
_selectedSlot = slot;
if (_selectedSlot != null)
{
_selectedSlot.Select();
}
}
///
/// Starts the card replacement process.
///
private void BegniReplaceCard(Card card)
{
_usedCard = card;
_deckPanelBackground.color = Color.yellow;
}
///
/// Ends the card replacement process.
///
private void EndReplaceCard()
{
_usedCard = null;
_deckPanelBackground.color = Color.white;
}
///
/// Replaces a card from the list of unused cards with a card from the deck.
/// Sends the request to the server.
///
private async Task ReplaceCardAsync(Card removedCard, Card usedCard)
{
// Perform card swap on server
CardOperationResponse canReplace = await DeckBuildingManager.SwapAsync(removedCard, usedCard);
if (canReplace.response == false)
{
Debug.LogWarning("Couldn't swap cards: " + canReplace.message);
return false;
}
// Get indices of supplied cards
int usedIndex = _deck.unusedCards.IndexOf(usedCard);
int removedIndex = _deck.usedCards.IndexOf(removedCard);
// Replace cards
_deck.usedCards.RemoveAt(removedIndex);
_deck.unusedCards.RemoveAt(usedIndex);
_deck.usedCards.Insert(removedIndex, usedCard);
_deck.unusedCards.Insert(usedIndex, removedCard);
// Store changes locally
usedCard.isUsed = true;
removedCard.isUsed = false;
// Update UI
RefreshUsedCards(_deck.usedCards);
RefreshUnusedCards(_deck.unusedCards);
// Change selected card's background color
CardSlotUI slot = _deckCardsDisplays.Find(x => x.Card == usedCard);
return true;
}
///
/// Upgrades a card to the next level and removes the second.
///
private async void OnMerge(Card upgraded, Card removed)
{
// Perform card upgrade on server
CardOperationResponse canMerge = await DeckBuildingManager.MergeAsync(upgraded, removed);
if (canMerge.response == false)
{
Debug.LogWarning("Couldn't merge cards: " + canMerge.message);
return;
}
_deck.unusedCards.Remove(removed);
// Create a new card based on the upgraded card
Card card = new Card();
card.cardType = upgraded.cardType;
card.level = upgraded.level + 1;
card.isUsed = upgraded.isUsed;
// Replace the upgraded card with newly created copy
if (upgraded.isUsed == true)
{
int index = _deck.usedCards.IndexOf(upgraded);
_deck.usedCards.RemoveAt(index);
_deck.usedCards.Insert(index, card);
}
else
{
_deck.unusedCards.Remove(upgraded);
_deck.unusedCards.Add(card);
}
// Update UI
await UpdateFundsCounterAsync();
RefreshUsedCards(_deck.usedCards);
RefreshUnusedCards(_deck.unusedCards);
CardSlotUI slot = null;
if (card.isUsed == true)
{
slot = _deckCardsDisplays.Find(x => x.Card.IsCopy(card));
}
else
{
slot = _ownedCardsDisplays.Find(x => x.Card.IsCopy(card));
}
OnCardSelected(slot);
}
///
/// Debug method used to add a random card to the owned card list.
///
private async void GetRandomCard()
{
CardOperationResponse response = await DeckBuildingManager.DebugAddRandomCardAsync();
if (response.response == false)
{
Debug.Log("Couldn't receive random card: " + response.message);
return;
}
Deck deck = await _deckStorage.LoadDataAsync("deck");
if (deck != null)
{
await UpdateFundsCounterAsync();
_deck.usedCards = deck.usedCards;
_deck.unusedCards = deck.unusedCards;
RefreshUsedCards(_deck.usedCards);
RefreshUnusedCards(_deck.unusedCards);
}
}
///
/// Generates a new deck based on .
/// Sends the deck to Nakama server and then stores it locally.
///
private async void ClearCards()
{
CardOperationResponse response = await DeckBuildingManager.DebugClearDeckAsync();
if (response.response == false)
{
Debug.Log("Couldn't clear deck: " + response.message);
return;
}
Deck deck = GenerateDefaultDeck(_defaultDeck);
deck.deckName = _deck.deckName;
bool good = await _deckStorage.StoreDataAsync(deck);
if (good == true)
{
_deck.usedCards = deck.usedCards;
_deck.unusedCards = deck.unusedCards;
RefreshUsedCards(_deck.usedCards);
RefreshUnusedCards(_deck.unusedCards);
}
}
///
/// Adds free gems to user's wallet.
///
///
/// This method is created for demo purpose only.
///
private async void AddFreeGems()
{
CardOperationResponse response = await DeckBuildingManager.DebugAddGemsAsync();
if (response.response == false)
{
Debug.Log("Couldn't add gems: " + response.message);
return;
}
await UpdateFundsCounterAsync();
}
///
/// Updates gold counter.
///
public async override void Show()
{
await UpdateFundsCounterAsync();
base.Show();
}
///
/// Retrieves gold count owned by the user and sets the value in UI.
///
private async Task UpdateFundsCounterAsync()
{
IApiAccount account = await NakamaSessionManager.Instance.GetAccountAsync();
string wallet = account.Wallet;
try
{
Dictionary currency = wallet.FromJson>();
_funds = currency["gold"];
}
catch (Exception)
{
}
_fundsText.text = _funds.ToString();
_buyRandomCardButton.interactable = _funds >= 50;
}
#endregion
}
}