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; }
/// Coin units from matchmaker rc_prize; used for in-game prize UI.
public static int MatchRcPrizeCoins { 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}");
}
/// Outcome of (POST /internal/rematch).
public readonly struct DedicatedRematchResult
{
public readonly bool Success;
public readonly long HttpStatusCode;
public readonly MatchmadeResponse Response;
public readonly string ErrorMessage;
public DedicatedRematchResult(bool success, long httpStatusCode, MatchmadeResponse response, string errorMessage)
{
Success = success;
HttpStatusCode = httpStatusCode;
Response = response;
ErrorMessage = errorMessage ?? "";
}
}
///
/// Dedicated server only: POST /internal/rematch with X-Dedicated-Server-Secret.
/// Blocks until the request finishes (same pattern as ).
/// On HTTP 200, returns the same JSON envelope as a successful matchmaker GET room fill: .
///
/// users.id for red.
/// users.id for blue; must differ from red.
/// RC stake per player (non-negative).
/// Overrides when non-empty.
/// Overrides when non-empty.
public static DedicatedRematchResult RequestDedicatedRematch(int userRedId, int userBlueId, int entryFee, string secret = null, string internalApiBaseUrl = null)
{
string s = string.IsNullOrEmpty(secret) ? DedicatedMatchSecret : secret;
string baseUrl = string.IsNullOrEmpty(internalApiBaseUrl) ? DedicatedInternalApiBase : internalApiBaseUrl.TrimEnd('/');
if (string.IsNullOrEmpty(s))
return new DedicatedRematchResult(false, 0, null, "Missing dedicated server secret");
if (string.IsNullOrEmpty(baseUrl))
return new DedicatedRematchResult(false, 0, null, "Missing internal API base URL");
string json = JsonUtility.ToJson(new DedicatedRematchRequestBody
{
user_red_id = userRedId,
user_blue_id = userBlueId,
entry_fee = entryFee
});
using (var req = DedicatedMatschInternalApi.BuildRematchRequest(baseUrl, s, json))
{
var op = req.SendWebRequest();
while (!op.isDone)
System.Threading.Thread.Sleep(16);
string text = req.downloadHandler != null ? req.downloadHandler.text : "";
long code = req.responseCode;
if (req.result != UnityWebRequest.Result.Success &&
req.result != UnityWebRequest.Result.ProtocolError)
{
string netErr = string.IsNullOrEmpty(req.error) ? "Network error" : req.error;
Logger.Log("Dedicated rematch POST failed (transport): " + netErr + " " + req.url);
return new DedicatedRematchResult(false, code, null, netErr);
}
if (code == 200)
{
try
{
var env = JsonUtility.FromJson(text);
if (env != null && env.ok)
return new DedicatedRematchResult(true, code, env, "");
string apiErr = TryParseRematchErrorMessage(text);
Logger.Log("Dedicated rematch POST 200 but ok=false or parse issue: " + text);
return new DedicatedRematchResult(false, code, env, string.IsNullOrEmpty(apiErr) ? "Unexpected response" : apiErr);
}
catch (Exception e)
{
Logger.Log("Dedicated rematch POST: could not parse success body: " + e.Message + " body=" + text);
return new DedicatedRematchResult(false, code, null, e.Message);
}
}
string err = TryParseRematchErrorMessage(text);
if (string.IsNullOrEmpty(err))
err = string.IsNullOrEmpty(text) ? "HTTP " + code : text;
Logger.Log("Dedicated rematch POST failed: " + code + " " + err);
return new DedicatedRematchResult(false, code, null, err);
}
}
static string TryParseRematchErrorMessage(string json)
{
if (string.IsNullOrEmpty(json))
return "";
try
{
var err = JsonUtility.FromJson(json);
if (err != null && !string.IsNullOrEmpty(err.error))
return err.error;
}
catch
{
/* ignore */
}
return "";
}
/// 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;
[SyncVar]
public bool isGameEnded = 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;
[Header("Field bounds")]
[Tooltip("Pitch width (X). Length (Y) is fieldSize × 2. Origin-centered; used for boundary-based camera zoom while dragging.")]
public float fieldSize = 10f;
[Tooltip("Exponent on distance-to-boundary (0–1). >1 = subtle at center, ramps up mostly near the boundary; 1 = linear.")]
[Min(0.01f)] public float cameraStretchBoundaryExponent = 2.5f;
[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;
}
///
/// Blends from at the pitch center to
/// at the boundary (and beyond), using the max normalized axis distance inside the field rectangle.
///
public float GetCameraStretchDistanceMultiplier(Vector3 puckWorldPosition, float centerMultiplier = 0.3f, float boundaryMultiplier = 1f)
{
if (fieldSize <= Mathf.Epsilon)
return boundaryMultiplier;
float halfWidth = fieldSize * 0.5f;
float halfHeight = fieldSize;
float nx = Mathf.Abs(puckWorldPosition.x) / halfWidth;
float ny = Mathf.Abs(puckWorldPosition.y) / halfHeight;
float t = Mathf.Clamp01(Mathf.Max(nx, ny));
float shaped = Mathf.Pow(t, cameraStretchBoundaryExponent);
return Mathf.Lerp(centerMultiplier, boundaryMultiplier, shaped);
}
void OnDrawGizmos()
{
if (fieldSize <= Mathf.Epsilon)
return;
float halfWidth = fieldSize * 0.5f;
float halfHeight = fieldSize;
Vector3 c = Vector3.zero;
Vector3 right = new Vector3(halfWidth, 0f, 0f);
Vector3 up = new Vector3(0f, halfHeight, 0f);
Vector3 a = c - right - up;
Vector3 b = c + right - up;
Vector3 d = c + right + up;
Vector3 e = c - right + up;
Color prev = Gizmos.color;
Gizmos.color = new Color(1f, 0.92f, 0.016f, 0.85f);
Gizmos.DrawLine(a, b);
Gizmos.DrawLine(b, d);
Gizmos.DrawLine(d, e);
Gizmos.DrawLine(e, a);
Gizmos.color = prev;
}
/// 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.LogWinnerPatchResult(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);
if (newPuck == null && isClient && CameraEffects.instance != null){
CameraEffects.instance.ReleaseStretchFactor();
}
}
public static Puck SelectedPuck{
get{
return instance != null ? instance.selectedPuck : null;
}
set{
if (instance == null)
return;
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 == null)
continue;
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(selectedPuck == null){
return;
}
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;
// Clear any active drag/selection before changing turns.
// This prevents stale selected-puck visuals when the timer expires mid-hold.
if (selectedPuck != null){
SetSelectedPuck(null);
}
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;
}
}
Team GetOpposingTeam(Team team){
return team == Team.Red ? Team.Blue : Team.Red;
}
void SetKickoffTeam(Team kickoffTeam){
turnTimerCounter = 0;
SelectedTeam = kickoffTeam;
OnTeamChanged?.Invoke(SelectedTeam);
}
public void OnGoal(Team team){
if(!isServer){
Debug.LogWarning("OnGoal called on client, skipping");
return;}
if(!CanProcessGoal){
return;
}
RpcPlayGoalScoredSfx();
freezeInput=true;
Team concedingTeam = team;
if(hitCounter == 1){
//kickoff goal
Logger.Log("Kickoff goal");
SetKickoffTeam(concedingTeam);
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);
Logger.Log($"Blue goal scored, blue score is now {blueScore}");
}else{
redScore++;
redScoreText.text = redScore.ToString();
PlayGoalEffects(Team.Red);
Logger.Log($"Red goal scored, red score is now {redScore}");
}
SetKickoffTeam(concedingTeam);
if(blueScore >= 3){
isGameEnded = true;
ReportDedicatedMatchGameOver(Team.Blue);
RpcGameOver(Team.Blue);
gameOver(Team.Blue);
}else if(redScore >= 3){
isGameEnded = true;
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);
GameOverCanvas.instance.Show(team, MyTeam, redScore, blueScore);
// LevelLoadManager.LoadLevel("MainMenu");
}
public 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;
public bool CanProcessGoal => isServer && !freezeInput && !isGameEnded;
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);
}
public static UnityWebRequest BuildRematchRequest(string baseUrl, string secret, string jsonBody)
{
string url = baseUrl.TrimEnd('/') + "/internal/rematch";
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)
{
uploadHandler = new UploadHandlerRaw(bodyRaw),
downloadHandler = new DownloadHandlerBuffer()
};
req.SetRequestHeader("Content-Type", "application/json");
req.SetRequestHeader("X-Dedicated-Server-Secret", secret);
return req;
}
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);
}
}
/// Handles PATCH /internal/match/:id/winner: 200 parses economy; 409 idempotent (already paid).
public static void LogWinnerPatchResult(UnityWebRequest req)
{
long code = req.responseCode;
string body = req.downloadHandler != null ? req.downloadHandler.text : "";
if (code == 200 && req.result == UnityWebRequest.Result.Success)
{
try
{
var parsed = JsonUtility.FromJson(body);
if (parsed != null && parsed.ok && parsed.economy != null)
GameManager.MatchRcPrizeCoins = parsed.economy.rc_prize;
}
catch (Exception e)
{
Logger.Log("Dedicated winner PATCH: could not parse economy: " + e.Message + " body=" + body);
}
Logger.Log("Dedicated match winner PATCH success: " + code + " " + body);
return;
}
if (code == 409)
{
Logger.Log("Dedicated match winner PATCH: winner already recorded (409 idempotent), no second payout. " + body);
return;
}
Logger.Log("Dedicated match winner PATCH failed: " + code + " " + req.error + " " + body);
Logger.Log("More details : " + req.url + " " + req.method + " " + body);
}
}
[Serializable]
class DedicatedWinnerPatchEconomy
{
public int entry_fee_rc;
public int rc_prize;
public int participant_cc;
}
[Serializable]
class DedicatedWinnerPatchResponse
{
public bool ok;
public int id;
public int winner_id;
public DedicatedWinnerPatchEconomy economy;
}
[Serializable]
class DedicatedRematchRequestBody
{
public int user_red_id;
public int user_blue_id;
public int entry_fee;
}
[Serializable]
class DedicatedRematchErrorEnvelope
{
public bool ok;
public string error;
}