441 lines
15 KiB
C#
441 lines
15 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using UnityEngine;
|
||
|
|
using WarGame.Data;
|
||
|
|
using WarGame.Deployment;
|
||
|
|
using WarGame.Units;
|
||
|
|
|
||
|
|
namespace WarGame.Managers
|
||
|
|
{
|
||
|
|
public enum BattleState
|
||
|
|
{
|
||
|
|
Preparation,
|
||
|
|
BattleInProgress,
|
||
|
|
Victory,
|
||
|
|
Defeat
|
||
|
|
}
|
||
|
|
|
||
|
|
[System.Serializable]
|
||
|
|
public class EnemyWaveConfig
|
||
|
|
{
|
||
|
|
public string waveName = "Wave 1";
|
||
|
|
public List<EnemySpawnInfo> enemies = new List<EnemySpawnInfo>();
|
||
|
|
public int rewardGold = 100;
|
||
|
|
public int waveBonusExp = 40;
|
||
|
|
}
|
||
|
|
|
||
|
|
[System.Serializable]
|
||
|
|
public class EnemySpawnInfo
|
||
|
|
{
|
||
|
|
public UnitDataSO unitData;
|
||
|
|
public int level = 1;
|
||
|
|
public int count = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
public class BattleManager : MonoBehaviour
|
||
|
|
{
|
||
|
|
public static BattleManager Instance { get; private set; }
|
||
|
|
|
||
|
|
[Header("Deployment Zones")]
|
||
|
|
[SerializeField] private DeploymentZone playerDeploymentZone;
|
||
|
|
[SerializeField] private DeploymentZone enemyDeploymentZone;
|
||
|
|
|
||
|
|
[Header("Default Unit Prefab (Fallback if UnitData lacks prefab)")]
|
||
|
|
[SerializeField] private GameObject defaultUnitPrefab;
|
||
|
|
|
||
|
|
[Header("Economy & Progression")]
|
||
|
|
[SerializeField] private int startingGold = 150;
|
||
|
|
[SerializeField] private int baseWaveClearGold = 75;
|
||
|
|
[SerializeField] private int killBountyGold = 10;
|
||
|
|
[SerializeField] private int battleParticipationExp = 30;
|
||
|
|
|
||
|
|
[Header("Waves Setup")]
|
||
|
|
[SerializeField] private List<EnemyWaveConfig> predefinedWaves = new List<EnemyWaveConfig>();
|
||
|
|
[SerializeField] private List<UnitDataSO> defaultEnemyUnitTypes = new List<UnitDataSO>();
|
||
|
|
|
||
|
|
// Current state
|
||
|
|
public BattleState CurrentBattleState { get; private set; } = BattleState.Preparation;
|
||
|
|
public int CurrentGold { get; private set; }
|
||
|
|
public int CurrentWave { get; private set; } = 1;
|
||
|
|
|
||
|
|
// Ongoing Live Unit Tracking
|
||
|
|
private readonly List<Unit> allActiveUnits = new List<Unit>();
|
||
|
|
private readonly List<Unit> playerArmyUnits = new List<Unit>();
|
||
|
|
private readonly List<Unit> enemyArmyUnits = new List<Unit>();
|
||
|
|
|
||
|
|
public List<Unit> AllActiveUnits => allActiveUnits;
|
||
|
|
public List<Unit> PlayerArmyUnits => playerArmyUnits;
|
||
|
|
public List<Unit> EnemyArmyUnits => enemyArmyUnits;
|
||
|
|
|
||
|
|
public int PlayerAliveCount { get; private set; }
|
||
|
|
public int EnemyAliveCount { get; private set; }
|
||
|
|
public int PlayerTotalCount => playerArmyUnits.Count;
|
||
|
|
public int EnemyTotalCount => enemyArmyUnits.Count;
|
||
|
|
|
||
|
|
// Events
|
||
|
|
public event Action<BattleState> OnBattleStateChanged;
|
||
|
|
public event Action<int, int> OnUnitCountsChanged; // playerAlive, enemyAlive
|
||
|
|
public event Action<int> OnGoldChanged;
|
||
|
|
public event Action<int> OnWaveChanged;
|
||
|
|
public event Action<BattleResultData> OnBattleCompleted;
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
if (Instance != null && Instance != this)
|
||
|
|
{
|
||
|
|
Destroy(gameObject);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Instance = this;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Start()
|
||
|
|
{
|
||
|
|
CurrentGold = startingGold;
|
||
|
|
OnGoldChanged?.Invoke(CurrentGold);
|
||
|
|
OnWaveChanged?.Invoke(CurrentWave);
|
||
|
|
SetBattleState(BattleState.Preparation);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SetBattleState(BattleState newState)
|
||
|
|
{
|
||
|
|
CurrentBattleState = newState;
|
||
|
|
OnBattleStateChanged?.Invoke(newState);
|
||
|
|
}
|
||
|
|
|
||
|
|
#region Economy & Unit Buying
|
||
|
|
|
||
|
|
public bool CanAffordUnit(UnitDataSO unitData)
|
||
|
|
{
|
||
|
|
return unitData != null && CurrentGold >= unitData.goldCost;
|
||
|
|
}
|
||
|
|
|
||
|
|
public Unit BuyAndDeployUnit(UnitDataSO unitData)
|
||
|
|
{
|
||
|
|
if (!CanAffordUnit(unitData))
|
||
|
|
{
|
||
|
|
Debug.LogWarning("Not enough gold to buy " + unitData.unitName);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (CurrentBattleState != BattleState.Preparation)
|
||
|
|
{
|
||
|
|
Debug.LogWarning("Cannot buy units while battle is in progress!");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deduct gold
|
||
|
|
CurrentGold -= unitData.goldCost;
|
||
|
|
OnGoldChanged?.Invoke(CurrentGold);
|
||
|
|
|
||
|
|
// Spawn unit in player zone
|
||
|
|
Vector3 spawnPos = playerDeploymentZone != null ? playerDeploymentZone.GetNextSpawnPosition() : transform.position;
|
||
|
|
Quaternion spawnRot = Quaternion.identity;
|
||
|
|
|
||
|
|
GameObject prefabToSpawn = unitData.unitPrefab != null ? unitData.unitPrefab : defaultUnitPrefab;
|
||
|
|
if (prefabToSpawn == null)
|
||
|
|
{
|
||
|
|
Debug.LogError("No prefab found for unit: " + unitData.unitName);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
GameObject instance = Instantiate(prefabToSpawn, spawnPos, spawnRot);
|
||
|
|
Unit unit = instance.GetComponent<Unit>();
|
||
|
|
if (unit == null) unit = instance.AddComponent<Unit>();
|
||
|
|
|
||
|
|
unit.Initialize(unitData, UnitTeam.Player, 1);
|
||
|
|
|
||
|
|
playerArmyUnits.Add(unit);
|
||
|
|
allActiveUnits.Add(unit);
|
||
|
|
|
||
|
|
UpdateUnitCounts();
|
||
|
|
|
||
|
|
return unit;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void AddGold(int amount)
|
||
|
|
{
|
||
|
|
CurrentGold += amount;
|
||
|
|
OnGoldChanged?.Invoke(CurrentGold);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region Battle Flow
|
||
|
|
|
||
|
|
public void StartBattle()
|
||
|
|
{
|
||
|
|
if (CurrentBattleState != BattleState.Preparation) return;
|
||
|
|
if (playerArmyUnits.Count == 0)
|
||
|
|
{
|
||
|
|
Debug.LogWarning("Recruit at least 1 unit before starting battle!");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up any remaining previous enemies
|
||
|
|
ClearEnemies();
|
||
|
|
|
||
|
|
// Spawn Enemy Wave
|
||
|
|
SpawnEnemyWave(CurrentWave);
|
||
|
|
|
||
|
|
// Reset player positions and heal to full
|
||
|
|
if (playerDeploymentZone != null) playerDeploymentZone.ResetSlots();
|
||
|
|
for (int i = 0; i < playerArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (playerArmyUnits[i] != null)
|
||
|
|
{
|
||
|
|
playerArmyUnits[i].gameObject.SetActive(true);
|
||
|
|
Vector3 pos = playerDeploymentZone != null ? playerDeploymentZone.GetNextSpawnPosition() : playerArmyUnits[i].transform.position;
|
||
|
|
playerArmyUnits[i].WarpToPosition(pos, Quaternion.LookRotation(Vector3.forward));
|
||
|
|
playerArmyUnits[i].HealToFull();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateUnitCounts();
|
||
|
|
SetBattleState(BattleState.BattleInProgress);
|
||
|
|
|
||
|
|
// Start battle for all units
|
||
|
|
for (int i = 0; i < allActiveUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (allActiveUnits[i] != null && allActiveUnits[i].gameObject.activeSelf)
|
||
|
|
{
|
||
|
|
allActiveUnits[i].StartBattle();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SpawnEnemyWave(int waveNumber)
|
||
|
|
{
|
||
|
|
enemyArmyUnits.Clear();
|
||
|
|
if (enemyDeploymentZone != null) enemyDeploymentZone.ResetSlots();
|
||
|
|
|
||
|
|
// Check if there is a predefined wave configuration
|
||
|
|
EnemyWaveConfig waveConfig = null;
|
||
|
|
if (waveNumber - 1 < predefinedWaves.Count)
|
||
|
|
{
|
||
|
|
waveConfig = predefinedWaves[waveNumber - 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (waveConfig != null && waveConfig.enemies.Count > 0)
|
||
|
|
{
|
||
|
|
foreach (EnemySpawnInfo spawnInfo in waveConfig.enemies)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < spawnInfo.count; i++)
|
||
|
|
{
|
||
|
|
SpawnSingleEnemy(spawnInfo.unitData, spawnInfo.level);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Procedural wave scaling if beyond predefined waves
|
||
|
|
int enemyCount = 2 + waveNumber;
|
||
|
|
int enemyLevel = Mathf.Max(1, 1 + (waveNumber / 2));
|
||
|
|
|
||
|
|
for (int i = 0; i < enemyCount; i++)
|
||
|
|
{
|
||
|
|
UnitDataSO randomType = null;
|
||
|
|
if (defaultEnemyUnitTypes.Count > 0)
|
||
|
|
{
|
||
|
|
randomType = defaultEnemyUnitTypes[UnityEngine.Random.Range(0, defaultEnemyUnitTypes.Count)];
|
||
|
|
}
|
||
|
|
|
||
|
|
SpawnSingleEnemy(randomType, enemyLevel);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SpawnSingleEnemy(UnitDataSO unitData, int level)
|
||
|
|
{
|
||
|
|
Vector3 spawnPos = enemyDeploymentZone != null ? enemyDeploymentZone.GetNextSpawnPosition() : transform.position + Vector3.forward * 10f;
|
||
|
|
Quaternion spawnRot = Quaternion.LookRotation(Vector3.back);
|
||
|
|
|
||
|
|
GameObject prefabToSpawn = (unitData != null && unitData.unitPrefab != null) ? unitData.unitPrefab : defaultUnitPrefab;
|
||
|
|
if (prefabToSpawn == null) return;
|
||
|
|
|
||
|
|
GameObject instance = Instantiate(prefabToSpawn, spawnPos, spawnRot);
|
||
|
|
Unit enemyUnit = instance.GetComponent<Unit>();
|
||
|
|
if (enemyUnit == null) enemyUnit = instance.AddComponent<Unit>();
|
||
|
|
|
||
|
|
enemyUnit.Initialize(unitData, UnitTeam.Enemy, level);
|
||
|
|
|
||
|
|
enemyArmyUnits.Add(enemyUnit);
|
||
|
|
allActiveUnits.Add(enemyUnit);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ClearEnemies()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < enemyArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (enemyArmyUnits[i] != null)
|
||
|
|
{
|
||
|
|
allActiveUnits.Remove(enemyArmyUnits[i]);
|
||
|
|
Destroy(enemyArmyUnits[i].gameObject);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
enemyArmyUnits.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void OnUnitDefeated(Unit unit)
|
||
|
|
{
|
||
|
|
if (unit.Team == UnitTeam.Enemy)
|
||
|
|
{
|
||
|
|
AddGold(killBountyGold);
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateUnitCounts();
|
||
|
|
|
||
|
|
if (CurrentBattleState == BattleState.BattleInProgress)
|
||
|
|
{
|
||
|
|
CheckBattleResolution();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdateUnitCounts()
|
||
|
|
{
|
||
|
|
int pAlive = 0;
|
||
|
|
for (int i = 0; i < playerArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (playerArmyUnits[i] != null && playerArmyUnits[i].IsAlive()) pAlive++;
|
||
|
|
}
|
||
|
|
|
||
|
|
int eAlive = 0;
|
||
|
|
for (int i = 0; i < enemyArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (enemyArmyUnits[i] != null && enemyArmyUnits[i].IsAlive()) eAlive++;
|
||
|
|
}
|
||
|
|
|
||
|
|
PlayerAliveCount = pAlive;
|
||
|
|
EnemyAliveCount = eAlive;
|
||
|
|
|
||
|
|
OnUnitCountsChanged?.Invoke(PlayerAliveCount, EnemyAliveCount);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void CheckBattleResolution()
|
||
|
|
{
|
||
|
|
if (EnemyAliveCount <= 0)
|
||
|
|
{
|
||
|
|
ResolveBattle(isVictory: true);
|
||
|
|
}
|
||
|
|
else if (PlayerAliveCount <= 0)
|
||
|
|
{
|
||
|
|
ResolveBattle(isVictory: false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ResolveBattle(bool isVictory)
|
||
|
|
{
|
||
|
|
SetBattleState(isVictory ? BattleState.Victory : BattleState.Defeat);
|
||
|
|
|
||
|
|
// Stop battle on surviving units
|
||
|
|
for (int i = 0; i < allActiveUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (allActiveUnits[i] != null && allActiveUnits[i].IsAlive())
|
||
|
|
{
|
||
|
|
allActiveUnits[i].EndBattle(restorePosition: false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate post-battle rewards
|
||
|
|
int earnedGold = isVictory ? (baseWaveClearGold + CurrentWave * 20) : 25;
|
||
|
|
AddGold(earnedGold);
|
||
|
|
|
||
|
|
List<UnitLevelUpReport> levelUps = new List<UnitLevelUpReport>();
|
||
|
|
|
||
|
|
// Award participation XP and battle survival to player units
|
||
|
|
for (int i = 0; i < playerArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
Unit pUnit = playerArmyUnits[i];
|
||
|
|
if (pUnit == null || pUnit.Progression == null) continue;
|
||
|
|
|
||
|
|
int prevLevel = pUnit.Progression.CurrentLevel;
|
||
|
|
int xpGain = battleParticipationExp + (pUnit.IsAlive() ? 20 : 0);
|
||
|
|
pUnit.Progression.AddExperience(xpGain);
|
||
|
|
|
||
|
|
if (pUnit.IsAlive())
|
||
|
|
{
|
||
|
|
pUnit.Progression.RecordBattleSurvived();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pUnit.Progression.CurrentLevel > prevLevel)
|
||
|
|
{
|
||
|
|
levelUps.Add(new UnitLevelUpReport
|
||
|
|
{
|
||
|
|
unitName = pUnit.UnitData != null ? pUnit.UnitData.unitName : "Unit",
|
||
|
|
oldLevel = prevLevel,
|
||
|
|
newLevel = pUnit.Progression.CurrentLevel
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
BattleResultData result = new BattleResultData
|
||
|
|
{
|
||
|
|
isVictory = isVictory,
|
||
|
|
waveNumber = CurrentWave,
|
||
|
|
goldEarned = earnedGold,
|
||
|
|
survivingPlayerUnits = PlayerAliveCount,
|
||
|
|
totalPlayerUnits = PlayerTotalCount,
|
||
|
|
enemyCasualties = EnemyTotalCount,
|
||
|
|
levelUpReports = levelUps
|
||
|
|
};
|
||
|
|
|
||
|
|
OnBattleCompleted?.Invoke(result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void ContinueToNextRound()
|
||
|
|
{
|
||
|
|
if (CurrentBattleState == BattleState.Victory)
|
||
|
|
{
|
||
|
|
CurrentWave++;
|
||
|
|
OnWaveChanged?.Invoke(CurrentWave);
|
||
|
|
}
|
||
|
|
|
||
|
|
ClearEnemies();
|
||
|
|
|
||
|
|
// Restore player army to deployment positions and heal
|
||
|
|
if (playerDeploymentZone != null) playerDeploymentZone.ResetSlots();
|
||
|
|
for (int i = 0; i < playerArmyUnits.Count; i++)
|
||
|
|
{
|
||
|
|
if (playerArmyUnits[i] != null)
|
||
|
|
{
|
||
|
|
playerArmyUnits[i].gameObject.SetActive(true);
|
||
|
|
Vector3 pos = playerDeploymentZone != null ? playerDeploymentZone.GetNextSpawnPosition() : playerArmyUnits[i].transform.position;
|
||
|
|
playerArmyUnits[i].WarpToPosition(pos, Quaternion.LookRotation(Vector3.forward));
|
||
|
|
playerArmyUnits[i].HealToFull();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateUnitCounts();
|
||
|
|
SetBattleState(BattleState.Preparation);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SetGameSpeed(float speedMultiplier)
|
||
|
|
{
|
||
|
|
Time.timeScale = Mathf.Clamp(speedMultiplier, 0.5f, 4.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
}
|
||
|
|
|
||
|
|
[System.Serializable]
|
||
|
|
public class BattleResultData
|
||
|
|
{
|
||
|
|
public bool isVictory;
|
||
|
|
public int waveNumber;
|
||
|
|
public int goldEarned;
|
||
|
|
public int survivingPlayerUnits;
|
||
|
|
public int totalPlayerUnits;
|
||
|
|
public int enemyCasualties;
|
||
|
|
public List<UnitLevelUpReport> levelUpReports;
|
||
|
|
}
|
||
|
|
|
||
|
|
[System.Serializable]
|
||
|
|
public class UnitLevelUpReport
|
||
|
|
{
|
||
|
|
public string unitName;
|
||
|
|
public int oldLevel;
|
||
|
|
public int newLevel;
|
||
|
|
}
|
||
|
|
}
|