chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
656
Assets/Scripts/CashSystem/UI/MatchResultUI.cs
Normal file
656
Assets/Scripts/CashSystem/UI/MatchResultUI.cs
Normal file
@@ -0,0 +1,656 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/UI/MatchResultUI.cs.meta
Normal file
2
Assets/Scripts/CashSystem/UI/MatchResultUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fcb73e900cc542498962703b55def9e
|
||||
222
Assets/Scripts/CashSystem/UI/MatchStatTracker.cs
Normal file
222
Assets/Scripts/CashSystem/UI/MatchStatTracker.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// MatchStatTracker — Listens to GameEvents during a match and records
|
||||
/// per-character stats (kills, deaths, assists, damage dealt, cash deposited, cash stolen).
|
||||
/// Singleton that lives for the duration of the match scene.
|
||||
/// MatchResultUI reads from this at match end.
|
||||
/// </summary>
|
||||
public class MatchStatTracker : MonoBehaviour
|
||||
{
|
||||
/// <summary>Stats for one character.</summary>
|
||||
public class CharacterStats
|
||||
{
|
||||
public string name;
|
||||
public int kills;
|
||||
public int deaths;
|
||||
public int assists;
|
||||
public float damageDealt;
|
||||
public float cashDeposited;
|
||||
public float cashStolen;
|
||||
public float cashCollected;
|
||||
}
|
||||
|
||||
// Key = character GameObject instance ID
|
||||
private Dictionary<int, CharacterStats> _stats = new Dictionary<int, CharacterStats>();
|
||||
|
||||
// Track recent attackers for assist detection (victimID → list of attackerIDs within last N seconds)
|
||||
private Dictionary<int, List<(int attackerID, float time)>> _recentAttackers
|
||||
= new Dictionary<int, List<(int, float)>>();
|
||||
private const float ASSIST_WINDOW = 5f;
|
||||
|
||||
private static MatchStatTracker _instance;
|
||||
public static MatchStatTracker Instance => _instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
GameEvents.OnKill += OnKill;
|
||||
GameEvents.OnDamageDealt += OnDamageDealt;
|
||||
GameEvents.OnCharacterDied += OnCharacterDied;
|
||||
GameEvents.OnCashDeposited += OnCashDeposited;
|
||||
GameEvents.OnVaultStolen += OnVaultStolen;
|
||||
GameEvents.OnBagPickedUp += OnBagPickedUp;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
GameEvents.OnKill -= OnKill;
|
||||
GameEvents.OnDamageDealt -= OnDamageDealt;
|
||||
GameEvents.OnCharacterDied -= OnCharacterDied;
|
||||
GameEvents.OnCashDeposited -= OnCashDeposited;
|
||||
GameEvents.OnVaultStolen -= OnVaultStolen;
|
||||
GameEvents.OnBagPickedUp -= OnBagPickedUp;
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────
|
||||
|
||||
/// <summary>Get stats for a specific character. Creates entry if missing.</summary>
|
||||
public CharacterStats GetStats(GameObject character)
|
||||
{
|
||||
if (character == null) return new CharacterStats { name = "Unknown" };
|
||||
int id = character.GetInstanceID();
|
||||
if (!_stats.ContainsKey(id))
|
||||
{
|
||||
_stats[id] = new CharacterStats
|
||||
{
|
||||
name = character.name.Replace("(Clone)", "").Trim()
|
||||
};
|
||||
}
|
||||
return _stats[id];
|
||||
}
|
||||
|
||||
/// <summary>Get stats for all characters in a list (team).</summary>
|
||||
public List<CharacterStats> GetTeamStats(List<GameObject> teamCharacters)
|
||||
{
|
||||
List<CharacterStats> result = new List<CharacterStats>();
|
||||
if (teamCharacters == null) return result;
|
||||
foreach (var c in teamCharacters)
|
||||
{
|
||||
if (c != null) result.Add(GetStats(c));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Event Handlers ──────────────────────────────────────
|
||||
|
||||
private void OnKill(GameObject killer, GameObject victim)
|
||||
{
|
||||
if (killer != null)
|
||||
GetStats(killer).kills++;
|
||||
if (victim != null)
|
||||
GetStats(victim).deaths++;
|
||||
|
||||
// Award assists to other recent attackers of the victim
|
||||
if (victim != null)
|
||||
{
|
||||
int victimID = victim.GetInstanceID();
|
||||
int killerID = killer != null ? killer.GetInstanceID() : -1;
|
||||
|
||||
if (_recentAttackers.ContainsKey(victimID))
|
||||
{
|
||||
HashSet<int> assistedIDs = new HashSet<int>();
|
||||
foreach (var (attackerID, time) in _recentAttackers[victimID])
|
||||
{
|
||||
if (attackerID != killerID && Time.time - time <= ASSIST_WINDOW && !assistedIDs.Contains(attackerID))
|
||||
{
|
||||
assistedIDs.Add(attackerID);
|
||||
if (_stats.ContainsKey(attackerID))
|
||||
_stats[attackerID].assists++;
|
||||
}
|
||||
}
|
||||
_recentAttackers.Remove(victimID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDamageDealt(GameObject attacker, GameObject victim, float amount)
|
||||
{
|
||||
if (attacker != null)
|
||||
GetStats(attacker).damageDealt += amount;
|
||||
|
||||
// Track for assists
|
||||
if (attacker != null && victim != null)
|
||||
{
|
||||
int victimID = victim.GetInstanceID();
|
||||
int attackerID = attacker.GetInstanceID();
|
||||
|
||||
if (!_recentAttackers.ContainsKey(victimID))
|
||||
_recentAttackers[victimID] = new List<(int, float)>();
|
||||
|
||||
_recentAttackers[victimID].Add((attackerID, Time.time));
|
||||
|
||||
// Prune old entries
|
||||
_recentAttackers[victimID].RemoveAll(e => Time.time - e.time > ASSIST_WINDOW);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCharacterDied(GameObject character)
|
||||
{
|
||||
// Death is already tracked via OnKill, but if OnKill isn't fired for some deaths:
|
||||
if (character != null)
|
||||
{
|
||||
var s = GetStats(character);
|
||||
// Only increment if OnKill hasn't already done it this frame
|
||||
// (simple guard — check if death count was already incremented)
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCashDeposited(string teamTag, float value, int combo)
|
||||
{
|
||||
// Attribute to the character who deposited — find the closest carrier
|
||||
CashSystemManager mgr = CashSystemManager.Instance;
|
||||
if (mgr == null) return;
|
||||
|
||||
var team = mgr.GetTeamCharacters(teamTag);
|
||||
if (team == null) return;
|
||||
|
||||
// Find the character who just deposited (nearest to their vault)
|
||||
TeamVault vault = mgr.GetVault(teamTag);
|
||||
if (vault == null) return;
|
||||
|
||||
float bestDist = float.MaxValue;
|
||||
GameObject depositor = null;
|
||||
foreach (var c in team)
|
||||
{
|
||||
if (c == null) continue;
|
||||
float d = Vector3.Distance(c.transform.position, vault.transform.position);
|
||||
if (d < bestDist)
|
||||
{
|
||||
bestDist = d;
|
||||
depositor = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (depositor != null)
|
||||
GetStats(depositor).cashDeposited += value;
|
||||
}
|
||||
|
||||
private void OnVaultStolen(TeamVault vault, float amount)
|
||||
{
|
||||
// Attribute to the closest enemy character to this vault
|
||||
if (vault == null) return;
|
||||
CashSystemManager mgr = CashSystemManager.Instance;
|
||||
if (mgr == null) return;
|
||||
|
||||
string enemyTag = vault.TeamTag == "Player" ? "Enemy" : "Player";
|
||||
var enemies = mgr.GetTeamCharacters(enemyTag);
|
||||
if (enemies == null) return;
|
||||
|
||||
float bestDist = float.MaxValue;
|
||||
GameObject thief = null;
|
||||
foreach (var c in enemies)
|
||||
{
|
||||
if (c == null) continue;
|
||||
float d = Vector3.Distance(c.transform.position, vault.transform.position);
|
||||
if (d < bestDist)
|
||||
{
|
||||
bestDist = d;
|
||||
thief = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (thief != null)
|
||||
GetStats(thief).cashStolen += amount;
|
||||
}
|
||||
|
||||
private void OnBagPickedUp(CashBag bag, CashCarrier carrier)
|
||||
{
|
||||
if (carrier != null && carrier.gameObject != null)
|
||||
GetStats(carrier.gameObject).cashCollected += bag != null ? bag.CashValue : 0f;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/UI/MatchStatTracker.cs.meta
Normal file
2
Assets/Scripts/CashSystem/UI/MatchStatTracker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d965cc3b37bf114f1a94af15fdcd2151
|
||||
429
Assets/Scripts/CashSystem/UI/PreMatchIntroUI.cs
Normal file
429
Assets/Scripts/CashSystem/UI/PreMatchIntroUI.cs
Normal file
@@ -0,0 +1,429 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// PreMatchIntroUI — "3 vs 3" matchup splash + countdown before gameplay.
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. Show team info (displayDuration seconds)
|
||||
/// 2. Countdown: 3 → 2 → 1 → FIGHT!
|
||||
/// 3. Fade out, set IsComplete = true
|
||||
/// 4. CashSystemManager waits for IsComplete before enabling combat
|
||||
///
|
||||
/// Assign portrait Image slots in the Inspector. The script loads character
|
||||
/// sprites into those Image components by team index.
|
||||
///
|
||||
/// Press Space (or skipButton) to skip intro → fast countdown then complete.
|
||||
///
|
||||
/// Uses CanvasGroup to hide (NOT SetActive(false)) so the script stays alive.
|
||||
/// </summary>
|
||||
public class PreMatchIntroUI : MonoBehaviour
|
||||
{
|
||||
[Header("Panel")]
|
||||
[SerializeField] private GameObject rootPanel;
|
||||
|
||||
[Header("Player Team (Left) — Assign portrait Image slots")]
|
||||
[SerializeField] private Image[] playerPortraits = new Image[3];
|
||||
[SerializeField] private TextMeshProUGUI[] playerNames = new TextMeshProUGUI[3];
|
||||
|
||||
[Header("Enemy Team (Right) — Assign portrait Image slots")]
|
||||
[SerializeField] private Image[] enemyPortraits = new Image[3];
|
||||
[SerializeField] private TextMeshProUGUI[] enemyNames = new TextMeshProUGUI[3];
|
||||
|
||||
[Header("Centre")]
|
||||
[SerializeField] private TextMeshProUGUI vsText;
|
||||
|
||||
[Header("Countdown")]
|
||||
[Tooltip("Text element for countdown (3, 2, 1, FIGHT!)")]
|
||||
[SerializeField] private TextMeshProUGUI countdownText;
|
||||
|
||||
[Header("Skip")]
|
||||
[Tooltip("Optional skip button — user can also press Space")]
|
||||
[SerializeField] private Button skipButton;
|
||||
[Tooltip("Text shown at bottom: 'Press Space to Skip'")]
|
||||
[SerializeField] private TextMeshProUGUI skipHintText;
|
||||
|
||||
[Header("Game Mode Info (optional)")]
|
||||
[SerializeField] private TextMeshProUGUI modeTitle;
|
||||
[SerializeField] private TextMeshProUGUI objectiveText;
|
||||
|
||||
[Header("Timing")]
|
||||
[SerializeField] private float displayDuration = 3.5f;
|
||||
[SerializeField] private float fadeInDuration = 0.4f;
|
||||
[SerializeField] private float fadeOutDuration = 0.5f;
|
||||
[SerializeField] private float countdownStepTime = 1f;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] private AudioClip introSFX;
|
||||
[SerializeField] private AudioClip countdownBeepSFX;
|
||||
[SerializeField] private AudioClip fightSFX;
|
||||
[SerializeField] private float sfxVolume = 1f;
|
||||
|
||||
[Header("Colors")]
|
||||
[SerializeField] private Color playerTeamColor = new Color(0.13f, 0.59f, 0.95f);
|
||||
[SerializeField] private Color enemyTeamColor = new Color(0.96f, 0.26f, 0.21f);
|
||||
[SerializeField] private Color countdownColor = Color.white;
|
||||
[SerializeField] private Color fightColor = new Color(1f, 0.84f, 0f); // Gold
|
||||
|
||||
// runtime
|
||||
private AudioSource _audio;
|
||||
private CanvasGroup _canvasGroup;
|
||||
private Coroutine _sequenceCoroutine;
|
||||
private bool _skipRequested;
|
||||
|
||||
/// <summary>True once intro sequence (including countdown) is fully done.</summary>
|
||||
public bool IsComplete { get; private set; }
|
||||
|
||||
private static PreMatchIntroUI _instance;
|
||||
public static PreMatchIntroUI Instance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a PreMatchIntroUI instance exists in the scene.
|
||||
/// Finds inactive instances first, or creates a minimal one on the existing Canvas.
|
||||
/// Call from CashSystemManager.Start() so the intro always works.
|
||||
/// </summary>
|
||||
public static void EnsureExists()
|
||||
{
|
||||
if (_instance != null) return;
|
||||
|
||||
// 1. Try to find an inactive instance already in the scene
|
||||
PreMatchIntroUI[] all = Resources.FindObjectsOfTypeAll<PreMatchIntroUI>();
|
||||
foreach (var p in all)
|
||||
{
|
||||
// Skip assets (prefabs) — only want scene objects
|
||||
if (p.gameObject.scene.name == null) continue;
|
||||
// Activate parents top-down first, then the object itself
|
||||
// (Awake only fires when the ENTIRE chain is active)
|
||||
List<GameObject> chain = new List<GameObject>();
|
||||
Transform t = p.transform;
|
||||
while (t != null)
|
||||
{
|
||||
if (!t.gameObject.activeSelf)
|
||||
chain.Add(t.gameObject);
|
||||
t = t.parent;
|
||||
}
|
||||
// Activate top-down
|
||||
for (int i = chain.Count - 1; i >= 0; i--)
|
||||
chain[i].SetActive(true);
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
Debug.Log("[PreMatchIntroUI] 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("PreMatchIntroUI_Auto");
|
||||
panelRoot.transform.SetParent(canvas.transform, false);
|
||||
|
||||
// Stretch to fill canvas
|
||||
RectTransform rt = panelRoot.AddComponent<RectTransform>();
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
|
||||
// Background image (dark overlay)
|
||||
Image bg = panelRoot.AddComponent<Image>();
|
||||
bg.color = new Color(0.05f, 0.05f, 0.1f, 0.85f);
|
||||
|
||||
// Countdown text in centre
|
||||
GameObject textObj = new GameObject("CountdownText");
|
||||
textObj.transform.SetParent(panelRoot.transform, false);
|
||||
RectTransform trt = textObj.AddComponent<RectTransform>();
|
||||
trt.anchorMin = new Vector2(0.3f, 0.3f);
|
||||
trt.anchorMax = new Vector2(0.7f, 0.7f);
|
||||
trt.offsetMin = Vector2.zero;
|
||||
trt.offsetMax = Vector2.zero;
|
||||
TextMeshProUGUI txt = textObj.AddComponent<TextMeshProUGUI>();
|
||||
txt.text = "";
|
||||
txt.fontSize = 120;
|
||||
txt.alignment = TextAlignmentOptions.Center;
|
||||
txt.color = Color.white;
|
||||
|
||||
// VS text
|
||||
GameObject vsObj = new GameObject("VSText");
|
||||
vsObj.transform.SetParent(panelRoot.transform, false);
|
||||
RectTransform vrt = vsObj.AddComponent<RectTransform>();
|
||||
vrt.anchorMin = new Vector2(0.35f, 0.55f);
|
||||
vrt.anchorMax = new Vector2(0.65f, 0.75f);
|
||||
vrt.offsetMin = Vector2.zero;
|
||||
vrt.offsetMax = Vector2.zero;
|
||||
TextMeshProUGUI vsTxt = vsObj.AddComponent<TextMeshProUGUI>();
|
||||
vsTxt.text = "VS";
|
||||
vsTxt.fontSize = 80;
|
||||
vsTxt.alignment = TextAlignmentOptions.Center;
|
||||
vsTxt.color = Color.yellow;
|
||||
|
||||
PreMatchIntroUI intro = panelRoot.AddComponent<PreMatchIntroUI>();
|
||||
intro.countdownText = txt;
|
||||
intro.vsText = vsTxt;
|
||||
|
||||
Debug.Log("[PreMatchIntroUI] Auto-created minimal intro panel on Canvas");
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
// Hide via CanvasGroup — keep MonoBehaviour alive for Show() calls
|
||||
HideImmediate();
|
||||
|
||||
// Wire skip button
|
||||
if (skipButton != null)
|
||||
skipButton.onClick.AddListener(Skip);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Space key skip
|
||||
if (!IsComplete && Input.GetKeyDown(KeyCode.Space))
|
||||
_skipRequested = true;
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────
|
||||
|
||||
public void Show()
|
||||
{
|
||||
Debug.Log("[PreMatchIntroUI] Show() called");
|
||||
IsComplete = false;
|
||||
_skipRequested = false;
|
||||
|
||||
rootPanel.SetActive(true);
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
|
||||
// Populate from SelectionOptions
|
||||
string[] playerTeam = SelectionOptions.Instance != null ? SelectionOptions.Instance.playerTeam : null;
|
||||
string[] enemyTeam = SelectionOptions.Instance != null ? SelectionOptions.Instance.enemyTeam : null;
|
||||
|
||||
PopulateSide(playerTeam, playerPortraits, playerNames, playerTeamColor);
|
||||
PopulateSide(enemyTeam, enemyPortraits, enemyNames, enemyTeamColor);
|
||||
|
||||
if (vsText != null) vsText.text = "VS";
|
||||
|
||||
// Game mode info
|
||||
if (modeTitle != null) modeTitle.text = "CASH SYSTEM";
|
||||
if (objectiveText != null) objectiveText.text = "Collect cash and deposit it in your vault!";
|
||||
|
||||
// Skip hint
|
||||
if (skipHintText != null) skipHintText.text = "Press Space to Skip";
|
||||
|
||||
// Clear countdown at start
|
||||
if (countdownText != null) countdownText.text = "";
|
||||
|
||||
// Play intro sound
|
||||
if (introSFX != null) _audio.PlayOneShot(introSFX, sfxVolume);
|
||||
|
||||
// Start the full intro sequence (fade in → display → countdown → fade out)
|
||||
if (_sequenceCoroutine != null) StopCoroutine(_sequenceCoroutine);
|
||||
_sequenceCoroutine = StartCoroutine(IntroSequence());
|
||||
}
|
||||
|
||||
/// <summary>Force hide — called if CashSystemManager needs to tear down early.</summary>
|
||||
public void Hide()
|
||||
{
|
||||
if (_sequenceCoroutine != null) StopCoroutine(_sequenceCoroutine);
|
||||
HideImmediate();
|
||||
IsComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>Skip the intro — jumps to fast countdown then completes.</summary>
|
||||
public void Skip()
|
||||
{
|
||||
_skipRequested = true;
|
||||
}
|
||||
|
||||
// ─── Intro Sequence ──────────────────────────────────────
|
||||
|
||||
private IEnumerator IntroSequence()
|
||||
{
|
||||
// Phase 1: Fade in
|
||||
yield return StartCoroutine(FadeIn());
|
||||
|
||||
// Phase 2: Display team info for displayDuration (or until skip)
|
||||
float elapsed = 0f;
|
||||
while (elapsed < displayDuration && !_skipRequested)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Phase 3: Countdown (3, 2, 1, FIGHT!)
|
||||
// If skip was requested, do a fast countdown
|
||||
float stepTime = _skipRequested ? 0.25f : countdownStepTime;
|
||||
|
||||
// Hide skip hint during countdown
|
||||
if (skipHintText != null) skipHintText.text = "";
|
||||
|
||||
string[] steps = { "3", "2", "1", "FIGHT!" };
|
||||
foreach (string step in steps)
|
||||
{
|
||||
if (countdownText != null)
|
||||
{
|
||||
countdownText.text = step;
|
||||
countdownText.color = (step == "FIGHT!") ? fightColor : countdownColor;
|
||||
|
||||
// Scale punch — make text pop
|
||||
countdownText.transform.localScale = Vector3.one * 1.5f;
|
||||
float scaleTimer = 0f;
|
||||
float scaleDuration = stepTime * 0.6f;
|
||||
while (scaleTimer < scaleDuration)
|
||||
{
|
||||
scaleTimer += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(scaleTimer / scaleDuration);
|
||||
countdownText.transform.localScale = Vector3.Lerp(
|
||||
Vector3.one * 1.5f, Vector3.one, t);
|
||||
yield return null;
|
||||
}
|
||||
countdownText.transform.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
// Fire event so other systems (like existing countdown UI) can react
|
||||
GameEvents.FireCountdownTick(step);
|
||||
|
||||
// SFX
|
||||
if (step == "FIGHT!")
|
||||
{
|
||||
if (fightSFX != null) _audio.PlayOneShot(fightSFX, sfxVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (countdownBeepSFX != null) _audio.PlayOneShot(countdownBeepSFX, sfxVolume);
|
||||
}
|
||||
|
||||
// If skipping again during countdown, accelerate further
|
||||
if (_skipRequested) stepTime = 0.15f;
|
||||
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0.05f, stepTime - (stepTime * 0.6f)));
|
||||
}
|
||||
|
||||
// Brief hold on "FIGHT!" before fading
|
||||
yield return new WaitForSecondsRealtime(_skipRequested ? 0.2f : 0.8f);
|
||||
|
||||
// Clear countdown text
|
||||
if (countdownText != null) countdownText.text = "";
|
||||
GameEvents.FireCountdownTick("");
|
||||
|
||||
// Phase 4: Fade out
|
||||
yield return StartCoroutine(FadeOut());
|
||||
|
||||
// Done — CashSystemManager checks this to enable combat
|
||||
IsComplete = true;
|
||||
Debug.Log("[PreMatchIntroUI] Intro complete");
|
||||
}
|
||||
|
||||
// ─── Population ──────────────────────────────────────────
|
||||
|
||||
private void PopulateSide(string[] names, Image[] portraits, TextMeshProUGUI[] labels, Color teamColor)
|
||||
{
|
||||
bool isPlayerSide = (portraits == playerPortraits);
|
||||
string sideLabel = isPlayerSide ? "Player" : "Enemy";
|
||||
|
||||
// Get portrait sprites stored from the selection screen (via SelectionOptions)
|
||||
Sprite[] teamSprites = null;
|
||||
if (SelectionOptions.Instance != null)
|
||||
teamSprites = isPlayerSide
|
||||
? SelectionOptions.Instance.playerTeamSprites
|
||||
: SelectionOptions.Instance.enemyTeamSprites;
|
||||
|
||||
Debug.Log($"[PreMatchIntroUI] PopulateSide({sideLabel}) — names: {(names != null ? string.Join(", ", names) : "null")}, sprites: {(teamSprites != null ? teamSprites.Length.ToString() : "null")}");
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
bool hasChar = names != null && i < names.Length && !string.IsNullOrEmpty(names[i]);
|
||||
|
||||
if (portraits != null && i < portraits.Length && portraits[i] != null)
|
||||
{
|
||||
if (hasChar)
|
||||
{
|
||||
// Use the sprite stored from the selection screen
|
||||
Sprite portrait = (teamSprites != null && i < teamSprites.Length) ? teamSprites[i] : null;
|
||||
|
||||
if (portrait != null)
|
||||
{
|
||||
portraits[i].sprite = portrait;
|
||||
portraits[i].color = Color.white;
|
||||
portraits[i].preserveAspect = true;
|
||||
Debug.Log($"[PreMatchIntroUI] {sideLabel}[{i}] portrait set for {names[i]} ({portrait.name})");
|
||||
}
|
||||
else
|
||||
{
|
||||
portraits[i].color = teamColor * 0.6f;
|
||||
Debug.LogWarning($"[PreMatchIntroUI] {sideLabel}[{i}] no portrait for {names[i]}, using team color");
|
||||
}
|
||||
portraits[i].gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
portraits[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (labels != null && i < labels.Length && labels[i] != null)
|
||||
{
|
||||
labels[i].text = hasChar ? names[i].ToUpper() : "";
|
||||
labels[i].color = teamColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Fade / Hide ─────────────────────────────────────────
|
||||
|
||||
private void HideImmediate()
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
private IEnumerator FadeIn()
|
||||
{
|
||||
float elapsed = 0f;
|
||||
while (elapsed < fadeInDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
_canvasGroup.alpha = Mathf.Clamp01(elapsed / fadeInDuration);
|
||||
yield return null;
|
||||
}
|
||||
_canvasGroup.alpha = 1f;
|
||||
}
|
||||
|
||||
private IEnumerator FadeOut()
|
||||
{
|
||||
float elapsed = 0f;
|
||||
float startAlpha = _canvasGroup.alpha;
|
||||
while (elapsed < fadeOutDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
_canvasGroup.alpha = Mathf.Lerp(startAlpha, 0f, elapsed / fadeOutDuration);
|
||||
yield return null;
|
||||
}
|
||||
HideImmediate();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/UI/PreMatchIntroUI.cs.meta
Normal file
2
Assets/Scripts/CashSystem/UI/PreMatchIntroUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 124c27138f4bba3059796c7a104e7b04
|
||||
368
Assets/Scripts/CashSystem/UI/TeamHUDPanel.cs
Normal file
368
Assets/Scripts/CashSystem/UI/TeamHUDPanel.cs
Normal file
@@ -0,0 +1,368 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// TeamHUDPanel — FRAG-style circular portrait HUD showing team health and character switching.
|
||||
///
|
||||
/// SETUP (Inspector):
|
||||
/// 1. Create your HUD layout manually — place circular Image slots wherever you want.
|
||||
/// 2. Assign them into the lists below:
|
||||
/// - characterPortraits[] — Image where the character icon/portrait sprite will go
|
||||
/// - healthDialImages[] — Image used as a radial-fill health ring around each portrait
|
||||
/// - dialBackgrounds[] — (optional) background ring images behind the health dials
|
||||
/// - nameLabels[] — (optional) TMP text for character names
|
||||
/// 3. The script fills sprites and updates health dials every frame.
|
||||
/// 4. Click/tap a portrait to switch control to that character.
|
||||
///
|
||||
/// Uses CanvasGroup to hide initially — stays alive for Initialize() call.
|
||||
/// </summary>
|
||||
public class TeamHUDPanel : MonoBehaviour
|
||||
{
|
||||
[Header("Character Portraits (assign in order: index 0 = team slot 0, etc.)")]
|
||||
[Tooltip("Image components where character icon sprites will be placed")]
|
||||
[SerializeField] private List<Image> characterPortraits = new List<Image>();
|
||||
|
||||
[Header("Health Dial Rings (same index order as portraits)")]
|
||||
[Tooltip("Image components used as radial-fill health rings — set to Filled/Radial360 at runtime")]
|
||||
[SerializeField] private List<Image> healthDialImages = new List<Image>();
|
||||
|
||||
[Header("Dial Backgrounds (optional, same index order)")]
|
||||
[Tooltip("Background ring images behind each health dial")]
|
||||
[SerializeField] private List<Image> dialBackgrounds = new List<Image>();
|
||||
|
||||
[Header("Name Labels (optional, same index order)")]
|
||||
[SerializeField] private List<TextMeshProUGUI> nameLabels = new List<TextMeshProUGUI>();
|
||||
|
||||
[Header("Dead Overlay (optional, same index order)")]
|
||||
[Tooltip("GameObjects to show when a character is dead (e.g. a dark overlay with X)")]
|
||||
[SerializeField] private List<GameObject> deadOverlays = new List<GameObject>();
|
||||
|
||||
[Header("Highlight Border (optional, same index order)")]
|
||||
[Tooltip("Image used as a highlight border — only visible on the active character")]
|
||||
[SerializeField] private List<Image> highlightBorders = new List<Image>();
|
||||
|
||||
[Header("Dial Sprite")]
|
||||
[Tooltip("Sprite for the health dial ring. If empty, tries to copy from ImportUIPanel's ProgressDial.")]
|
||||
[SerializeField] private Sprite dialSprite;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] private AudioClip switchSFX;
|
||||
[SerializeField] private AudioClip buttonHoverSFX;
|
||||
[SerializeField] private float sfxVolume = 1f;
|
||||
|
||||
[Header("Colors")]
|
||||
[SerializeField] private Color healthFullColor = new Color(0.30f, 0.87f, 0.30f, 1f);
|
||||
[SerializeField] private Color healthLowColor = new Color(0.96f, 0.26f, 0.21f, 1f);
|
||||
[SerializeField] private Color highlightColor = new Color(1f, 0.84f, 0f, 1f);
|
||||
[SerializeField] private Color defaultBorderColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
|
||||
|
||||
// runtime
|
||||
private List<GameObject> _characters = new List<GameObject>();
|
||||
private List<HealthNew> _healthComps = new List<HealthNew>();
|
||||
private int _currentHighlight = -1;
|
||||
private CanvasGroup _canvasGroup;
|
||||
private AudioSource _audio;
|
||||
|
||||
private static TeamHUDPanel _instance;
|
||||
public static TeamHUDPanel Instance => _instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_canvasGroup == null) _canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
|
||||
_audio = GetComponent<AudioSource>();
|
||||
if (_audio == null) _audio = gameObject.AddComponent<AudioSource>();
|
||||
_audio.playOnAwake = false;
|
||||
_audio.spatialBlend = 0f;
|
||||
|
||||
// Hide via CanvasGroup until Initialize is called
|
||||
HideImmediate();
|
||||
|
||||
// Try to find dial sprite from ImportUIPanel if not assigned
|
||||
if (dialSprite == null)
|
||||
{
|
||||
var importPanel = GameObject.Find("ImportUIPanel");
|
||||
if (importPanel != null)
|
||||
{
|
||||
var dial = importPanel.transform.Find("ProgressDial");
|
||||
if (dial != null)
|
||||
{
|
||||
var img = dial.GetComponent<Image>();
|
||||
if (img != null) dialSprite = img.sprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
GameEvents.OnCharacterSwitched += OnCharacterSwitched;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
GameEvents.OnCharacterSwitched -= OnCharacterSwitched;
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the HUD with the player team characters.
|
||||
/// Call after characters are spawned (from CashSystemManager.SpawnTeams).
|
||||
/// </summary>
|
||||
public void Initialize(List<GameObject> playerTeamCharacters, int currentControlledIndex)
|
||||
{
|
||||
DevLog.Log($"[TeamHUDPanel] Initialize with {playerTeamCharacters?.Count ?? 0} characters, active={currentControlledIndex}");
|
||||
|
||||
_characters.Clear();
|
||||
_healthComps.Clear();
|
||||
|
||||
if (playerTeamCharacters == null || playerTeamCharacters.Count == 0) return;
|
||||
|
||||
int count = Mathf.Min(playerTeamCharacters.Count, characterPortraits.Count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
GameObject character = playerTeamCharacters[i];
|
||||
_characters.Add(character);
|
||||
_healthComps.Add(character != null ? character.GetComponent<HealthNew>() : null);
|
||||
|
||||
// Set portrait sprite
|
||||
if (i < characterPortraits.Count && characterPortraits[i] != null && character != null)
|
||||
{
|
||||
Sprite icon = GetCharacterIcon(character);
|
||||
if (icon != null)
|
||||
{
|
||||
characterPortraits[i].sprite = icon;
|
||||
characterPortraits[i].color = Color.white;
|
||||
characterPortraits[i].preserveAspect = true;
|
||||
}
|
||||
|
||||
// Add click handler
|
||||
int capturedIndex = i;
|
||||
Button btn = characterPortraits[i].GetComponent<Button>();
|
||||
if (btn == null) btn = characterPortraits[i].gameObject.AddComponent<Button>();
|
||||
btn.transition = Selectable.Transition.None;
|
||||
btn.onClick.RemoveAllListeners();
|
||||
btn.onClick.AddListener(() => OnSlotClicked(capturedIndex));
|
||||
}
|
||||
|
||||
// Configure health dial as radial fill
|
||||
if (i < healthDialImages.Count && healthDialImages[i] != null)
|
||||
{
|
||||
Image dial = healthDialImages[i];
|
||||
if (dialSprite != null) dial.sprite = dialSprite;
|
||||
dial.type = Image.Type.Filled;
|
||||
dial.fillMethod = Image.FillMethod.Radial360;
|
||||
dial.fillOrigin = (int)Image.Origin360.Top;
|
||||
dial.fillClockwise = true;
|
||||
dial.fillAmount = 1f;
|
||||
dial.color = healthFullColor;
|
||||
}
|
||||
|
||||
// Set name label
|
||||
if (i < nameLabels.Count && nameLabels[i] != null && character != null)
|
||||
{
|
||||
nameLabels[i].text = GetCharacterName(character);
|
||||
}
|
||||
|
||||
// Hide dead overlay
|
||||
if (i < deadOverlays.Count && deadOverlays[i] != null)
|
||||
{
|
||||
deadOverlays[i].SetActive(false);
|
||||
}
|
||||
|
||||
// Default border
|
||||
if (i < highlightBorders.Count && highlightBorders[i] != null)
|
||||
{
|
||||
highlightBorders[i].color = defaultBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide any extra slots beyond team size
|
||||
for (int i = count; i < characterPortraits.Count; i++)
|
||||
{
|
||||
if (characterPortraits[i] != null) characterPortraits[i].gameObject.SetActive(false);
|
||||
if (i < healthDialImages.Count && healthDialImages[i] != null) healthDialImages[i].gameObject.SetActive(false);
|
||||
if (i < dialBackgrounds.Count && dialBackgrounds[i] != null) dialBackgrounds[i].gameObject.SetActive(false);
|
||||
if (i < nameLabels.Count && nameLabels[i] != null) nameLabels[i].gameObject.SetActive(false);
|
||||
if (i < deadOverlays.Count && deadOverlays[i] != null) deadOverlays[i].SetActive(false);
|
||||
if (i < highlightBorders.Count && highlightBorders[i] != null) highlightBorders[i].gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
SetHighlight(currentControlledIndex);
|
||||
|
||||
// Show panel
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
/// <summary>Update health dials + keyboard shortcuts. Health polling throttled to ~10Hz.</summary>
|
||||
private void Update()
|
||||
{
|
||||
// Keyboard shortcuts: 1, 2, 3 to switch characters (always responsive)
|
||||
if (_characters.Count > 0 && _canvasGroup.alpha > 0f)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Alpha1) && _characters.Count > 0) OnSlotClicked(0);
|
||||
if (Input.GetKeyDown(KeyCode.Alpha2) && _characters.Count > 1) OnSlotClicked(1);
|
||||
if (Input.GetKeyDown(KeyCode.Alpha3) && _characters.Count > 2) OnSlotClicked(2);
|
||||
}
|
||||
|
||||
// Throttle health polling to every 6th frame (~10Hz at 60fps)
|
||||
// Health bars update smoothly enough at 10Hz — indistinguishable from 60Hz
|
||||
if (Time.frameCount % 6 != 0) return;
|
||||
|
||||
for (int i = 0; i < _healthComps.Count; i++)
|
||||
{
|
||||
HealthNew h = _healthComps[i];
|
||||
if (h == null) continue;
|
||||
|
||||
// Health ring fill
|
||||
float maxHP = h.maxHealth > 0 ? h.maxHealth : 100f;
|
||||
float pct = Mathf.Clamp01((float)h.currentHealth / maxHP);
|
||||
|
||||
if (i < healthDialImages.Count && healthDialImages[i] != null)
|
||||
{
|
||||
healthDialImages[i].fillAmount = pct;
|
||||
healthDialImages[i].color = Color.Lerp(healthLowColor, healthFullColor, pct);
|
||||
}
|
||||
|
||||
// Dead overlay
|
||||
bool dead = h.isDead;
|
||||
if (i < deadOverlays.Count && deadOverlays[i] != null)
|
||||
{
|
||||
if (deadOverlays[i].activeSelf != dead)
|
||||
deadOverlays[i].SetActive(dead);
|
||||
}
|
||||
|
||||
// Disable click on dead characters
|
||||
if (i < characterPortraits.Count && characterPortraits[i] != null)
|
||||
{
|
||||
Button btn = characterPortraits[i].GetComponent<Button>();
|
||||
if (btn != null) btn.interactable = !dead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Events ──────────────────────────────────────────────
|
||||
|
||||
private void OnCharacterSwitched(GameObject oldChar, GameObject newChar)
|
||||
{
|
||||
if (newChar == null) return;
|
||||
for (int i = 0; i < _characters.Count; i++)
|
||||
{
|
||||
if (_characters[i] == newChar)
|
||||
{
|
||||
SetHighlight(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHighlight(int index)
|
||||
{
|
||||
// Remove old highlight
|
||||
if (_currentHighlight >= 0 && _currentHighlight < highlightBorders.Count
|
||||
&& highlightBorders[_currentHighlight] != null)
|
||||
{
|
||||
highlightBorders[_currentHighlight].color = defaultBorderColor;
|
||||
}
|
||||
if (_currentHighlight >= 0 && _currentHighlight < characterPortraits.Count
|
||||
&& characterPortraits[_currentHighlight] != null)
|
||||
{
|
||||
characterPortraits[_currentHighlight].transform.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
_currentHighlight = index;
|
||||
|
||||
// Apply new highlight
|
||||
if (index >= 0 && index < highlightBorders.Count && highlightBorders[index] != null)
|
||||
{
|
||||
highlightBorders[index].color = highlightColor;
|
||||
}
|
||||
if (index >= 0 && index < characterPortraits.Count && characterPortraits[index] != null)
|
||||
{
|
||||
characterPortraits[index].transform.localScale = Vector3.one * 1.1f;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotClicked(int index)
|
||||
{
|
||||
if (index < 0 || index >= _characters.Count) return;
|
||||
if (index < _healthComps.Count && _healthComps[index] != null && _healthComps[index].isDead) return;
|
||||
if (index == _currentHighlight) return;
|
||||
|
||||
// Play switch SFX
|
||||
if (switchSFX != null && _audio != null)
|
||||
_audio.PlayOneShot(switchSFX, sfxVolume);
|
||||
|
||||
CashSystemManager mgr = CashSystemManager.Instance;
|
||||
if (mgr != null)
|
||||
{
|
||||
mgr.SwitchToCharacterAtIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void HideImmediate()
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
// ─── Utility ─────────────────────────────────────────────
|
||||
|
||||
private Sprite GetCharacterIcon(GameObject character)
|
||||
{
|
||||
if (character == null) return null;
|
||||
|
||||
var ai = character.GetComponent<CharacterAIController>();
|
||||
if (ai != null)
|
||||
{
|
||||
var field = typeof(CharacterAIController)
|
||||
.GetField("characterStats",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
{
|
||||
CharacterStats stats = field.GetValue(ai) as CharacterStats;
|
||||
if (stats != null)
|
||||
{
|
||||
if (stats.characterIcon != null) return stats.characterIcon;
|
||||
if (stats.characterPortrait != null) return stats.characterPortrait;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string cleanName = character.name.Replace("(Clone)", "").Trim();
|
||||
return Resources.Load<Sprite>($"CharacterPortraits/{cleanName}");
|
||||
}
|
||||
|
||||
private string GetCharacterName(GameObject character)
|
||||
{
|
||||
if (character == null) return "?";
|
||||
|
||||
var ai = character.GetComponent<CharacterAIController>();
|
||||
if (ai != null)
|
||||
{
|
||||
var field = typeof(CharacterAIController)
|
||||
.GetField("characterStats",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
{
|
||||
CharacterStats stats = field.GetValue(ai) as CharacterStats;
|
||||
if (stats != null && !string.IsNullOrEmpty(stats.displayName))
|
||||
return stats.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
return character.name.Replace("(Clone)", "").Trim();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/UI/TeamHUDPanel.cs.meta
Normal file
2
Assets/Scripts/CashSystem/UI/TeamHUDPanel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56699a52ec9e6fe46aaae72918a28303
|
||||
Reference in New Issue
Block a user