223 lines
7.6 KiB
C#
223 lines
7.6 KiB
C#
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;
|
|
}
|
|
}
|