using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace TKS { /// 화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 아이콘(추후)·결과 오버레이. public class BattleHUD : MonoBehaviour { BattleManager battle; Camera uiCam; CameraController camCtrl; Text stageText; Text turnText; Text countText; Text messageText; Text hintText; Text unitText; GameObject resultOverlay; Text resultTitle; Text resultSub; Unit selected; Image minimapBg; Image minimapView; readonly Dictionary minimapDots = new Dictionary(); public void Build(BattleManager b, Camera cam, CameraController cc) { battle = b; uiCam = cam; camCtrl = cc; var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = cam; canvas.planeDistance = 1f; canvas.sortingOrder = 100; var scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1080f, 1920f); scaler.matchWidthOrHeight = 0.5f; gameObject.AddComponent(); if (EventSystem.current == null && FindFirstObjectByType() == null) { var es = new GameObject("EventSystem"); es.AddComponent(); es.AddComponent(); } stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(760f, 70f), 40, TextAnchor.UpperLeft); countText = MakeText("Count", new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-30f, -28f), new Vector2(520f, 70f), 36, TextAnchor.UpperRight); turnText = MakeText("Turn", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -92f), new Vector2(700f, 70f), 46, TextAnchor.UpperCenter); turnText.color = new Color(1f, 0.95f, 0.6f); messageText = MakeText("Message", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -160f), new Vector2(1040f, 60f), 30, TextAnchor.UpperCenter); messageText.color = new Color(0.9f, 0.95f, 1f); hintText = MakeText("Hint", new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 36f), new Vector2(1040f, 60f), 28, TextAnchor.LowerCenter); hintText.color = new Color(0.85f, 0.85f, 0.85f); unitText = MakeText("UnitInfo", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 110f), new Vector2(760f, 170f), 32, TextAnchor.LowerLeft); // 스킬 아이콘(초기 단계 미구현 · 자리만) var skill = MakeImage("SkillIcon", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-36f, 110f), new Vector2(170f, 170f), new Color(0.3f, 0.3f, 0.33f, 0.8f)); var skillLabel = MakeText("Label", new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, 30, TextAnchor.MiddleCenter); skillLabel.transform.SetParent(skill.transform, false); skillLabel.text = "스킬\n(추후)"; skillLabel.color = new Color(0.75f, 0.75f, 0.75f); // 미니맵 (전장 비율 · 탭하면 그 지점으로 카메라 이동) Vector2 bs = battle.Config.boardSize; float mmW = 180f; float mmH = mmW * bs.y / Mathf.Max(0.01f, bs.x); minimapBg = MakeImage("Minimap", transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-30f, -215f), new Vector2(mmW, mmH), new Color(0.06f, 0.1f, 0.06f, 0.8f)); var mmBorder = MakeImage("Border", minimapBg.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(6f, 6f), new Color(0.85f, 0.75f, 0.45f, 0.9f)); mmBorder.transform.SetAsFirstSibling(); var mmMid = MakeImage("Mid", minimapBg.transform, new Vector2(0f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(0f, 2f), new Color(1f, 1f, 1f, 0.25f)); minimapView = MakeImage("View", minimapBg.transform, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f)); resultOverlay = MakeImage("ResultOverlay", transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject; resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter); resultTitle.transform.SetParent(resultOverlay.transform, false); resultSub = MakeText("Sub", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -80f), new Vector2(1000f, 120f), 40, TextAnchor.MiddleCenter); resultSub.transform.SetParent(resultOverlay.transform, false); resultOverlay.SetActive(false); } Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align) { var go = new GameObject(name); go.transform.SetParent(transform, false); var rt = go.AddComponent(); rt.anchorMin = anchorMin; rt.anchorMax = anchorMax; rt.pivot = pivot; rt.anchoredPosition = anchoredPos; rt.sizeDelta = size; var t = go.AddComponent(); t.font = TextUtil.Font; t.fontSize = fontSize; t.alignment = align; t.color = Color.white; t.horizontalOverflow = HorizontalWrapMode.Overflow; t.verticalOverflow = VerticalWrapMode.Overflow; t.raycastTarget = false; var sh = go.AddComponent(); sh.effectDistance = new Vector2(2f, -2f); sh.effectColor = new Color(0f, 0f, 0f, 0.8f); return t; } Image MakeImage(string name, Transform parent, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, Color color) { var go = new GameObject(name); go.transform.SetParent(parent, false); var rt = go.AddComponent(); rt.anchorMin = anchorMin; rt.anchorMax = anchorMax; rt.pivot = pivot; rt.anchoredPosition = anchoredPos; rt.sizeDelta = size; var img = go.AddComponent(); img.color = color; img.raycastTarget = false; return img; } public void SetSelected(Unit u) { selected = u; } /// 화면 점이 미니맵 안이면 해당 월드 좌표를 돌려준다. public bool TryMinimapTap(Vector2 screen, out Vector2 world) { world = Vector2.zero; if (minimapBg == null || battle == null || battle.Board == null) return false; var rt = minimapBg.rectTransform; if (!RectTransformUtility.RectangleContainsScreenPoint(rt, screen, uiCam)) return false; Vector2 local; if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, screen, uiCam, out local)) return false; var r = rt.rect; float nx = Mathf.Clamp01((local.x - r.xMin) / r.width); float ny = Mathf.Clamp01((local.y - r.yMin) / r.height); var b = battle.Board.Rect; world = new Vector2(b.xMin + nx * b.width, b.yMin + ny * b.height); return true; } void LateUpdate() { if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return; Refresh(); RefreshMinimap(); } void Refresh() { var stage = battle.Stage; stageText.text = "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle); countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy); switch (battle.Phase) { case BattlePhase.PlayerTurn: turnText.text = "아군 턴 " + battle.TurnNumber; break; case BattlePhase.Resolving: turnText.text = "진행 중…"; break; case BattlePhase.EnemyTurn: turnText.text = "적군 턴 " + battle.TurnNumber; break; case BattlePhase.StageClear: turnText.text = "승리"; break; case BattlePhase.Defeat: turnText.text = "패배"; break; default: turnText.text = ""; break; } messageText.text = battle.LastMessage; if (battle.Phase == BattlePhase.PlayerTurn) { hintText.text = battle.OnlyBonusMovesRemain() ? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀" : "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동"; } else { hintText.text = ""; } if (selected != null && selected.IsAlive) { var u = selected; string side = u.Faction == Faction.Player ? "아군" : "적군"; string line1 = side + " · " + u.DisplayName + " · " + UnitTypeInfo.Korean(u.Type) + (u.IsBoss ? " (장수)" : ""); string line2 = "HP " + u.Hp + "/" + u.MaxHp + " 공 " + u.Atk + " 방 " + u.Def; string line3 = "슬링 " + u.SlingMaxDistance.ToString("0.#"); if (u.Type == UnitType.Archer) line3 += " 사거리 " + u.AttackRange.ToString("0.#") + " (슬링 +" + Mathf.RoundToInt(battle.Config.archerRangeRatio * 100f) + " %)"; if (u.Type == UnitType.Spear) line3 += " 견제 반경 " + u.AttackRange.ToString("0.#"); string state = ""; if (u.Faction == Faction.Player) { if (u.BonusMoveAvailable) state = " [추가 이동]"; else if (u.ActedThisTurn) state = " [행동 완료]"; } if (u.Overwatch) state += " [대기 중]"; unitText.text = line1 + "\n" + line2 + "\n" + line3 + state; } else { unitText.text = "유닛을 선택하세요"; } bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat; if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result); if (result) { bool win = battle.Phase == BattlePhase.StageClear; resultTitle.text = win ? "승리" : "패배"; resultTitle.color = win ? new Color(1f, 0.9f, 0.4f) : new Color(1f, 0.5f, 0.5f); resultSub.text = win ? stage.title + " 제압!\n화면을 탭하면 다음 전투로" : stage.title + " 에서 패배\n화면을 탭하면 다시 도전"; } } void RefreshMinimap() { if (minimapBg == null || battle.Board == null) return; var r = minimapBg.rectTransform.rect; var b = battle.Board.Rect; float sx = r.width / b.width; float sy = r.height / b.height; // 카메라 시야 사각형 if (camCtrl != null) { var v = camCtrl.ViewRect; float x0 = Mathf.Clamp((v.xMin - b.xMin) * sx, 0f, r.width); float y0 = Mathf.Clamp((v.yMin - b.yMin) * sy, 0f, r.height); float x1 = Mathf.Clamp((v.xMax - b.xMin) * sx, 0f, r.width); float y1 = Mathf.Clamp((v.yMax - b.yMin) * sy, 0f, r.height); minimapView.rectTransform.anchoredPosition = new Vector2(x0, y0); minimapView.rectTransform.sizeDelta = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0)); } // 유닛 점 foreach (var u in battle.Units) { if (u == null) continue; Image dot; if (!minimapDots.TryGetValue(u, out dot)) { dot = MakeImage("Dot", minimapBg.transform, Vector2.zero, Vector2.zero, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(u.IsBoss ? 14f : 10f, u.IsBoss ? 14f : 10f), u.Faction == Faction.Player ? UnitView.PlayerColor : UnitView.EnemyColor); if (u.IsBoss) dot.color = new Color(1f, 0.85f, 0.3f); minimapDots[u] = dot; } bool show = u.IsAlive; if (dot.gameObject.activeSelf != show) dot.gameObject.SetActive(show); if (show) dot.rectTransform.anchoredPosition = new Vector2((u.Position.x - b.xMin) * sx, (u.Position.y - b.yMin) * sy); } // 사라진 유닛 정리 var dead = new List(); foreach (var kv in minimapDots) { if (kv.Key == null) dead.Add(kv.Key); } foreach (var k in dead) { if (minimapDots[k] != null) Destroy(minimapDots[k].gameObject); minimapDots.Remove(k); } } } }