using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Networking; using UnityEngine.Serialization; using TMPro; using Mirror; public class GameManager : NetworkBehaviour { public static Team MyTeam { get; set; } public static MatchmadePlayer RedPlayer { get; set; } public static MatchmadePlayer BluePlayer { get; set; } /// Set by dedicated server startup (e.g. CupidConnector parsing -matchId). PATCH is skipped if id ≤ 0 or secret is empty. public static int DedicatedMatchId { get; private set; } /// Sent as X-Dedicated-Server-Secret. From -dedicatedSecret arg or DEDICATED_SERVER_SECRET env. public static string DedicatedMatchSecret { get; private set; } = ""; /// Base URL for internal match API (no trailing slash), default http://127.0.0.1:2612. public static string DedicatedInternalApiBase { get; private set; } = LoginManager.AuthBaseUrl; public static void ConfigureDedicatedMatchReporting(int matchId, string secret=null, string internalApiBaseUrl = null) { DedicatedMatchId = matchId; DedicatedMatchSecret = secret ?? "38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328"; if (!string.IsNullOrWhiteSpace(internalApiBaseUrl)) DedicatedInternalApiBase = internalApiBaseUrl.TrimEnd('/'); Logger.Log($"Configured dedicated match reporting for match id {matchId} with secret {DedicatedMatchSecret} and internal api base {DedicatedInternalApiBase}"); } /// Sets match status to -1 (room closed). Safe to call from shutdown paths; runs synchronously so it completes before Application.Quit. public static void ReportDedicatedMatchRoomClosed() { if (_dedicatedRoomClosedReported || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret)) return; _dedicatedRoomClosedReported = true; if (DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret)) return; using (var req = DedicatedMatschInternalApi.BuildRequest(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, "{\"status\":-1}")) { var op = req.SendWebRequest(); while (!op.isDone) System.Threading.Thread.Sleep(16); DedicatedMatschInternalApi.LogIfFailed(req); } } static bool _dedicatedRoomClosedReported; static string Iso8601UtcNow() => DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", System.Globalization.CultureInfo.InvariantCulture); bool _dedicatedReportedRedJoin; bool _dedicatedReportedBlueJoin; bool _dedicatedReportedGameOver; public static Action OnTeamChanged; public static bool isMoving{ get{ return instance.IsMoving(); } } [SyncVar] public bool m_isMoving =false; [SyncVar(hook = nameof(OnGameStartedChanged))] public bool gameStarted = false; void OnGameStartedChanged(bool oldStarted, bool newStarted){ if(newStarted){ GameCanvas.instance.HideWaitingForOpponentPanel(); if (AudioManager.instance != null) AudioManager.instance.SetCrowdNoiseType(CrowdNoiseType.Normal); }else{ GameCanvas.instance.ShowWaitingForOpponentPanel(); } } [SyncVar(hook = nameof(OnSelectedTeamChanged))] public Team SelectedTeam = Team.Red; void OnSelectedTeamChanged(Team oldTeam, Team newTeam){ Logger.Log($"Selected team changed from {oldTeam} to {newTeam}"); OnTeamChanged?.Invoke(newTeam); if (!isClient || !gameStarted || oldTeam == newTeam) return; if (AudioManager.instance != null) AudioManager.instance.PlayRefereeWhistle(); } public static bool NoPuckSelected { get{ return instance.selectedPuck == null; } } public int turnTimer = 10; [SyncVar] public float turnTimerCounter; public Rigidbody2D ball; public float puckForce = 20f; public AnimationCurve puckForceCurve = AnimationCurve.Linear(0,3,1,1); public float ballMoveTime = 3f; public float puckDragClampMax = 5f; public float puckDragMinClamp = 1f; [SyncVar(hook = nameof(OnScoreChanged))] public int redScore=0; [SyncVar(hook = nameof(OnScoreChanged))] public int blueScore=0; [Header("Effects")] public ParticleSystem[] redGoalEffects; public ParticleSystem[] blueGoalEffects; [Header("UI")] public TMP_Text redScoreText; public TMP_Text blueScoreText; public CanvasGroupUtils blueTimerGroup; [FormerlySerializedAs("timerGroup")] public CanvasGroupUtils redTimerGroup; [Header("Timer Flash Settings")] [Tooltip("Minimum flash speed when timer is at 5 seconds")] public float minFlashSpeed = 10f; [Tooltip("Maximum flash speed when timer is at 0 seconds")] public float maxFlashSpeed = 20f; [Header("Misc")] public List pucks = new List(); public float curPuckPullForce; [SyncVar] public int hitCounter = 0; public static GameManager instance; public float puckMass{ get{ if(pucks.Count > 0){return pucks[0].rb.mass;}else{return 1f;} }set{ foreach(Puck puck in pucks){ puck.rb.mass = value; } } } public float puckDrag{ get{ if(pucks.Count > 0){return pucks[0].rb.linearDamping;}else{return 0f;} }set{ foreach(Puck puck in pucks){ puck.rb.linearDamping = value; } } } void Awake() { instance = this; } /// Server-only: called from after myTeam is set. public void OnMatchPlayerTeamAssigned(Team team) { if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret)) return; int n = FindObjectsByType(FindObjectsSortMode.None).Length; string ts = Iso8601UtcNow(); if (team == Team.Red && !_dedicatedReportedRedJoin) { _dedicatedReportedRedJoin = true; int status = n >= 2 ? 2 : 1; string json = "{\"red_joined_at\":\"" + ts + "\",\"status\":" + status + "}"; StartCoroutine(DedicatedMatchPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json)); // StartCoroutine(DedicatedMatchInternalApi.Patch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json)); } else if (team == Team.Blue && !_dedicatedReportedBlueJoin) { _dedicatedReportedBlueJoin = true; int status = n >= 2 ? 2 : 1; string json = "{\"blue_joined_at\":\"" + ts + "\",\"status\":" + status + "}"; StartCoroutine(DedicatedMatchPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json)); } } IEnumerator DedicatedMatchPatch(string baseUrl, int matchId, string secret, string json){ if (matchId <= 0 || string.IsNullOrEmpty(secret)) yield break; using (var req = DedicatedMatschInternalApi.BuildRequest(baseUrl, matchId, secret, json)) { yield return req.SendWebRequest(); DedicatedMatschInternalApi.LogIfFailed(req); } } public IEnumerator DedicatedMatchPatchWinner(string baseUrl, int matchId, string secret, string winnerLower) { if (matchId <= 0 || string.IsNullOrEmpty(secret)) yield break; string jsonBody = "{\"winner\":\"" + winnerLower + "\"}"; using (var req = DedicatedMatschInternalApi.BuildWinnerRequest(baseUrl, matchId, secret, jsonBody)) { yield return req.SendWebRequest(); DedicatedMatschInternalApi.LogIfFailed(req); } } void ReportDedicatedMatchGameOver(Team winningTeam) { if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret) || _dedicatedReportedGameOver) return; _dedicatedReportedGameOver = true; StartCoroutine(CoReportDedicatedMatchGameOver(winningTeam)); } IEnumerator CoReportDedicatedMatchGameOver(Team winningTeam) { yield return DedicatedMatchPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, "{\"status\":3}"); string winner = winningTeam == Team.Red ? "red" : "blue"; yield return DedicatedMatchPatchWinner(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, winner); } void Start() { GameEvents.OnSelectedPuckChanged?.Invoke(null); #if UNITY_EDITOR #else Application.targetFrameRate = isServer ? 30 : 100; #endif } public void RegisterPuck(Puck puck){ pucks.Add(puck); } public void DisposePuck(Puck puck){ pucks.Remove(puck); } [SyncVar(hook = nameof(OnSelectedPuckChanged))] [SerializeField]Puck selectedPuck; void OnSelectedPuckChanged(Puck oldPuck, Puck newPuck){ // Logger.Log($"Selected puck changed from {oldPuck} to {newPuck}"); GameEvents.OnSelectedPuckChanged?.Invoke(newPuck); } public static Puck SelectedPuck{ get{ return instance.selectedPuck; } set{ instance.SetSelectedPuck(value); } } void SetSelectedPuck(Puck puck) { selectedPuck = puck; GameEvents.OnSelectedPuckChanged?.Invoke(puck); } public Puck GetClosestPuck(Vector2 position){ if(IsMoving()){return null;} //Do not do anything if shits moving // Convert screen position to world position // Vector3 worldPosition = Camera.main.ScreenToWorldPoint(new Vector3(position.x, position.y, Camera.main.nearClipPlane)); // Find all pucks in the scene Puck[] pucks = FindObjectsOfType(); float minDistance = float.MaxValue; Puck closestPuck = null; foreach (Puck puck in pucks) { if(puck.team != SelectedTeam){continue;} float dist = Vector2.Distance(position, puck.transform.position); if (dist < minDistance) { minDistance = dist; closestPuck = puck; } } return closestPuck; } Coroutine coroutinePostLaunch; public void OnPointerUp(Vector2 direction) { if(direction.magnitude < puckDragMinClamp){ selectedPuck = null; GameEvents.OnSelectedPuckChanged?.Invoke(null); return; } if(coroutinePostLaunch != null){ StopCoroutine(coroutinePostLaunch); } coroutinePostLaunch = StartCoroutine(CoroutinePostLaunch()); curPuckPullForce = direction.magnitude; float force = puckForce * puckForceCurve.Evaluate(curPuckPullForce); Logger.Log($"force = {puckForce} * {puckForceCurve.Evaluate(curPuckPullForce)} = {force}"); Logger.Log($"launching puck at {direction} with {force * -direction} force"); selectedPuck.GetComponent().AddForce(-direction * force, ForceMode2D.Impulse); hitCounter++; selectedPuck = null; GameEvents.OnSelectedPuckChanged?.Invoke(null); SwitchTeams(); } void SwitchTeams(){ if(!isServer){ Debug.LogWarning("SwitchTeams called on client, skipping"); return; } StartCoroutine(CoroutineSwitchTeams()); } bool switchingTeams=false; IEnumerator CoroutineSwitchTeams(){ switchingTeams=true; for(int i=0; i < 3; i++){ yield return null; } while(m_isMoving){ yield return null; } turnTimerCounter = 0; SelectedTeam = SelectedTeam == Team.Red ? Team.Blue : Team.Red; OnTeamChanged?.Invoke(SelectedTeam); switchingTeams=false; } IEnumerator CoroutinePostLaunch(){ float t = 0; while ( t < 1){ t += Time.deltaTime / ballMoveTime; ball.linearDamping = Mathf.Lerp(0, 10, t); yield return null; } } bool IsMoving(){ if(freezeInput){return true;} float minMagnitude = 0.01f; foreach(Puck puck in pucks){ if(puck.rb.linearVelocity.magnitude > minMagnitude){ return true; } } if(ball.linearVelocity.magnitude > minMagnitude){ return true; } return false; } void Update() { if(isServer){ m_isMoving=IsMoving(); HandleTurnTimer(); if(!gameStarted){ NetPlayer[] players = FindObjectsOfType(); #if UNITY_EDITOR if(Input.GetKeyDown(KeyCode.Space)){ gameStarted = true; GameCanvas.instance.HideWaitingForOpponentPanel(); Logger.Log("Game started On Server, EDITOR ONLY, DEBUG ONLY"); } #endif if (players.Length == 2) { gameStarted = true; GameCanvas.instance.HideWaitingForOpponentPanel(); Logger.Log("Game started On Server"); } } } UpdateLocalTurnTimerBeep(); UpdateTimerColor(); } void UpdateLocalTurnTimerBeep() { if (!isClient || NetPlayer.localPlayer == null) return; if (!gameStarted || SelectedTeam != NetPlayer.localPlayer.myTeam) { _lastTurnTimerBeepFloor = -1; return; } if (m_isMoving || turnTimerCounter >= turnTimer) return; float remaining = turnTimer - turnTimerCounter; int floorSec = Mathf.FloorToInt(remaining); if (floorSec != _lastTurnTimerBeepFloor) { if (_lastTurnTimerBeepFloor >= 0 && floorSec < _lastTurnTimerBeepFloor && AudioManager.instance != null) AudioManager.instance.PlayTimerBeep(); _lastTurnTimerBeepFloor = floorSec; } } void HandleTurnTimer(){ if(turnTimerCounter < turnTimer){ if(!isMoving && !switchingTeams && gameStarted){ turnTimerCounter += Time.deltaTime; } }else{ SwitchTeams(); turnTimerCounter = 0; } } float flashPhase = 0f; float lastUpdateTime = 0f; float lastRemainingTime = 10f; int _lastTurnTimerBeepFloor = -1; void UpdateTimerColor(){ float remainingTime = turnTimer - turnTimerCounter; // Detect timer reset: if remainingTime jumped from low to high, reset phase if(remainingTime > lastRemainingTime + 2f){ flashPhase = 0f; lastUpdateTime = 0f; } lastRemainingTime = remainingTime; CanvasGroupUtils activeTimerGroup = SelectedTeam == Team.Red ? redTimerGroup : blueTimerGroup; CanvasGroupUtils inactiveTimerGroup = SelectedTeam == Team.Red ? blueTimerGroup : redTimerGroup; if(inactiveTimerGroup != null){ inactiveTimerGroup.overrideColor = Color.white; } if(activeTimerGroup == null){ if(remainingTime >= 5f || !gameStarted){ flashPhase = 0f; lastUpdateTime = 0f; } return; } if(remainingTime < 5f && gameStarted){ // Calculate intensity based on how close timer is to 0 // Intensity increases from 0 to 1 as timer goes from 5 to 0 float intensity = 1f - (remainingTime / 5f); // Create flashing effect using sine wave // Flash speed increases as timer approaches 0 float flashSpeed = Mathf.Lerp(minFlashSpeed, maxFlashSpeed, intensity); // Update phase based on delta time and current flash speed // This ensures smooth flashing that resets with the timer if(lastUpdateTime > 0f){ float deltaTime = Time.time - lastUpdateTime; flashPhase += flashSpeed * deltaTime; } else { // First frame in warning zone, initialize phase flashPhase = 0f; } lastUpdateTime = Time.time; float flashValue = (Mathf.Sin(flashPhase) + 1f) * 0.5f; // 0 to 1 // Combine intensity with flash for final red intensity // As timer approaches 0, intensity increases and flash becomes more prominent float finalRedIntensity = intensity * (0.3f + flashValue * 0.7f); // Range: intensity*0.3 to intensity*1 // Lerp between white and red based on final intensity, keeping alpha at 1 Color white = Color.white; Color red = Color.red; activeTimerGroup.overrideColor = Color.Lerp(white, red, finalRedIntensity); }else{ // Reset phase when timer is above 5 seconds or game hasn't started flashPhase = 0f; lastUpdateTime = 0f; activeTimerGroup.overrideColor = Color.white; } } public void OnGoal(Team team){ if(!isServer){ Debug.LogWarning("OnGoal called on client, skipping"); return;} RpcPlayGoalScoredSfx(); freezeInput=true; if(hitCounter == 1){ //kickoff goal Logger.Log("Kickoff goal"); StartCoroutine(CoroutineOnGoal(team,true));//true = Kickoff goal, reset only the ball hitCounter = 0; return; } if(team == Team.Blue){ blueScore++; blueScoreText.text = blueScore.ToString(); PlayGoalEffects(Team.Blue); }else{ redScore++; redScoreText.text = redScore.ToString(); PlayGoalEffects(Team.Red); } SwitchTeams(); if(blueScore >= 3){ ReportDedicatedMatchGameOver(Team.Blue); RpcGameOver(Team.Blue); gameOver(Team.Blue); }else if(redScore >= 3){ ReportDedicatedMatchGameOver(Team.Red); RpcGameOver(Team.Red); gameOver(Team.Red); }else{ StartCoroutine(CoroutineOnGoal(team)); } hitCounter = 0; } void PlayGoalEffects(Team team){ if(isServer){ m_PlayGoalEffects(team); RpcPlayGoalEffects(team); }else{ CmdPlayGoalEffects(team); } } [Command] void CmdPlayGoalEffects(Team team){ m_PlayGoalEffects(team); RpcPlayGoalEffects(team); } [ClientRpc] void RpcPlayGoalEffects(Team team){ m_PlayGoalEffects(team); } void m_PlayGoalEffects(Team team){ if(team == Team.Blue){ foreach(ParticleSystem effect in blueGoalEffects){ effect.Play(); } }else{ foreach(ParticleSystem effect in redGoalEffects){ effect.Play(); } } } [ClientRpc] void RpcPlayGoalScoredSfx() { if (AudioManager.instance != null) AudioManager.instance.PlayGoalScoredSfxSequence(); } [ClientRpc] void RpcGameOver(Team team){ gameOver(team); } void gameOver(Team team){ StartCoroutine(CoroutineGameOver(team)); } IEnumerator CoroutineGameOver(Team team){ yield return new WaitForSeconds(2f); yield return CoroutineFetchBothPlayersL10(); StopClient(); LevelLoadManager.instance.SetupGameOver(team,redScore,blueScore); LevelLoadManager.LoadLevel("MainMenu"); } public void Leave(){ StopClient(); LevelLoadManager.LoadLevel("MainMenu"); } void StopClient(){ try{ NetworkManager.singleton.StopClient(); }catch(Exception e){ Logger.Log("Error stopping client: " + e.Message); } } IEnumerator CoroutineFetchBothPlayersL10() { var my = LevelLoadManager.myPlayer; var opp = LevelLoadManager.opponentPlayer; if (my == null || opp == null) yield break; string baseUrl = (DedicatedInternalApiBase ?? "").TrimEnd('/'); if (string.IsNullOrEmpty(baseUrl)) yield break; if (my.UserId > 0){ yield return CoFetchPlayerL10Apply(baseUrl, my); } if (opp.UserId > 0 && opp.UserId != my.UserId){ yield return CoFetchPlayerL10Apply(baseUrl, opp); } LevelLoadManager.myPlayer = my; LevelLoadManager.opponentPlayer = opp; } static IEnumerator CoFetchPlayerL10Apply(string baseUrl, MatchmadePlayer player) { string url = baseUrl + "/players/" + player.UserId + "/l10"; using (var req = UnityWebRequest.Get(url)) { yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { Logger.Log("Player L10 fetch failed: " + req.error + " " + url); yield break; } var resp = JsonUtility.FromJson(req.downloadHandler.text); if (resp != null && resp.ok) { player.l10_wins = resp.l10_wins; player.l10_losses = resp.l10_losses; } } } [Serializable] class PlayerL10Response { public bool ok; public int player_id; public int l10_wins; public int l10_losses; } bool freezeInput = false; IEnumerator CoroutineOnGoal(Team team, bool kickoff = false){ if(coroutinePostLaunch!=null){ StopCoroutine(coroutinePostLaunch); } float t=0; while(t < 1){ t +=Time.deltaTime * 2f; ball.linearDamping = Mathf.Lerp(0, 100, t); yield return null; } yield return new WaitForSeconds(2f); Reset(); } void OnScoreChanged(int oldScore, int newScore){ Logger.Log($"Score changed from {oldScore} to {newScore}"); redScoreText.text = redScore.ToString(); blueScoreText.text = blueScore.ToString(); } public void Reset(bool kickoff = false) { StartCoroutine(CoroutineReset(kickoff)); } IEnumerator CoroutineReset(bool kickoff = false){ float resetDuration = 0.5f; if(!kickoff){ foreach(Puck puck in pucks){ puck.Reset(null, resetDuration); } } ball.GetComponent().Reset(resetDuration); yield return new WaitForSeconds(resetDuration); freezeInput=false; } } /// Headless dedicated server → matchmaker internal PATCH. Runs only where started (server-side coroutines). static class DedicatedMatschInternalApi { public static UnityWebRequest BuildRequest(string baseUrl, int matchId, string secret, string jsonBody) { string url = baseUrl.TrimEnd('/') + "/internal/match/" + matchId; return BuildPatchJson(url, secret, jsonBody); } public static UnityWebRequest BuildWinnerRequest(string baseUrl, int matchId, string secret, string jsonBody) { string url = baseUrl.TrimEnd('/') + "/internal/match/" + matchId + "/winner"; return BuildPatchJson(url, secret, jsonBody); } static UnityWebRequest BuildPatchJson(string url, string secret, string jsonBody) { byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody); var req = new UnityWebRequest(url, "PATCH") { uploadHandler = new UploadHandlerRaw(bodyRaw), downloadHandler = new DownloadHandlerBuffer() }; req.SetRequestHeader("Content-Type", "application/json"); req.SetRequestHeader("X-Dedicated-Server-Secret", secret); return req; } public static void LogIfFailed(UnityWebRequest req) { if (req.result == UnityWebRequest.Result.Success){ Logger.Log("Dedicated match PATCH success: " + req.responseCode + " " + (req.downloadHandler != null ? req.downloadHandler.text : "")); }else{ Logger.Log("Dedicated match PATCH failed: " + req.responseCode + " " + req.error + " " + (req.downloadHandler != null ? req.downloadHandler.text : "")); Logger.Log("More details : " + req.url + " " + req.method + " " + req.uploadHandler.data + " " + req.downloadHandler.text); } } }