1632 lines
66 KiB
C#
1632 lines
66 KiB
C#
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.InputSystem;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// CashSystemManager - Main controller for Cash System game mode.
|
|
/// Handles team spawning, win conditions, and game flow.
|
|
/// </summary>
|
|
public class CashSystemManager : MonoBehaviour
|
|
{
|
|
[Header("Match Settings")]
|
|
[SerializeField] private float matchDuration = 300f; // 5 minutes
|
|
[SerializeField] private float targetCashToWin = 2000f;
|
|
[SerializeField] private bool endOnThreshold = true;
|
|
|
|
[Header("Team Spawn Points")]
|
|
[SerializeField] private Transform[] playerTeamSpawnPoints;
|
|
[SerializeField] private Transform[] enemyTeamSpawnPoints;
|
|
|
|
[Header("Vault References")]
|
|
[SerializeField] private TeamVault playerVault;
|
|
[SerializeField] private TeamVault enemyVault;
|
|
|
|
[Header("Cashout Stations (Extraction Mode)")]
|
|
[SerializeField] private CashoutStation playerStation;
|
|
[SerializeField] private CashoutStation enemyStation;
|
|
|
|
[Header("Extraction Mode Settings")]
|
|
[SerializeField] private float killReward = 200f; // $200 per kill
|
|
[SerializeField] private float teamWipePenaltyPercent = 0.30f; // -30% cash on team wipe
|
|
[SerializeField] private float respawnTime = 10f; // 10s respawn timer
|
|
[SerializeField] private GameObject vaultSpawnerPrefab; // Optional prefab for vault spawner
|
|
|
|
[Header("Character Switching")]
|
|
[SerializeField] private KeyCode switchCharacterKey = KeyCode.Tab;
|
|
[SerializeField] private float switchCooldown = 1f;
|
|
|
|
[Header("UI")]
|
|
[SerializeField] private GameObject victoryScreen;
|
|
[SerializeField] private GameObject defeatScreen;
|
|
|
|
[Header("Buzzer Beater")]
|
|
[SerializeField] private float buzzerBeaterWindow = 15f;
|
|
[SerializeField] private float buzzerSlowMotion = 0.6f;
|
|
|
|
// Runtime state
|
|
private List<GameObject> playerTeamCharacters = new List<GameObject>();
|
|
private List<GameObject> enemyTeamCharacters = new List<GameObject>();
|
|
private int currentControlledIndex = 0;
|
|
private float matchTimer;
|
|
private bool matchEnded = false;
|
|
private float lastSwitchTime = 0f;
|
|
private bool _buzzerActive;
|
|
|
|
public float MatchTimer => matchTimer;
|
|
public float MatchDuration => matchDuration;
|
|
public bool MatchEnded => matchEnded;
|
|
public GameObject CurrentControlledCharacter =>
|
|
currentControlledIndex < playerTeamCharacters.Count ? playerTeamCharacters[currentControlledIndex] : null;
|
|
|
|
private static CashSystemManager instance;
|
|
public static CashSystemManager Instance => instance;
|
|
|
|
private void Awake()
|
|
{
|
|
if (instance != null && instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
instance = this;
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
GameEvents.OnCashDeposited += OnCashDepositedEvent;
|
|
GameEvents.OnVaultStolen += OnVaultStolenEvent;
|
|
GameEvents.OnKill += OnKillEvent;
|
|
GameEvents.OnCharacterDied += OnCharacterDiedEvent;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
GameEvents.OnCashDeposited -= OnCashDepositedEvent;
|
|
GameEvents.OnVaultStolen -= OnVaultStolenEvent;
|
|
GameEvents.OnKill -= OnKillEvent;
|
|
GameEvents.OnCharacterDied -= OnCharacterDiedEvent;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
// Only activate in Cash System mode
|
|
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
|
{
|
|
DevLog.Log("[CashSystemManager] Not in Cash System mode, disabling.");
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
DevLog.Log("[CashSystemManager] Cash System mode active, initializing...");
|
|
|
|
// Ensure TeamControlManager exists
|
|
if (TeamControlManager.Instance == null)
|
|
{
|
|
GameObject tcmObj = new GameObject("TeamControlManager");
|
|
tcmObj.AddComponent<TeamControlManager>();
|
|
DevLog.Log("[CashSystemManager] Created TeamControlManager");
|
|
}
|
|
|
|
// Ensure AudioListenerManager exists for sound to work
|
|
if (AudioListenerManager.Instance == null)
|
|
{
|
|
GameObject almObj = new GameObject("AudioListenerManager");
|
|
almObj.AddComponent<AudioListenerManager>();
|
|
DevLog.Log("[CashSystemManager] Created AudioListenerManager");
|
|
}
|
|
|
|
// Ensure TeamIndicatorManager exists for visual team differentiation
|
|
if (TeamIndicatorManager.Instance == null)
|
|
{
|
|
GameObject timObj = new GameObject("TeamIndicatorManager");
|
|
timObj.AddComponent<TeamIndicatorManager>();
|
|
DevLog.Log("[CashSystemManager] Created TeamIndicatorManager");
|
|
}
|
|
|
|
// Ensure MinimapMarkerManager exists for minimap team markers
|
|
if (MinimapMarkerManager.Instance == null)
|
|
{
|
|
GameObject mmmObj = new GameObject("MinimapMarkerManager");
|
|
mmmObj.AddComponent<MinimapMarkerManager>();
|
|
DevLog.Log("[CashSystemManager] Created MinimapMarkerManager");
|
|
}
|
|
|
|
// Find vaults if not assigned
|
|
if (playerVault == null || enemyVault == null)
|
|
{
|
|
TeamVault[] vaults = FindObjectsOfType<TeamVault>();
|
|
foreach (TeamVault vault in vaults)
|
|
{
|
|
if (vault.TeamTag == "Player") playerVault = vault;
|
|
else if (vault.TeamTag == "Enemy") enemyVault = vault;
|
|
}
|
|
}
|
|
|
|
// Find CashoutStations if not assigned (extraction mode)
|
|
if (playerStation == null || enemyStation == null)
|
|
{
|
|
CashoutStation[] stations = FindObjectsOfType<CashoutStation>();
|
|
foreach (CashoutStation station in stations)
|
|
{
|
|
if (station.TeamTag == "Player") playerStation = station;
|
|
else if (station.TeamTag == "Enemy") enemyStation = station;
|
|
}
|
|
}
|
|
|
|
// Ensure VaultSpawner exists for extraction mode
|
|
if (FindObjectOfType<VaultSpawner>() == null)
|
|
{
|
|
GameObject spawnerObj = new GameObject("VaultSpawner");
|
|
spawnerObj.AddComponent<VaultSpawner>();
|
|
DevLog.Log("[CashSystemManager] Created VaultSpawner");
|
|
}
|
|
|
|
matchTimer = matchDuration;
|
|
|
|
// Initialize UI with vault values
|
|
StartCoroutine(InitializeUIDelayed());
|
|
|
|
// Ensure UI panels exist (activates inactive instances or auto-creates minimal ones)
|
|
PreMatchIntroUI.EnsureExists();
|
|
MatchResultUI.EnsureExists();
|
|
|
|
// Show pre-match intro before spawning
|
|
if (PreMatchIntroUI.Instance != null)
|
|
{
|
|
PreMatchIntroUI.Instance.Show();
|
|
DevLog.Log("[CashSystemManager] PreMatchIntroUI.Show() called");
|
|
}
|
|
else
|
|
{
|
|
DevLog.LogWarning("[CashSystemManager] PreMatchIntroUI.Instance is STILL null after EnsureExists!");
|
|
}
|
|
|
|
if (MatchResultUI.Instance != null)
|
|
DevLog.Log("[CashSystemManager] MatchResultUI ready for end-of-match");
|
|
else
|
|
DevLog.LogWarning("[CashSystemManager] MatchResultUI.Instance is STILL null after EnsureExists!");
|
|
|
|
StartCoroutine(SpawnTeams());
|
|
}
|
|
|
|
private IEnumerator InitializeUIDelayed()
|
|
{
|
|
yield return null;
|
|
yield return new WaitForSeconds(0.2f);
|
|
|
|
// Fire events instead of FindObjectOfType — UI subscribes to these
|
|
GameEvents.FireTimerUpdated(matchTimer);
|
|
if (playerStation != null) GameEvents.FireScoreChanged("Player", playerStation.StoredCash);
|
|
else if (playerVault != null) GameEvents.FireScoreChanged("Player", playerVault.StoredCash);
|
|
if (enemyStation != null) GameEvents.FireScoreChanged("Enemy", enemyStation.StoredCash);
|
|
else if (enemyVault != null) GameEvents.FireScoreChanged("Enemy", enemyVault.StoredCash);
|
|
}
|
|
|
|
// Throttle timer UI updates to once per displayed second (was 60Hz)
|
|
private int _lastDisplayedSecond = -1;
|
|
|
|
private void Update()
|
|
{
|
|
if (matchEnded) return;
|
|
|
|
// Timer tick
|
|
matchTimer -= Time.deltaTime;
|
|
|
|
// Only fire timer event when the displayed second changes (60Hz → 1Hz)
|
|
int displayedSecond = Mathf.CeilToInt(matchTimer);
|
|
if (displayedSecond != _lastDisplayedSecond)
|
|
{
|
|
_lastDisplayedSecond = displayedSecond;
|
|
GameEvents.FireTimerUpdated(matchTimer);
|
|
}
|
|
|
|
if (matchTimer <= 0)
|
|
{
|
|
matchTimer = 0;
|
|
EndMatch();
|
|
return;
|
|
}
|
|
|
|
// Buzzer beater slow-motion
|
|
if (!_buzzerActive && matchTimer <= buzzerBeaterWindow)
|
|
{
|
|
float playerScore = GetTeamTotalCash("Player");
|
|
float enemyScore = GetTeamTotalCash("Enemy");
|
|
float scoreDiff = Mathf.Abs(playerScore - enemyScore);
|
|
if (scoreDiff < 500f) // Close game
|
|
{
|
|
_buzzerActive = true;
|
|
Time.timeScale = buzzerSlowMotion;
|
|
Time.fixedDeltaTime = 0.02f * buzzerSlowMotion;
|
|
GameEvents.FirePopupMessage("BUZZER BEATER!", 2f);
|
|
}
|
|
}
|
|
|
|
// Threshold win — check both TeamVaults and CashoutStations
|
|
if (endOnThreshold)
|
|
{
|
|
float playerScore = GetTeamTotalCash("Player");
|
|
float enemyScore = GetTeamTotalCash("Enemy");
|
|
|
|
if (playerScore >= targetCashToWin)
|
|
{ EndMatch(true); return; }
|
|
if (enemyScore >= targetCashToWin)
|
|
{ EndMatch(false); return; }
|
|
}
|
|
|
|
// Character switching
|
|
if (Input.GetKeyDown(switchCharacterKey) && Time.time - lastSwitchTime > switchCooldown)
|
|
{
|
|
SwitchControlledCharacter();
|
|
lastSwitchTime = Time.time;
|
|
}
|
|
}
|
|
|
|
private IEnumerator SpawnTeams()
|
|
{
|
|
yield return new WaitForSeconds(0.5f); // Wait for scene to settle
|
|
|
|
// Get team data from SelectionOptions
|
|
string[] playerTeam = SelectionOptions.Instance.playerTeam;
|
|
string[] enemyTeam = SelectionOptions.Instance.enemyTeam;
|
|
int captainIndex = SelectionOptions.Instance.captainIndex;
|
|
|
|
DevLog.Log($"[CashSystemManager] Spawning teams - Captain index: {captainIndex}");
|
|
DevLog.Log($"[CashSystemManager] Player team: {string.Join(", ", playerTeam ?? new string[0])}");
|
|
DevLog.Log($"[CashSystemManager] Enemy team: {string.Join(", ", enemyTeam ?? new string[0])}");
|
|
|
|
// If teams not set, generate them
|
|
if (playerTeam == null || playerTeam.Length == 0 || string.IsNullOrEmpty(playerTeam[0]))
|
|
{
|
|
DevLog.Log("[CashSystemManager] Generating default teams...");
|
|
GenerateDefaultTeams();
|
|
playerTeam = SelectionOptions.Instance.playerTeam;
|
|
enemyTeam = SelectionOptions.Instance.enemyTeam;
|
|
}
|
|
|
|
// Get spawn positions from vaults if spawn points not assigned
|
|
Vector3 playerVaultPos = playerVault != null ? playerVault.transform.position : new Vector3(-10f, 0f, 0f);
|
|
Vector3 enemyVaultPos = enemyVault != null ? enemyVault.transform.position : new Vector3(10f, 0f, 0f);
|
|
|
|
DevLog.Log($"[CashSystemManager] Player vault position: {playerVaultPos}");
|
|
DevLog.Log($"[CashSystemManager] Enemy vault position: {enemyVaultPos}");
|
|
DevLog.Log($"[CashSystemManager] Player spawn points assigned: {playerTeamSpawnPoints != null && playerTeamSpawnPoints.Length > 0}");
|
|
DevLog.Log($"[CashSystemManager] Enemy spawn points assigned: {enemyTeamSpawnPoints != null && enemyTeamSpawnPoints.Length > 0}");
|
|
|
|
// Spawn player team - use assigned spawn points if available, otherwise vault positions
|
|
for (int i = 0; i < playerTeam.Length; i++)
|
|
{
|
|
if (string.IsNullOrEmpty(playerTeam[i])) continue;
|
|
|
|
// First try to use assigned spawn points
|
|
Vector3 spawnPos;
|
|
if (playerTeamSpawnPoints != null && i < playerTeamSpawnPoints.Length && playerTeamSpawnPoints[i] != null)
|
|
{
|
|
spawnPos = playerTeamSpawnPoints[i].position;
|
|
DevLog.Log($"[CashSystemManager] Using assigned spawn point for player {i}: {spawnPos}");
|
|
}
|
|
else
|
|
{
|
|
spawnPos = GetSpawnPositionNearVault(playerVaultPos, i, true);
|
|
DevLog.Log($"[CashSystemManager] Using vault-based spawn for player {i}: {spawnPos}");
|
|
}
|
|
|
|
bool isPlayerControlled = (i == captainIndex);
|
|
|
|
DevLog.Log($"[CashSystemManager] Spawning player {playerTeam[i]} at {spawnPos}, isPlayerControlled: {isPlayerControlled}");
|
|
|
|
GameObject character = SpawnCharacter(playerTeam[i], spawnPos, "Player", isPlayerControlled);
|
|
if (character != null)
|
|
{
|
|
playerTeamCharacters.Add(character);
|
|
// CashCarrier and AI components are now added in SpawnCharacter/ConfigureCharacterControllers
|
|
DevLog.Log($"[CashSystemManager] Player {character.name} spawned successfully at {character.transform.position}");
|
|
}
|
|
}
|
|
|
|
// Spawn enemy team - use assigned spawn points if available, otherwise vault positions
|
|
for (int i = 0; i < enemyTeam.Length; i++)
|
|
{
|
|
if (string.IsNullOrEmpty(enemyTeam[i])) continue;
|
|
|
|
// First try to use assigned spawn points
|
|
Vector3 spawnPos;
|
|
if (enemyTeamSpawnPoints != null && i < enemyTeamSpawnPoints.Length && enemyTeamSpawnPoints[i] != null)
|
|
{
|
|
spawnPos = enemyTeamSpawnPoints[i].position;
|
|
DevLog.Log($"[CashSystemManager] Using assigned spawn point for enemy {i}: {spawnPos}");
|
|
}
|
|
else
|
|
{
|
|
spawnPos = GetSpawnPositionNearVault(enemyVaultPos, i, false);
|
|
DevLog.Log($"[CashSystemManager] Using vault-based spawn for enemy {i}: {spawnPos}");
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Spawning enemy {enemyTeam[i]} at {spawnPos}");
|
|
|
|
GameObject character = SpawnCharacter(enemyTeam[i], spawnPos, "Enemy", false);
|
|
if (character != null)
|
|
{
|
|
enemyTeamCharacters.Add(character);
|
|
// CashCarrier, CashSystemAI, and EnemyAIController are now added in SpawnCharacter/ConfigureCharacterControllers
|
|
DevLog.Log($"[CashSystemManager] Enemy {character.name} spawned successfully at {character.transform.position}");
|
|
}
|
|
}
|
|
|
|
// Note: CashSystemAI is now added in ConfigureCharacterControllers, no need to add again
|
|
|
|
currentControlledIndex = captainIndex;
|
|
|
|
DevLog.Log($"[CashSystemManager] Teams spawned: {playerTeamCharacters.Count} players, {enemyTeamCharacters.Count} enemies");
|
|
|
|
// Assign deterministic AI roles: 1 Defender + 2 Collectors per team
|
|
// Must happen AFTER all CashSystemAI components are fully initialized
|
|
yield return null; // Give CashSystemAI.Start() a frame to run
|
|
CashSystemAI.AssignTeamRoles();
|
|
|
|
// Initialize team HUD with player team characters (HUD is CanvasGroup-hidden, Initialize shows it)
|
|
if (TeamHUDPanel.Instance != null)
|
|
TeamHUDPanel.Instance.Initialize(playerTeamCharacters, currentControlledIndex);
|
|
|
|
// Wait for intro panel to finish (including its countdown: 3-2-1-FIGHT!)
|
|
// instead of hiding it early. The intro sets IsComplete when done.
|
|
if (PreMatchIntroUI.Instance != null)
|
|
{
|
|
DevLog.Log("[CashSystemManager] Waiting for intro to complete...");
|
|
while (!PreMatchIntroUI.Instance.IsComplete)
|
|
yield return null;
|
|
DevLog.Log("[CashSystemManager] Intro complete, enabling combat");
|
|
}
|
|
|
|
// Show the Cash System HUD now that intro is done
|
|
CashSystemUI cashUI = FindObjectOfType<CashSystemUI>();
|
|
if (cashUI != null)
|
|
cashUI.ShowHUD();
|
|
|
|
// Enable combat — either intro did the countdown, or we do it ourselves
|
|
if (PreMatchIntroUI.Instance != null && PreMatchIntroUI.Instance.IsComplete)
|
|
{
|
|
// Intro already fired countdown ticks; just enable combat now
|
|
GameEvents.FireCombatEnabled(true);
|
|
foreach (var c in playerTeamCharacters)
|
|
if (c != null) EnableCharacterCombat(c);
|
|
foreach (var c in enemyTeamCharacters)
|
|
if (c != null) EnableCharacterCombat(c);
|
|
}
|
|
else
|
|
{
|
|
// No intro panel present — use standalone countdown fallback
|
|
StartCoroutine(MatchCountdown());
|
|
}
|
|
}
|
|
|
|
private void GenerateDefaultTeams()
|
|
{
|
|
// Use all 6 characters, split between teams
|
|
string[] allCharacters = { "Windham", "Amira", "Bahman", "Ziggy", "Amon", "Imani" };
|
|
|
|
// Shuffle
|
|
for (int i = allCharacters.Length - 1; i > 0; i--)
|
|
{
|
|
int j = Random.Range(0, i + 1);
|
|
string temp = allCharacters[i];
|
|
allCharacters[i] = allCharacters[j];
|
|
allCharacters[j] = temp;
|
|
}
|
|
|
|
// Assign to teams
|
|
SelectionOptions.Instance.playerTeam = new string[] { allCharacters[0], allCharacters[1], allCharacters[2] };
|
|
SelectionOptions.Instance.enemyTeam = new string[] { allCharacters[3], allCharacters[4], allCharacters[5] };
|
|
SelectionOptions.Instance.captainIndex = 0;
|
|
|
|
DevLog.Log($"[CashSystemManager] Generated teams - Player: {string.Join(", ", SelectionOptions.Instance.playerTeam)}, Enemy: {string.Join(", ", SelectionOptions.Instance.enemyTeam)}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get spawn position near a vault - spreads characters in a line
|
|
/// </summary>
|
|
private Vector3 GetSpawnPositionNearVault(Vector3 vaultPosition, int index, bool isPlayerTeam)
|
|
{
|
|
// Offset from vault position (spawn a bit away from vault)
|
|
float spawnDistance = 3f; // Distance from vault center
|
|
float spacing = 2f; // Horizontal spacing between characters
|
|
|
|
// Center the team spawn with offset from vault
|
|
float xOffset = (index - 1) * spacing; // -2, 0, +2 for 3 characters
|
|
|
|
// Different direction for player vs enemy
|
|
if (isPlayerTeam)
|
|
{
|
|
// Player spawns "forward" from vault (opposite to enemy)
|
|
return new Vector3(vaultPosition.x + xOffset, vaultPosition.y, vaultPosition.z + spawnDistance);
|
|
}
|
|
else
|
|
{
|
|
// Enemy spawns "forward" from their vault (opposite to player)
|
|
return new Vector3(vaultPosition.x + xOffset, vaultPosition.y, vaultPosition.z - spawnDistance);
|
|
}
|
|
}
|
|
|
|
private Vector3 GetSpawnPosition(Transform[] spawnPoints, int index)
|
|
{
|
|
if (spawnPoints != null && index < spawnPoints.Length && spawnPoints[index] != null)
|
|
{
|
|
return spawnPoints[index].position;
|
|
}
|
|
|
|
// Default positions
|
|
float xOffset = 3f * index;
|
|
return new Vector3(xOffset, 0f, (spawnPoints == playerTeamSpawnPoints) ? 10f : -10f);
|
|
}
|
|
|
|
private GameObject SpawnCharacter(string characterName, Vector3 position, string teamTag, bool isPlayerControlled)
|
|
{
|
|
// IMPORTANT: Always load the PLAYER prefab - NOT the Enemy variant!
|
|
// The old code loaded "Enemy" prefabs which caused confusion
|
|
// Now we use ONE prefab per character and configure it based on team
|
|
GameObject prefab = CharacterPrefabManager.Instance.GetCharacterPrefab(characterName, isAI: false);
|
|
|
|
if (prefab == null)
|
|
{
|
|
Debug.LogError($"[CashSystemManager] Failed to load prefab for {characterName}");
|
|
return null;
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Loading prefab: {prefab.name} for {characterName}");
|
|
|
|
GameObject character = Instantiate(prefab, position, Quaternion.identity);
|
|
|
|
// Snap to NavMesh surface so characters don't spawn underground with new environments
|
|
UnityEngine.AI.NavMeshHit navHit;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(position, out navHit, 10f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
character.transform.position = navHit.position;
|
|
|
|
// Warp NavMeshAgent to the snapped position so it doesn't fight physics
|
|
var agent = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (agent != null)
|
|
{
|
|
agent.enabled = false;
|
|
character.transform.position = navHit.position;
|
|
agent.enabled = true;
|
|
agent.Warp(navHit.position);
|
|
}
|
|
|
|
// For AI characters: make Rigidbody kinematic so NavMeshAgent controls position
|
|
// For player-controlled: leave Rigidbody non-kinematic (PlayerMovementBehaviour uses MovePosition)
|
|
if (!isPlayerControlled)
|
|
{
|
|
var rb = character.GetComponent<Rigidbody>();
|
|
if (rb != null && agent != null)
|
|
{
|
|
rb.isKinematic = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Player-controlled: DISABLE NavMeshAgent entirely after snapping.
|
|
// PlayerMovementBehaviour uses Rigidbody.MovePosition — an enabled NavMeshAgent
|
|
// creates internal velocity corrections that fight the Rigidbody and cause drag.
|
|
var agent2 = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (agent2 != null)
|
|
{
|
|
agent2.enabled = false;
|
|
}
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Snapped {characterName} to NavMesh at Y={navHit.position.y} (was Y={position.y})");
|
|
}
|
|
else
|
|
{
|
|
DevLog.LogWarning($"[CashSystemManager] No NavMesh found near {position} for {characterName} — character may be underground!");
|
|
}
|
|
|
|
// Add TeamMember component if not present
|
|
TeamMember teamMember = character.GetComponent<TeamMember>();
|
|
if (teamMember == null)
|
|
{
|
|
teamMember = character.AddComponent<TeamMember>();
|
|
}
|
|
|
|
// Set team based on tag
|
|
bool isPlayerTeam = (teamTag == "Player");
|
|
teamMember.CurrentTeam = isPlayerTeam ? TeamMember.Team.PlayerTeam : TeamMember.Team.EnemyTeam;
|
|
|
|
// Set proper tag (TeamMember.UpdateLayerAndTag will handle this but we set it here too)
|
|
character.tag = teamTag;
|
|
|
|
// Use the new ControlType system - this handles ALL component configuration!
|
|
TeamMember.ControlType controlType;
|
|
if (isPlayerControlled)
|
|
{
|
|
controlType = TeamMember.ControlType.HumanControlled;
|
|
}
|
|
else if (isPlayerTeam)
|
|
{
|
|
controlType = TeamMember.ControlType.PlayerAI;
|
|
}
|
|
else
|
|
{
|
|
controlType = TeamMember.ControlType.EnemyAI;
|
|
}
|
|
teamMember.SetControlType(controlType);
|
|
|
|
// CRITICAL: For player-controlled characters, bind input action callbacks
|
|
// This is what Practice mode does in SelectionOptions.SetUpPlayerInputAI
|
|
if (isPlayerControlled)
|
|
{
|
|
BindPlayerInputActions(character);
|
|
}
|
|
|
|
// Add CashCarrier for carrying cash
|
|
if (character.GetComponent<CashCarrier>() == null)
|
|
{
|
|
character.AddComponent<CashCarrier>();
|
|
}
|
|
|
|
// Register with TeamControlManager for switching
|
|
if (TeamControlManager.Instance != null && isPlayerTeam)
|
|
{
|
|
TeamControlManager.Instance.RegisterPlayerTeamCharacter(character, isPlayerControlled);
|
|
}
|
|
|
|
// Register with CameraManager AND directly set target if player-controlled
|
|
if (CameraManager.Instance != null)
|
|
{
|
|
CameraManager.Instance.RegisterTeamMember(teamMember);
|
|
|
|
// DIRECTLY set camera target for the captain
|
|
if (isPlayerControlled)
|
|
{
|
|
CameraManager.Instance.SetTarget(teamMember);
|
|
DevLog.Log($"[CashSystemManager] Set CameraManager target to captain: {character.name}");
|
|
}
|
|
}
|
|
|
|
// Disable attack until countdown finishes (combat control is independent of ControlType)
|
|
DisableCharacterCombat(character);
|
|
|
|
// Configure AI controller scripts (CashSystemAI, TeamAIController, etc.)
|
|
if (!isPlayerControlled)
|
|
{
|
|
ConfigureCharacterControllers(character, isPlayerTeam, isPlayerControlled);
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Spawned {characterName} as {controlType}, Team: {(isPlayerTeam ? "PLAYER" : "ENEMY")}");
|
|
|
|
return character;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure the appropriate controller scripts based on team and control mode
|
|
/// </summary>
|
|
private void ConfigureCharacterControllers(GameObject character, bool isPlayerTeam, bool isPlayerControlled)
|
|
{
|
|
// Get existing components
|
|
PlayerScript playerScript = character.GetComponent<PlayerScript>();
|
|
CharacterAIController aiController = character.GetComponent<CharacterAIController>();
|
|
|
|
if (isPlayerControlled)
|
|
{
|
|
// PLAYER CONTROLLED: Enable player input scripts, disable AI scripts
|
|
if (playerScript != null) playerScript.enabled = true;
|
|
if (aiController != null) aiController.enabled = false;
|
|
|
|
// Enable new modular input handler if present
|
|
CharacterInputHandler inputHandler = character.GetComponent<CharacterInputHandler>();
|
|
if (inputHandler != null) inputHandler.enabled = true;
|
|
|
|
// Disable CharacterAIController for player-controlled
|
|
if (aiController != null) aiController.enabled = false;
|
|
|
|
// Don't add AI controllers to player-controlled character
|
|
}
|
|
else if (isPlayerTeam)
|
|
{
|
|
// PLAYER TEAM AI: Enable navigation, use TeamAIController + CharacterAIController
|
|
if (playerScript != null) playerScript.enabled = false;
|
|
|
|
// Disable CharacterInputHandler on AI characters
|
|
CharacterInputHandler inputHandler = character.GetComponent<CharacterInputHandler>();
|
|
if (inputHandler != null) inputHandler.enabled = false;
|
|
|
|
// Enable CharacterAIController if present, or add it
|
|
if (aiController == null)
|
|
{
|
|
aiController = character.AddComponent<CharacterAIController>();
|
|
}
|
|
aiController.enabled = true;
|
|
|
|
// CRITICAL: Disable PlayerInput on AI characters to prevent duplicate input
|
|
PlayerInput playerInput = character.GetComponent<PlayerInput>();
|
|
if (playerInput != null)
|
|
{
|
|
playerInput.enabled = false;
|
|
DevLog.Log($"[CashSystemManager] Disabled PlayerInput on AI teammate {character.name}");
|
|
}
|
|
|
|
// Disable AudioListener on AI characters (only player should have one)
|
|
AudioListener audioListener = character.GetComponent<AudioListener>();
|
|
if (audioListener != null)
|
|
{
|
|
audioListener.enabled = false;
|
|
}
|
|
|
|
// CRITICAL: Disable ALL embedded cameras on AI characters
|
|
// This prevents multiple cameras from being active in the scene
|
|
Camera[] cameras = character.GetComponentsInChildren<Camera>(true);
|
|
foreach (Camera cam in cameras)
|
|
{
|
|
cam.enabled = false;
|
|
DevLog.Log($"[CashSystemManager] Disabled embedded camera {cam.name} on AI teammate {character.name}");
|
|
}
|
|
|
|
// Also disable any Cinemachine cameras on AI characters
|
|
Cinemachine.CinemachineBrain[] brains = character.GetComponentsInChildren<Cinemachine.CinemachineBrain>(true);
|
|
foreach (var brain in brains)
|
|
{
|
|
brain.enabled = false;
|
|
}
|
|
|
|
// Add TeamAIController if not present
|
|
TeamAIController teamAI = character.GetComponent<TeamAIController>();
|
|
if (teamAI == null)
|
|
{
|
|
teamAI = character.AddComponent<TeamAIController>();
|
|
}
|
|
teamAI.enabled = true;
|
|
|
|
// Add CashSystemAI for cash-related decisions
|
|
CashSystemAI cashAI = character.GetComponent<CashSystemAI>();
|
|
if (cashAI == null)
|
|
{
|
|
cashAI = character.AddComponent<CashSystemAI>();
|
|
}
|
|
cashAI.enabled = true;
|
|
|
|
// Keep NavMeshAgent for movement
|
|
UnityEngine.AI.NavMeshAgent navAgent = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (navAgent != null) navAgent.enabled = true;
|
|
|
|
// Disable CharacterMovement - AI uses NavMeshAgent, not joystick-based movement
|
|
CharacterMovement charMovement = character.GetComponent<CharacterMovement>();
|
|
if (charMovement != null) charMovement.enabled = false;
|
|
|
|
DevLog.Log($"[CashSystemManager] Configured {character.name} as PLAYER TEAM AI");
|
|
}
|
|
else
|
|
{
|
|
// ENEMY TEAM AI: Use EnemyAIController + CharacterAIController
|
|
if (playerScript != null) playerScript.enabled = false;
|
|
|
|
// Disable CharacterInputHandler on AI characters
|
|
CharacterInputHandler inputHandler = character.GetComponent<CharacterInputHandler>();
|
|
if (inputHandler != null) inputHandler.enabled = false;
|
|
|
|
// Enable CharacterAIController if present, or add it
|
|
if (aiController == null)
|
|
{
|
|
aiController = character.AddComponent<CharacterAIController>();
|
|
}
|
|
aiController.enabled = true;
|
|
|
|
// CRITICAL: Disable PlayerInput on AI characters to prevent duplicate input
|
|
PlayerInput playerInput = character.GetComponent<PlayerInput>();
|
|
if (playerInput != null)
|
|
{
|
|
playerInput.enabled = false;
|
|
DevLog.Log($"[CashSystemManager] Disabled PlayerInput on enemy AI {character.name}");
|
|
}
|
|
|
|
// Disable AudioListener on AI characters (only player should have one)
|
|
AudioListener audioListener = character.GetComponent<AudioListener>();
|
|
if (audioListener != null)
|
|
{
|
|
audioListener.enabled = false;
|
|
}
|
|
|
|
// CRITICAL: Disable ALL embedded cameras on AI characters
|
|
// This prevents multiple cameras from being active in the scene
|
|
Camera[] cameras = character.GetComponentsInChildren<Camera>(true);
|
|
foreach (Camera cam in cameras)
|
|
{
|
|
cam.enabled = false;
|
|
DevLog.Log($"[CashSystemManager] Disabled embedded camera {cam.name} on enemy AI {character.name}");
|
|
}
|
|
|
|
// Also disable any Cinemachine cameras on AI characters
|
|
Cinemachine.CinemachineBrain[] brains = character.GetComponentsInChildren<Cinemachine.CinemachineBrain>(true);
|
|
foreach (var brain in brains)
|
|
{
|
|
brain.enabled = false;
|
|
}
|
|
|
|
// Add EnemyAIController if not present
|
|
EnemyAIController enemyAI = character.GetComponent<EnemyAIController>();
|
|
if (enemyAI == null)
|
|
{
|
|
enemyAI = character.AddComponent<EnemyAIController>();
|
|
}
|
|
enemyAI.enabled = true;
|
|
|
|
// Add CashSystemAI for cash-related decisions
|
|
CashSystemAI cashAI = character.GetComponent<CashSystemAI>();
|
|
if (cashAI == null)
|
|
{
|
|
cashAI = character.AddComponent<CashSystemAI>();
|
|
}
|
|
cashAI.enabled = true;
|
|
|
|
// Keep NavMeshAgent for movement
|
|
UnityEngine.AI.NavMeshAgent navAgent = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (navAgent != null) navAgent.enabled = true;
|
|
|
|
// Disable CharacterMovement - AI uses NavMeshAgent, not joystick-based movement
|
|
CharacterMovement charMovement = character.GetComponent<CharacterMovement>();
|
|
if (charMovement != null) charMovement.enabled = false;
|
|
|
|
DevLog.Log($"[CashSystemManager] Configured {character.name} as ENEMY TEAM AI");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set up player control on the captain character
|
|
/// </summary>
|
|
private void SetupPlayerControl(GameObject character)
|
|
{
|
|
DevLog.Log($"[CashSystemManager] Setting up player control for {character.name}");
|
|
|
|
// Find head transform for camera tracking (like Practice mode)
|
|
Transform targetTransform = FindCharacterHead(character);
|
|
DevLog.Log($"[CashSystemManager] Camera target: {targetTransform?.name ?? "character root"}");
|
|
|
|
// Set up camera to follow this character via CameraManager (single authority)
|
|
if (CameraManager.Instance != null)
|
|
{
|
|
TeamMember teamMember = character.GetComponent<TeamMember>();
|
|
if (teamMember != null)
|
|
{
|
|
CameraManager.Instance.SetTarget(teamMember);
|
|
}
|
|
else
|
|
{
|
|
CameraManager.Instance.SetTarget(character);
|
|
}
|
|
DevLog.Log($"[CashSystemManager] Camera target set via CameraManager for {character.name}");
|
|
}
|
|
else
|
|
{
|
|
// Fallback: set Cinemachine targets directly with correct Follow/LookAt separation
|
|
Transform followTarget = character.transform; // Root = orbit center
|
|
Transform lookAtTarget = FindCharacterHead(character); // Head = aim point
|
|
|
|
Cinemachine.CinemachineFreeLook[] freeLookCameras = FindObjectsOfType<Cinemachine.CinemachineFreeLook>();
|
|
foreach (var freeLookCam in freeLookCameras)
|
|
{
|
|
if (freeLookCam.gameObject.activeInHierarchy)
|
|
{
|
|
freeLookCam.Follow = followTarget;
|
|
freeLookCam.LookAt = lookAtTarget;
|
|
}
|
|
}
|
|
DevLog.Log($"[CashSystemManager] Camera targets set directly (fallback) for {character.name}");
|
|
}
|
|
|
|
// Enable player input components
|
|
PlayerScript playerScript = character.GetComponent<PlayerScript>();
|
|
if (playerScript != null)
|
|
{
|
|
playerScript.enabled = true;
|
|
// NOTE: attack is left false here - EnableCharacterCombat will set it to true after countdown
|
|
DevLog.Log("[CashSystemManager] Enabled PlayerScript (attack will be enabled after countdown)");
|
|
}
|
|
|
|
// Enable and configure PlayerInput component (critical for receiving input!)
|
|
UnityEngine.InputSystem.PlayerInput playerInput = character.GetComponent<UnityEngine.InputSystem.PlayerInput>();
|
|
if (playerInput != null)
|
|
{
|
|
playerInput.enabled = true;
|
|
|
|
// Switch to Player Controls action map
|
|
try
|
|
{
|
|
playerInput.SwitchCurrentActionMap("Player Controls");
|
|
DevLog.Log("[CashSystemManager] Switched to Player Controls action map");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
DevLog.LogWarning($"[CashSystemManager] Could not switch action map: {e.Message}");
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] PlayerInput enabled, current map: {playerInput.currentActionMap?.name ?? "null"}");
|
|
}
|
|
else
|
|
{
|
|
DevLog.LogWarning("[CashSystemManager] No PlayerInput component found on player character!");
|
|
}
|
|
|
|
// Disable AI components if present
|
|
CharacterAIController aiController = character.GetComponent<CharacterAIController>();
|
|
if (aiController != null)
|
|
{
|
|
aiController.enabled = false;
|
|
DevLog.Log("[CashSystemManager] Disabled CharacterAIController");
|
|
}
|
|
|
|
// Disable any AI brain
|
|
UnityEngine.AI.NavMeshAgent navMeshAgent = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (navMeshAgent != null && navMeshAgent.enabled)
|
|
{
|
|
// NavMeshAgent should stay enabled for movement, but we'll let PlayerScript control it
|
|
DevLog.Log("[CashSystemManager] NavMeshAgent present");
|
|
}
|
|
|
|
// Disable CashSystemAI if added
|
|
CashSystemAI cashAI = character.GetComponent<CashSystemAI>();
|
|
if (cashAI != null)
|
|
{
|
|
Destroy(cashAI);
|
|
DevLog.Log("[CashSystemManager] Removed CashSystemAI from player");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bind input action callbacks to PlayerScript methods.
|
|
/// This mirrors what Practice mode does in SelectionOptions.SetUpPlayerInputAI.
|
|
/// Without this explicit binding, the PlayerInput component won't route actions to PlayerScript.
|
|
/// </summary>
|
|
private void BindPlayerInputActions(GameObject character)
|
|
{
|
|
PlayerInput playerInput = character.GetComponent<PlayerInput>();
|
|
PlayerScript playerScript = character.GetComponent<PlayerScript>();
|
|
|
|
if (playerInput == null || playerScript == null)
|
|
{
|
|
DevLog.LogWarning($"[CashSystemManager] Cannot bind input actions - PlayerInput: {playerInput != null}, PlayerScript: {playerScript != null}");
|
|
return;
|
|
}
|
|
|
|
// Ensure PlayerInput is enabled with the right action map
|
|
playerInput.enabled = true;
|
|
try
|
|
{
|
|
playerInput.SwitchCurrentActionMap("Player Controls");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
DevLog.LogWarning($"[CashSystemManager] Could not switch action map: {e.Message}");
|
|
}
|
|
|
|
var actionMap = playerInput.currentActionMap;
|
|
if (actionMap == null)
|
|
{
|
|
Debug.LogError("[CashSystemManager] No current action map found!");
|
|
return;
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Binding input actions on action map: {actionMap.name}");
|
|
|
|
// CRITICAL: Bind the Move action - this is what Practice mode does!
|
|
TryBindActionPerformed(actionMap, "Move", playerScript.OnMove); // performed
|
|
TryBindActionCanceled(actionMap, "Move", playerScript.OnMove); // canceled
|
|
|
|
// Bind attack actions
|
|
TryBindActionPerformed(actionMap, "Jab", playerScript.OnJab);
|
|
TryBindActionPerformed(actionMap, "Right", playerScript.OnRight);
|
|
TryBindActionPerformed(actionMap, "LeftHook", playerScript.OnLeftHook);
|
|
TryBindActionPerformed(actionMap, "RightHook", playerScript.OnRightHook);
|
|
TryBindActionPerformed(actionMap, "LeftElbow", playerScript.OnLeftElbow);
|
|
TryBindActionPerformed(actionMap, "RightElbow", playerScript.OnRightElbow);
|
|
TryBindActionPerformed(actionMap, "RightBody", playerScript.OnRightBody);
|
|
TryBindActionPerformed(actionMap, "PowerPunchLeft", playerScript.OnPowerPunchLeft);
|
|
TryBindActionPerformed(actionMap, "SupermanPunch", playerScript.OnSupermanPunch);
|
|
|
|
// Kick actions
|
|
TryBindActionPerformed(actionMap, "LegKickRight", playerScript.OnLegKickRight);
|
|
TryBindActionPerformed(actionMap, "BodyKickRight", playerScript.OnBodyKickRight);
|
|
TryBindActionPerformed(actionMap, "KneeRight", playerScript.OnKneeRight);
|
|
TryBindActionPerformed(actionMap, "BackSideKick", playerScript.OnBackSideKick);
|
|
TryBindActionPerformed(actionMap, "FrontKickRight", playerScript.OnFrontKickRight);
|
|
TryBindActionPerformed(actionMap, "LegPowerKickRight", playerScript.OnLegPowerKickRight);
|
|
|
|
// Dodge actions
|
|
TryBindActionPerformed(actionMap, "JumpBack", playerScript.OnJumpBack);
|
|
TryBindActionPerformed(actionMap, "JumpLeft", playerScript.OnJumpLeft);
|
|
TryBindActionPerformed(actionMap, "JumpRight", playerScript.OnJumpRight);
|
|
|
|
// Block actions
|
|
TryBindActionPerformed(actionMap, "BlockStepBack", playerScript.OnBlockStepBack);
|
|
TryBindActionPerformed(actionMap, "BlockBodyLeft", playerScript.OnBlockBodyLeft);
|
|
TryBindActionPerformed(actionMap, "BlockLeg", playerScript.OnBlockLeg);
|
|
|
|
// Special/Vicious attacks
|
|
TryBindActionPerformed(actionMap, "Chokeslam", playerScript.OnChokeslam);
|
|
TryBindActionPerformed(actionMap, "Suplex", playerScript.OnSuplex);
|
|
TryBindActionPerformed(actionMap, "GiantSwing", playerScript.OnGiantSwing);
|
|
TryBindActionPerformed(actionMap, "DiamondCrusher", playerScript.OnDiamondCrusher);
|
|
TryBindActionPerformed(actionMap, "SumoSlap", playerScript.OnSumoSlap);
|
|
TryBindActionPerformed(actionMap, "JavelinTackle", playerScript.OnJavelinTackle);
|
|
TryBindActionPerformed(actionMap, "RKO", playerScript.OnRKO);
|
|
TryBindActionPerformed(actionMap, "Spear", playerScript.OnSpear);
|
|
TryBindActionPerformed(actionMap, "RockBottom", playerScript.OnRockBottom);
|
|
TryBindActionPerformed(actionMap, "F5", playerScript.OnF5);
|
|
|
|
DevLog.Log($"[CashSystemManager] Input actions bound successfully for {character.name}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to safely bind an action's performed callback
|
|
/// </summary>
|
|
private void TryBindActionPerformed(InputActionMap actionMap, string actionName, System.Action<InputAction.CallbackContext> callback)
|
|
{
|
|
InputAction action = actionMap.FindAction(actionName);
|
|
if (action != null)
|
|
{
|
|
action.performed += callback;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to safely bind an action's canceled callback
|
|
/// </summary>
|
|
private void TryBindActionCanceled(InputActionMap actionMap, string actionName, System.Action<InputAction.CallbackContext> callback)
|
|
{
|
|
InputAction action = actionMap.FindAction(actionName);
|
|
if (action != null)
|
|
{
|
|
action.canceled += callback;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to safely bind an action (both performed and started)
|
|
/// </summary>
|
|
private void TryBindAction(InputActionMap actionMap, string actionName, System.Action<InputAction.CallbackContext> startedCallback, System.Action<InputAction.CallbackContext> performedCallback)
|
|
{
|
|
InputAction action = actionMap.FindAction(actionName);
|
|
if (action != null)
|
|
{
|
|
if (startedCallback != null) action.started += startedCallback;
|
|
if (performedCallback != null) action.performed += performedCallback;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the character's head transform for camera targeting (matches Practice mode)
|
|
/// </summary>
|
|
private Transform FindCharacterHead(GameObject character)
|
|
{
|
|
Transform headTransform = null;
|
|
|
|
try
|
|
{
|
|
// Common head bone names in character rigs
|
|
string[] headNames = { "Head", "head", "Bip001 Head", "mixamorig:Head", "Bip01 Head" };
|
|
|
|
foreach (string headName in headNames)
|
|
{
|
|
headTransform = character.transform.Find(headName);
|
|
if (headTransform != null) break;
|
|
|
|
// Also search recursively
|
|
headTransform = FindChildRecursive(character.transform, headName);
|
|
if (headTransform != null) break;
|
|
}
|
|
|
|
// If no head found, try to find any transform with "head" in name
|
|
if (headTransform == null)
|
|
{
|
|
headTransform = FindChildWithPartialName(character.transform, "head");
|
|
}
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
DevLog.LogWarning($"[CashSystemManager] Error finding head for {character.name}: {e.Message}");
|
|
}
|
|
|
|
return headTransform ?? character.transform;
|
|
}
|
|
|
|
private Transform FindChildRecursive(Transform parent, string name)
|
|
{
|
|
foreach (Transform child in parent)
|
|
{
|
|
if (child.name == name) return child;
|
|
Transform found = FindChildRecursive(child, name);
|
|
if (found != null) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private Transform FindChildWithPartialName(Transform parent, string partialName)
|
|
{
|
|
foreach (Transform child in parent)
|
|
{
|
|
if (child.name.ToLower().Contains(partialName.ToLower())) return child;
|
|
Transform found = FindChildWithPartialName(child, partialName);
|
|
if (found != null) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disable combat for a character (used before countdown)
|
|
/// </summary>
|
|
private void DisableCharacterCombat(GameObject character)
|
|
{
|
|
PlayerScript playerScript = character.GetComponent<PlayerScript>();
|
|
if (playerScript != null)
|
|
{
|
|
playerScript.attack = false;
|
|
}
|
|
|
|
CharacterAIController aiController = character.GetComponent<CharacterAIController>();
|
|
if (aiController != null)
|
|
{
|
|
aiController.attack = false;
|
|
}
|
|
}
|
|
|
|
private IEnumerator MatchCountdown()
|
|
{
|
|
DevLog.Log("[CashSystemManager] Starting match countdown...");
|
|
|
|
// Try to find existing countdown UI
|
|
TMPro.TextMeshProUGUI countdownText = null;
|
|
GameObject uiObject = GameObject.Find("UI");
|
|
if (uiObject != null)
|
|
{
|
|
Transform roundInfo = uiObject.transform.Find("Canvas/rounds info (TMP)");
|
|
if (roundInfo != null)
|
|
countdownText = roundInfo.GetComponent<TMPro.TextMeshProUGUI>();
|
|
}
|
|
|
|
yield return new WaitForSecondsRealtime(1f);
|
|
|
|
string[] steps = { "3", "2", "1", "FIGHT!" };
|
|
foreach (string step in steps)
|
|
{
|
|
if (countdownText != null) countdownText.text = step;
|
|
GameEvents.FireCountdownTick(step);
|
|
yield return new WaitForSecondsRealtime(1f);
|
|
}
|
|
|
|
// Clear
|
|
if (countdownText != null) countdownText.text = "";
|
|
GameEvents.FireCountdownTick("");
|
|
|
|
// Enable combat
|
|
GameEvents.FireCombatEnabled(true);
|
|
foreach (var c in playerTeamCharacters)
|
|
if (c != null) EnableCharacterCombat(c);
|
|
foreach (var c in enemyTeamCharacters)
|
|
if (c != null) EnableCharacterCombat(c);
|
|
}
|
|
|
|
private void EnableCharacterCombat(GameObject character)
|
|
{
|
|
// Enable attack for player characters
|
|
PlayerScript playerScript = character.GetComponent<PlayerScript>();
|
|
if (playerScript != null)
|
|
{
|
|
playerScript.attack = true;
|
|
}
|
|
|
|
// Enable attack for AI characters
|
|
CharacterAIController aiController = character.GetComponent<CharacterAIController>();
|
|
if (aiController != null)
|
|
{
|
|
aiController.attack = true;
|
|
}
|
|
}
|
|
|
|
private void SwitchControlledCharacter()
|
|
{
|
|
if (playerTeamCharacters.Count <= 1) return;
|
|
|
|
// Use TeamControlManager if available
|
|
if (TeamControlManager.Instance != null)
|
|
{
|
|
TeamControlManager.Instance.SwitchToNextCharacter();
|
|
|
|
// Update local tracking
|
|
GameObject newControlled = TeamControlManager.Instance.GetCurrentControlledCharacter();
|
|
if (newControlled != null)
|
|
{
|
|
GameObject oldControlled = currentControlledIndex < playerTeamCharacters.Count
|
|
? playerTeamCharacters[currentControlledIndex] : null;
|
|
currentControlledIndex = playerTeamCharacters.IndexOf(newControlled);
|
|
SelectionOptions.Instance.currentControlledIndex = currentControlledIndex;
|
|
GameEvents.FireCharacterSwitched(oldControlled, newControlled);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fallback: Manual switching
|
|
int previousIndex = currentControlledIndex;
|
|
|
|
// Find next alive character
|
|
for (int i = 1; i <= playerTeamCharacters.Count; i++)
|
|
{
|
|
int nextIndex = (currentControlledIndex + i) % playerTeamCharacters.Count;
|
|
GameObject character = playerTeamCharacters[nextIndex];
|
|
|
|
if (character != null)
|
|
{
|
|
HealthNew health = character.GetComponent<HealthNew>();
|
|
if (health == null || !health.isDead)
|
|
{
|
|
currentControlledIndex = nextIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentControlledIndex != previousIndex)
|
|
{
|
|
SelectionOptions.Instance.currentControlledIndex = currentControlledIndex;
|
|
|
|
// Disable control on previous character
|
|
GameObject previousCharacter = playerTeamCharacters[previousIndex];
|
|
if (previousCharacter != null)
|
|
{
|
|
PlayerScript prevPS = previousCharacter.GetComponent<PlayerScript>();
|
|
if (prevPS != null) prevPS.attack = false;
|
|
|
|
TeamAIController prevAI = previousCharacter.GetComponent<TeamAIController>();
|
|
if (prevAI != null) prevAI.enabled = true;
|
|
}
|
|
|
|
// Enable control on new character
|
|
GameObject newCharacter = playerTeamCharacters[currentControlledIndex];
|
|
if (newCharacter != null)
|
|
{
|
|
PlayerScript newPS = newCharacter.GetComponent<PlayerScript>();
|
|
if (newPS != null)
|
|
{
|
|
newPS.enabled = true;
|
|
newPS.attack = true;
|
|
}
|
|
|
|
TeamAIController newAI = newCharacter.GetComponent<TeamAIController>();
|
|
if (newAI != null) newAI.enabled = false;
|
|
|
|
// Update camera
|
|
UpdateCameraToCharacter(newCharacter);
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Switched control to {playerTeamCharacters[currentControlledIndex].name}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update camera to follow a specific character
|
|
/// </summary>
|
|
private void UpdateCameraToCharacter(GameObject character)
|
|
{
|
|
// Route through CameraManager (single authority) with correct Follow/LookAt separation
|
|
if (CameraManager.Instance != null)
|
|
{
|
|
TeamMember teamMember = character.GetComponent<TeamMember>();
|
|
if (teamMember != null)
|
|
{
|
|
CameraManager.Instance.SetTarget(teamMember);
|
|
}
|
|
else
|
|
{
|
|
CameraManager.Instance.SetTarget(character);
|
|
}
|
|
DevLog.Log($"[CashSystemManager] Updated camera via CameraManager to follow {character.name}");
|
|
return;
|
|
}
|
|
|
|
// Fallback: direct Cinemachine target update with correct separation
|
|
Transform followTarget = character.transform; // Root = orbit center
|
|
Transform lookAtTarget = FindCharacterHead(character); // Head = aim point
|
|
|
|
Cinemachine.CinemachineFreeLook[] freeLookCameras = FindObjectsOfType<Cinemachine.CinemachineFreeLook>();
|
|
foreach (var cam in freeLookCameras)
|
|
{
|
|
if (cam.gameObject.activeInHierarchy)
|
|
{
|
|
cam.Follow = followTarget;
|
|
cam.LookAt = lookAtTarget;
|
|
}
|
|
}
|
|
|
|
Cinemachine.CinemachineVirtualCamera[] virtualCameras = FindObjectsOfType<Cinemachine.CinemachineVirtualCamera>();
|
|
foreach (var cam in virtualCameras)
|
|
{
|
|
if (cam.gameObject.activeInHierarchy)
|
|
{
|
|
cam.Follow = followTarget;
|
|
cam.LookAt = lookAtTarget;
|
|
}
|
|
}
|
|
|
|
DevLog.Log($"[CashSystemManager] Updated camera to follow {character.name} (direct fallback)");
|
|
}
|
|
|
|
public void OnVaultUpdated(TeamVault vault)
|
|
{
|
|
// Legacy callback kept for backwards compat; real routing now via GameEvents
|
|
GameEvents.FireScoreChanged(vault.TeamTag, vault.StoredCash);
|
|
}
|
|
|
|
// ─── Event Handlers ──────────────────────────────────────
|
|
|
|
private void OnCashDepositedEvent(string teamTag, float value, int combo)
|
|
{
|
|
// UI updates itself via GameEvents — manager just tracks win conditions
|
|
}
|
|
|
|
private void OnVaultStolenEvent(TeamVault vault, float amount)
|
|
{
|
|
// UI updates itself via GameEvents — manager just tracks win conditions
|
|
}
|
|
|
|
// ─── Kill Reward System ($200 per kill) ──────────────────
|
|
|
|
private void OnKillEvent(GameObject killer, GameObject victim)
|
|
{
|
|
if (matchEnded || killer == null || victim == null) return;
|
|
|
|
string killerTeamTag = killer.tag;
|
|
|
|
// Award $200 to killer's team
|
|
AwardCashToTeam(killerTeamTag, killReward);
|
|
|
|
GameEvents.FireKillReward(killer, killReward);
|
|
GameEvents.FireKillFeedEntry(killer.name, "ELIMINATED", $"{victim.name} (+${killReward})");
|
|
GameEvents.FirePopupMessage($"+${killReward} KILL REWARD", 1.5f);
|
|
|
|
DevLog.Log($"[CashSystemManager] Kill reward: ${killReward} to {killerTeamTag} ({killer.name} killed {victim.name})");
|
|
|
|
// Drop vault if victim was carrying one
|
|
DropVaultOnDeath(victim);
|
|
|
|
// Check for team wipe
|
|
CheckTeamWipe(victim.tag);
|
|
}
|
|
|
|
// ─── Character Death + Respawn ───────────────────────────
|
|
|
|
private void OnCharacterDiedEvent(GameObject character)
|
|
{
|
|
if (matchEnded || character == null) return;
|
|
|
|
// Start respawn timer
|
|
StartCoroutine(RespawnRoutine(character));
|
|
}
|
|
|
|
private IEnumerator RespawnRoutine(GameObject character)
|
|
{
|
|
if (character == null) yield break;
|
|
|
|
HealthNew hp = character.GetComponent<HealthNew>();
|
|
string teamTag = character.tag;
|
|
|
|
DevLog.Log($"[CashSystemManager] {character.name} died, respawning in {respawnTime}s");
|
|
|
|
// Countdown
|
|
float remaining = respawnTime;
|
|
while (remaining > 0f)
|
|
{
|
|
remaining -= Time.deltaTime;
|
|
GameEvents.FireRespawnTimerUpdate(character, remaining);
|
|
yield return null;
|
|
}
|
|
|
|
if (character == null || matchEnded) yield break;
|
|
|
|
// Respawn at team spawn area
|
|
Vector3 respawnPos = GetRespawnPosition(teamTag);
|
|
|
|
// Reset health
|
|
if (hp != null)
|
|
{
|
|
hp.currentHealth = hp.maxHealth;
|
|
hp.isDead = false;
|
|
if (hp.healthSlider != null) hp.healthSlider.value = hp.currentHealth;
|
|
}
|
|
|
|
// Teleport to respawn position
|
|
UnityEngine.AI.NavMeshAgent agent = character.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (agent != null && agent.enabled)
|
|
{
|
|
agent.enabled = false;
|
|
character.transform.position = respawnPos;
|
|
agent.enabled = true;
|
|
agent.Warp(respawnPos);
|
|
}
|
|
else
|
|
{
|
|
character.transform.position = respawnPos;
|
|
}
|
|
|
|
// Re-enable combat
|
|
EnableCharacterCombat(character);
|
|
|
|
// Re-enable the character if deactivated
|
|
if (!character.activeInHierarchy)
|
|
character.SetActive(true);
|
|
|
|
GameEvents.FireCharacterRespawned(character);
|
|
GameEvents.FireKillFeedEntry(character.name, "RESPAWNED", "");
|
|
|
|
DevLog.Log($"[CashSystemManager] {character.name} respawned at {respawnPos}");
|
|
}
|
|
|
|
private Vector3 GetRespawnPosition(string teamTag)
|
|
{
|
|
// Use team spawn points or vault/station position
|
|
Transform[] spawnPts = teamTag == "Player" ? playerTeamSpawnPoints : enemyTeamSpawnPoints;
|
|
if (spawnPts != null && spawnPts.Length > 0)
|
|
{
|
|
Transform pt = spawnPts[Random.Range(0, spawnPts.Length)];
|
|
if (pt != null) return pt.position;
|
|
}
|
|
|
|
// Fallback: near station or vault
|
|
if (teamTag == "Player")
|
|
{
|
|
if (playerStation != null) return playerStation.transform.position + Vector3.forward * 3f;
|
|
if (playerVault != null) return playerVault.transform.position + Vector3.forward * 3f;
|
|
}
|
|
else
|
|
{
|
|
if (enemyStation != null) return enemyStation.transform.position + Vector3.back * 3f;
|
|
if (enemyVault != null) return enemyVault.transform.position + Vector3.back * 3f;
|
|
}
|
|
|
|
return Vector3.zero;
|
|
}
|
|
|
|
// ─── Team Wipe Detection (-30% cash penalty) ─────────────
|
|
|
|
private void CheckTeamWipe(string victimTeamTag)
|
|
{
|
|
List<GameObject> teamChars = GetTeamCharacters(victimTeamTag);
|
|
if (teamChars == null || teamChars.Count == 0) return;
|
|
|
|
bool allDead = true;
|
|
for (int i = 0; i < teamChars.Count; i++)
|
|
{
|
|
if (teamChars[i] == null) continue;
|
|
HealthNew hp = teamChars[i].GetComponent<HealthNew>();
|
|
if (hp == null || !hp.isDead)
|
|
{
|
|
allDead = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allDead)
|
|
{
|
|
ApplyTeamWipePenalty(victimTeamTag);
|
|
GameEvents.FireTeamWipe(victimTeamTag);
|
|
}
|
|
}
|
|
|
|
private void ApplyTeamWipePenalty(string teamTag)
|
|
{
|
|
float currentCash = GetTeamTotalCash(teamTag);
|
|
float penalty = currentCash * teamWipePenaltyPercent;
|
|
|
|
if (penalty <= 0f) return;
|
|
|
|
// Remove from CashoutStation first, then TeamVault
|
|
if (teamTag == "Player" && playerStation != null)
|
|
playerStation.RemoveCash(penalty);
|
|
else if (teamTag == "Enemy" && enemyStation != null)
|
|
enemyStation.RemoveCash(penalty);
|
|
else if (teamTag == "Player" && playerVault != null)
|
|
playerVault.DepositCash(-penalty); // Negative deposit = removal
|
|
else if (teamTag == "Enemy" && enemyVault != null)
|
|
enemyVault.DepositCash(-penalty);
|
|
|
|
GameEvents.FirePopupMessage($"TEAM WIPE! -{teamWipePenaltyPercent * 100:F0}% CASH (${penalty:F0})", 3f);
|
|
GameEvents.FireKillFeedEntry(teamTag, "TEAM WIPED!", $"-${penalty:F0}");
|
|
|
|
DevLog.Log($"[CashSystemManager] Team wipe penalty: {teamTag} lost ${penalty:F0} ({teamWipePenaltyPercent * 100}%)");
|
|
}
|
|
|
|
// ─── Vault Drop on Death ─────────────────────────────────
|
|
|
|
private void DropVaultOnDeath(GameObject character)
|
|
{
|
|
var vaults = EntityRegistry<ExtractionVault>.GetAll();
|
|
for (int i = 0; i < vaults.Count; i++)
|
|
{
|
|
if (vaults[i] != null && vaults[i].IsCarried && vaults[i].Carrier == character)
|
|
{
|
|
vaults[i].Drop(character.transform.position);
|
|
GameEvents.FirePopupMessage("VAULT DROPPED!", 2f);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Cash Helpers ────────────────────────────────────────
|
|
|
|
/// <summary>Get total cash for a team from all sources (CashoutStation + TeamVault).</summary>
|
|
public float GetTeamTotalCash(string teamTag)
|
|
{
|
|
float total = 0f;
|
|
|
|
if (teamTag == "Player")
|
|
{
|
|
if (playerStation != null) total += playerStation.StoredCash;
|
|
if (playerVault != null) total += playerVault.StoredCash;
|
|
}
|
|
else
|
|
{
|
|
if (enemyStation != null) total += enemyStation.StoredCash;
|
|
if (enemyVault != null) total += enemyVault.StoredCash;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
/// <summary>Award cash to a team's CashoutStation (or fallback to TeamVault).</summary>
|
|
private void AwardCashToTeam(string teamTag, float amount)
|
|
{
|
|
if (teamTag == "Player")
|
|
{
|
|
if (playerStation != null)
|
|
playerStation.AddDirectCash(amount);
|
|
else if (playerVault != null)
|
|
playerVault.DepositCash(amount);
|
|
}
|
|
else
|
|
{
|
|
if (enemyStation != null)
|
|
enemyStation.AddDirectCash(amount);
|
|
else if (enemyVault != null)
|
|
enemyVault.DepositCash(amount);
|
|
}
|
|
}
|
|
|
|
private void EndMatch(bool? playerWon = null)
|
|
{
|
|
if (matchEnded) return;
|
|
matchEnded = true;
|
|
|
|
// Restore time scale if buzzer was active
|
|
if (_buzzerActive)
|
|
{
|
|
Time.timeScale = 1f;
|
|
Time.fixedDeltaTime = 0.02f;
|
|
}
|
|
|
|
if (playerWon == null)
|
|
{
|
|
float playerScore = GetTeamTotalCash("Player");
|
|
float enemyScore = GetTeamTotalCash("Enemy");
|
|
playerWon = playerScore > enemyScore;
|
|
}
|
|
|
|
string winnerTag = playerWon == true ? "Player" : "Enemy";
|
|
|
|
GameEvents.FireCombatEnabled(false);
|
|
GameEvents.FireMatchEnd(winnerTag);
|
|
GameEvents.FireMatchResult(playerWon == true);
|
|
|
|
// MatchResultUI subscribes to OnMatchResult and shows itself.
|
|
// Keep legacy screens as fallback only if MatchResultUI is absent.
|
|
if (MatchResultUI.Instance == null)
|
|
{
|
|
if (playerWon == true && victoryScreen != null)
|
|
victoryScreen.SetActive(true);
|
|
else if (playerWon == false && defeatScreen != null)
|
|
defeatScreen.SetActive(true);
|
|
}
|
|
|
|
StartCoroutine(EndMatchSequence());
|
|
}
|
|
|
|
private IEnumerator EndMatchSequence()
|
|
{
|
|
// If MatchResultUI is present, it handles navigation via Continue/Restart buttons.
|
|
// Only auto-navigate if MatchResultUI is NOT present (legacy fallback).
|
|
if (MatchResultUI.Instance != null)
|
|
{
|
|
// Let the player use the Continue / Restart buttons — don't auto-load.
|
|
yield break;
|
|
}
|
|
|
|
yield return new WaitForSeconds(5f);
|
|
|
|
GameEvents.ClearAll();
|
|
CashSystemAI.ClearAll();
|
|
SelectionOptions.Instance.isCashSystemSelected = false;
|
|
SceneManager.LoadScene("MenuScene");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all characters on a team
|
|
/// </summary>
|
|
public List<GameObject> GetTeamCharacters(string teamTag)
|
|
{
|
|
return teamTag == "Player" ? playerTeamCharacters : enemyTeamCharacters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the vault for a team
|
|
/// </summary>
|
|
public TeamVault GetVault(string teamTag)
|
|
{
|
|
return teamTag == "Player" ? playerVault : enemyVault;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch control to a specific character index in the player team.
|
|
/// Called by TeamHUDPanel when a portrait is clicked.
|
|
/// </summary>
|
|
public void SwitchToCharacterAtIndex(int index)
|
|
{
|
|
if (index < 0 || index >= playerTeamCharacters.Count) return;
|
|
if (index == currentControlledIndex) return;
|
|
|
|
GameObject targetCharacter = playerTeamCharacters[index];
|
|
if (targetCharacter == null) return;
|
|
|
|
HealthNew targetHealth = targetCharacter.GetComponent<HealthNew>();
|
|
if (targetHealth != null && targetHealth.isDead) return;
|
|
|
|
// Check TeamControlManager first
|
|
if (TeamControlManager.Instance != null)
|
|
{
|
|
TeamControlManager.Instance.SwitchToCharacter(index);
|
|
|
|
GameObject newControlled = TeamControlManager.Instance.GetCurrentControlledCharacter();
|
|
if (newControlled != null)
|
|
{
|
|
GameObject oldControlled = currentControlledIndex < playerTeamCharacters.Count
|
|
? playerTeamCharacters[currentControlledIndex] : null;
|
|
currentControlledIndex = index;
|
|
SelectionOptions.Instance.currentControlledIndex = currentControlledIndex;
|
|
GameEvents.FireCharacterSwitched(oldControlled, newControlled);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fallback: Manual switching
|
|
int previousIndex = currentControlledIndex;
|
|
currentControlledIndex = index;
|
|
SelectionOptions.Instance.currentControlledIndex = currentControlledIndex;
|
|
|
|
// Disable control on previous character
|
|
GameObject previousCharacter = playerTeamCharacters[previousIndex];
|
|
if (previousCharacter != null)
|
|
{
|
|
PlayerScript prevPS = previousCharacter.GetComponent<PlayerScript>();
|
|
if (prevPS != null) prevPS.attack = false;
|
|
|
|
TeamAIController prevAI = previousCharacter.GetComponent<TeamAIController>();
|
|
if (prevAI != null) prevAI.enabled = true;
|
|
}
|
|
|
|
// Enable control on new character
|
|
if (targetCharacter != null)
|
|
{
|
|
PlayerScript newPS = targetCharacter.GetComponent<PlayerScript>();
|
|
if (newPS != null)
|
|
{
|
|
newPS.enabled = true;
|
|
newPS.attack = true;
|
|
}
|
|
|
|
TeamAIController newAI = targetCharacter.GetComponent<TeamAIController>();
|
|
if (newAI != null) newAI.enabled = false;
|
|
|
|
UpdateCameraToCharacter(targetCharacter);
|
|
}
|
|
|
|
GameEvents.FireCharacterSwitched(previousCharacter, targetCharacter);
|
|
DevLog.Log($"[CashSystemManager] Switched control to index {index}: {targetCharacter.name}");
|
|
}
|
|
}
|