Files
soccar2d/Assets/Scripts/GameManager.cs
T
2026-05-04 17:48:09 +05:30

909 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
/// <summary>Coin units from matchmaker <c>rc_prize</c>; used for in-game prize UI.</summary>
public static int MatchRcPrizeCoins { get; set; }
/// <summary>Set by dedicated server startup (e.g. CupidConnector parsing <c>-matchId</c>). PATCH is skipped if id ≤ 0 or secret is empty.</summary>
public static int DedicatedMatchId { get; private set; }
/// <summary>Sent as <c>X-Dedicated-Server-Secret</c>. From <c>-dedicatedSecret</c> arg or <c>DEDICATED_SERVER_SECRET</c> env.</summary>
public static string DedicatedMatchSecret { get; private set; } = "";
/// <summary>Base URL for internal match API (no trailing slash), default <c>http://127.0.0.1:2612</c>.</summary>
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}");
}
/// <summary>Sets match <c>status</c> to <c>-1</c> (room closed). Safe to call from shutdown paths; runs synchronously so it completes before <c>Application.Quit</c>.</summary>
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<Team> 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 (01). >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<Puck> pucks = new List<Puck>();
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;
}
/// <summary>
/// Blends from <paramref name="centerMultiplier"/> at the pitch center to <paramref name="boundaryMultiplier"/>
/// at the boundary (and beyond), using the max normalized axis distance inside the field rectangle.
/// </summary>
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;
}
/// <summary>Server-only: called from <see cref="NetPlayer.CmdSetTeam"/> after <c>myTeam</c> is set.</summary>
public void OnMatchPlayerTeamAssigned(Team team)
{
if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
return;
int n = FindObjectsByType<NetPlayer>(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.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<Puck>();
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<Rigidbody2D>().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<NetPlayer>();
#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 Leave(){
NetManager.MarkIntentionalClientDisconnect();
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<PlayerL10Response>(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<Ball>().Reset(resetDuration);
yield return new WaitForSeconds(resetDuration);
freezeInput=false;
}
}
/// <summary>Headless dedicated server → matchmaker internal PATCH. Runs only where started (server-side coroutines).</summary>
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);
}
}
/// <summary>Handles <c>PATCH /internal/match/:id/winner</c>: 200 parses economy; 409 idempotent (already paid).</summary>
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<DedicatedWinnerPatchResponse>(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;
}