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,415 @@
using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;
/// <summary>
/// TeamMember - Defines which team a character belongs to and who controls them.
/// This is the foundation for the unified character control system.
/// </summary>
public class TeamMember : MonoBehaviour
{
#region Static Registry
/// <summary>
/// Zero-allocation registry of all active TeamMembers.
/// Use this instead of FindObjectsOfType.
/// </summary>
private static readonly List<TeamMember> _allMembers = new List<TeamMember>(16);
public static IReadOnlyList<TeamMember> AllMembers => _allMembers;
#endregion
/// <summary>
/// Team affiliation enum
/// </summary>
public enum Team
{
PlayerTeam, // The player's team (human + AI teammates)
EnemyTeam // The opposing team (all AI)
}
/// <summary>
/// Control type - determines who controls this character
/// </summary>
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;
/// <summary>
/// The team this character belongs to
/// </summary>
public Team CurrentTeam
{
get => _team;
set
{
_team = value;
UpdateLayerAndTag();
}
}
/// <summary>
/// The control type for this character
/// </summary>
public ControlType CurrentControlType
{
get => _controlType;
set => SetControlType(value);
}
/// <summary>
/// Whether this character is currently controlled by the human player
/// (Backward compatible property - derives from ControlType)
/// </summary>
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);
}
}
}
/// <summary>
/// Event fired when control type changes (useful for camera, UI, input routing)
/// </summary>
public event System.Action<ControlType> OnControlTypeChanged;
/// <summary>
/// Event fired when control status changes (backward compatible)
/// </summary>
public event System.Action<bool> OnControlChanged;
/// <summary>
/// Event fired when team changes
/// </summary>
public event System.Action<Team> 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;
/// <summary>
/// Set the control type and configure all relevant components
/// </summary>
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);
}
/// <summary>
/// Force reconfiguration even if type matches (useful for debugging)
/// </summary>
public void ForceReconfigure()
{
_hasBeenConfigured = false;
SetControlType(_controlType);
}
/// <summary>
/// Configure all components based on control type
/// </summary>
private void ConfigureControlComponents(ControlType controlType)
{
// Get components
PlayerScript playerScript = GetComponent<PlayerScript>();
PlayerInput playerInput = GetComponent<PlayerInput>();
CharacterInputHandler inputHandler = GetComponent<CharacterInputHandler>();
CharacterAIController aiController = GetComponent<CharacterAIController>();
CharacterMovement charMovement = GetComponent<CharacterMovement>();
UnityEngine.AI.NavMeshAgent navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
AudioListener audioListener = GetComponent<AudioListener>();
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;
}
}
/// <summary>
/// Disable all cameras embedded in this character prefab
/// </summary>
private void DisableEmbeddedCameras()
{
Camera[] cameras = GetComponentsInChildren<Camera>(true);
foreach (Camera cam in cameras)
{
cam.enabled = false;
}
// Also disable Cinemachine components
Cinemachine.CinemachineVirtualCameraBase[] vcams = GetComponentsInChildren<Cinemachine.CinemachineVirtualCameraBase>(true);
foreach (var vcam in vcams)
{
vcam.enabled = false;
}
}
/// <summary>
/// Check if another TeamMember is an enemy
/// </summary>
public bool IsEnemy(TeamMember other)
{
if (other == null) return false;
return other.CurrentTeam != this.CurrentTeam;
}
/// <summary>
/// Check if another TeamMember is an ally
/// </summary>
public bool IsAlly(TeamMember other)
{
if (other == null) return false;
return other.CurrentTeam == this.CurrentTeam;
}
/// <summary>
/// Updates the layer and tag based on team
/// </summary>
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);
}
/// <summary>
/// Static helper to get all team members of a specific team
/// </summary>
public static TeamMember[] GetTeamMembers(Team team)
{
var result = new List<TeamMember>();
for (int i = 0; i < _allMembers.Count; i++)
{
if (_allMembers[i] != null && _allMembers[i].CurrentTeam == team)
result.Add(_allMembers[i]);
}
return result.ToArray();
}
/// <summary>
/// Get the character stats for this character
/// </summary>
public CharacterStats GetCharacterStats()
{
return _characterStats;
}
/// <summary>
/// Set the character stats for this character
/// </summary>
public void SetCharacterStats(CharacterStats stats)
{
_characterStats = stats;
}
/// <summary>
/// Get the team this character belongs to
/// </summary>
public Team GetTeam()
{
return _team;
}
/// <summary>
/// Static helper to get the currently player-controlled character
/// </summary>
public static TeamMember GetPlayerControlledMember()
{
for (int i = 0; i < _allMembers.Count; i++)
{
if (_allMembers[i] != null && _allMembers[i].IsPlayerControlled)
return _allMembers[i];
}
return null;
}
/// <summary>
/// Find the nearest enemy to this character
/// </summary>
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;
}
/// <summary>
/// Find the nearest ally to this character
/// </summary>
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;
}
}