imtial commit
This commit is contained in:
612
Assets/Scripts/Units/Unit.cs
Normal file
612
Assets/Scripts/Units/Unit.cs
Normal file
@@ -0,0 +1,612 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user