613 lines
21 KiB
C#
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
|
||
|
|
}
|
||
|
|
}
|