using UnityEngine; using UnityEngine.InputSystem; using System.Collections.Generic; /// /// TeamMember - Defines which team a character belongs to and who controls them. /// This is the foundation for the unified character control system. /// public class TeamMember : MonoBehaviour { #region Static Registry /// /// Zero-allocation registry of all active TeamMembers. /// Use this instead of FindObjectsOfType. /// private static readonly List _allMembers = new List(16); public static IReadOnlyList AllMembers => _allMembers; #endregion /// /// Team affiliation enum /// public enum Team { PlayerTeam, // The player's team (human + AI teammates) EnemyTeam // The opposing team (all AI) } /// /// Control type - determines who controls this character /// public enum ControlType { HumanControlled, // Currently controlled by human player (reads input) PlayerAI, // AI teammate on player's team EnemyAI // AI enemy on opposing team } [Header("Character Data")] [SerializeField] private CharacterStats _characterStats; [Header("Team Settings")] [SerializeField] private Team _team = Team.PlayerTeam; [Header("Control Settings")] [SerializeField] private ControlType _controlType = ControlType.EnemyAI; /// /// The team this character belongs to /// public Team CurrentTeam { get => _team; set { _team = value; UpdateLayerAndTag(); } } /// /// The control type for this character /// public ControlType CurrentControlType { get => _controlType; set => SetControlType(value); } /// /// Whether this character is currently controlled by the human player /// (Backward compatible property - derives from ControlType) /// public bool IsPlayerControlled { get => _controlType == ControlType.HumanControlled; set { if (value) { SetControlType(ControlType.HumanControlled); } else { // If turning off player control, make them AI based on team SetControlType(_team == Team.PlayerTeam ? ControlType.PlayerAI : ControlType.EnemyAI); } } } /// /// Event fired when control type changes (useful for camera, UI, input routing) /// public event System.Action OnControlTypeChanged; /// /// Event fired when control status changes (backward compatible) /// public event System.Action OnControlChanged; /// /// Event fired when team changes /// public event System.Action OnTeamChanged; private void Awake() { UpdateLayerAndTag(); } private void OnEnable() { if (!_allMembers.Contains(this)) _allMembers.Add(this); } private void OnDisable() { _allMembers.Remove(this); } // Track if we've done initial configuration private bool _hasBeenConfigured = false; /// /// Set the control type and configure all relevant components /// public void SetControlType(ControlType newType) { // Skip if already this type AND already configured if (_controlType == newType && _hasBeenConfigured) { Debug.Log($"[TeamMember] {gameObject.name}: Already configured as {newType}, skipping"); return; } ControlType oldType = _controlType; _controlType = newType; _hasBeenConfigured = true; Debug.Log($"[TeamMember] {gameObject.name}: Control changed from {oldType} to {newType}"); // Configure components based on control type ConfigureControlComponents(newType); // Update layer and tag UpdateLayerAndTag(); // Fire events OnControlTypeChanged?.Invoke(newType); OnControlChanged?.Invoke(newType == ControlType.HumanControlled); } /// /// Force reconfiguration even if type matches (useful for debugging) /// public void ForceReconfigure() { _hasBeenConfigured = false; SetControlType(_controlType); } /// /// Configure all components based on control type /// private void ConfigureControlComponents(ControlType controlType) { // Get components PlayerScript playerScript = GetComponent(); PlayerInput playerInput = GetComponent(); CharacterInputHandler inputHandler = GetComponent(); CharacterAIController aiController = GetComponent(); CharacterMovement charMovement = GetComponent(); UnityEngine.AI.NavMeshAgent navAgent = GetComponent(); AudioListener audioListener = GetComponent(); switch (controlType) { case ControlType.HumanControlled: // Enable player input components if (playerScript != null) playerScript.enabled = true; if (playerInput != null) { playerInput.enabled = true; try { playerInput.SwitchCurrentActionMap("Player Controls"); } catch { } } if (inputHandler != null) inputHandler.enabled = true; if (charMovement != null) charMovement.enabled = true; // Disable AI components if (aiController != null) aiController.enabled = false; if (navAgent != null) navAgent.enabled = false; // Enable audio listener only on player-controlled if (audioListener != null) audioListener.enabled = true; // Disable embedded cameras (main camera will follow) DisableEmbeddedCameras(); Debug.Log($"[TeamMember] {gameObject.name}: Configured for HUMAN control"); break; case ControlType.PlayerAI: case ControlType.EnemyAI: // Disable player input components if (playerScript != null) playerScript.enabled = false; if (playerInput != null) playerInput.enabled = false; if (inputHandler != null) inputHandler.enabled = false; if (charMovement != null) charMovement.enabled = false; // AI uses NavMesh // Enable AI components if (aiController != null) aiController.enabled = true; if (navAgent != null) { navAgent.enabled = true; // CRITICAL: Warp agent to NavMesh IMMEDIATELY after enabling. // Without this, there is a 1-frame gap where the agent is enabled // but not on NavMesh, causing floating. CharacterAIController.Start() // also warps, but it runs on the NEXT frame — too late. UnityEngine.AI.NavMeshHit navHit; if (UnityEngine.AI.NavMesh.SamplePosition(transform.position, out navHit, 20f, UnityEngine.AI.NavMesh.AllAreas)) { navAgent.Warp(navHit.position); Debug.Log($"[TeamMember] {gameObject.name}: Warped to NavMesh at Y={navHit.position.y}"); } else { Debug.LogWarning($"[TeamMember] {gameObject.name}: No NavMesh found within 20m at {transform.position}!"); } } // Disable audio listener on AI if (audioListener != null) audioListener.enabled = false; // Disable embedded cameras DisableEmbeddedCameras(); Debug.Log($"[TeamMember] {gameObject.name}: Configured for AI control ({controlType})"); break; } } /// /// Disable all cameras embedded in this character prefab /// private void DisableEmbeddedCameras() { Camera[] cameras = GetComponentsInChildren(true); foreach (Camera cam in cameras) { cam.enabled = false; } // Also disable Cinemachine components Cinemachine.CinemachineVirtualCameraBase[] vcams = GetComponentsInChildren(true); foreach (var vcam in vcams) { vcam.enabled = false; } } /// /// Check if another TeamMember is an enemy /// public bool IsEnemy(TeamMember other) { if (other == null) return false; return other.CurrentTeam != this.CurrentTeam; } /// /// Check if another TeamMember is an ally /// public bool IsAlly(TeamMember other) { if (other == null) return false; return other.CurrentTeam == this.CurrentTeam; } /// /// Updates the layer and tag based on team /// private void UpdateLayerAndTag() { // Set appropriate tag (for compatibility with existing systems) if (_team == Team.PlayerTeam) { if (_controlType == ControlType.HumanControlled) { gameObject.tag = "Player"; // Use Player One layer if available int playerLayer = LayerMask.NameToLayer("Player One"); if (playerLayer >= 0) gameObject.layer = playerLayer; } else { // AI teammate - still on player team but different handling // Keep "Player" tag for combat targeting but mark as ally gameObject.tag = "Player"; int playerLayer = LayerMask.NameToLayer("Player One"); if (playerLayer >= 0) gameObject.layer = playerLayer; } } else // Enemy Team { gameObject.tag = "Enemy"; int enemyLayer = LayerMask.NameToLayer("Player Two"); if (enemyLayer >= 0) gameObject.layer = enemyLayer; } OnTeamChanged?.Invoke(_team); } /// /// Static helper to get all team members of a specific team /// public static TeamMember[] GetTeamMembers(Team team) { var result = new List(); for (int i = 0; i < _allMembers.Count; i++) { if (_allMembers[i] != null && _allMembers[i].CurrentTeam == team) result.Add(_allMembers[i]); } return result.ToArray(); } /// /// Get the character stats for this character /// public CharacterStats GetCharacterStats() { return _characterStats; } /// /// Set the character stats for this character /// public void SetCharacterStats(CharacterStats stats) { _characterStats = stats; } /// /// Get the team this character belongs to /// public Team GetTeam() { return _team; } /// /// Static helper to get the currently player-controlled character /// public static TeamMember GetPlayerControlledMember() { for (int i = 0; i < _allMembers.Count; i++) { if (_allMembers[i] != null && _allMembers[i].IsPlayerControlled) return _allMembers[i]; } return null; } /// /// Find the nearest enemy to this character /// public TeamMember FindNearestEnemy() { TeamMember nearest = null; float nearestDistance = float.MaxValue; for (int i = 0; i < _allMembers.Count; i++) { TeamMember other = _allMembers[i]; if (other == null || other == this || other.CurrentTeam == _team) continue; float distance = Vector3.Distance(transform.position, other.transform.position); if (distance < nearestDistance) { nearestDistance = distance; nearest = other; } } return nearest; } /// /// Find the nearest ally to this character /// public TeamMember FindNearestAlly() { TeamMember nearest = null; float nearestDistance = float.MaxValue; for (int i = 0; i < _allMembers.Count; i++) { TeamMember other = _allMembers[i]; if (other == null || other == this || other.CurrentTeam != _team) continue; float distance = Vector3.Distance(transform.position, other.transform.position); if (distance < nearestDistance) { nearestDistance = distance; nearest = other; } } return nearest; } }