ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UI/OutGameRoot.cs

573 lines
25 KiB
C#
Raw Normal View History

아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>아웃게임 화면 1장(본영 로비 · 영웅 · 편성 · 거래). OutGameRoot 가 만들고 전환한다.</summary>
public abstract class OutGameScreen
{
public OutGameRoot Root;
public RectTransform Rt;
/// <summary>하단 탭 강조(0 영웅 · 4 거래 · −1 없음).</summary>
public int Tab = -1;
public OutGameSession S
{
get { return Root.Session; }
}
public abstract void Build();
public virtual void Refresh() { }
public virtual void OnShow() { }
/// <summary>뒤로 가기를 화면 안에서 처리했으면 true.</summary>
public virtual bool OnBack() { return false; }
/// <summary>제목 띠(재화 바 아래) · back = 「◀」 버튼.</summary>
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);
}
/// <summary>제목 띠 아래 ~ 하단 탭 위 본문.</summary>
protected RectTransform Body(string name = "Body")
{
return UiKit.Fill(name, Rt, 0f, 0f, OutGameRoot.TopBarH + OutGameRoot.HeaderH, OutGameRoot.TabBarH);
}
}
/// <summary>
/// 아웃게임 루트(2026-11-05 #1093 시스템 기획 §1 · §10 A) — 게임 시작 = 본영 로비 · 출정 → 기존 챕터 월드맵 · 편성 · 영웅 · 거래 · 준비 중 팝업 ·
/// 상단 재화 바(골드 · 보옥 · 군량 n/최대 + 회복 타이머 · 월드맵에도 표시) · 하단 탭 5 · 뒤로 가기(◀ · Esc) · 전투 결과 보상 패널.
/// 데이터 = OutGameSession(표 + 세이브) · 화면은 도형 + 글자 와이어프레임(목업 img03·06·08·11 배치).
/// </summary>
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<OutGameScreen> screens = new List<OutGameScreen>();
OutGameScreen current;
RectTransform barRoot;
Text goldText;
Text gemText;
Text staminaText;
Text staminaTimer;
readonly List<Image> tabImages = new List<Image>();
readonly List<Text> tabTexts = new List<Text>();
GameObject modal;
GameObject toast;
Text toastText;
float toastUntil;
float nextTick;
BattleResultPanel result;
bool resultDone;
public OutGameScreen Current
{
get { return current; }
}
/// <summary>아웃게임 화면(로비 등)이 보이는가(월드맵·전투 중 = false).</summary>
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>(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<Canvas>();
c.renderMode = RenderMode.ScreenSpaceCamera; // 카메라 캡처(검증)에도 찍히게
c.worldCamera = Cam;
c.planeDistance = plane;
c.sortingOrder = order;
var sc = go.AddComponent<CanvasScaler>();
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
sc.referenceResolution = Reference;
sc.matchWidthOrHeight = 0.5f;
go.AddComponent<GraphicRaycaster>();
return c;
}
// ---------- 화면 전환 ----------
/// <summary>본영 로비(게임 시작 화면) — 월드맵·전투 HUD·조작을 끄고 로비를 연다.</summary>
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);
}
/// <summary>출정 = 챕터 월드맵(도전 중인 마지막 열린 챕터).</summary>
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;
}
/// <summary>뒤로 가기(◀ · Esc): 팝업 닫기 → 화면 안 처리 → 영웅 상세 = 목록 → 나머지 = 로비 · 월드맵 = 로비.</summary>
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);
}
/// <summary>「준비 중」(연무장·보물·내정) · 「추후 업데이트」(동맹) 안내.</summary>
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>();
}
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<Button>();
btn.onClick.AddListener(() => OnTab(k));
var dot = UiKit.Dot("Icon", cell, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -18f), 66f, i == 3 ? new Color(0.35f, 0.3f, 0.28f) : new Color(0.38f, 0.31f, 0.28f));
UiKit.FillLabel(dot.transform, marks[i], 32, i == 3 ? new Color(0.7f, 0.66f, 0.62f) : UiKit.White);
var t = UiKit.Label(cell, names[i], 32, i == 3 ? new Color(0.62f, 0.58f, 0.55f) : UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(200f, 48f));
tabImages.Add(img);
tabTexts.Add(t);
}
}
public void OnTab(int i)
{
switch (i)
{
case 0: Show(HeroList); break;
case 1: ComingSoon("보물"); break;
case 2: ComingSoon("내정 (훈련 · 순찰 · 개발)"); break;
case 3: ComingSoon("동맹", true); break;
case 4: Show(Trade); break;
}
}
// ---------- 팝업 · 알림 ----------
public struct ModalButton
{
public string label;
public Color color;
public Action action;
public bool keepOpen;
public ModalButton(string label, Color color, Action action, bool keepOpen = false)
{
this.label = label;
this.color = color;
this.action = action;
this.keepOpen = keepOpen;
}
}
public void Modal(string title, string body, params ModalButton[] buttons)
{
var content = ModalCustom(title, 0f, buttons);
var t = UiKit.FillLabel(content, body, 36, UiKit.Ink, TextAnchor.MiddleCenter, false, 10f);
t.lineSpacing = 1.1f;
var rt = (RectTransform)content.parent;
float h = Mathf.Clamp(t.preferredHeight + 330f, 480f, 1500f);
rt.sizeDelta = new Vector2(rt.sizeDelta.x, h);
}
/// <summary>제목 + 내용 칸 + 아래 버튼 줄 · 반환 = 내용 칸(높이 = height · 0 이면 Modal 이 맞춤).</summary>
public RectTransform ModalCustom(string title, float height, params ModalButton[] buttons)
{
CloseModal();
var dim = UiKit.Fill("Modal", TopRoot);
var dimImg = UiKit.Paint(dim, new Color(0f, 0f, 0f, 0.6f), false, true);
dimImg.raycastTarget = true;
modal = dim.gameObject;
float h = height > 0f ? height : 600f;
var panel = UiKit.Box("Panel", dim, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 20f), new Vector2(940f, h), UiKit.Paper);
panel.raycastTarget = true;
UiKit.Border(panel, UiKit.Line, 4f);
var head = UiKit.TopBand("Head", panel.transform, 0f, 110f);
UiKit.Paint(head, UiKit.Dark);
UiKit.FillLabel(head, title, 44, UiKit.White);
var content = UiKit.Fill("Content", panel.transform, 40f, 40f, 130f, 160f);
int n = Mathf.Max(1, buttons.Length);
float bw = Mathf.Min(400f, (860f - (n - 1) * 20f) / n);
for (int i = 0; i < buttons.Length; i++)
{
var mb = buttons[i];
float x = (i - (n - 1) / 2f) * (bw + 20f);
UiKit.Btn("B" + i, panel.transform, mb.label, 34, mb.color, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(x, 36f), new Vector2(bw, 100f), () =>
{
if (!mb.keepOpen) CloseModal();
if (mb.action != null) mb.action();
});
}
return content;
}
public void CloseModal()
{
if (modal == null) return;
modal.SetActive(false);
Destroy(modal);
modal = null;
}
public void Toast(string msg)
{
if (toast == null)
{
var img = UiKit.Box("Toast", TopRoot, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, TabBarH + 40f), new Vector2(940f, 110f), new Color(0.1f, 0.08f, 0.07f, 0.92f));
toast = img.gameObject;
toastText = UiKit.FillLabel(img.transform, "", 32, UiKit.White, TextAnchor.MiddleCenter, false, 16f);
}
toast.transform.SetAsLastSibling();
toastText.text = msg;
toast.SetActive(true);
toastUntil = Time.unscaledTime + 2.2f;
}
public string LastToast
{
get { return toast != null && toast.activeSelf ? toastText.text : ""; }
}
// ---------- 군량 ----------
/// <summary>군량 충전(보옥 구매 · 광고 스텁) 팝업.</summary>
public void StaminaModal()
{
StaminaModal(null);
}
void StaminaModal(string why)
{
var e = Session.Db.Economy;
var d = Session.Save;
EconomyRules.RollDaily(d, Session.Now);
string body = (why != null ? why + "\n\n" : "") +
"군량 " + d.stamina + " / " + e.StaminaMax + " · " + (e.StaminaRegenSec / 60) + "분마다 1 회복(오프라인 포함)\n" +
"스테이지 " + e.StageCost + " · 보스 " + e.BossCost + " 소모";
Modal("군량 충전", body,
new ModalButton("보옥 " + e.GemStaminaCost + " → +" + e.GemStaminaAmount + "\n(오늘 " + d.adsUsed.gemStamina + "/" + e.GemStaminaDaily + ")", UiKit.Jade, () =>
{
var w = Session.BuyStamina();
StaminaModal(w ?? "군량 +" + e.GemStaminaAmount);
if (Map != null) Map.RefreshPopup();
}, true),
new ModalButton("광고(시험) +" + e.AdStamina + "\n(오늘 " + d.adsUsed.stamina + "/" + e.AdStaminaDaily + ")", UiKit.Grain, () =>
{
var w = Session.AdStamina();
StaminaModal(w ?? "군량 +" + e.AdStamina + " (광고 SDK 미연동 · 시험 지급)");
if (Map != null) Map.RefreshPopup();
}, true),
new ModalButton("닫기", UiKit.Dark, null));
}
// ---------- 월드맵 연결 ----------
string PopupInfo(int si)
{
var s = Battle.Config.stages[si];
var db = Session.Db;
var d = Session.Save;
var e = db.Economy;
var row = db.Rewards.For(s.chapter);
if (row == null) return "";
var st = d.Stage(s.chapter, s.stage, false);
bool first = st == null || st.clears == 0;
int cost = EconomyRules.StageCost(db, s);
float baseGold = row.goldBase + s.stage * e.GoldPerStage;
int gold = Mathf.RoundToInt(baseGold * (first ? e.FirstClearGoldMult : e.RepeatRewardMult));
int exp = Mathf.RoundToInt(row.expBase * (first ? 1f : e.RepeatRewardMult));
bool boss = s.kind == StageKind.Boss;
var sb = new System.Text.StringBuilder();
sb.Append("<color=#8a3a22>군량 ").Append(cost).Append(" 소모</color> (보유 ").Append(d.stamina).Append("/").Append(e.StaminaMax).Append(")");
sb.Append("\n보상 골드 ").Append(gold).Append(first ? "(첫 클리어)" : "(반복 ×" + e.RepeatRewardMult + ")").Append(" · 경험 ").Append(exp).Append("(출전 전원)");
if (first) sb.Append(" · 보옥 ").Append(row.gemFirst + (boss ? row.gemBoss : 0));
float drop = boss ? e.BossGearDropPct : row.gearDropPct;
sb.Append("\n장비 드랍 ").Append(Mathf.RoundToInt(drop * 100f)).Append("% (").Append(UiKit.GradeKo(row.gearGradeMax)).Append(" 등급 이하)");
int stars = st != null ? st.stars : 0;
sb.Append("\n별 ").Append(UiKit.Stars(stars, 3)).Append(" 클리어 · 전원 생존 · ").Append(row.starTurns).Append("턴 이내");
if (stars >= 3) sb.Append("\n<color=#3a6a30>별 3 = 소탕 가능(군량만 · 전투 없음)</color>");
return sb.ToString();
}
bool TryEnter(int si)
{
if (Session.TryPayEntry(Battle.Config.stages[si])) return true;
StaminaModal("군량이 부족합니다 — 필요 " + EconomyRules.StageCost(Session.Db, Battle.Config.stages[si]));
return false;
}
void DoSweep(int si)
{
var s = Battle.Config.stages[si];
var r = Session.Sweep(s);
if (r == null)
{
StaminaModal("군량이 부족합니다 — 필요 " + EconomyRules.StageCost(Session.Db, s));
return;
}
Modal("소탕 " + s.chapter + "-" + s.stage, BattleResultPanel.RewardText(Session, r), new ModalButton("확인", UiKit.Dark, null));
if (Map != null) Map.RefreshPopup();
}
// ---------- 전투 결과 ----------
void OnPhase(BattlePhase p)
{
if (p != BattlePhase.StageClear && p != BattlePhase.Defeat) return;
if (resultDone) return;
resultDone = true;
var r = Session.OnBattleEnd(Battle, p == BattlePhase.StageClear);
result.Fill(r);
}
}
}