Files

657 lines
25 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// MatchResultUI — Unified VICTORY / DEFEAT end-of-match panel.
/// Works for both Cash System (team 3v3) and Practice (1v1) modes.
///
/// Stats auto-populate based on mode:
/// Cash System → Kills, Cash Deposited, Cash Stolen, Damage
/// Practice → Kills, Deaths, Assists, Damage
///
/// Hierarchy (from scene):
/// Stats (root, this script)
/// ├─ continue (Button)
/// ├─ restart (Button)
/// ├─ Title — result header "VICTORY" / "DEFEAT"
/// ├─ Title(1) — score text "24 VICTORY 20"
/// ├─ Playerstatsui
/// │ ├─ PlayerName
/// │ ├─ kills
/// │ ├─ deaths
/// │ ├─ assists
/// │ └─ damage
/// ├─ Enemystatsui
/// │ ├─ EnemyName
/// │ ├─ kills
/// │ ├─ deaths
/// │ ├─ assists
/// │ └─ damage
/// └─ Victory tab
/// </summary>
public class MatchResultUI : MonoBehaviour
{
[Header("Panel Root")]
[Tooltip("The root panel that gets shown/hidden. If null, uses this.gameObject.")]
[SerializeField] private GameObject rootPanel;
[Header("Header")]
[SerializeField] private TextMeshProUGUI resultHeaderText; // "VICTORY" / "DEFEAT"
[SerializeField] private TextMeshProUGUI scoreText; // "24 VICTORY 20" style
[Header("Team Names")]
[SerializeField] private TextMeshProUGUI playerTeamLabel; // e.g. "CHEETAH WIN!"
[SerializeField] private TextMeshProUGUI enemyTeamLabel; // e.g. "RABBIT LOSE!"
[Header("Player Stats Texts")]
[Tooltip("Player/team name text inside Playerstatsui")]
[SerializeField] private TextMeshProUGUI playerNameText;
[Tooltip("Stat row 1 — Kills (both modes)")]
[SerializeField] private TextMeshProUGUI playerKillsText;
[Tooltip("Stat row 2 — Deaths (practice) / Cash Deposited (cash system)")]
[SerializeField] private TextMeshProUGUI playerDeathsText;
[Tooltip("Stat row 3 — Assists (practice) / Cash Stolen (cash system)")]
[SerializeField] private TextMeshProUGUI playerAssistsText;
[Tooltip("Stat row 4 — Damage (both modes)")]
[SerializeField] private TextMeshProUGUI playerDamageText;
[Header("Enemy Stats Texts")]
[Tooltip("Enemy/team name text inside Enemystatsui")]
[SerializeField] private TextMeshProUGUI enemyNameText;
[SerializeField] private TextMeshProUGUI enemyKillsText;
[SerializeField] private TextMeshProUGUI enemyDeathsText;
[SerializeField] private TextMeshProUGUI enemyAssistsText;
[SerializeField] private TextMeshProUGUI enemyDamageText;
[Header("Buttons")]
[SerializeField] private Button continueButton;
[SerializeField] private Button restartButton;
[Header("Colors")]
[SerializeField] private Color victoryColor = new Color(1f, 0.84f, 0f); // Gold
[SerializeField] private Color defeatColor = new Color(0.96f, 0.26f, 0.21f); // Red
[SerializeField] private Color panelBgColor = new Color(0.08f, 0.12f, 0.18f, 0.92f);
[Header("Audio")]
[SerializeField] private AudioClip victorySFX;
[SerializeField] private AudioClip defeatSFX;
[SerializeField] private AudioClip buttonClickSFX;
[SerializeField] private float sfxVolume = 1f;
[Header("Animation")]
[SerializeField] private float revealDuration = 0.6f;
// runtime
private AudioSource _audio;
private CanvasGroup _canvasGroup;
private float _savedFixedDeltaTime;
private static MatchResultUI _instance;
public static MatchResultUI Instance => _instance;
/// <summary>
/// Ensures a MatchResultUI instance exists in the scene.
/// Finds inactive instances first, or creates a minimal one with buttons on the existing Canvas.
/// Call from CashSystemManager.Start() so end-of-match always shows a panel.
/// </summary>
public static void EnsureExists()
{
if (_instance != null) return;
// 1. Try to find an inactive instance already in the scene
MatchResultUI[] all = Resources.FindObjectsOfTypeAll<MatchResultUI>();
foreach (var m in all)
{
if (m.gameObject.scene.name == null) continue;
// Activate parents top-down first, then the object itself
List<GameObject> chain = new List<GameObject>();
Transform t = m.transform;
while (t != null)
{
if (!t.gameObject.activeSelf)
chain.Add(t.gameObject);
t = t.parent;
}
for (int i = chain.Count - 1; i >= 0; i--)
chain[i].SetActive(true);
if (_instance != null)
{
DevLog.Log("[MatchResultUI] Found existing inactive panel, activated it");
return;
}
}
// 2. Create a minimal panel on an existing Canvas
Canvas canvas = Object.FindObjectOfType<Canvas>();
if (canvas == null)
{
GameObject canvasObj = new GameObject("UICanvas");
canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100;
canvasObj.AddComponent<UnityEngine.UI.CanvasScaler>();
canvasObj.AddComponent<UnityEngine.UI.GraphicRaycaster>();
}
GameObject panelRoot = new GameObject("MatchResultUI_Auto");
panelRoot.transform.SetParent(canvas.transform, false);
RectTransform rt = panelRoot.AddComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
// Background
Image bg = panelRoot.AddComponent<Image>();
bg.color = new Color(0.08f, 0.12f, 0.18f, 0.92f);
// Result header
GameObject headerObj = new GameObject("ResultHeader");
headerObj.transform.SetParent(panelRoot.transform, false);
RectTransform hrt = headerObj.AddComponent<RectTransform>();
hrt.anchorMin = new Vector2(0.2f, 0.7f);
hrt.anchorMax = new Vector2(0.8f, 0.9f);
hrt.offsetMin = Vector2.zero;
hrt.offsetMax = Vector2.zero;
TextMeshProUGUI headerTxt = headerObj.AddComponent<TextMeshProUGUI>();
headerTxt.text = "MATCH RESULT";
headerTxt.fontSize = 80;
headerTxt.alignment = TextAlignmentOptions.Center;
headerTxt.color = Color.white;
// Score text
GameObject scoreObj = new GameObject("ScoreText");
scoreObj.transform.SetParent(panelRoot.transform, false);
RectTransform srt = scoreObj.AddComponent<RectTransform>();
srt.anchorMin = new Vector2(0.2f, 0.55f);
srt.anchorMax = new Vector2(0.8f, 0.7f);
srt.offsetMin = Vector2.zero;
srt.offsetMax = Vector2.zero;
TextMeshProUGUI scoreTxt = scoreObj.AddComponent<TextMeshProUGUI>();
scoreTxt.text = "";
scoreTxt.fontSize = 48;
scoreTxt.alignment = TextAlignmentOptions.Center;
scoreTxt.color = Color.white;
// Continue button
Button continueBtn = CreateAutoButton(panelRoot.transform, "Continue",
new Vector2(0.25f, 0.1f), new Vector2(0.48f, 0.22f), new Color(0.2f, 0.7f, 0.3f));
// Restart button
Button restartBtn = CreateAutoButton(panelRoot.transform, "Restart",
new Vector2(0.52f, 0.1f), new Vector2(0.75f, 0.22f), new Color(0.8f, 0.4f, 0.1f));
MatchResultUI ui = panelRoot.AddComponent<MatchResultUI>();
ui.resultHeaderText = headerTxt;
ui.scoreText = scoreTxt;
ui.continueButton = continueBtn;
ui.restartButton = restartBtn;
DevLog.Log("[MatchResultUI] Auto-created minimal result panel on Canvas");
}
private static Button CreateAutoButton(Transform parent, string label,
Vector2 anchorMin, Vector2 anchorMax, Color bgColor)
{
GameObject btnObj = new GameObject(label + "Button");
btnObj.transform.SetParent(parent, false);
RectTransform brt = btnObj.AddComponent<RectTransform>();
brt.anchorMin = anchorMin;
brt.anchorMax = anchorMax;
brt.offsetMin = Vector2.zero;
brt.offsetMax = Vector2.zero;
Image btnBg = btnObj.AddComponent<Image>();
btnBg.color = bgColor;
Button btn = btnObj.AddComponent<Button>();
GameObject btnLabelObj = new GameObject("Label");
btnLabelObj.transform.SetParent(btnObj.transform, false);
RectTransform lrt = btnLabelObj.AddComponent<RectTransform>();
lrt.anchorMin = Vector2.zero;
lrt.anchorMax = Vector2.one;
lrt.offsetMin = Vector2.zero;
lrt.offsetMax = Vector2.zero;
TextMeshProUGUI txt = btnLabelObj.AddComponent<TextMeshProUGUI>();
txt.text = label;
txt.fontSize = 42;
txt.alignment = TextAlignmentOptions.Center;
txt.color = Color.white;
return btn;
}
// ─── Lifecycle ───────────────────────────────────────────
private void Awake()
{
_instance = this;
if (rootPanel == null) rootPanel = gameObject;
_audio = GetComponent<AudioSource>();
if (_audio == null) _audio = gameObject.AddComponent<AudioSource>();
_audio.playOnAwake = false;
_audio.spatialBlend = 0f;
_canvasGroup = rootPanel.GetComponent<CanvasGroup>();
if (_canvasGroup == null) _canvasGroup = rootPanel.AddComponent<CanvasGroup>();
// Ensure MatchStatTracker exists to record stats from match start
if (MatchStatTracker.Instance == null)
{
GameObject trackerObj = new GameObject("MatchStatTracker");
trackerObj.AddComponent<MatchStatTracker>();
}
// IMPORTANT: Do NOT use SetActive(false) — that disables the MonoBehaviour
// and unsubscribes from GameEvents.OnMatchResult.
// Instead hide via CanvasGroup so the script stays alive.
HideImmediate();
}
private void OnEnable()
{
GameEvents.OnMatchResult += OnMatchResult;
// Wire button listeners
WireButtonListeners();
}
private void OnDisable()
{
GameEvents.OnMatchResult -= OnMatchResult;
if (continueButton != null) continueButton.onClick.RemoveListener(OnContinueClicked);
if (restartButton != null) restartButton.onClick.RemoveListener(OnRestartClicked);
}
/// <summary>
/// Late-wire buttons — handles auto-created panels where buttons
/// are assigned after Awake/OnEnable.
/// </summary>
private void Start()
{
WireButtonListeners();
}
private void WireButtonListeners()
{
if (continueButton != null)
{
continueButton.onClick.RemoveListener(OnContinueClicked);
continueButton.onClick.AddListener(OnContinueClicked);
}
if (restartButton != null)
{
restartButton.onClick.RemoveListener(OnRestartClicked);
restartButton.onClick.AddListener(OnRestartClicked);
}
}
// ─── Event ───────────────────────────────────────────────
private void OnMatchResult(bool playerWon)
{
Show(playerWon);
}
// ─── Public API ──────────────────────────────────────────
/// <summary>Show the result screen. Called automatically via GameEvents or manually.</summary>
public void Show(bool playerWon)
{
DevLog.Log($"[MatchResultUI] Show(playerWon={playerWon})");
rootPanel.SetActive(true);
// Don't set alpha here — RevealRoutine fades from 0 to 1
_canvasGroup.alpha = 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
// Pause game so combat freezes behind the result panel
_savedFixedDeltaTime = Time.fixedDeltaTime;
Time.timeScale = 0f;
// NOTE: Do NOT set fixedDeltaTime to 0 — timeScale=0 already pauses FixedUpdate.
// Setting fixedDeltaTime=0 caused an infinite loop / hard freeze.
bool isCashSystem = SelectionOptions.Instance != null && SelectionOptions.Instance.isCashSystemSelected;
// ── Header ──
if (resultHeaderText != null)
{
resultHeaderText.text = playerWon ? "VICTORY" : "DEFEAT";
resultHeaderText.color = playerWon ? victoryColor : defeatColor;
}
// ── Scores ──
if (isCashSystem)
PopulateCashSystemScores(playerWon);
else
PopulatePracticeScores(playerWon);
// ── Team labels ──
PopulateTeamLabels(playerWon, isCashSystem);
// ── Stats ──
if (isCashSystem)
PopulateCashSystemStats();
else
PopulatePracticeStats();
// ── Sound ──
AudioClip clip = playerWon ? victorySFX : defeatSFX;
if (clip != null) _audio.PlayOneShot(clip, sfxVolume);
// ── Animate ──
StartCoroutine(RevealRoutine());
}
// ─── Cash System Stats ───────────────────────────────────
private void PopulateCashSystemScores(bool playerWon)
{
CashSystemManager mgr = CashSystemManager.Instance;
float playerScore = 0f, enemyScore = 0f;
if (mgr != null)
{
var pv = mgr.GetVault("Player");
var ev = mgr.GetVault("Enemy");
if (pv != null) playerScore = pv.StoredCash;
if (ev != null) enemyScore = ev.StoredCash;
}
if (scoreText != null)
{
string resultWord = playerWon ? "VICTORY" : "DEFEAT";
scoreText.text = $"{playerScore:F0} {resultWord} {enemyScore:F0}";
}
}
private void PopulateCashSystemStats()
{
CashSystemManager mgr = CashSystemManager.Instance;
if (mgr == null) { ClearStats(true); return; }
MatchStatTracker tracker = MatchStatTracker.Instance;
var playerChars = mgr.GetTeamCharacters("Player");
var enemyChars = mgr.GetTeamCharacters("Enemy");
// Get vault totals for the score header
var pv = mgr.GetVault("Player");
var ev = mgr.GetVault("Enemy");
float playerVaultTotal = pv != null ? pv.StoredCash : 0f;
float enemyVaultTotal = ev != null ? ev.StoredCash : 0f;
int pKills = 0;
float pDeposited = 0f, pStolen = 0f, pCollected = 0f;
int eKills = 0;
float eDeposited = 0f, eStolen = 0f, eCollected = 0f;
if (tracker != null)
{
foreach (var s in tracker.GetTeamStats(playerChars))
{
pKills += s.kills;
pDeposited += s.cashDeposited;
pStolen += s.cashStolen;
pCollected += s.cashCollected;
}
foreach (var s in tracker.GetTeamStats(enemyChars))
{
eKills += s.kills;
eDeposited += s.cashDeposited;
eStolen += s.cashStolen;
eCollected += s.cashCollected;
}
}
// Player side — Cash System stats (4 text slots used smartly for cash data)
SetStatText(playerNameText, "PLAYER TEAM");
SetStatText(playerKillsText, $"Vault Total: ${playerVaultTotal:F0}");
SetStatText(playerDeathsText, $"Cash Deposited: ${pDeposited:F0}");
SetStatText(playerAssistsText, $"Cash Stolen: ${pStolen:F0}");
SetStatText(playerDamageText, $"Eliminations: {pKills}");
// Enemy side
SetStatText(enemyNameText, "ENEMY TEAM");
SetStatText(enemyKillsText, $"Vault Total: ${enemyVaultTotal:F0}");
SetStatText(enemyDeathsText, $"Cash Deposited: ${eDeposited:F0}");
SetStatText(enemyAssistsText, $"Cash Stolen: ${eStolen:F0}");
SetStatText(enemyDamageText, $"Eliminations: {eKills}");
}
// ─── Practice / 1v1 Stats ────────────────────────────────
private void PopulatePracticeScores(bool playerWon)
{
if (scoreText != null)
{
scoreText.text = playerWon ? "VICTORY" : "DEFEAT";
}
}
private void PopulatePracticeStats()
{
MatchStatTracker tracker = MatchStatTracker.Instance;
// In practice mode: find Player and Enemy tagged objects
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
GameObject enemyObj = GameObject.FindGameObjectWithTag("Enemy");
int pKills = 0, pDeaths = 0, pAssists = 0;
float pDamage = 0f;
int eKills = 0, eDeaths = 0, eAssists = 0;
float eDamage = 0f;
if (tracker != null)
{
if (playerObj != null)
{
var s = tracker.GetStats(playerObj);
pKills = s.kills;
pDeaths = s.deaths;
pAssists = s.assists;
pDamage = s.damageDealt;
}
if (enemyObj != null)
{
var s = tracker.GetStats(enemyObj);
eKills = s.kills;
eDeaths = s.deaths;
eAssists = s.assists;
eDamage = s.damageDealt;
}
}
else
{
// Fallback from HealthNew
if (playerObj != null) FallbackSingle(playerObj, out pKills, out pDeaths, out pDamage);
if (enemyObj != null) FallbackSingle(enemyObj, out eKills, out eDeaths, out eDamage);
}
// Get actual character display names for practice mode
string playerName = GetCharacterDisplayName(playerObj, "PLAYER");
string enemyName = GetCharacterDisplayName(enemyObj, "ENEMY");
// Player side — practice labels (K/D/A + Damage)
SetStatText(playerNameText, playerName);
SetStatText(playerKillsText, $"Kills: {pKills}");
SetStatText(playerDeathsText, $"Deaths: {pDeaths}");
SetStatText(playerAssistsText, $"Assists: {pAssists}");
SetStatText(playerDamageText, $"Damage: {pDamage:F0}");
// Enemy side
SetStatText(enemyNameText, enemyName);
SetStatText(enemyKillsText, $"Kills: {eKills}");
SetStatText(enemyDeathsText, $"Deaths: {eDeaths}");
SetStatText(enemyAssistsText, $"Assists: {eAssists}");
SetStatText(enemyDamageText, $"Damage: {eDamage:F0}");
}
// ─── Common Helpers ──────────────────────────────────────
private void PopulateTeamLabels(bool playerWon, bool isCashSystem)
{
if (isCashSystem)
{
if (playerTeamLabel != null)
playerTeamLabel.text = playerWon ? "PLAYER TEAM WIN!" : "PLAYER TEAM LOSE!";
if (enemyTeamLabel != null)
enemyTeamLabel.text = playerWon ? "ENEMY TEAM LOSE!" : "ENEMY TEAM WIN!";
}
else
{
// Practice mode — use actual character names
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
GameObject enemyObj = GameObject.FindGameObjectWithTag("Enemy");
string pName = GetCharacterDisplayName(playerObj, "YOU");
string eName = GetCharacterDisplayName(enemyObj, "ENEMY");
if (playerTeamLabel != null)
playerTeamLabel.text = playerWon ? $"{pName} WIN!" : $"{pName} LOSE!";
if (enemyTeamLabel != null)
enemyTeamLabel.text = playerWon ? $"{eName} LOSE!" : $"{eName} WIN!";
}
}
/// <summary>Get the display name for a character GameObject (e.g. "WINDHAM").</summary>
private string GetCharacterDisplayName(GameObject character, string fallback)
{
if (character == null) return fallback;
if (CharacterPrefabManager.Instance != null)
{
string name = CharacterPrefabManager.Instance.GetCharacterDisplayName(character);
if (!string.IsNullOrEmpty(name)) return name.ToUpper();
}
return character.name.Replace("(Clone)", "").Trim().ToUpper();
}
private void SetStatText(TextMeshProUGUI tmp, string value)
{
if (tmp != null) tmp.text = value;
}
private void ClearStats(bool isCashSystem = false)
{
if (isCashSystem)
{
SetStatText(playerNameText, "PLAYER TEAM");
SetStatText(playerKillsText, "Vault Total: $0");
SetStatText(playerDeathsText, "Cash Deposited: $0");
SetStatText(playerAssistsText, "Cash Stolen: $0");
SetStatText(playerDamageText, "Eliminations: 0");
SetStatText(enemyNameText, "ENEMY TEAM");
SetStatText(enemyKillsText, "Vault Total: $0");
SetStatText(enemyDeathsText, "Cash Deposited: $0");
SetStatText(enemyAssistsText, "Cash Stolen: $0");
SetStatText(enemyDamageText, "Eliminations: 0");
}
else
{
SetStatText(playerNameText, "PLAYER");
SetStatText(playerKillsText, "Kills: 0");
SetStatText(playerDeathsText, "Deaths: 0");
SetStatText(playerAssistsText, "Assists: 0");
SetStatText(playerDamageText, "Damage: 0");
SetStatText(enemyNameText, "ENEMY");
SetStatText(enemyKillsText, "Kills: 0");
SetStatText(enemyDeathsText, "Deaths: 0");
SetStatText(enemyAssistsText, "Assists: 0");
SetStatText(enemyDamageText, "Damage: 0");
}
}
/// <summary>Fallback stats from HealthNew for a team list.</summary>
private void FallbackStats(List<GameObject> team, out int kills, out int deaths, out float damage)
{
kills = 0; deaths = 0; damage = 0f;
if (team == null) return;
foreach (var c in team)
{
if (c == null) continue;
var h = c.GetComponent<HealthNew>();
if (h == null) continue;
if (h.isDead) deaths++;
damage += Mathf.Max(0, h.maxHealth - h.currentHealth);
}
}
/// <summary>Fallback stats from HealthNew for a single character.</summary>
private void FallbackSingle(GameObject character, out int kills, out int deaths, out float damage)
{
kills = 0; deaths = 0; damage = 0f;
if (character == null) return;
var h = character.GetComponent<HealthNew>();
if (h == null) return;
if (h.isDead) deaths = 1;
damage = Mathf.Max(0, h.maxHealth - h.currentHealth);
}
/// <summary>Hide panel via CanvasGroup — keeps MonoBehaviour alive.</summary>
private void HideImmediate()
{
_canvasGroup.alpha = 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
}
// ─── Animation ───────────────────────────────────────────
private IEnumerator RevealRoutine()
{
_canvasGroup.alpha = 0f;
_canvasGroup.blocksRaycasts = true;
_canvasGroup.interactable = true;
float elapsed = 0f;
while (elapsed < revealDuration)
{
elapsed += Time.unscaledDeltaTime;
_canvasGroup.alpha = Mathf.Clamp01(elapsed / revealDuration);
yield return null;
}
_canvasGroup.alpha = 1f;
}
// ─── Button Callbacks ────────────────────────────────────
private void OnContinueClicked()
{
PlayButtonClick();
// Restore time before scene transition
Time.timeScale = 1f;
Time.fixedDeltaTime = _savedFixedDeltaTime;
GameEvents.ClearAll();
GameStatsManager statsManager = FindObjectOfType<GameStatsManager>();
if (statsManager != null)
statsManager.match_end();
else
UnityEngine.SceneManagement.SceneManager.LoadScene("MenuScene");
}
private void OnRestartClicked()
{
PlayButtonClick();
// Restore time before scene transition
Time.timeScale = 1f;
Time.fixedDeltaTime = _savedFixedDeltaTime;
GameEvents.ClearAll();
// Mode flags are preserved on SelectionOptions (DontDestroyOnLoad)
UnityEngine.SceneManagement.SceneManager.LoadScene(
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name);
}
private void PlayButtonClick()
{
if (buttonClickSFX != null && _audio != null)
_audio.PlayOneShot(buttonClickSFX, sfxVolume);
}
}