/**
* Copyright 2019 The Knights Of Unity, created by Piotr Stoch, Pawel Stolarczyk
*
* 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DemoGame.Scripts.Gameplay.Cards;
using DemoGame.Scripts.Gameplay.Hands;
using DemoGame.Scripts.Gameplay.NetworkCommunication;
using DemoGame.Scripts.Gameplay.NetworkCommunication.MatchStates;
using DemoGame.Scripts.Gameplay.Nodes;
using DemoGame.Scripts.Gameplay.UI;
using DemoGame.Scripts.Gameplay.Units;
using DemoGame.Scripts.Session;
using DemoGame.Scripts.Utils;
using Nakama;
using UnityEngine;
namespace DemoGame.Scripts.Gameplay
{
///
/// Core gameplay manager. Handles card playing, game ending and initialization.
///
public class GameManager : Singleton
{
#region Fields
///
/// Reference to camera parent object.
/// This transform will be rotated by 180 degrees if local user is not host.
///
[SerializeField] private Transform _cameraHolder = null;
///
/// Reference to summary menu shown at the end of the match.
///
[SerializeField] private SummaryMenu _summary = null;
///
/// In-game time of beginning of the match.
///
private float _timerStart;
#region Hands
[Header("Hands")]
///
/// Reference to the hand side bar with cards.
///
[SerializeField] private HandPanel _localHandPanel = null;
///
/// Local user hand manager.
/// Used only by host.
///
[SerializeField] private Hand _localHand = null;
///
/// Opponent hand manager.
/// Used only by host.
///
[SerializeField] private Hand _opponentHand = null;
#endregion
#region Gold
[Header("Gold")]
///
/// Reference to the bottom panel displaying current gold.
///
[SerializeField] private GoldPanel _localGoldPanel = null;
///
/// Local user gold manager.
/// Used only by host.
///
[SerializeField] private Gold _localGold = null;
///
/// Opponent golr manager
/// Used only by host.
///
[SerializeField] private Gold _opponentGold = null;
#endregion
#region Structures
///
/// List of towers owned by the local user.
///
private List _allyTowers;
///
/// List of towers owned by the opponent.
///
private List _enemyTowers;
///
/// Local user castle reference.
///
private Unit _allyCastle;
///
/// Opponen castle reference.
///
private Unit _enemyCastle;
#endregion
#endregion
#region Properties
///
/// 2d array of nodes units can move to during a match.
/// Initialized by .
///
public Node[,] Nodes { get; private set; }
///
/// Map size in nodes.
///
public Vector2Int MapSize { get; private set; }
#endregion
#region Monobehaviour
///
/// Subscribes to message events and initializes the game.
///
private void Start()
{
MatchCommunicationManager.Instance.OnCardRequested += OnCardRequested;
MatchCommunicationManager.Instance.OnCardPlayed += OnCardPlayed;
MatchCommunicationManager.Instance.OnStartingHandReceived += OnStartingHandReceived;
MatchCommunicationManager.Instance.OnGameEnded += OnGameEnded;
_localHandPanel.OnCardPlayed += OnCardRequested;
if (MatchCommunicationManager.Instance.GameStarted == true)
{
OnMatchJoined();
}
else
{
MatchCommunicationManager.Instance.OnGameStarted += OnMatchJoined;
}
}
///
/// Unsubscribes from all message events.
///
protected override void OnDestroy()
{
MatchCommunicationManager.Instance.OnCardRequested -= OnCardRequested;
MatchCommunicationManager.Instance.OnCardPlayed -= OnCardPlayed;
MatchCommunicationManager.Instance.OnStartingHandReceived -= OnStartingHandReceived;
MatchCommunicationManager.Instance.OnGameStarted -= OnMatchJoined;
MatchCommunicationManager.Instance.OnGameEnded -= OnGameEnded;
_localHandPanel.OnCardPlayed -= OnCardRequested;
}
///
/// Handles exitting the game.
///
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape) == true)
{
MatchCommunicationManager.Instance.LeaveGame();
}
}
#endregion
#region Methods
///
/// Initializes nodes in the map.
/// Invoked by .
///
public void InitMap(Node[,] nodes, Vector2Int mapSize)
{
Nodes = nodes;
MapSize = mapSize;
}
///
/// Invoked on successful match join.
/// If local user is host, initializes structures and sends starting hands to all players.
/// Otherwise, rotates camera by 180 degrees.
///
private async void OnMatchJoined()
{
MatchCommunicationManager.Instance.OnGameStarted -= OnMatchJoined;
if (MatchCommunicationManager.Instance.IsHost == true)
{
foreach (IUserPresence presence in MatchCommunicationManager.Instance.Players)
{
UnitsManager.Instance.BuildStartingStructures(presence.UserId);
await SendStartingHandAsync(presence);
}
_timerStart = Time.unscaledTime;
}
else
{
_cameraHolder.Rotate(Vector3.up, 180);
}
}
///
/// Invoked upon receiving end game message.
/// Shows summary.
///
private async void OnGameEnded(MatchMessageGameEnded message)
{
bool localWin = message.winnerId == NakamaSessionManager.Instance.Session.UserId;
int reward = await MatchCommunicationManager.Instance.GetMatchRewardAsync(message.matchId);
_summary.SetResult(localWin, reward);
_summary.Show();
}
#region Starting Hand
///
/// Selects a number of cards equal to from players deck
/// and sends them to that player.
///
private async Task SendStartingHandAsync(IUserPresence presence)
{
if (presence.UserId == MatchCommunicationManager.Instance.HostId)
{
List cards = await _localHand.InitAsync(presence.UserId);
MatchMessageStartingHand message = new MatchMessageStartingHand(presence.UserId, cards);
MatchCommunicationManager.Instance.SendMatchStateMessageSelf(MatchMessageType.StartingHand, message);
}
else
{
List cards = await _opponentHand.InitAsync(presence.UserId);
MatchMessageStartingHand message = new MatchMessageStartingHand(presence.UserId, cards);
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.StartingHand, message);
_opponentGold.Restart();
}
}
///
/// Adds cards to local user's hand.
///
private void OnStartingHandReceived(MatchMessageStartingHand message)
{
if (message.PlayerId == NakamaSessionManager.Instance.Session.UserId)
{
Debug.Log("Starting hands received");
for (int i = 0; i < message.Cards.Count; i++)
{
_localHandPanel.DrawCard(message.Cards[i], i);
}
_localGold.Restart();
_localGoldPanel.Init(_localGold);
}
}
#endregion
#region Cards
///
/// User requested card play.
/// Host will handle the request or cancel it if it was illegal.
///
private void OnCardRequested(MatchMessageCardPlayRequest message)
{
if (message.PlayerId == NakamaSessionManager.Instance.Session.UserId)
{
if (MatchCommunicationManager.Instance.IsHost == true)
{
HandleCardRequest(message, _localHand, _localGold);
}
else
{
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.CardPlayRequest, message);
}
}
else
{
if (MatchCommunicationManager.Instance.IsHost == true)
{
HandleCardRequest(message, _opponentHand, _opponentGold);
}
else
{
// Only host can handle incomming card play requests; do nothing
}
}
}
///
/// Handles card play request.
/// If user playing the card has insufficiend gold or used card can not be played on specified node,
/// a cancel message is sent to that card owner and its effects don't resolve.
///
private void HandleCardRequest(MatchMessageCardPlayRequest message, Hand hand, Gold gold)
{
if (gold.CurrentGold < message.Card.GetCardInfo().Cost)
{
SendCardCanceledMessage(message);
}
else
{
bool isHost = MatchCommunicationManager.Instance.HostId == message.PlayerId;
Vector3 position = new Vector3(message.X, message.Y, message.Z);
Vector2Int nodePosition = ScreenToNodePos(position, isHost, message.Card.GetCardInfo().DropRegion);
Node node = Nodes[nodePosition.x, nodePosition.y];
if (node != null && (node.Unit == null || message.Card.GetCardInfo().CanBeDroppedOverOtherUnits == true))
{
SendCardPlayedMessage(message, hand, nodePosition);
}
else
{
SendCardCanceledMessage(message);
}
}
}
///
/// Requests card play.
///
private void SendCardPlayedMessage(MatchMessageCardPlayRequest message, Hand userHand, Vector2Int nodePosition)
{
Card newCard = userHand.DrawCard();
MatchMessageCardPlayed matchMessageCardPlayed = new MatchMessageCardPlayed(
message.PlayerId,
message.Card,
message.CardSlotIndex,
newCard,
nodePosition.x,
nodePosition.y);
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.CardPlayed, matchMessageCardPlayed);
MatchCommunicationManager.Instance.SendMatchStateMessageSelf(MatchMessageType.CardPlayed, matchMessageCardPlayed);
userHand.CardPlayed(message.CardSlotIndex);
}
///
/// Cancels played card and returns it to its owner's hand.
///
private void SendCardCanceledMessage(MatchMessageCardPlayRequest message)
{
MatchMessageCardCanceled cardCanceled = new MatchMessageCardCanceled(message.PlayerId, message.CardSlotIndex);
if (message.PlayerId == NakamaSessionManager.Instance.Session.UserId)
{
MatchCommunicationManager.Instance.SendMatchStateMessageSelf(MatchMessageType.CardCanceled, cardCanceled);
}
else
{
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.CardCanceled, cardCanceled);
}
}
///
/// Invoked whenever a card is played and it wan't canceled.
/// Removes that card from its owner's hand and reduces their gold count by .
/// Card owner draws a new card from their deck.
///
///
private void OnCardPlayed(MatchMessageCardPlayed message)
{
Debug.Log("playing " + message.Card.cardType);
if (message.PlayerId == NakamaSessionManager.Instance.Session.UserId)
{
_localHandPanel.ResolveCardPlay(message);
_localHandPanel.DrawCard(message.NewCard, message.CardSlotIndex);
_localGold.ChangeGoldCount(-message.Card.GetCardInfo().Cost);
}
else if (MatchCommunicationManager.Instance.IsHost == true)
{
_opponentGold.ChangeGoldCount(-message.Card.GetCardInfo().Cost);
}
}
#endregion
#region Node Positions
///
/// Given the , returns a node closest to given world position.
///
public Vector2Int ScreenToNodePos(Vector3 position, bool isHost, DropRegion dropRegion)
{
switch (dropRegion)
{
case DropRegion.WholeMap:
return GetClosestNode_WholeMap(position);
case DropRegion.EnemyHalf:
return GetClosestNode_HalfMap(position, isHost ? false : true);
case DropRegion.EnemySpawn:
return GetClosestNode_Spawn(position, isHost ? false : true);
case DropRegion.AllyHalf:
return GetClosestNode_HalfMap(position, isHost ? true : false);
case DropRegion.AllySpawn:
return GetClosestNode_Spawn(position, isHost ? true : false);
default:
break;
}
Debug.LogError("Drop region " + dropRegion + " was not handled");
return new Vector2Int();
}
///
/// Returns a node from the first (or last) column of the map (spawn region).
///
private Vector2Int GetClosestNode_Spawn(Vector3 position, bool leftSide)
{
float minDistance = float.MaxValue;
Node closestNode = null;
int x = leftSide == true ? 0 : Nodes.GetLength(0) - 1;
for (int y = 0; y < Nodes.GetLength(1); y += 2)
{
Node node = Nodes[x, y];
if (node == null) continue;
float distance = Vector3.Distance(position, node.transform.position);
if (distance < minDistance)
{
minDistance = distance;
closestNode = node;
}
}
return closestNode.Position;
}
///
/// Returns a node cosest to the from a half of the map,
/// depending on the value of .
///
private Vector2Int GetClosestNode_HalfMap(Vector3 position, bool leftSide)
{
float minDistance = float.MaxValue;
Node closestNode = null;
for (int y = 0; y < Nodes.GetLength(1); y++)
{
int width = y % 2 == 0 ? Nodes.GetLength(0) : Nodes.GetLength(0) - 1;
int half = width / 2;
int beg = leftSide == true ? 0 : width - half;
int end = leftSide == true ? half : width;
for (int x = beg; x < end; x++)
{
Node node = Nodes[x, y];
if (node == null) continue;
float distance = Vector3.Distance(position, node.transform.position);
if (distance < minDistance)
{
minDistance = distance;
closestNode = node;
}
}
}
return closestNode.Position;
}
///
/// Returns a node closest to .
///
private Vector2Int GetClosestNode_WholeMap(Vector3 position)
{
float minDistance = float.MaxValue;
Node closestNode = null;
for (int x = 0; x < Nodes.GetLength(0); ++x)
{
for (int y = 0; y < Nodes.GetLength(1); ++y)
{
Node node = Nodes[x, y];
if (node == null) continue;
float distance = Vector3.Distance(position, node.transform.position);
if (distance < minDistance)
{
minDistance = distance;
closestNode = node;
}
}
}
return closestNode.Position;
}
#endregion
#region Structures
///
/// Sets towers references.
///
public void SetTowers(PlayerColor player, Unit castle, Unit tower1, Unit tower2)
{
if (player == PlayerColor.Black)
{
_allyTowers = new List { tower1, tower2 };
_allyCastle = castle;
_allyCastle.OnDestroy += OnCastleDestroyed;
}
else
{
_enemyTowers = new List { tower1, tower2 };
_enemyCastle = castle;
_enemyCastle.OnDestroy += OnCastleDestroyed;
}
}
///
/// Invoked upon destruction of player's main castle.
/// Sends game ending message.
///
private void OnCastleDestroyed(Unit destroyedCastle)
{
if (destroyedCastle == _allyCastle)
{
Debug.Log("Enemy win");
string winner = MatchCommunicationManager.Instance.OpponentId;
string loser = NakamaSessionManager.Instance.Session.UserId;
string matchId = MatchCommunicationManager.Instance.MatchId;
float matchDuration = Time.unscaledTime - _timerStart;
int winnerTowersDestroyed = 1 + _allyTowers.Count(x => x.IsDestroyed == true);
int loserTowersDestroyed = 0 + _enemyTowers.Count(x => x.IsDestroyed == true);
MatchMessageGameEnded message = new MatchMessageGameEnded(winner, loser, matchId, winnerTowersDestroyed, loserTowersDestroyed, matchDuration);
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.MatchEnded, message);
MatchCommunicationManager.Instance.SendMatchStateMessageSelf(MatchMessageType.MatchEnded, message);
}
else
{
Debug.Log("Ally win");
string winner = NakamaSessionManager.Instance.Session.UserId;
string loser = MatchCommunicationManager.Instance.OpponentId;
string matchId = MatchCommunicationManager.Instance.MatchId;
float matchDuration = Time.unscaledTime - _timerStart;
int winnerTowersDestroyed = 1 + _enemyTowers.Count(x => x.IsDestroyed == true);
int loserTowersDestroyed = 0 + _allyTowers.Count(x => x.IsDestroyed == true);
MatchMessageGameEnded message = new MatchMessageGameEnded(winner, loser, matchId, winnerTowersDestroyed, loserTowersDestroyed, matchDuration);
MatchCommunicationManager.Instance.SendMatchStateMessage(MatchMessageType.MatchEnded, message);
MatchCommunicationManager.Instance.SendMatchStateMessageSelf(MatchMessageType.MatchEnded, message);
}
}
#endregion
#endregion
}
}