chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View 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;
}
}