using UnityEngine; using System.Collections.Generic; /// /// 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. /// public class MatchStatTracker : MonoBehaviour { /// Stats for one character. 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 _stats = new Dictionary(); // Track recent attackers for assist detection (victimID → list of attackerIDs within last N seconds) private Dictionary> _recentAttackers = new Dictionary>(); 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 ────────────────────────────────────────── /// Get stats for a specific character. Creates entry if missing. 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]; } /// Get stats for all characters in a list (team). public List GetTeamStats(List teamCharacters) { List result = new List(); 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 assistedIDs = new HashSet(); 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(CashMag bag, CashCarrier carrier) { if (carrier != null && carrier.gameObject != null) GetStats(carrier.gameObject).cashCollected += bag != null ? bag.CashValue : 0f; } }