using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TKS { public enum BattlePhase { Setup, PlayerTurn, Resolving, EnemyTurn, StageClear, Defeat } /// 턴 진행·유닛 관리·승패 판정. 아군은 모든 유닛이 행동하면 자동으로 적군 턴이 된다(버튼 없음). public class BattleManager : MonoBehaviour { public static BattleManager Instance { get; private set; } public BalanceConfig Config; public Board Board; public BattleHUD Hud; public SlingshotInput Sling; public BattlePhase Phase { get; private set; } public int StageIndex { get; private set; } public int TurnNumber { get; private set; } public System.Random Rng { get; private set; } public ActionResolver Resolver { get; private set; } public string LastMessage = ""; readonly List units = new List(); public IReadOnlyList Units { get { return units; } } public BalanceConfig.StageDef Stage { get { return Config.stages[Mathf.Clamp(StageIndex, 0, Config.stages.Length - 1)]; } } public event Action Changed; void Awake() { Instance = this; Rng = new System.Random(Environment.TickCount); Fx.Runner = this; Phase = BattlePhase.Setup; } void OnDestroy() { if (Instance == this) Instance = null; if (Fx.Runner == this) Fx.Runner = null; } public void Configure(BalanceConfig cfg, Board board) { Config = cfg; Board = board; Resolver = new ActionResolver(cfg, this, Rng); } public void NotifyChanged() { if (Changed != null) Changed(); } // ---------- 유닛 ---------- public List AliveUnits(Faction f) { var list = new List(); for (int i = 0; i < units.Count; i++) { var u = units[i]; if (u != null && u.IsAlive && u.Faction == f) list.Add(u); } return list; } public int AliveCount(Faction f) { int n = 0; for (int i = 0; i < units.Count; i++) { var u = units[i]; if (u != null && u.IsAlive && u.Faction == f) n++; } return n; } public Unit UnitNear(Vector2 p, Faction? faction) { return Combat.UnitNear(units, p, faction, 0.25f); } public Unit SpawnUnit(UnitType type, Faction faction, Vector2 pos, string name, float hpMult = 1f, float atkMult = 1f, bool boss = false) { var go = new GameObject("Unit"); go.transform.position = new Vector3(pos.x, pos.y, 0f); var u = go.AddComponent(); u.Init(Config, type, faction, name, hpMult, atkMult, boss); u.Position = Board.ClampInside(pos, u.Radius); units.Add(u); return u; } public void ClearUnits() { for (int i = 0; i < units.Count; i++) { if (units[i] == null) continue; units[i].gameObject.SetActive(false); // 콜라이더를 즉시 물리 월드에서 제거 Destroy(units[i].gameObject); } units.Clear(); } // ---------- 스테이지 ---------- public void StartStage(int index) { StopAllCoroutines(); ClearUnits(); StageIndex = Mathf.Clamp(index, 0, Config.stages.Length - 1); var stage = Config.stages[StageIndex]; int heroCount = Mathf.Min(Config.heroes.Length, Config.playerSpawnPoints.Length, 5); for (int i = 0; i < heroCount; i++) { var h = Config.heroes[i]; SpawnUnit(h.type, Faction.Player, Config.playerSpawnPoints[i], h.name, h.hpMult, h.atkMult, false); } foreach (var e in stage.enemies) { SpawnUnit(e.type, Faction.Enemy, e.position, e.name, e.hpMult, e.atkMult, e.isBoss); } TurnNumber = 1; Phase = BattlePhase.PlayerTurn; LastMessage = stage.title + " — 아군 차례"; NotifyChanged(); } public void ContinueAfterResult() { if (Phase == BattlePhase.StageClear) { StartStage((StageIndex + 1) % Config.stages.Length); } else if (Phase == BattlePhase.Defeat) { StartStage(StageIndex); } } // ---------- 아군 행동 ---------- public bool CanAct(Unit u) { return Phase == BattlePhase.PlayerTurn && u != null && u.Faction == Faction.Player && u.IsAlive && !u.ActedThisTurn; } public bool RequestPlayerAction(Unit u, Vector2 target) { if (!CanAct(u)) return false; Phase = BattlePhase.Resolving; NotifyChanged(); StartCoroutine(PlayerActionRoutine(u, target)); return true; } IEnumerator PlayerActionRoutine(Unit u, Vector2 target) { bool moveOnly = u.BonusMoveAvailable; var o = new ActionOutcome(); yield return Resolver.Resolve(u, target, moveOnly, o); yield return ApplyOverwatch(u, o.Path); if (u.IsAlive && o.BonusMove) u.GrantBonusMove(); else u.MarkActed(); LastMessage = Describe(u, o, moveOnly); NotifyChanged(); if (CheckBattleEnd()) yield break; if (AllPlayersActed()) { yield return EnemyTurnRoutine(); } else { Phase = BattlePhase.PlayerTurn; NotifyChanged(); } } bool AllPlayersActed() { foreach (var u in AliveUnits(Faction.Player)) { if (!u.ActedThisTurn) return false; } return true; } public bool OnlyBonusMovesRemain() { bool any = false; foreach (var u in AliveUnits(Faction.Player)) { if (u.ActedThisTurn) continue; if (!u.BonusMoveAvailable) return false; any = true; } return any; } /// 빈 곳 탭: 남은 행동이 추가 이동뿐이면 건너뛰고 적군 턴으로. public void OnEmptyTap() { if (Phase != BattlePhase.PlayerTurn) return; if (!OnlyBonusMovesRemain()) return; foreach (var u in AliveUnits(Faction.Player)) { if (u.BonusMoveAvailable) u.MarkActed(); } Phase = BattlePhase.Resolving; NotifyChanged(); StartCoroutine(EnemyTurnRoutine()); } // ---------- 적군 턴 ---------- public Coroutine RunEnemyTurn() { return StartCoroutine(EnemyTurnRoutine()); } IEnumerator EnemyTurnRoutine() { Phase = BattlePhase.EnemyTurn; LastMessage = "적군 차례"; NotifyChanged(); foreach (var e in AliveUnits(Faction.Enemy)) e.ResetForNewTurn(); yield return Fx.Wait(0.5f); var enemies = AliveUnits(Faction.Enemy); float accuracy = Stage.aiAccuracy; for (int i = 0; i < enemies.Count; i++) { var e = enemies[i]; if (!e.IsAlive) continue; var players = AliveUnits(Faction.Player); if (players.Count == 0) break; Vector2 target = EnemyAI.Decide(e, players, accuracy, Rng, Board); var o = new ActionOutcome(); yield return Resolver.Resolve(e, target, false, o); yield return ApplyOverwatch(e, o.Path); LastMessage = Describe(e, o, false); NotifyChanged(); if (CheckBattleEnd()) yield break; yield return Fx.Wait(0.25f); } foreach (var u in AliveUnits(Faction.Player)) u.ResetForNewTurn(); TurnNumber++; Phase = BattlePhase.PlayerTurn; LastMessage = TurnNumber + "번째 턴 — 아군 차례"; NotifyChanged(); } /// 창병 대기: 상대 유닛이 지나간 경로가 대기 중인 창병 반경을 스치면 피해(창병마다 1회). public IEnumerator ApplyOverwatch(Unit mover, List path) { if (mover == null || path == null || path.Count < 2) yield break; Faction other = mover.Faction == Faction.Player ? Faction.Enemy : Faction.Player; foreach (var spear in AliveUnits(other)) { if (spear.Type != UnitType.Spear || !spear.Overwatch) continue; if (!mover.IsAlive) yield break; bool triggered = false; for (int i = 0; i < path.Count - 1; i++) { Vector2 a = path[i]; Vector2 b = path[i + 1]; if (Vector2.Distance(a, b) < 0.05f) continue; if (Combat.SegmentPointDistance(a, b, spear.Position) <= spear.AttackRange + mover.Radius) { triggered = true; break; } } if (!triggered) continue; int dmg = Combat.OverwatchDamage(Config, spear, mover, Rng); yield return Fx.Lunge(spear, mover.Position); mover.TakeDamage(dmg); Fx.Popup(mover.Position, dmg + " 견제", new Color(0.6f, 0.85f, 1f)); NotifyChanged(); yield return Fx.Wait(0.2f); } } // ---------- 승패 ---------- public bool CheckBattleEnd() { if (AliveCount(Faction.Enemy) == 0) { Phase = BattlePhase.StageClear; LastMessage = "승리 — 전장의 적을 모두 처치했습니다"; NotifyChanged(); return true; } if (AliveCount(Faction.Player) == 0) { Phase = BattlePhase.Defeat; LastMessage = "패배 — 아군이 전멸했습니다"; NotifyChanged(); return true; } return false; } // ---------- 테스트·자동 플레이용 ---------- public IEnumerator ResolveForTest(Unit u, Vector2 target, bool moveOnly, ActionOutcome o) { yield return Resolver.Resolve(u, target, moveOnly, o); } public void ForcePhase(BattlePhase p) { Phase = p; NotifyChanged(); } static string Describe(Unit u, ActionOutcome o, bool moveOnly) { string who = u.DisplayName + "(" + UnitTypeInfo.Korean(u.Type) + ")"; if (o.Attacked.Count == 0) { return who + (moveOnly ? " 추가 이동" : " 이동"); } var names = new List(); foreach (var a in o.Attacked) { if (!names.Contains(a.DisplayName)) names.Add(a.DisplayName); } string s = who + " → " + string.Join(", ", names.ToArray()) + " 공격 " + o.DamageDealt; if (o.Crit) s += " 크리티컬!"; if (o.CounterTaken > 0) s += " · 반격 " + o.CounterTaken; if (o.BonusMove) s += " · 추가 이동!"; if (o.Returned) s += " · 제자리 복귀"; if (o.Overwatch) s += " · 대기(견제) 발동"; return s; } } }