Files
WarGame/Assets/Scripts/Units/Unit.cs
2026-09-30 23:45:58 +01:00

613 lines
21 KiB
C#

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<Unit> OnUnitDied;
public event Action<float, float> OnHealthChanged;
private void Awake()
{
if (agent == null) agent = GetComponent<NavMeshAgent>();
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<Unit> 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
}
}