using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using WarGame.Data; namespace WarGame.Units { public enum UnitTeam { Player, Enemy } public enum UnitState { Idle, Approaching, Flanking, Attacking, FeintRetreating, Dead } [RequireComponent(typeof(Collider))] public class Unit : MonoBehaviour { [Header("Identity & Team")] [SerializeField] private UnitTeam team = UnitTeam.Player; [SerializeField] private UnitDataSO unitData; [SerializeField] private int initialLevel = 1; [Header("Components & Visuals")] [SerializeField] private NavMeshAgent agent; [SerializeField] private UnitHealthBar healthBar; [SerializeField] private Animator animator; [SerializeField] private Renderer mainRenderer; // Runtime progression & stats public UnitProgression Progression { get; private set; } public UnitTeam Team => team; public UnitDataSO UnitData => unitData; public UnitState CurrentState { get; private set; } = UnitState.Idle; public Unit CurrentTarget { get; private set; } public float CurrentHealth { get; private set; } public float MaxHealth { get; private set; } public float AttackDamage { get; private set; } public float DefenseArmor { get; private set; } public float MoveSpeed { get; private set; } public float AttackRange { get; private set; } public float AttackInterval { get; private set; } public float CriticalChance { get; private set; } // Battle state private bool isBattleActive = false; private float lastAttackTime = -999f; private Vector3 initialSpawnPosition; private Quaternion initialSpawnRotation; // Tactics State private float feintRetreatCooldownTimer = 0f; private float feintRetreatDurationTimer = 0f; private bool hasTriggeredFeintThisBattle = false; private Vector3 flankDestination; private float flankSearchTimer = 0f; private float targetSearchTimer = 0f; // Animation IDs (if animator present) private static readonly int SpeedHash = Animator.StringToHash("Speed"); private static readonly int AttackHash = Animator.StringToHash("Attack"); private static readonly int DieHash = Animator.StringToHash("Die"); private static readonly int HitHash = Animator.StringToHash("Hit"); // Events public event Action OnUnitDied; public event Action OnHealthChanged; private void Awake() { if (agent == null) agent = GetComponent(); initialSpawnPosition = transform.position; initialSpawnRotation = transform.rotation; } public void Initialize(UnitDataSO data, UnitTeam unitTeam, int startingLevel = 1) { unitData = data; team = unitTeam; initialLevel = startingLevel; Progression = new UnitProgression(unitData, initialLevel); Progression.OnLevelUp += HandleLevelUp; Progression.OnTacticUnlocked += HandleTacticUnlocked; RefreshStats(); CurrentHealth = MaxHealth; ApplyTeamColors(); if (healthBar != null) { healthBar.Initialize(transform, team == UnitTeam.Player, unitData != null ? unitData.unitName : name, Progression.CurrentLevel); healthBar.UpdateHealth(CurrentHealth, MaxHealth); } } public void SetProgression(UnitProgression existingProgression) { if (existingProgression != null) { Progression = existingProgression; unitData = Progression.UnitData; Progression.OnLevelUp += HandleLevelUp; Progression.OnTacticUnlocked += HandleTacticUnlocked; RefreshStats(); CurrentHealth = MaxHealth; if (healthBar != null) { healthBar.UpdateLevel(Progression.CurrentLevel); healthBar.UpdateHealth(CurrentHealth, MaxHealth); } } } private void RefreshStats() { if (Progression == null) return; MaxHealth = Progression.GetMaxHealth(); AttackDamage = Progression.GetAttackDamage(); DefenseArmor = Progression.GetDefenseArmor(); MoveSpeed = Progression.GetMoveSpeed(); AttackRange = Progression.GetAttackRange(); AttackInterval = Progression.GetAttackInterval(); CriticalChance = Progression.GetCriticalChance(); if (agent != null) { agent.speed = MoveSpeed; agent.stoppingDistance = Mathf.Max(0.5f, AttackRange * 0.8f); } } private void ApplyTeamColors() { if (mainRenderer != null) { Color tint = team == UnitTeam.Player ? new Color(0.2f, 0.6f, 1f) : new Color(1f, 0.25f, 0.25f); mainRenderer.material.color = tint; } } public void StartBattle() { isBattleActive = true; CurrentState = UnitState.Approaching; hasTriggeredFeintThisBattle = false; feintRetreatDurationTimer = 0f; lastAttackTime = Time.time - UnityEngine.Random.Range(0f, AttackInterval * 0.5f); } public void EndBattle(bool restorePosition = false) { isBattleActive = false; CurrentState = UnitState.Idle; StopMovement(); if (restorePosition && CurrentHealth > 0) { // Move back to spawn position and full heal between rounds WarpToPosition(initialSpawnPosition, initialSpawnRotation); HealToFull(); } } public void HealToFull() { CurrentHealth = MaxHealth; OnHealthChanged?.Invoke(CurrentHealth, MaxHealth); if (healthBar != null) healthBar.UpdateHealth(CurrentHealth, MaxHealth); } private void Update() { if (CurrentState == UnitState.Dead) return; // Handle Tactic Cooldowns if (feintRetreatCooldownTimer > 0) feintRetreatCooldownTimer -= Time.deltaTime; UpdateAnimation(); if (!isBattleActive) return; // AI Decision Loop switch (CurrentState) { case UnitState.Approaching: HandleApproachingState(); break; case UnitState.Flanking: HandleFlankingState(); break; case UnitState.Attacking: HandleAttackingState(); break; case UnitState.FeintRetreating: HandleFeintRetreatState(); break; case UnitState.Idle: FindBestTarget(); if (CurrentTarget != null) CurrentState = UnitState.Approaching; break; } } #region Tactical AI Logic private void FindBestTarget() { targetSearchTimer -= Time.deltaTime; if (targetSearchTimer > 0 && CurrentTarget != null && CurrentTarget.IsAlive()) return; targetSearchTimer = 0.5f; Unit closest = null; float closestDistanceSqr = float.MaxValue; List allUnits = BattleManager.Instance != null ? BattleManager.Instance.AllActiveUnits : null; if (allUnits == null) return; for (int i = 0; i < allUnits.Count; i++) { Unit candidate = allUnits[i]; if (candidate == null || candidate == this || candidate.Team == team || !candidate.IsAlive()) continue; float distSqr = (candidate.transform.position - transform.position).sqrMagnitude; if (distSqr < closestDistanceSqr) { closestDistanceSqr = distSqr; closest = candidate; } } CurrentTarget = closest; } private void HandleApproachingState() { if (CurrentTarget == null || !CurrentTarget.IsAlive()) { FindBestTarget(); if (CurrentTarget == null) { StopMovement(); CurrentState = UnitState.Idle; return; } } // Check if Feint Retreat should be triggered if (CheckAndTriggerFeintRetreat()) return; // Check if Flanking maneuver should be initiated if (CheckAndTriggerFlanking()) return; float distance = Vector3.Distance(transform.position, CurrentTarget.transform.position); if (distance <= AttackRange) { StopMovement(); CurrentState = UnitState.Attacking; } else { MoveTowards(CurrentTarget.transform.position); } } private bool CheckAndTriggerFlanking() { if (Progression == null || CurrentTarget == null) return false; flankSearchTimer -= Time.deltaTime; if (flankSearchTimer > 0) return false; flankSearchTimer = 1.5f; if (Progression.HasTactic(UnitTacticType.Flanking, out UnlockedTactic flankConfig)) { // Flanking is ideal if target is engaged with another friendly unit or target is not facing this unit Vector3 toAttacker = (transform.position - CurrentTarget.transform.position).normalized; float angle = Vector3.Angle(CurrentTarget.transform.forward, toAttacker); // If already at flank (angle > 50 degrees) or if we want to reposition to rear if (angle < 60f) { // Compute flanking point: 90 degrees offset to left or right, or behind Vector3 flankSide = UnityEngine.Random.value > 0.5f ? CurrentTarget.transform.right : -CurrentTarget.transform.right; Vector3 flankPos = CurrentTarget.transform.position + (-CurrentTarget.transform.forward * 1.5f + flankSide * 2f); flankDestination = flankPos; CurrentState = UnitState.Flanking; if (healthBar != null) { healthBar.ShowTacticAlert("FLANKING!", new Color(1f, 0.85f, 0.2f), 2f); } return true; } } return false; } private void HandleFlankingState() { if (CurrentTarget == null || !CurrentTarget.IsAlive()) { CurrentState = UnitState.Approaching; return; } if (CheckAndTriggerFeintRetreat()) return; float distanceToTarget = Vector3.Distance(transform.position, CurrentTarget.transform.position); if (distanceToTarget <= AttackRange) { StopMovement(); CurrentState = UnitState.Attacking; return; } // Move along flank perimeter MoveTowards(flankDestination); if (Vector3.Distance(transform.position, flankDestination) < 1.0f) { // Reached flank position, now attack! CurrentState = UnitState.Attacking; } } private bool CheckAndTriggerFeintRetreat() { if (hasTriggeredFeintThisBattle || feintRetreatCooldownTimer > 0) return false; if (Progression == null) return false; if (Progression.HasTactic(UnitTacticType.FeintRetreat, out UnlockedTactic config)) { float hpPercent = CurrentHealth / MaxHealth; if (hpPercent <= config.triggerThreshold) { hasTriggeredFeintThisBattle = true; feintRetreatDurationTimer = config.duration; feintRetreatCooldownTimer = config.cooldown; CurrentState = UnitState.FeintRetreating; if (healthBar != null) { healthBar.ShowTacticAlert("FEINT RETREAT!", new Color(0.4f, 1f, 0.4f), 3f); } return true; } } return false; } private void HandleFeintRetreatState() { feintRetreatDurationTimer -= Time.deltaTime; // Run away from current target or towards initial spawn/friendly lines Vector3 retreatDirection; if (CurrentTarget != null) { retreatDirection = (transform.position - CurrentTarget.transform.position).normalized; } else { retreatDirection = (initialSpawnPosition - transform.position).normalized; } Vector3 retreatTargetPos = transform.position + retreatDirection * 5f; MoveTowards(retreatTargetPos, MoveSpeed * 1.3f); // Sprint during retreat if (feintRetreatDurationTimer <= 0) { // Feint ended! Rally, turn around and counter-attack with a temporary morale boost if (healthBar != null) { healthBar.ShowTacticAlert("COUNTER-STRIKE!", Color.cyan, 2f); } CurrentState = UnitState.Approaching; } } private void HandleAttackingState() { if (CurrentTarget == null || !CurrentTarget.IsAlive()) { CurrentState = UnitState.Approaching; return; } if (CheckAndTriggerFeintRetreat()) return; float distance = Vector3.Distance(transform.position, CurrentTarget.transform.position); if (distance > AttackRange * 1.25f) { CurrentState = UnitState.Approaching; return; } // Face target Vector3 lookDirection = (CurrentTarget.transform.position - transform.position).normalized; lookDirection.y = 0; if (lookDirection != Vector3.zero) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(lookDirection), Time.deltaTime * 10f); } // Attack on cooldown if (Time.time - lastAttackTime >= AttackInterval) { ExecuteAttack(); } } private void ExecuteAttack() { lastAttackTime = Time.time; if (animator != null) { animator.SetTrigger(AttackHash); } if (CurrentTarget == null || !CurrentTarget.IsAlive()) return; // Calculate flanking angle: angle between target forward and vector towards attacker Vector3 toAttacker = (transform.position - CurrentTarget.transform.position).normalized; float angle = Vector3.Angle(CurrentTarget.transform.forward, toAttacker); bool isFlankAttack = angle > 60f; float finalDamage = AttackDamage; // Critical hit calculation bool isCrit = UnityEngine.Random.value < CriticalChance; if (isCrit) finalDamage *= 1.5f; // Flank bonus calculation if flanking tactic is unlocked if (isFlankAttack && Progression != null && Progression.HasTactic(UnitTacticType.Flanking, out UnlockedTactic flankConfig)) { finalDamage *= flankConfig.valueMultiplier; if (healthBar != null) { healthBar.ShowTacticAlert("FLANK STRIKE!", new Color(1f, 0.4f, 0.1f), 1f); } } CurrentTarget.TakeDamage(finalDamage, this, isFlankAttack); } #endregion #region Damage & Progression public void TakeDamage(float rawDamage, Unit attacker, bool isFlank) { if (CurrentState == UnitState.Dead) return; // Mitigate with defense armor float effectiveDamage = Mathf.Max(1f, rawDamage - DefenseArmor); CurrentHealth -= effectiveDamage; if (animator != null) { animator.SetTrigger(HitHash); } OnHealthChanged?.Invoke(CurrentHealth, MaxHealth); if (healthBar != null) { healthBar.UpdateHealth(CurrentHealth, MaxHealth); } if (CurrentHealth <= 0) { Die(attacker); } } private void Die(Unit killer) { CurrentHealth = 0; CurrentState = UnitState.Dead; StopMovement(); if (animator != null) { animator.SetTrigger(DieHash); } // Award XP to killer if (killer != null && killer.Progression != null) { int xpReward = unitData != null ? Mathf.RoundToInt(unitData.baseExpRequirement * 0.5f) : 25; killer.Progression.AddExperience(xpReward); killer.Progression.RecordKill(); } OnUnitDied?.Invoke(this); if (BattleManager.Instance != null) { BattleManager.Instance.OnUnitDefeated(this); } // Start coroutine to disable or cleanup StartCoroutine(DeathFadeRoutine()); } private IEnumerator DeathFadeRoutine() { yield return new WaitForSeconds(2.5f); if (healthBar != null) healthBar.gameObject.SetActive(false); gameObject.SetActive(false); } private void HandleLevelUp(int newLevel) { RefreshStats(); CurrentHealth = MaxHealth; // Heal up on level up if (healthBar != null) { healthBar.UpdateLevel(newLevel); healthBar.UpdateHealth(CurrentHealth, MaxHealth); healthBar.ShowTacticAlert($"LEVEL UP! (Lv.{newLevel})", Color.yellow, 2.5f); } } private void HandleTacticUnlocked(UnlockedTactic tactic) { if (healthBar != null) { healthBar.ShowTacticAlert($"NEW TACTIC: {tactic.tacticType}!", Color.magenta, 3f); } } #endregion #region Movement & Navigation private void MoveTowards(Vector3 destination, float customSpeed = -1f) { float speedToUse = customSpeed > 0 ? customSpeed : MoveSpeed; if (agent != null && agent.isOnNavMesh) { agent.isStopped = false; agent.speed = speedToUse; agent.SetDestination(destination); } else { // Fallback direct movement if NavMeshAgent is absent or off mesh Vector3 direction = (destination - transform.position).normalized; direction.y = 0; if (direction != Vector3.zero) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f); transform.position += direction * (speedToUse * Time.deltaTime); } } } private void StopMovement() { if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.ResetPath(); } } public void WarpToPosition(Vector3 position, Quaternion rotation) { transform.position = position; transform.rotation = rotation; if (agent != null && agent.isOnNavMesh) { agent.Warp(position); } } private void UpdateAnimation() { if (animator == null) return; float currentSpeed = 0f; if (agent != null && agent.isOnNavMesh) { currentSpeed = agent.velocity.magnitude; } animator.SetFloat(SpeedHash, currentSpeed); } public bool IsAlive() => CurrentState != UnitState.Dead && CurrentHealth > 0; #endregion } }