using System; using System.Collections.Generic; using System.IO; using System.Reflection; using log4net; using PrimeTween; using UnityEditor; using UnityEngine; using GameLogic; using System.Threading.Tasks; using System.Text.RegularExpressions; using Unity.VisualScripting; using System.Linq; /// /// States the match can be in. /// public enum MatchState { CharacterSelect, Pause, Starting, Match, End } /// /// Update data relevant to this matches rules. /// public class MatchConditionUpdate { public WinCondition Condition { get; set; } public Ship Ship { get; set; } public int Count { get; set; } } /// /// The properties a player is assigned at the start /// of the match, according to the rules. /// public class MatchPlayerStatistic { public bool IsOut { get; set; } public int RoundsWon { get; set; } public int Lives { get; set; } public int Score { get; set; } public int Time { get; set; } } namespace Managers { /// /// Manages the initialization and progression of matches. /// public class MatchManager : MonoBehaviour { private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static readonly string arenaAssetsBundleName = "arenas"; private static readonly string ruleAssetsBundleName = "rules"; private static readonly string arenaAssetsPath = "Assets/ScriptedAssets/Arenas"; private static readonly string ruleAssetsPath = "Assets/ScriptedAssets/Rules"; private GameResult CurrentMatchResult; public GameObject MatchArena; public GameObject MatchCamera; public event Action OnStartPressed; /// /// Globally accessible member to use manager with. /// public static MatchManager G { get; private set; } /// /// The current state of the match. /// public MatchState matchState = MatchState.CharacterSelect; /// /// The statistics regarding the current match mapped to the players. /// public Dictionary MatchPlayerStatistics { get; set; } = new Dictionary(); /// /// Bundle of properties regarding the arena. /// public Arena ArenaProperties { get; private set; } /// /// The rules under which this match takes place. /// public MatchRule MatchRule { get; private set; } /// /// List of all the arenas in the assets. /// public List AvailableArenas { get; private set; } = new List(); /// /// List of all the rules/game modes in the assets. /// public List AvailableRules { get; private set; } = new List(); void Awake() { G = this; Log.Info("Awake"); } void Start() { LoadAvailableRules(); LoadAvailableArenas(); } /// /// Loads the different rules/game modes which are available, /// at the fixed asset path. /// private void LoadAvailableRules() { #if UNITY_EDITOR string[] files = Directory.GetFiles(arenaAssetsPath, "*.asset", SearchOption.TopDirectoryOnly); foreach (var file in files) { AvailableArenas.Add(AssetDatabase.LoadAssetAtPath(file)); } #else var ruleAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, ruleAssetsBundleName)); if (ruleAssetBundle == null) { Log.Error("Failed to load rules asset bundle!"); return; } AvailableRules.AddRange(ruleAssetBundle.LoadAllAssets()); #endif } /// /// Loads the characters a player can choose from a fixed path /// in the project. /// private void LoadAvailableArenas() { #if UNITY_EDITOR string[] files = Directory.GetFiles(ruleAssetsPath, "*.asset", SearchOption.TopDirectoryOnly); foreach (var file in files) { AvailableRules.Add(AssetDatabase.LoadAssetAtPath(file)); } #else var arenasAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, arenaAssetsBundleName)); if (arenasAssetBundle == null) { Log.Error("Failed to load arenas asset bundle!"); return; } AvailableArenas.AddRange(arenasAssetBundle.LoadAllAssets()); #endif } /// /// Update and check the match conditions in regards to the rules. /// /// A change in the matches progression. public void UpdateMatchCondition(MatchConditionUpdate update) { Player updatedPlayer = null; foreach (Player p in PlayerManager.G.MatchPlayers) { if (p.character.shipName == update.Ship.props.shipName) { updatedPlayer = p; Log.Debug($"Players: {p.name} match statistic will be updated."); } } if (updatedPlayer == null) { Log.Error($"Ship: {update.Ship.props.shipName} does not belong to a player in this match." + " Can't update match."); return; } if (CurrentMatchResult == null) { Log.Error("Match has no result statistics attached and was set up incorrectly!"); return; } MatchLogic.UpdateMatchResult(updatedPlayer, update, MatchPlayerStatistics, CurrentMatchResult); if (CurrentMatchResult.IsMatchWon) { Log.Info("Match has ended, winner will be declared."); AnnounceMatchWinner(CurrentMatchResult); } else { Log.Info($"Round {CurrentMatchResult.RoundsPlayed} of {MatchRule.rounds} has ended." + $"{CurrentMatchResult.Winner?.name} won this round."); AnnounceRoundWinner(CurrentMatchResult); } } /// /// Creates statistics for the players participating in the match. /// public void SetupMatchPlayerStatistics() { MatchPlayerStatistics.Clear(); foreach (Player p in PlayerManager.G.MatchPlayers) { MatchPlayerStatistic mps = new MatchPlayerStatistic { IsOut = false, Lives = MatchRule.lives, Score = MatchRule.score, Time = MatchRule.time }; if (!MatchPlayerStatistics.TryAdd(p, mps)) { Log.Info($"Player {p.name} already has statistics set up."); } } } /// /// Just assigns new arena properties for now. /// TODO: Should initiate all the necessary steps upon switching the arena. /// /// Index of the arena (arenas scripted asset folder order from 0) public void LoadArenaProperties(int arenaIndex) { if (AvailableArenas.Count - 1 < arenaIndex) { Log.Error($"There are only: {AvailableArenas.Count} arenas loaded." + $" Couldn't load arena number: {arenaIndex + 1}"); return; } ArenaProperties = AvailableArenas[arenaIndex]; } /// /// Assigns rules to the match. /// /// Index of the rule to be loaded /// (rules scripted asset folder order from 0) public void LoadMatchRules(int ruleIndex) { if (AvailableArenas.Count - 1 < ruleIndex) { Log.Error($"There are only: {AvailableArenas.Count} rules loaded." + $" Couldn't load match rule number: {ruleIndex + 1}"); return; } MatchRule = AvailableRules[ruleIndex]; MatchLogic.currentRule = MatchRule; } /// /// Just sets the match state for now. /// TODO: maybe use in character selection /// public void StartCharacterSelect() { matchState = MatchState.CharacterSelect; } /// /// Announcement of who won the match. /// /// Result data of the completed match. async public void AnnounceMatchWinner(GameResult mr) { UIManager.G.Announcments.QueueAnnounceText($"{mr.Winner.playerName}" + " has won the match!", 2f); await Tween.Delay(2f * 0.33f); matchState = MatchState.End; await Tween.Delay(2f * 0.66f); UIManager.G.ShowMatchEndMenu(MatchArena.transform); } async public void StartRematch() { ResetMatch(); UIManager.G.Announcments.QueueAnnounceText("Starting rematch.", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Starting rematch..", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Starting rematch...", 0.3f); UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f); await Tween.Delay(0.9f); matchState = MatchState.Match; } /// /// Announcement of who won the Round. /// /// Result data of the completed match. async public void AnnounceRoundWinner(GameResult mr) { var winnerStats = MatchPlayerStatistics[mr.Winner]; UIManager.G.Announcments.QueueAnnounceText($"{mr.Winner.playerName}" + " has won the Round! \n" + $"They won {winnerStats.RoundsWon} out of {MatchRule.rounds}.", 1.618f); await Tween.Delay(1.618f * 0.33f); matchState = MatchState.Pause; await Tween.Delay(1.618f * 0.66f); ResetMatchCharacters(); UIManager.G.Announcments.QueueAnnounceText("Starting next round.", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Starting next round..", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Starting next round...", 0.3f); UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f); await Tween.Delay(0.9f); matchState = MatchState.Match; } /// /// Resets player positions and statistics. /// public void ResetMatch() { CurrentMatchResult = new GameResult(); ResetMatchCharacters(); SetupMatchPlayerStatistics(); } /// /// Initializes the match, waits for match begin /// confirmation by the players and counts down to start. /// async public void StartMatch() { foreach (Player p in PlayerManager.G.MatchPlayers) { p.spawnedCharacter.transform.localPosition = ArenaProperties.spawnPositions[p.playerNumber - 1]; } SetupMatchPlayerStatistics(); CurrentMatchResult = new GameResult(); matchState = MatchState.Starting; UIManager.G.ShowMatchStartPrompt(); // Wait until someone pressed start var startPressed = new TaskCompletionSource(); OnStartPressed += () => ConfirmMatchStart(startPressed); // TODO: This could break in WebGL due to threading await startPressed.Task; OnStartPressed = null; UIManager.G.HideAnnouncement(); UIManager.G.Announcments.QueueAnnounceText("3", 1); UIManager.G.Announcments.QueueAnnounceText("2", 1); UIManager.G.Announcments.QueueAnnounceText("1", 1); UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f); await Tween.Delay(3); matchState = MatchState.Match; } public void StartTestMatch() { foreach (Player p in PlayerManager.G.MatchPlayers) { p.spawnedCharacter.transform.localPosition = ArenaProperties.spawnPositions[p.playerNumber - 1]; } SetupMatchPlayerStatistics(); CurrentMatchResult = new GameResult(); matchState = MatchState.Match; } public void ConfirmMatchStart(TaskCompletionSource startPressed) { startPressed.TrySetResult(true); } /// /// Is called by the GameplayMetaInputEvents object, when someone pressed start. /// public void StartPressed() { if (matchState != MatchState.Match) { OnStartPressed?.Invoke(); return; } } public void PausePressed() { // TODO: MetaInputEvent in InGameUI scene -> PausePressed here -> ShowPauseMenu in UIManager -> PauseMenu Show // When clicking continue in PauseMenu -> PausePressed here -> UIManager HidePause -> PauseMenu Hide if (matchState == MatchState.Pause) { ContinueMatch(); UIManager.G.HidePauseMenu(); MatchCamera.SetActive(true); return; } if (matchState != MatchState.Match) { Log.Info("Can only pause when match is in progress."); return; } matchState = MatchState.Pause; UIManager.G.ShowPauseMenu(MatchArena.transform); MatchCamera.SetActive(false); } async private void ContinueMatch() { UIManager.G.Announcments.QueueAnnounceText("Get Ready.", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Get Ready..", 0.3f); UIManager.G.Announcments.QueueAnnounceText("Get Ready...", 0.3f); UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f); await Tween.Delay(0.9f); matchState = MatchState.Match; } public void AssignMatchObjects() { MatchArena = GameObject.Find("Arena Placement"); MatchCamera = GameObject.Find("Match Camera"); } /// /// Resets the position, rotation and state of the spawned characters. /// public void ResetMatchCharacters() { foreach (Player mp in PlayerManager.G.MatchPlayers) { GameObject shipGO = mp.spawnedCharacter; shipGO.TryGetComponent(out Ship ship); shipGO.TryGetComponent(out Rigidbody body); ship.state.boostCapacity = ship.props.maxBoostCapacity; body.velocity = Vector3.zero; shipGO.transform.localPosition = ArenaProperties.spawnPositions[mp.playerNumber - 1]; shipGO.transform.rotation = shipGO.transform.parent.rotation; } } } }