using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace TKS.OutGame { /// 아웃게임 화면 1장(본영 로비 · 영웅 · 편성 · 거래). OutGameRoot 가 만들고 전환한다. public abstract class OutGameScreen { public OutGameRoot Root; public RectTransform Rt; /// 하단 탭 강조(0 영웅 · 4 거래 · −1 없음). public int Tab = -1; public OutGameSession S { get { return Root.Session; } } public abstract void Build(); public virtual void Refresh() { } public virtual void OnShow() { } /// 뒤로 가기를 화면 안에서 처리했으면 true. public virtual bool OnBack() { return false; } /// 제목 띠(재화 바 아래) · back = 「◀」 버튼. protected void Header(string title, string sub, bool back) { var band = UiKit.TopBand("Header", Rt, OutGameRoot.TopBarH, OutGameRoot.HeaderH); UiKit.Paint(band, UiKit.Header, false); float x = 36f; if (back) { UiKit.Btn("Back", band, "◀", 44, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(100f, 76f), () => Root.Back()); x = 146f; } var t = UiKit.Label(band, title, 52, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(x, 0f), new Vector2(360f, 90f)); t.horizontalOverflow = HorizontalWrapMode.Overflow; UiKit.Label(band, sub, 30, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(x + t.preferredWidth + 18f, -4f), new Vector2(700f, 60f), false); } /// 제목 띠 아래 ~ 하단 탭 위 본문. protected RectTransform Body(string name = "Body") { return UiKit.Fill(name, Rt, 0f, 0f, OutGameRoot.TopBarH + OutGameRoot.HeaderH, OutGameRoot.TabBarH); } } /// /// 아웃게임 루트(2026-11-05 #1093 시스템 기획 §1 · §10 A) — 게임 시작 = 본영 로비 · 출정 → 기존 챕터 월드맵 · 편성 · 영웅 · 거래 · 준비 중 팝업 · /// 상단 재화 바(골드 · 보옥 · 군량 n/최대 + 회복 타이머 · 월드맵에도 표시) · 하단 탭 5 · 뒤로 가기(◀ · Esc) · 전투 결과 보상 패널. /// 데이터 = OutGameSession(표 + 세이브) · 화면은 도형 + 글자 와이어프레임(목업 img03·06·08·11 배치). /// public class OutGameRoot : MonoBehaviour { public static OutGameRoot Instance { get; private set; } public const float TopBarH = 120f; public const float HeaderH = 104f; public const float TabBarH = 150f; public static readonly Vector2 Reference = new Vector2(1080f, 1920f); public OutGameSession Session { get; private set; } public BattleManager Battle { get; private set; } public WorldMapScreen Map { get; private set; } public Camera Cam { get; private set; } BattleHUD hud; SlingshotInput sling; Canvas mainCanvas; Canvas topCanvas; Canvas resultCanvas; public RectTransform TopRoot { get; private set; } public LobbyScreen Lobby { get; private set; } public HeroListScreen HeroList { get; private set; } public HeroDetailScreen HeroDetail { get; private set; } public FormationScreen Formation { get; private set; } public TradeScreen Trade { get; private set; } readonly List screens = new List(); OutGameScreen current; RectTransform barRoot; Text goldText; Text gemText; Text staminaText; Text staminaTimer; readonly List tabImages = new List(); readonly List tabTexts = new List(); GameObject modal; GameObject toast; Text toastText; float toastUntil; float nextTick; BattleResultPanel result; bool resultDone; public OutGameScreen Current { get { return current; } } /// 아웃게임 화면(로비 등)이 보이는가(월드맵·전투 중 = false). public bool ScreensVisible { get { return mainCanvas != null && mainCanvas.gameObject.activeSelf; } } public bool LobbyVisible { get { return ScreensVisible && current == Lobby; } } public bool ModalVisible { get { return modal != null; } } public bool ResultVisible { get { return result != null && result.Visible; } } void Awake() { Instance = this; } void OnDestroy() { if (Instance == this) Instance = null; } public void Build(BattleManager b, BattleHUD h, SlingshotInput s, WorldMapScreen wm, Camera cam) { Battle = b; hud = h; sling = s; Map = wm; Cam = cam; Session = OutGameSession.Start(OutGameData.Default, b.Config); Session.Changed += RefreshAll; // 전투 연결: 출전 = 편성 프리셋 · 결과 = 보상 b.LineupProvider = Session.BuildLineup; b.StageStarted += () => resultDone = false; b.PhaseChanged += OnPhase; // 월드맵 연결: 팝업 줄 · 군량 · 별 · 소탕 · 본영 if (wm != null) { wm.TopInset = TopBarH; wm.PopupInfo = PopupInfo; wm.TryEnter = TryEnter; wm.StarsOf = si => EconomyRules.StarsOf(Session.Save, b.Config.stages[si]); wm.CanSweep = si => EconomyRules.CanSweep(Session.Save, b.Config.stages[si]); wm.Sweep = DoSweep; wm.BackRequested = ShowLobby; } mainCanvas = MakeCanvas("OutGameCanvas", 350, 0.45f); resultCanvas = MakeCanvas("BattleResultCanvas", 200, 0.8f); topCanvas = MakeCanvas("OutGameTopCanvas", 450, 0.4f); TopRoot = (RectTransform)topCanvas.transform; var bg = UiKit.Fill("Bg", mainCanvas.transform); UiKit.Paint(bg, UiKit.Bg, false, true); Lobby = Add(new LobbyScreen()); HeroList = Add(new HeroListScreen()); HeroDetail = Add(new HeroDetailScreen()); Formation = Add(new FormationScreen()); Trade = Add(new TradeScreen()); BuildTabBar(); BuildCurrencyBar(); result = new BattleResultPanel(this, (RectTransform)resultCanvas.transform); mainCanvas.gameObject.SetActive(false); } T Add(T scr) where T : OutGameScreen { scr.Root = this; scr.Rt = UiKit.Fill(typeof(T).Name, mainCanvas.transform); scr.Build(); scr.Rt.gameObject.SetActive(false); screens.Add(scr); return scr; } Canvas MakeCanvas(string name, int order, float plane) { var go = new GameObject(name); go.transform.SetParent(transform, false); var c = go.AddComponent(); c.renderMode = RenderMode.ScreenSpaceCamera; // 카메라 캡처(검증)에도 찍히게 c.worldCamera = Cam; c.planeDistance = plane; c.sortingOrder = order; var sc = go.AddComponent(); sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; sc.referenceResolution = Reference; sc.matchWidthOrHeight = 0.5f; go.AddComponent(); return c; } // ---------- 화면 전환 ---------- /// 본영 로비(게임 시작 화면) — 월드맵·전투 HUD·조작을 끄고 로비를 연다. public void ShowLobby() { if (Map != null) Map.Hide(); if (Battle != null && Battle.Phase != BattlePhase.Setup) Battle.GoIdle(); Show(Lobby); } public void Show(OutGameScreen scr) { if (hud != null) hud.gameObject.SetActive(false); if (sling != null) sling.enabled = false; if (Map != null && Map.IsVisible) Map.Hide(); mainCanvas.gameObject.SetActive(true); foreach (var s in screens) s.Rt.gameObject.SetActive(s == scr); current = scr; for (int i = 0; i < tabImages.Count; i++) { bool on = scr.Tab == i; tabImages[i].color = on ? UiKit.Red : UiKit.Dark; } scr.OnShow(); scr.Refresh(); RefreshBar(); } public void ShowHero(string id) { HeroDetail.Select(id); Show(HeroDetail); } /// 출정 = 챕터 월드맵(도전 중인 마지막 열린 챕터). public void OpenWorldMap() { if (Map == null || !Map.HasData) return; CloseModal(); mainCanvas.gameObject.SetActive(false); Map.Show(CurrentChapter()); } public int CurrentChapter() { if (Map == null) return 0; int ch = 0; for (int i = 0; i < Map.ChapterCount; i++) if (Map.ChapterUnlocked(i)) ch = i; return ch; } /// 뒤로 가기(◀ · Esc): 팝업 닫기 → 화면 안 처리 → 영웅 상세 = 목록 → 나머지 = 로비 · 월드맵 = 로비. public void Back() { if (modal != null) { CloseModal(); return; } if (Map != null && Map.IsVisible) { if (Map.PopupVisible) Map.ClosePopup(); else ShowLobby(); return; } if (!ScreensVisible || current == null) return; if (current.OnBack()) return; if (current == HeroDetail) Show(HeroList); else if (current != Lobby) Show(Lobby); } /// 「준비 중」(연무장·보물·내정) · 「추후 업데이트」(동맹) 안내. public void ComingSoon(string what, bool later = false) { Modal(what, later ? "추후 업데이트 예정입니다." : "준비 중입니다.\n(2차 개발 — 시스템 기획 §7~§9)", new ModalButton("확인", UiKit.Dark, null)); } void Update() { if (Battle == null || Session == null) return; bool inBattle = Battle.Phase != BattlePhase.Setup; bool onMap = Map != null && Map.IsVisible; if (inBattle && ScreensVisible) mainCanvas.gameObject.SetActive(false); // 테스트 등이 바로 StartStage 를 불러도 화면이 전투를 가리지 않게 bool barOn = !inBattle; if (barRoot.gameObject.activeSelf != barOn) barRoot.gameObject.SetActive(barOn); result.SetVisible(inBattle && resultDone && (Battle.Phase == BattlePhase.StageClear || Battle.Phase == BattlePhase.Defeat)); if (Time.unscaledTime >= nextTick) { nextTick = Time.unscaledTime + 0.5f; Session.TickStamina(); RefreshBar(); } if (toast != null && Time.unscaledTime > toastUntil) toast.SetActive(false); if (Input.GetKeyDown(KeyCode.Escape) && !inBattle) Back(); } void RefreshAll() { RefreshBar(); if (ScreensVisible && current != null) current.Refresh(); } // ---------- 상단 재화 바 ---------- void BuildCurrencyBar() { barRoot = UiKit.TopBand("CurrencyBar", TopRoot, 0f, TopBarH); UiKit.Paint(barRoot, new Color(0.15f, 0.11f, 0.09f), false, true); goldText = Pill(0, "골", "골드", UiKit.Coin, () => Toast("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)")); gemText = Pill(1, "보", "보옥", UiKit.Jade, () => Toast("보옥 구매(과금) = 준비 중 · 첫 클리어 · 보스 보상")); staminaText = Pill(2, "군", "군량", UiKit.Grain, StaminaModal); staminaTimer = staminaText.transform.parent.Find("Name").GetComponent(); } Text Pill(int i, string mark, string name, Color c, Action plus) { float w = 336f; var p = UiKit.Box("Pill" + i, barRoot, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f + i * (w + 18f), 0f), new Vector2(w, 84f), new Color(0.27f, 0.21f, 0.18f)); var dot = UiKit.Dot("Icon", p.transform, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(8f, 0f), 64f, c); UiKit.FillLabel(dot.transform, mark, 32, UiKit.White); var n = UiKit.Label(p.transform, name, 26, new Color(0.85f, 0.8f, 0.75f), TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(80f, 0f), new Vector2(110f, 60f), false); n.gameObject.name = "Name"; var v = UiKit.Label(p.transform, "", 34, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-70f, 0f), new Vector2(170f, 60f)); v.horizontalOverflow = HorizontalWrapMode.Overflow; UiKit.Btn("Plus", p.transform, "+", 40, new Color(0.42f, 0.35f, 0.31f), UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-8f, 0f), new Vector2(56f, 56f), plus); return v; } public void RefreshBar() { if (Session == null || goldText == null) return; var d = Session.Save; goldText.text = d.gold.ToString("N0"); gemText.text = d.gem.ToString("N0"); staminaText.text = d.stamina + "/" + Session.Db.Economy.StaminaMax; int left = Session.StaminaSecondsToNext(); staminaTimer.text = left > 0 ? (left / 60).ToString("00") + ":" + (left % 60).ToString("00") : "군량"; } // ---------- 하단 탭 ---------- void BuildTabBar() { var bar = UiKit.Rect("TabBar", mainCanvas.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(Reference.x, TabBarH)); bar.anchorMin = new Vector2(0f, 0f); bar.anchorMax = new Vector2(1f, 0f); bar.offsetMin = new Vector2(0f, 0f); bar.offsetMax = new Vector2(0f, TabBarH); UiKit.Paint(bar, UiKit.Dark, false, true); string[] names = { "영웅", "보물", "내정", "동맹", "거래" }; string[] marks = { "영", "보", "내", "잠", "거" }; for (int i = 0; i < names.Length; i++) { int k = i; var cell = UiKit.Rect("Tab" + i, bar, new Vector2(0f, 0f), new Vector2(0f, 0f), Vector2.zero, Vector2.zero); cell.anchorMin = new Vector2(i / 5f, 0f); cell.anchorMax = new Vector2((i + 1) / 5f, 1f); cell.offsetMin = cell.offsetMax = Vector2.zero; var img = UiKit.Paint(cell, UiKit.Dark, false, true); var btn = cell.gameObject.AddComponent