679 lines
39 KiB
C#
679 lines
39 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
namespace TKS
|
|
{
|
|
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 버튼(PD #1093 B)·스킬 발동 한 줄·결과 오버레이.</summary>
|
|
public class BattleHUD : MonoBehaviour
|
|
{
|
|
// 색 = UI 테마(PD #1100 · ui_theme.csv skill_*) · 괄호 값 = 테마 없을 때
|
|
static Color SkillReadyColor { get { return UISkin.Col("skill_ready", new Color(0.72f, 0.2f, 0.14f, 0.94f)); } }
|
|
static Color SkillReservedColor { get { return UISkin.Col("skill_reserved", new Color(0.96f, 0.76f, 0.22f, 0.97f)); } }
|
|
static Color SkillOffColor { get { return UISkin.Col("skill_off", new Color(0.3f, 0.3f, 0.33f, 0.88f)); } }
|
|
static Color SkillPassiveOnlyColor { get { return UISkin.Col("skill_passive", new Color(0.18f, 0.2f, 0.26f, 0.85f)); } }
|
|
static readonly Color SkillLogColor = new Color(0.6f, 1f, 0.85f);
|
|
const float SkillLogSeconds = 3f;
|
|
|
|
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 skillPanel;
|
|
Image skillCool;
|
|
Image turnBadge;
|
|
Text skillName;
|
|
Text skillState;
|
|
Text skillPassive;
|
|
Text skillLog;
|
|
|
|
// 유닛 정보 판 · 수 칩 · 스킬 빛(Fable 제안서 §4-8 · #1104 · 테마가 있을 때만)
|
|
RectTransform unitCard;
|
|
RectTransform unitBody;
|
|
Image portraitRing;
|
|
Image portraitArt;
|
|
Image typeRing;
|
|
Image typeIcon;
|
|
RectTransform hpFill;
|
|
Text nameText;
|
|
Text hpText;
|
|
Text atkText;
|
|
Text defText;
|
|
Text extraText;
|
|
Text stateText;
|
|
Text emptyText;
|
|
Unit shownUnit;
|
|
Text allyCount;
|
|
Text enemyCount;
|
|
Image skillGlow;
|
|
Image msgChip;
|
|
const float HpBarW = 360f;
|
|
|
|
/// <summary>결과 패널이 따로 있으면(아웃게임) 결과 단계에서 HUD 전체를 숨긴다(검수 결함 1 · 제목 2중 방지).</summary>
|
|
public bool HideOnResult;
|
|
CanvasGroup group;
|
|
|
|
Image minimapBg;
|
|
Image minimapView;
|
|
RectTransform minimapLive;
|
|
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>();
|
|
|
|
// 캔버스 층(PD 2026-11-10 「얼마나 자주 바뀌는지에 따라 캔버스 분할」 · 프로젝트 CLAUDE.md 「UI 캔버스·아틀라스 구조」):
|
|
// 루트 = 정적 판(칩 바탕 · 유닛 판 바탕 · 미니맵 틀 · Static) → Info(글자·값·스킬 버튼 · 턴·선택·피해 때만 · Event)
|
|
// → MinimapLive(시야 사각형·유닛 점 · 매 프레임 · Frame) · SkillGlow(호흡 · Frame) → Result(결과 덮개 · Event)
|
|
RectTransform info;
|
|
RectTransform resultLayer;
|
|
Image hpFillImg;
|
|
|
|
public void Build(BattleManager b, Camera cam, CameraController cc)
|
|
{
|
|
battle = b;
|
|
uiCam = cam;
|
|
camCtrl = cc;
|
|
UiLayer.Root(gameObject, cam, 100, 1f, UiLayer.Reference, UiRate.Static);
|
|
group = gameObject.AddComponent<CanvasGroup>();
|
|
info = UiLayer.Group("Info", transform, UiRate.Event, false);
|
|
|
|
if (EventSystem.current == null && FindFirstObjectByType<EventSystem>() == null)
|
|
{
|
|
var es = new GameObject("EventSystem");
|
|
es.AddComponent<EventSystem>();
|
|
es.AddComponent<StandaloneInputModule>();
|
|
}
|
|
|
|
if (UISkin.On)
|
|
{
|
|
// 팩 칩(PD #1100): 스테이지 · 유닛 수 · 턴 띠 · 선택 유닛 정보 판 — 글자 뒤(먼저 만든다) · 반투명 진남
|
|
Color panelCol = UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f));
|
|
Chip("StageChip", transform, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(14f, -14f), new Vector2(730f, 86f), panelCol);
|
|
Chip("CountChip", transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(330f, 86f), panelCol);
|
|
// 턴 띠 = 아군/적군 턴마다 색이 바뀜 → Info 층
|
|
turnBadge = Chip("TurnBadge", info, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -106f), new Vector2(440f, 84f), UISkin.Col("hud_turn_player", Color.blue));
|
|
Chip("UnitPanel", transform, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(14f, 96f), new Vector2(790f, 196f), panelCol);
|
|
}
|
|
stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(700f, 70f), 40, TextAnchor.UpperLeft);
|
|
// 스테이지 표 제목(최대 ~20자)이 오른쪽 위 적 수와 겹치지 않게 칸 안에서 글자 크기를 줄인다
|
|
stageText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
stageText.verticalOverflow = VerticalWrapMode.Truncate;
|
|
stageText.resizeTextForBestFit = true;
|
|
stageText.resizeTextMinSize = 24;
|
|
stageText.resizeTextMaxSize = 40;
|
|
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);
|
|
if (UISkin.On)
|
|
{
|
|
turnText.color = Color.white;
|
|
turnText.font = UISkin.TitleFont;
|
|
turnText.rectTransform.anchoredPosition = new Vector2(0f, -114f);
|
|
}
|
|
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);
|
|
if (UISkin.On) messageText.rectTransform.anchoredPosition = new Vector2(0f, -206f); // 턴 띠 아래
|
|
|
|
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);
|
|
|
|
// 스킬 버튼(PD #1093 B · 유닛 선택 시 · 탭 = 예약/취소 · 즉시 발동) — 이름 · 쿨 · 사용 가능/불가 · 패시브 한 줄
|
|
skillPanel = MakeImage("SkillButton", info, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-30f, 110f), new Vector2(270f, 230f), SkillOffColor);
|
|
skillName = MakeText("Name", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), new Vector2(256f, 60f), 38, TextAnchor.UpperCenter);
|
|
skillName.transform.SetParent(skillPanel.transform, false);
|
|
skillName.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
skillName.resizeTextForBestFit = true;
|
|
skillName.resizeTextMinSize = 22;
|
|
skillName.resizeTextMaxSize = 38;
|
|
skillState = MakeText("State", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), new Vector2(256f, 80f), 26, TextAnchor.MiddleCenter);
|
|
skillState.transform.SetParent(skillPanel.transform, false);
|
|
skillState.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
skillPassive = MakeText("Passive", new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(256f, 40f), 23, TextAnchor.LowerCenter);
|
|
skillPassive.transform.SetParent(skillPanel.transform, false);
|
|
skillPassive.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
skillPassive.color = new Color(0.8f, 0.9f, 1f);
|
|
if (UISkin.Apply(skillPanel, "skill_btn"))
|
|
{
|
|
// 원형 스킬 버튼(팩 Button_Circle · PD #1100) + 쿨 라디얼 · 이름·상태 = 원 안 · 패시브 한 줄 = 원 위
|
|
var rt = skillPanel.rectTransform;
|
|
rt.sizeDelta = new Vector2(250f, 250f);
|
|
rt.anchoredPosition = new Vector2(-24f, 100f);
|
|
skillPanel.preserveAspect = true;
|
|
skillCool = MakeImage("Cool", skillPanel.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(-26f, -26f), UISkin.Col("skill_cool", new Color(0f, 0f, 0f, 0.6f)));
|
|
if (UISkin.Apply(skillCool, "skill_cool"))
|
|
{
|
|
skillCool.type = Image.Type.Filled;
|
|
skillCool.fillMethod = Image.FillMethod.Radial360;
|
|
skillCool.fillOrigin = (int)Image.Origin360.Top;
|
|
skillCool.fillClockwise = false;
|
|
skillCool.fillAmount = 0f;
|
|
}
|
|
skillCool.transform.SetAsFirstSibling();
|
|
Place(skillName, new Vector2(0f, -54f), new Vector2(196f, 56f));
|
|
skillName.font = UISkin.TitleFont;
|
|
Place(skillState, new Vector2(0f, -10f), new Vector2(200f, 80f));
|
|
skillState.resizeTextForBestFit = true;
|
|
skillState.verticalOverflow = VerticalWrapMode.Truncate;
|
|
skillState.resizeTextMinSize = 18;
|
|
skillState.resizeTextMaxSize = 32;
|
|
var pr = skillPassive.rectTransform;
|
|
pr.anchorMin = pr.anchorMax = new Vector2(0.5f, 1f);
|
|
pr.pivot = new Vector2(0.5f, 0f);
|
|
pr.anchoredPosition = new Vector2(-20f, 6f);
|
|
pr.sizeDelta = new Vector2(320f, 40f);
|
|
skillPassive.alignment = TextAnchor.LowerCenter;
|
|
}
|
|
skillPanel.gameObject.SetActive(false);
|
|
|
|
// 스킬 발동 한 줄(액티브·패시브 · 최근 몇 초)
|
|
skillLog = MakeText("SkillLog", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 292f), new Vector2(1020f, 80f), 28, TextAnchor.LowerLeft);
|
|
skillLog.color = SkillLogColor;
|
|
|
|
// 미니맵 (전장 비율 · 탭하면 그 지점으로 카메라 이동)
|
|
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));
|
|
// 미니맵 틀(정적) 위 = 시야 사각형 · 유닛 점만 담는 Frame 층(카메라·유닛이 움직일 때마다 이 층만 다시 묶음)
|
|
minimapLive = UiLayer.Group("MinimapLive", minimapBg.transform, UiRate.Frame, false);
|
|
minimapView = MakeImage("View", minimapLive, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f));
|
|
|
|
if (UISkin.Has("hp_bg") && UISkin.Has("chip") && UISkin.Has("circle_frame")) BuildKit();
|
|
|
|
resultLayer = UiLayer.Group("Result", transform, UiRate.Event, false);
|
|
resultOverlay = MakeImage("ResultOverlay", resultLayer, 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);
|
|
if (UISkin.On) resultTitle.font = UISkin.TitleFont;
|
|
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);
|
|
// 그리는 순서 = 정적 판(루트 자식들) → Info → Result
|
|
info.SetAsLastSibling();
|
|
resultLayer.SetAsLastSibling();
|
|
}
|
|
|
|
/// <summary>같은 부모 좌표계에서 src 와 같은 자리·크기의 빈 칸(정적 판 위에 얹는 Info 층 내용 칸).</summary>
|
|
static RectTransform Like(string name, Transform parent, RectTransform src)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
go.transform.SetParent(parent, false);
|
|
var rt = (RectTransform)go.transform;
|
|
rt.anchorMin = src.anchorMin;
|
|
rt.anchorMax = src.anchorMax;
|
|
rt.pivot = src.pivot;
|
|
rt.anchoredPosition = src.anchoredPosition;
|
|
rt.sizeDelta = src.sizeDelta;
|
|
return rt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fable 제안서 §4-8(#1104): 아래 유닛 판(초상 원 150 + 이름 · 병종 원 · 체력 바 · 공/방 칩 · 슬링/사거리 칩) ·
|
|
/// 오른쪽 위 수 칩 = 아군·적 아이콘 + 숫자 · 스킬 버튼 뒤 빛(사용 가능일 때 호흡) · 기본 조작 문장 삭제(상황 안내만 유지).
|
|
/// </summary>
|
|
void BuildKit()
|
|
{
|
|
var soft = UISkin.Col("text_soft", new Color(0.78f, 0.8f, 0.84f));
|
|
// 유닛 판(기존 UnitPanel 판 대신 · 왼쪽 아래 24 · 판 위 내용)
|
|
var old = transform.Find("UnitPanel");
|
|
if (old != null) old.gameObject.SetActive(false);
|
|
unitText.gameObject.SetActive(false);
|
|
var card = MakeImage("UnitCard", transform, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(24f, 150f), new Vector2(720f, 204f), UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f)));
|
|
UISkin.Apply(card, "card");
|
|
// 판 바탕 = 루트(정적) · 내용(초상 · 이름 · 체력 · 칩) = 같은 자리의 Info 칸(선택·피해 때만 바뀜)
|
|
unitCard = Like("UnitCardInfo", info, card.rectTransform);
|
|
unitBody = OutGame.UiKit.Fill("Body", unitCard);
|
|
portraitRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Ring", 18f, 22f, 152f, 152f), "circle_frame", Color.white);
|
|
var mask = MakeImage("Mask", portraitRing.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(132f, 132f), Color.white);
|
|
mask.sprite = OutGame.UiKit.Circle;
|
|
mask.gameObject.AddComponent<Mask>().showMaskGraphic = false;
|
|
portraitArt = MakeImage("Art", mask.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), new Vector2(230f, 230f), Color.white);
|
|
portraitArt.preserveAspect = true;
|
|
// 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
|
|
typeRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "TypeRing", 188f, 20f, 52f, 52f), "circle_frame", Color.white);
|
|
typeRing.enabled = false;
|
|
typeIcon = OutGame.UiKit.Icon(typeRing.transform, OutGame.UiKit.TypeSymbolId(UnitType.Infantry), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 52f);
|
|
nameText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 36, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(250f, -18f), new Vector2(300f, 56f)), true);
|
|
nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
stateText = OutGame.UiKit.Label(unitBody, "", 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -22f), new Vector2(220f, 48f), false);
|
|
var hpBg = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Hp", 188f, 86f, HpBarW, 30f), "hp_bg", UISkin.Col("hp_bg", Color.white));
|
|
hpFill = OutGame.UiKit.At(hpBg.transform, "Fill", 4f, 4f, HpBarW - 8f, 22f);
|
|
hpFillImg = OutGame.UiKit.Skin(hpFill, "hp_fill", UISkin.Col("hp_hi", Color.green));
|
|
hpText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 28, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(188f + HpBarW + 14f, -80f), new Vector2(160f, 44f)), true);
|
|
hpText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
atkText = OutGame.UiKit.InfoChip(unitBody, "Atk", 188f, 130f, 156f, 56f, "sword", "", 30);
|
|
defText = OutGame.UiKit.InfoChip(unitBody, "Def", 354f, 130f, 156f, 56f, "shield", "", 30);
|
|
extraText = OutGame.UiKit.InfoChip(unitBody, "Extra", 520f, 130f, 180f, 56f, null, "", 26);
|
|
emptyText = OutGame.UiKit.Label(unitCard, "유닛을 선택하세요", 30, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(680f, 60f), false);
|
|
|
|
// 위 안내 한 줄 = 어두운 칩 위(글자가 바탕 없이 화면 끝에서 잘리던 것 · 검수2 결함 3) · 칸 안 맞춤 축소
|
|
msgChip = MakeImage("MsgChip", info, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(1032f, 58f), UISkin.Col("chip_dark", new Color(0f, 0f, 0f, 0.8f)));
|
|
UISkin.Apply(msgChip, "chip");
|
|
msgChip.transform.SetSiblingIndex(messageText.transform.GetSiblingIndex());
|
|
messageText.rectTransform.anchoredPosition = new Vector2(0f, -204f);
|
|
messageText.rectTransform.sizeDelta = new Vector2(990f, 50f);
|
|
messageText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
messageText.verticalOverflow = VerticalWrapMode.Truncate;
|
|
messageText.resizeTextForBestFit = true;
|
|
messageText.resizeTextMinSize = 18;
|
|
messageText.resizeTextMaxSize = 28;
|
|
messageText.alignment = TextAnchor.MiddleCenter;
|
|
|
|
// 수 칩 = 아군 · 적 아이콘 + 숫자(Sen)
|
|
// 칩 바탕 = 루트(정적) · 아이콘·숫자 = 같은 자리의 Info 칸(유닛이 죽을 때만 바뀜)
|
|
var ccPlate = transform.Find("CountChip") as RectTransform;
|
|
var cc = ccPlate != null ? Like("CountInfo", info, ccPlate) : null;
|
|
if (cc != null && OutGame.UiKit.Icon(cc, "hud_ally", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(48f, 0f), 56f) != null)
|
|
{
|
|
countText.gameObject.SetActive(false);
|
|
OutGame.UiKit.Icon(cc, "hud_enemy", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(200f, 0f), 56f);
|
|
allyCount = OutGame.UiKit.Title(OutGame.UiKit.Label(cc, "", 40, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(84f, 2f), new Vector2(80f, 60f)), true);
|
|
enemyCount = OutGame.UiKit.Title(OutGame.UiKit.Label(cc, "", 40, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(236f, 2f), new Vector2(80f, 60f)), true);
|
|
}
|
|
|
|
// 스킬 버튼 뒤 빛(사용 가능 = 호흡)
|
|
if (UISkin.Has("glow_circle"))
|
|
{
|
|
skillGlow = MakeImage("SkillGlow", info, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-24f - 125f, 100f + 125f), new Vector2(560f, 560f), UISkin.Col("skill_glow", new Color(0.85f, 0.25f, 0.18f, 0.4f)));
|
|
UISkin.Apply(skillGlow, "glow_circle");
|
|
skillGlow.transform.SetSiblingIndex(skillPanel.transform.GetSiblingIndex());
|
|
var pulse = skillGlow.gameObject.AddComponent<OutGame.UiPulse>();
|
|
pulse.min = 0.45f;
|
|
pulse.period = 1.2f;
|
|
skillGlow.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>유닛 판 값 갱신(매 프레임 · 초상은 선택이 바뀔 때만).</summary>
|
|
void RefreshUnitCard(Unit u)
|
|
{
|
|
bool on = u != null && u.IsAlive;
|
|
if (unitBody.gameObject.activeSelf != on) unitBody.gameObject.SetActive(on);
|
|
emptyText.gameObject.SetActive(!on);
|
|
if (!on)
|
|
{
|
|
shownUnit = null;
|
|
return;
|
|
}
|
|
if (shownUnit != u)
|
|
{
|
|
shownUnit = u;
|
|
var sp = OutGame.UiKit.ArtSprite(u.ArtId);
|
|
portraitArt.sprite = sp;
|
|
portraitArt.enabled = sp != null;
|
|
portraitRing.color = u.Faction == Faction.Player ? UISkin.Col("hud_turn_player", Color.blue) : UISkin.Col("hud_turn_enemy", Color.red);
|
|
var tsp = UISkin.Icon(OutGame.UiKit.TypeSymbolId(u.Type));
|
|
if (typeIcon != null && tsp != null) typeIcon.sprite = tsp;
|
|
}
|
|
nameText.text = u.DisplayName + (u.IsBoss ? " <size=24>장수</size>" : "");
|
|
float r = u.MaxHp > 0 ? Mathf.Clamp01(u.Hp / (float)u.MaxHp) : 0f;
|
|
// 값이 같으면 쓰지 않는다(매 프레임 호출 · 바뀔 때만 Info 층 재구성)
|
|
var hs = new Vector2(Mathf.Max(12f, (HpBarW - 8f) * r), hpFill.sizeDelta.y);
|
|
if (hpFill.sizeDelta != hs) hpFill.sizeDelta = hs;
|
|
hpFill.gameObject.SetActive(r > 0f);
|
|
hpFillImg.color = r > 0.5f ? UISkin.Col("hp_hi", Color.green) : r > 0.25f ? UISkin.Col("hp_mid", Color.yellow) : UISkin.Col("hp_low", Color.red);
|
|
hpText.text = u.Hp + "/" + u.MaxHp;
|
|
atkText.text = u.Atk.ToString();
|
|
defText.text = u.Def.ToString();
|
|
string extra = "슬링 " + SkillMods.SlingMax(u).ToString("0.#");
|
|
if (UnitTypeInfo.IsRanged(u.Type)) extra = "사거리 " + SkillMods.Range(u).ToString("0.#");
|
|
if (u.Type == UnitType.Strategist) extra = "광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#");
|
|
extraText.text = extra;
|
|
string state = u.Faction == Faction.Player ? "아군" : "적군";
|
|
if (u.Faction == Faction.Player)
|
|
{
|
|
if (u.BonusMoveAvailable) state = "추가 이동";
|
|
else if (u.ActedThisTurn) state = "행동 완료";
|
|
}
|
|
stateText.text = state;
|
|
}
|
|
|
|
/// <summary>글자 = Info 층(값이 바뀌어도 정적 판을 다시 묶지 않게).</summary>
|
|
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(info, false);
|
|
var rt = go.AddComponent<RectTransform>();
|
|
rt.anchorMin = anchorMin;
|
|
rt.anchorMax = anchorMax;
|
|
rt.pivot = pivot;
|
|
rt.anchoredPosition = anchoredPos;
|
|
rt.sizeDelta = size;
|
|
var t = go.AddComponent<Text>();
|
|
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<Shadow>();
|
|
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<RectTransform>();
|
|
rt.anchorMin = anchorMin;
|
|
rt.anchorMax = anchorMax;
|
|
rt.pivot = pivot;
|
|
rt.anchoredPosition = anchoredPos;
|
|
rt.sizeDelta = size;
|
|
var img = go.AddComponent<Image>();
|
|
img.color = color;
|
|
img.raycastTarget = false;
|
|
return img;
|
|
}
|
|
|
|
/// <summary>테마 칩(팩 카드 틀 · 색 틴트) — 글자보다 먼저 만들어 뒤에 깔린다.</summary>
|
|
Image Chip(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Color color)
|
|
{
|
|
var img = MakeImage(name, parent, anchor, anchor, pivot, pos, size, color);
|
|
UISkin.Apply(img, "card");
|
|
return img;
|
|
}
|
|
|
|
static void Place(Text t, Vector2 pos, Vector2 size)
|
|
{
|
|
t.rectTransform.anchoredPosition = pos;
|
|
t.rectTransform.sizeDelta = size;
|
|
}
|
|
|
|
public void SetSelected(Unit u)
|
|
{
|
|
selected = u;
|
|
}
|
|
|
|
public bool SkillButtonVisible
|
|
{
|
|
get { return skillPanel != null && skillPanel.gameObject.activeSelf; }
|
|
}
|
|
|
|
public string SkillButtonText
|
|
{
|
|
get { return skillPanel != null ? skillName.text + " | " + skillState.text + " | " + skillPassive.text : ""; }
|
|
}
|
|
|
|
public string SkillLogText
|
|
{
|
|
get { return skillLog != null ? skillLog.text : ""; }
|
|
}
|
|
|
|
/// <summary>화면 점이 스킬 버튼 위면 탭 처리(선택 유닛 액티브 예약·취소·즉시 발동) 후 true — 슬링샷 입력은 건너뛴다.</summary>
|
|
public bool TrySkillTap(Vector2 screen)
|
|
{
|
|
if (!SkillButtonVisible || battle == null) return false;
|
|
if (!RectTransformUtility.RectangleContainsScreenPoint(skillPanel.rectTransform, screen, uiCam)) return false;
|
|
PressSkill();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>스킬 버튼 누름(탭 · 테스트 · 캡처 공용).</summary>
|
|
public string PressSkill()
|
|
{
|
|
if (battle == null || battle.Skills == null || selected == null) return null;
|
|
var msg = battle.Skills.TapActive(selected);
|
|
if (!string.IsNullOrEmpty(msg)) battle.LastMessage = msg;
|
|
if (selected.View != null) selected.View.Refresh();
|
|
return msg;
|
|
}
|
|
|
|
void RefreshSkill()
|
|
{
|
|
var sys = battle.Skills;
|
|
var lines = sys != null ? sys.RecentLog(SkillLogSeconds, 2) : null;
|
|
skillLog.text = lines != null ? string.Join("\n", lines.ToArray()) : "";
|
|
|
|
var s = sys != null && selected != null && selected.IsAlive ? sys.Of(selected) : null;
|
|
bool show = s != null && (s.Active != null || s.Passive != null);
|
|
if (skillPanel.gameObject.activeSelf != show) skillPanel.gameObject.SetActive(show);
|
|
if (!show) return;
|
|
if (skillCool != null)
|
|
{
|
|
// 쿨 라디얼 = 남은 턴 / 쿨(쿨 중일 때만)
|
|
bool cooling = s.Active != null && selected.Faction == Faction.Player && sys.StateOf(selected) == SkillSystem.ActiveState.Cooldown && s.Active.Cooldown > 0;
|
|
skillCool.fillAmount = cooling ? Mathf.Clamp01(s.CooldownLeft(battle.TurnNumber) / (float)s.Active.Cooldown) : 0f;
|
|
}
|
|
|
|
skillPassive.text = s.Passive != null ? "패 · " + s.Passive.NameKo + (s.PassiveMult > 1f ? " ★" : "") : "";
|
|
if (s.Active == null || selected.Faction != Faction.Player)
|
|
{
|
|
skillName.text = s.Active != null ? s.Active.NameKo : "패시브";
|
|
skillState.text = selected.Faction != Faction.Player ? "적 무장 · 패시브만" : "액티브 없음";
|
|
skillPanel.color = SkillPassiveOnlyColor;
|
|
return;
|
|
}
|
|
skillName.text = s.Active.NameKo + " Lv" + s.ActiveLv;
|
|
var st = sys.StateOf(selected);
|
|
string kind = s.Active.Trigger == SkillTrigger.Instant ? "즉시" : "다음 행동";
|
|
switch (st)
|
|
{
|
|
case SkillSystem.ActiveState.Ready:
|
|
skillState.text = "사용 가능"; // 짧게(탭 동작 안내 줄 삭제 · 검수2 결함 2)
|
|
skillPanel.color = SkillReadyColor;
|
|
break;
|
|
case SkillSystem.ActiveState.Reserved:
|
|
skillState.text = "예약됨";
|
|
skillPanel.color = SkillReservedColor;
|
|
break;
|
|
case SkillSystem.ActiveState.Targeting:
|
|
skillState.text = "지점 선택";
|
|
skillPanel.color = SkillReservedColor;
|
|
break;
|
|
case SkillSystem.ActiveState.Cooldown:
|
|
skillState.text = "쿨 " + s.CooldownLeft(battle.TurnNumber) + "턴";
|
|
skillPanel.color = SkillOffColor;
|
|
break;
|
|
case SkillSystem.ActiveState.Acted:
|
|
skillState.text = "행동 완료";
|
|
skillPanel.color = SkillOffColor;
|
|
break;
|
|
case SkillSystem.ActiveState.BonusMove:
|
|
skillState.text = "추가 이동 중";
|
|
skillPanel.color = SkillOffColor;
|
|
break;
|
|
default:
|
|
skillState.text = kind + " · 쿨 " + s.Active.Cooldown;
|
|
skillPanel.color = SkillOffColor;
|
|
break;
|
|
}
|
|
bool dark = st == SkillSystem.ActiveState.Reserved || st == SkillSystem.ActiveState.Targeting;
|
|
skillName.color = skillState.color = dark ? new Color(0.15f, 0.1f, 0.05f) : Color.white;
|
|
}
|
|
|
|
/// <summary>화면 점이 미니맵 안이면 해당 월드 좌표를 돌려준다.</summary>
|
|
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 = stage.chapter > 0
|
|
? stage.chapter + "-" + stage.stage + " · " + stage.title
|
|
: "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle);
|
|
countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy);
|
|
if (allyCount != null)
|
|
{
|
|
allyCount.text = battle.AliveCount(Faction.Player).ToString();
|
|
enemyCount.text = battle.AliveCount(Faction.Enemy).ToString();
|
|
}
|
|
|
|
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;
|
|
bool msgOn = !string.IsNullOrEmpty(messageText.text);
|
|
if (msgChip != null && msgChip.gameObject.activeSelf != msgOn) msgChip.gameObject.SetActive(msgOn);
|
|
if (turnBadge != null)
|
|
{
|
|
bool on = turnText.text.Length > 0;
|
|
if (turnBadge.gameObject.activeSelf != on) turnBadge.gameObject.SetActive(on);
|
|
turnBadge.color = battle.Phase == BattlePhase.EnemyTurn || battle.Phase == BattlePhase.Defeat
|
|
? UISkin.Col("hud_turn_enemy", Color.red)
|
|
: UISkin.Col("hud_turn_player", Color.blue);
|
|
}
|
|
|
|
if (battle.Skills != null && battle.Skills.PendingTeleport != null)
|
|
{
|
|
hintText.text = "변진: 반경 안 빈 곳을 탭하면 이동(행동 소모 없음) · 다른 곳 탭 = 취소";
|
|
}
|
|
else if (battle.Phase == BattlePhase.PlayerTurn)
|
|
{
|
|
// 기본 조작 문장은 테마 HUD 에서 삭제(Fable §4-8 · 첫 전투 튜토리얼 손으로 대체 예정) · 상황 안내만 유지
|
|
hintText.text = battle.OnlyBonusMovesRemain()
|
|
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
|
|
: unitCard != null ? "" : "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동";
|
|
}
|
|
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 = "슬링 " + SkillMods.SlingMax(u).ToString("0.#");
|
|
if (UnitTypeInfo.IsRanged(u.Type)) line3 += " 사거리 " + SkillMods.Range(u).ToString("0.#") + " (접근 +" + u.ArcherMoveAllow.ToString("0.#") + ")";
|
|
if (u.Type == UnitType.Strategist) line3 += " 광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#");
|
|
string state = "";
|
|
if (u.Faction == Faction.Player)
|
|
{
|
|
if (u.BonusMoveAvailable) state = " [추가 이동]";
|
|
else if (u.ActedThisTurn) state = " [행동 완료]";
|
|
}
|
|
unitText.text = line1 + "\n" + line2 + "\n" + line3 + state;
|
|
}
|
|
else
|
|
{
|
|
unitText.text = "유닛을 선택하세요";
|
|
}
|
|
if (unitCard != null) RefreshUnitCard(selected);
|
|
RefreshSkill();
|
|
if (skillGlow != null)
|
|
{
|
|
bool ready = skillPanel.gameObject.activeSelf && battle.Skills != null && selected != null && selected.IsAlive && selected.Faction == Faction.Player
|
|
&& battle.Skills.StateOf(selected) == SkillSystem.ActiveState.Ready;
|
|
if (skillGlow.gameObject.activeSelf != ready) skillGlow.gameObject.SetActive(ready);
|
|
}
|
|
|
|
bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat;
|
|
float alpha = HideOnResult && result ? 0f : 1f;
|
|
if (group != null && group.alpha != alpha) group.alpha = alpha;
|
|
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);
|
|
bool toMap = battle.ResultHandler != null;
|
|
resultSub.text = win
|
|
? stage.title + " 제압!\n화면을 탭하면 " + (toMap ? "월드맵으로" : "다음 전투로")
|
|
: stage.title + " 에서 패배\n화면을 탭하면 " + (toMap ? "월드맵으로" : "다시 도전");
|
|
}
|
|
}
|
|
|
|
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);
|
|
// 값이 같으면 쓰지 않는다(카메라가 멈춰 있으면 MinimapLive 층도 다시 묶지 않음)
|
|
var vr = minimapView.rectTransform;
|
|
var vp = new Vector2(x0, y0);
|
|
var vs = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0));
|
|
if (vr.anchoredPosition != vp) vr.anchoredPosition = vp;
|
|
if (vr.sizeDelta != vs) vr.sizeDelta = vs;
|
|
}
|
|
|
|
// 유닛 점
|
|
foreach (var u in battle.Units)
|
|
{
|
|
if (u == null) continue;
|
|
Image dot;
|
|
if (!minimapDots.TryGetValue(u, out dot))
|
|
{
|
|
dot = MakeImage("Dot", minimapLive, 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)
|
|
{
|
|
var dp = new Vector2((u.Position.x - b.xMin) * sx, (u.Position.y - b.yMin) * sy);
|
|
if (dot.rectTransform.anchoredPosition != dp) dot.rectTransform.anchoredPosition = dp;
|
|
}
|
|
}
|
|
// 사라진 유닛 정리
|
|
var dead = new List<Unit>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|