Space-Smash-Out/Assets/Scripts/Managers/MatchManager.cs
2024-04-03 16:48:59 +02:00

363 lines
10 KiB
C#

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;
namespace Managers
{
/// <summary>
/// Manages the initialization and progression of matches.
/// </summary>
public class MatchManager : MonoBehaviour
{
private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static string arenaAssetsPath = "Assets/ScriptedAssets/Arenas";
private static string ruleAssetsPath = "Assets/ScriptedAssets/Rules";
public event Action OnStartPressed;
/// <summary>
/// Globally accessible member to use manager with.
/// </summary>
public static MatchManager G { get; private set; }
/// <summary>
/// The current state of the match.
/// </summary>
public MatchState matchState = MatchState.CharacterSelect;
/// <summary>
/// The players participating in the match.
/// </summary>
public List<Player> matchPlayers { get; private set; } =
new List<Player>();
/// <summary>
/// The statistics regarding the current match mapped to the players.
/// </summary>
public Dictionary<Player, MatchPlayerStatistic> matchPlayerStatistics { get; set; } =
new Dictionary<Player, MatchPlayerStatistic>();
/// <summary>
/// Bundle of properties regarding the arena.
/// </summary>
public Arena arenaProperties { get; private set; }
/// <summary>
/// The rules under which this match takes place.
/// </summary>
public MatchRule matchRule { get; private set; }
/// <summary>
/// List of all the arenas in the assets.
/// </summary>
public List<Arena> availableArenas { get; private set; } = new List<Arena>();
/// <summary>
/// List of all the rules/game modes in the assets.
/// </summary>
public List<MatchRule> availableRules { get; private set; } = new List<MatchRule>();
void Awake()
{
G = this;
Log.Info("Awake");
}
void Start()
{
LoadAvailableRules();
LoadAvailableArenas();
}
/// <summary>
/// Update and check the match conditions in regards to the rules.
/// </summary>
/// <param name="update">A change in the matches progression.</param>
public void UpdateMatchCondition(MatchConditionUpdate update)
{
Player updatedPlayer = null;
foreach (Player p in matchPlayers)
{
if (p.character.shipName == update.ship.props.shipName)
{
updatedPlayer = p;
Log.Debug($"Players: {p.name} match statistic will be updated.");
}
}
// TODO: Match Result should contain progression over multiple rounds.
MatchResult mr = MatchLogic.UpdateMatchResult(updatedPlayer, update, matchPlayerStatistics);
if (updatedPlayer != null && mr != null)
{
matchState = MatchState.End;
Log.Info("Match has ended, winner will be declared.");
// TODO: Take player decisions into account before restarting the match
// TODO: Include the statistics and match round progression in announcements
AnnounceWinner(mr);
return;
}
Log.Error($"Ship: {update.ship.props.shipName} does not belong to a player in this match."
+ " Can't update match.");
}
/// <summary>
/// Loads the characters a player can choose from a fixed path
/// in the project.
/// </summary>
private void LoadAvailableArenas()
{
string[] files = Directory.GetFiles(arenaAssetsPath, "*.asset",
SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
availableArenas.Add(AssetDatabase.LoadAssetAtPath<Arena>(file));
}
}
/// <summary>
/// Loads the different rules/game modes which are available,
/// at the fixed asset path.
/// </summary>
private void LoadAvailableRules()
{
string[] files = Directory.GetFiles(ruleAssetsPath, "*.asset",
SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
availableRules.Add(AssetDatabase.LoadAssetAtPath<MatchRule>(file));
}
}
/// <summary>
/// Creates statistics for the players participating in the match.
/// </summary>
public void SetupMatchPlayerStatistics()
{
matchPlayerStatistics.Clear();
foreach (Player p in matchPlayers)
{
MatchPlayerStatistic mps = new MatchPlayerStatistic
{
IsOut = false,
Lives = matchRule.lives,
Score = matchRule.score,
Time = matchRule.time
};
matchPlayerStatistics.Add(p, mps);
}
}
/// <summary>
/// Just assigns new arena properties for now.
/// TODO: Should initiate all the necessary steps upon switching the arena.
/// </summary>
/// <param name="arenaIndex">Index of the arena (arenas scripted asset folder order from 0)</param>
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];
}
/// <summary>
/// Assigns rules to the match.
/// </summary>
/// <param name="ruleIndex">Index of the rule to be loaded
/// (rules scripted asset folder order from 0)</param>
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;
}
/// <summary>
/// Just sets the match state for now.
/// TODO: maybe use in character selection
/// </summary>
public void StartCharacterSelect()
{
matchState = MatchState.CharacterSelect;
}
/// <summary>
/// Announcement of who won.
/// TODO: Also restarts the match right now, no matter what.
/// </summary>
/// <param name="mr">Result data of the completed match.</param>
async public void AnnounceWinner(MatchResult mr)
{
UIManager.G.announcments.QueueAnnounceText($"{mr.Winner.playerName}" +
" has won the match!", 1.618f);
await Tween.Delay(1.618f);
UIManager.G.announcments.QueueAnnounceText("Restarting.", 0.3f);
UIManager.G.announcments.QueueAnnounceText("Restarting..", 0.3f);
UIManager.G.announcments.QueueAnnounceText("Restarting...", 0.3f);
UIManager.G.announcments.QueueAnnounceText("GO!", 0.5f);
await Tween.Delay(0.9f);
RestartMatch();
}
/// <summary>
/// Starts the match anew.
/// Resetting player positions and statistics.
/// </summary>
public void RestartMatch()
{
ResetMatchCharacters();
SetupMatchPlayerStatistics();
matchState = MatchState.Match;
}
/// <summary>
/// Initializes the match, waits for match begin
/// confirmation by the players and counts down to start.
/// </summary>
async public void StartMatch()
{
foreach (Player p in matchPlayers)
{
p.spawnedCharacter.transform.localPosition =
arenaProperties.spawnPositions[p.playerNumber - 1];
}
SetupMatchPlayerStatistics();
matchState = MatchState.Starting;
UIManager.G.ShowMatchStartPrompt();
// Wait until someone pressed start
var startPressed = new TaskCompletionSource<bool>();
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 ConfirmMatchStart(TaskCompletionSource<bool> startPressed)
{
startPressed.TrySetResult(true);
}
/// <summary>
/// Is called by the GameplayMetaInputEvents object, when someone pressed start.
/// </summary>
public void StartPressed()
{
if (OnStartPressed != null)
{
OnStartPressed.Invoke();
}
}
/// <summary>
/// Spawns the characters which were chosen by the players
/// and sets them up to be playable.
/// </summary>
/// <param name="players">List of players for this match</param>
public void SpawnCharacters(List<Player> players)
{
// TODO: This belongs in another intialization method
GameObject arena = GameObject.Find("Arena Placement");
GameObject matchCamera = GameObject.Find("Match Camera");
if (arena == null)
{
Log.Error("ArenaPlacement not found in scenes. Cannot spawn players.");
return;
}
if (matchCamera == null)
{
Log.Error("Match camera not found in scenes. Cannot initialize spawned Characters.");
return;
}
foreach (Player p in players)
{
GameObject shipObject = Instantiate(p.character.shipObject);
p.spawnedCharacter = shipObject;
shipObject.TryGetComponent(out Ship ship);
ship.state = new ShipHandling.ShipState();
ship.cameraOperator = matchCamera.GetComponent<CameraOperator>();
shipObject.transform.SetParent(arena.transform, false);
shipObject.transform.localPosition =
arenaProperties.spawnPositions[p.playerNumber - 1];
shipObject.transform.localScale = new Vector3();
Tween.Scale(shipObject.transform, new Vector3(0.7f, 0.7f, 0.7f), 1f);
matchPlayers.Add(p);
}
}
/// <summary>
/// Resets the position, rotation and state of the spawned characters.
/// </summary>
public void ResetMatchCharacters()
{
foreach (Player mp in matchPlayers)
{
GameObject shipGO = mp.spawnedCharacter;
shipGO.TryGetComponent(out Ship ship);
ship.state.boostCapacity = ship.props.maxBoostCapacity;
shipGO.transform.localPosition =
arenaProperties.spawnPositions[mp.playerNumber - 1];
shipGO.transform.rotation = shipGO.transform.parent.rotation;
}
}
}
}
/// <summary>
/// States the match can be in.
/// </summary>
public enum MatchState
{
CharacterSelect,
Pause,
Starting,
Match,
End
}
/// <summary>
/// Update data relevant to this matches rules.
/// </summary>
public class MatchConditionUpdate
{
public WinCondition Condition { get; set; }
public Ship ship { get; set; }
public int count { get; set; }
}
/// <summary>
/// The properties a player is assigned at the start
/// of the match, according to the rules.
/// </summary>
public class MatchPlayerStatistic
{
public bool IsOut { get; set; }
public int Lives { get; set; }
public int Score { get; set; }
public int Time { get; set; }
}