ThreeKingdomsSlingshot/Assets/Scripts/OutGame/Domestic/DomesticScreen.cs

574 lines
36 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 내정(하단 탭 · 기획 S9~S11 · 목업 img13·05·14 · PD #1093 C) — 상단 탭 3(한 항목 = 한 페이지):
/// ① 훈련 = 병종 탭 5 · 랭크 바(현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 골드 · 남은 레벨) · 카드 3(HP·공·방 Lv n/Max · 골드) · 3항목 Max → 랭크 승급
/// ② 순찰 = 기준 챕터 · 누적 n/최대 시간 · 시간당 수확 4줄 · 쌓인 보상 4칸 · 수확 · 광고 2배(스텁) · 즉시 수확(보옥)
/// ③ 개발 = 갈래 ◀ 상업·농업·공업 ▶ 한 페이지씩 · 세로 트리(선행 조건 깊이로 줄 · 노드 n/Max · 큰 노드) · 노드 탭 = 강화 팝업 · 도면·골드 · 트리 초기화.
/// </summary>
public class DomesticScreen : OutGameScreen
{
public static readonly string[] PageNames = { "훈련", "순찰", "개발" };
int page;
int typeIdx;
int branchIdx;
readonly List<Button> tabs = new List<Button>();
readonly List<RectTransform> pages = new List<RectTransform>();
public int Page
{
get { return page; }
}
/// <summary>병종 탭 순서 = training.csv 행 순서(보·창·기·궁·책).</summary>
public List<UnitType> TrainTypes
{
get
{
var l = new List<UnitType>();
foreach (var r in S.Db.Training.rows)
if (!l.Contains(r.type)) l.Add(r.type);
return l;
}
}
public UnitType TrainType
{
get
{
var l = TrainTypes;
return l.Count > 0 ? l[Mathf.Clamp(typeIdx, 0, l.Count - 1)] : UnitType.Infantry;
}
}
public string Branch
{
get
{
var b = S.Db.Dev.Branches;
return b.Count > 0 ? b[Mathf.Clamp(branchIdx, 0, b.Count - 1)] : "";
}
}
/// <summary>개발 페이지에 그려진 노드 수(테스트).</summary>
public int NodeCount { get; private set; }
public override void Build()
{
Tab = 2;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("내정", "훈련 · 순찰 · 개발", true);
var body = Body();
for (int i = 0; i < PageNames.Length; i++)
{
int k = i;
tabs.Add(UISkin.On
? UiKit.SubTab(body, "Tab" + i, PageNames[i], 24f + i * 348f, 18f, 332f, 84f, () => SetPage(k))
: UiKit.Btn("Tab" + i, body, PageNames[i], 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
pages.Add(UiKit.Fill("Page" + i, body, 0f, 0f, 118f, 0f));
}
}
public void SetPage(int p)
{
page = Mathf.Clamp(p, 0, pages.Count - 1);
Refresh();
}
public void SetTrainType(UnitType t)
{
typeIdx = Mathf.Max(0, TrainTypes.IndexOf(t));
Refresh();
}
public void SetBranch(int i)
{
int n = Mathf.Max(1, S.Db.Dev.Branches.Count);
branchIdx = ((i % n) + n) % n;
Refresh();
}
public override void Refresh()
{
for (int i = 0; i < tabs.Count; i++)
{
bool on = i == page;
if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
else
{
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음)
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
}
pages[i].gameObject.SetActive(on);
}
UiKit.Clear(pages[page]);
if (page == 0) BuildTraining(pages[0]);
else if (page == 1)
{
if (UISkin.Has("panel_field") && UISkin.Has("btn_l_green")) BuildPatrolKit(pages[1]);
else BuildPatrol(pages[1]);
}
else BuildDev(pages[2]);
}
// ---------- ① 훈련 ----------
void BuildTraining(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var t = TrainType;
var types = TrainTypes;
for (int i = 0; i < types.Count; i++)
{
var ut = types[i];
bool on = ut == t;
UiKit.Btn("Type" + i, p, UnitTypeInfo.Mark(ut), 40, on ? UiKit.Red : UiKit.Paper, on ? UiKit.White : UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 208f, -6f), new Vector2(196f, 78f), () => SetTrainType(ut));
}
int rank = TrainingRules.Rank(d, t);
var stage = UiKit.TopBand("Unit", p, 100f, 470f, 24f, 24f);
UiKit.Paint(stage, new Color(0.24f, 0.19f, 0.25f));
UiKit.Label(stage, TrainingRules.RankName(db, rank) + " <size=40>랭크 " + rank + "/" + db.Training.MaxRank + "</size>", 60, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -20f), new Vector2(900f, 90f));
var halo = UiKit.Dot("Halo", stage, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), 300f, new Color(1f, 1f, 1f, 0.1f));
halo.raycastTarget = false;
var face = UiKit.Dot("Face", stage, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), 220f, UiKit.TypeColor(t));
UiKit.FillLabel(face.transform, UnitTypeInfo.Mark(t), 110, UiKit.White);
UiKit.Label(stage, UnitTypeInfo.Korean(t) + " 영웅·병사 전원", 30, new Color(0.85f, 0.8f, 0.86f), TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(900f, 50f), false);
// 랭크 바: 현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 비용 · 남은 레벨
var bar = UiKit.TopBand("RankBar", p, 586f, 230f, 24f, 24f);
UiKit.Paint(bar, new Color(0.17f, 0.14f, 0.19f));
UiKit.Label(bar, "랭크 진행", 30, new Color(0.85f, 0.8f, 0.86f), TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -12f), new Vector2(300f, 50f));
int left = TrainingRules.Remaining(db, d, t);
int total = db.Training.MaxLevel * TrainingTable.Stats.Length;
var fill = UiKit.Bar("Progress", bar, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 6f), new Vector2(760f, 30f), new Color(0.32f, 0.28f, 0.35f), new Color(0.95f, 0.68f, 0.2f));
float v = total > 0 ? (total - left) / (float)total : 0f;
UiKit.SetBar(fill, v);
var marker = UiKit.Dot("Marker", bar, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(24f + 760f * v, 6f), 70f, new Color(0.95f, 0.68f, 0.2f));
UiKit.FillLabel(marker.transform, "▲", 34, UiKit.Ink);
var r = db.Training.RankOf(rank);
UiKit.Label(bar, TrainingRules.RankName(db, rank) + " · 랭크 보너스 +" + (r != null ? r.rankPct : 0f).ToString("0.#") + "%", 32, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 16f), new Vector2(700f, 54f));
string promo;
if (rank >= db.Training.MaxRank) promo = "최종 랭크";
else promo = "다음 랭크 승급 비용\n<size=40>골드 ×" + TrainingRules.PromoteCost(db, d, t).ToString("N0") + "</size>";
UiKit.Label(bar, promo, 26, new Color(0.85f, 0.8f, 0.86f), TextAnchor.UpperRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -12f), new Vector2(300f, 110f), false);
UiKit.Label(bar, left + "\n<size=24>남은 레벨</size>", 40, UiKit.White, TextAnchor.LowerRight, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-24f, 12f), new Vector2(200f, 90f));
// 카드 3: HP · 공 · 방
string[] names = { "HP", "공격력", "방어력" };
string[] marks = { "H", "공", "방" };
float cw = 330f;
for (int i = 0; i < 3; i++)
{
int k = i;
int lv = TrainingRules.Lv(d, t, i);
bool max = lv >= db.Training.MaxLevel;
var card = UiKit.Box("Card" + i, p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * (cw + 21f), -838f), new Vector2(cw, 360f), max ? UiKit.Purple : UiKit.Paper);
UiKit.Border(card, UiKit.Line, 3f);
var head = UiKit.TopBand("Head", card.transform, 0f, 72f);
UiKit.Paint(head, max ? UiKit.Purple : UiKit.Dark);
UiKit.FillLabel(head, max ? "Max" : "LV." + lv, 36, max ? UiKit.White : UiKit.Ink);
var dot = UiKit.Dot("Icon", card.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -86f), 96f, UiKit.Slot);
UiKit.FillLabel(dot.transform, marks[i], 40, UiKit.Ink);
UiKit.Label(card.transform, names[i], 38, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -190f), new Vector2(310f, 50f));
UiKit.Label(card.transform, "LV " + lv + "/" + db.Training.MaxLevel + " +" + TrainingRules.Bonus(db, d, t, TrainingTable.Stats[i]).ToString("0.#") + "%", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -232f), new Vector2(310f, 36f), false);
if (max) UiKit.Label(card.transform, "<color=#3a8a30>✔</color>", 48, UiKit.Ok, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(300f, 70f));
else
{
var b = UiKit.Btn("Up", card.transform, TrainingRules.Cost(db, d, t, i).ToString("N0") + " 골드", 30, UiKit.Coin, UiKit.Ink, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(280f, 66f), () =>
{
var why = S.Do(() => TrainingRules.Level(S.Db, S.Save, TrainType, k));
if (why != null) Root.Toast(why);
});
b.interactable = TrainingRules.CanLevel(db, d, t, i) == null;
}
}
if (left == 0 && rank < db.Training.MaxRank)
{
var pb = UiKit.Btn("Promote", p, "랭크 승급 → " + TrainingRules.RankName(db, rank + 1) + " · 골드 " + TrainingRules.PromoteCost(db, d, t).ToString("N0"), 36, UiKit.Red, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 90f), new Vector2(900f, 110f), () =>
{
var why = S.Do(() => TrainingRules.Promote(S.Db, S.Save, TrainType));
Root.Toast(why ?? UnitTypeInfo.Korean(TrainType) + " 랭크 승급 → " + TrainingRules.RankName(S.Db, TrainingRules.Rank(S.Save, TrainType)));
});
pb.interactable = TrainingRules.CanPromote(db, d, t) == null;
}
UiKit.Label(p, UnitTypeInfo.Korean(t) + " 전원 HP +" + TrainingRules.Bonus(db, d, t, "hp").ToString("0.#") + "% · 공 +" + TrainingRules.Bonus(db, d, t, "atk").ToString("0.#") + "% · 방 +" +
TrainingRules.Bonus(db, d, t, "def").ToString("0.#") + "% (3항목 Max → 랭크 승급 · 레벨 리셋 · 랭크 보너스 유지)", 26, UiKit.InkSoft, TextAnchor.MiddleCenter,
new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(1030f, 60f), false);
}
// ---------- ② 순찰 ----------
void BuildPatrol(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var row = PatrolRules.Row(db, d);
float cap = PatrolRules.CapHours(db, d);
float h = PatrolRules.Hours(db, d, S.Now);
var basis = Panel(p, "Basis", 0f, 110f);
UiKit.Label(basis, "기준: 마지막 클리어 챕터 " + (row != null ? row.chapter : 0) + "장", 38, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -8f), new Vector2(900f, 54f));
UiKit.Label(basis, "제" + Mathf.Max(1, d.chapterMax) + "장 · 시간당 수확 · 오프라인 누적", 28, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 8f), new Vector2(900f, 44f), false);
var acc = Panel(p, "Accum", 126f, 170f);
UiKit.Label(acc, "누적 " + FormatHours(h) + " / 최대 " + FormatHours(cap), 40, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -8f), new Vector2(900f, 56f));
var fill = UiKit.Bar("Bar", acc, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -6f), new Vector2(980f, 34f), UiKit.Slot, UiKit.Red);
UiKit.SetBar(fill, cap > 0f ? h / cap : 0f);
UiKit.Label(acc, "성장 패스 24시간(준비 중) · 농업 개발로 +" + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.PatrolCap)) + "분", 26, UiKit.Red, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 8f), new Vector2(900f, 44f), false);
var rate = Panel(p, "Rate", 312f, 400f);
UiKit.Label(rate, "시간당 수확", 38, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -8f), new Vector2(600f, 56f));
string[] rn = { "골드", "병사 경험", "승급 장비", "보물 청동 상자" };
string[] rv =
{
PatrolRules.GoldPerHour(db, d).ToString("N0") + " / 시간",
PatrolRules.ExpPerHour(db, d).ToString("N0") + " / 시간(출전 편성)",
UiKit.GradeKo(PatrolRules.GearGrade(db, d)) + " · " + PatrolRules.GearPerHour(db, d).ToString("0.##") + " / 시간",
PatrolRules.ChestPerHour(db, d).ToString("0.##") + " / 시간",
};
Color[] rc = { UiKit.Coin, new Color(0.8f, 0.48f, 0.38f), UiKit.Ok, new Color(0.7f, 0.48f, 0.3f) };
string[] rIcon = { "cur_gold", "cur_exp", "cur_gear", "chest_bronze" };
for (int i = 0; i < 4; i++)
{
var line = UiKit.Box("Row" + i, rate, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -76f - i * 78f), new Vector2(980f, 68f), UiKit.Dark);
if (UiKit.Icon(line.transform, rIcon[i], new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(12f, 0f), 58f) == null)
UiKit.Dot("Dot", line.transform, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(16f, 0f), 50f, rc[i]);
UiKit.Label(line.transform, rn[i], 34, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(86f, 0f), new Vector2(400f, 56f));
UiKit.Label(line.transform, rv[i], 30, UiKit.Ink, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-20f, 0f), new Vector2(520f, 56f), false);
}
var y = PatrolRules.Preview(db, d, h, 1f);
var got = Panel(p, "Stored", 728f, 220f);
UiKit.Label(got, "쌓인 보상", 38, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -8f), new Vector2(600f, 56f));
string[] sv = { "골드\n" + y.gold.ToString("N0"), "병사 경험\n" + y.exp.ToString("N0"), "승급 장비\n" + y.gear, "청동 상자\n" + y.chests };
string[] sn = { y.gold.ToString("N0"), y.exp.ToString("N0"), y.gear.ToString(), y.chests.ToString() };
for (int i = 0; i < 4; i++)
{
if (UISkin.Has("item_bg") && UISkin.Icon(rIcon[i]) != null)
{
// 쌓인 보상 칸(설계서 §3⑦) = 팩 ItemFrame 5층 + 아이콘 + 수량
var cell = UiKit.Rect("Chip" + i, got, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f + i * 250f, 16f), new Vector2(232f, 130f));
UiKit.ItemSlot(cell, "gear_blue_", true);
UiKit.DropShadow(UiKit.Icon(cell, rIcon[i], new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(14f, 4f), 84f), 4f);
var v = UiKit.Title(UiKit.Label(cell, sn[i], 38, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-16f, 0f), new Vector2(130f, 60f)), true);
v.horizontalOverflow = HorizontalWrapMode.Overflow;
continue;
}
var chip = UiKit.Box("Chip" + i, got, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f + i * 250f, 16f), new Vector2(232f, 130f), UiKit.Dark);
UiKit.Border(chip, rc[i], 3f);
UiKit.FillLabel(chip.transform, sv[i], 30, UiKit.Ink);
}
var hb = UiKit.Btn("Harvest", p, "수확", 64, UiKit.Red, UiKit.White, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -966f), new Vector2(1030f, 140f), () => DoHarvest(false));
hb.interactable = h > 0f;
EconomyRules.RollDaily(d, S.Now);
var ad = UiKit.Btn("Ad", p, "광고 2배 수확\n<size=26>" + d.adsUsed.patrol + "/" + e.PatrolAdDaily + "회/일 (시험)</size>", 36, UiKit.Dark, UiKit.White, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -1124f), new Vector2(505f, 130f), () => DoHarvest(true));
ad.interactable = h > 0f && d.adsUsed.patrol < e.PatrolAdDaily;
UiKit.Btn("Instant", p, "즉시 수확 " + FormatHours(e.PatrolInstantHours) + "분\n<size=26>보옥 " + e.PatrolInstantGem + "</size>", 36, UiKit.Jade, UiKit.White, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -1124f), new Vector2(505f, 130f), () =>
{
string why;
var r = S.InstantHarvest(out why);
if (r == null) Root.Toast(why);
else ShowYield("즉시 수확", r);
});
}
/// <summary>
/// 순찰(Fable 제안서 §4-7 · #1104) — 섹션 「순찰」(기준 줄 + 레벨형 게이지 군량색 + 배지 「7h」) → 「시간당 수확」 스탯 칸 4(아이콘 64 · 라벨 · 값 Sen) →
/// 「쌓인 보상」 타일 4(ItemFrame 132 + 수량 칩) → 수확 Primary 녹 · 광고 2배 Secondary 하늘 + 광고 글리프 · 즉시 Secondary 보라 + 보옥 비용.
/// </summary>
void BuildPatrolKit(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var row = PatrolRules.Row(db, d);
float cap = PatrolRules.CapHours(db, d);
float h = PatrolRules.Hours(db, d, S.Now);
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
const float W = 1032f, Fw = W - 12f, GaugeBar = 800f;
// ① 순찰
RectTransform ar;
var a = UiKit.SectionPanel(p, "Patrol", 24f, 6f, W, 236f, false, false, out ar);
UiKit.SectionTitle(ar, "순찰");
var basis = UiKit.Label(ar, "기준 = 마지막 클리어 " + (row != null ? row.chapter : 0) + "장 · 오프라인 누적", 26, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -12f), new Vector2(620f, 48f), false);
basis.horizontalOverflow = HorizontalWrapMode.Overflow;
Text badge;
var fill = UiKit.Gauge(a, "Gauge", 16f, 66f, GaugeBar, "lv_badge", "grain", out badge);
UiKit.SetGauge(fill, cap > 0f ? h / cap : 0f, GaugeBar);
badge.text = Mathf.FloorToInt(h) + "h";
badge.fontSize = 30;
var cur = UiKit.Title(UiKit.Label(a, FormatHours(h) + " / " + FormatHours(cap), 30, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -164f), new Vector2(260f, 52f)), true);
cur.horizontalOverflow = HorizontalWrapMode.Overflow;
UiKit.Label(a, "성장 패스 24시간(준비 중) · 농업 개발 +" + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.PatrolCap)) + "분", 24, soft, TextAnchor.MiddleLeft, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -168f), new Vector2(Fw - 320f, 44f), false).alignment = TextAnchor.MiddleRight;
// ② 시간당 수확
RectTransform br;
var b = UiKit.SectionPanel(p, "Rate", 24f, 258f, W, 448f, false, false, out br);
UiKit.SectionTitle(br, "시간당 수확");
string[] rn = { "골드", "병사 경험", "승급 장비", "보물 청동 상자" };
string[] rv =
{
PatrolRules.GoldPerHour(db, d).ToString("N0"),
PatrolRules.ExpPerHour(db, d).ToString("N0"),
PatrolRules.GearPerHour(db, d).ToString("0.##"),
PatrolRules.ChestPerHour(db, d).ToString("0.##"),
};
string[] rs = { "/ 시간", "/ 시간 · 출전 편성", "/ 시간 · " + UiKit.GradeKo(PatrolRules.GearGrade(db, d)) + " 등급", "/ 시간" };
string[] rIcon = { "cur_gold", "cur_exp", "cur_gear", "chest_bronze" };
for (int i = 0; i < 4; i++)
{
var cell = UiKit.Skin(UiKit.At(b, "Row" + i, 14f, 76f + i * 90f, Fw - 28f, 82f), "panel12", UiKit.Col("row_on_field", UiKit.Paper)).transform;
var ic = UiKit.Icon(cell, rIcon[i], new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(50f, 0f), 64f);
if (ic != null) UiKit.DropShadow(ic, 3f);
UiKit.Label(cell, rn[i], 30, soft, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(98f, 0f), new Vector2(300f, 60f), false);
var sub = UiKit.Label(cell, rs[i], 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f, -2f), new Vector2(300f, 50f), false);
sub.horizontalOverflow = HorizontalWrapMode.Overflow;
var val = UiKit.Title(UiKit.Label(cell, rv[i], 44, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f - Mathf.Ceil(sub.preferredWidth) - 14f, 2f), new Vector2(260f, 70f)), true);
val.horizontalOverflow = HorizontalWrapMode.Overflow;
}
// ③ 쌓인 보상
var y = PatrolRules.Preview(db, d, h, 1f);
RectTransform cr;
var c = UiKit.SectionPanel(p, "Stored", 24f, 722f, W, 262f, false, false, out cr);
UiKit.SectionTitle(cr, "쌓인 보상");
string[] sn = { y.gold.ToString("N0"), y.exp.ToString("N0"), y.gear.ToString(), y.chests.ToString() };
const float Tile = 132f, Gap = 64f;
float tx0 = (Fw - 4f * Tile - 3f * Gap) * 0.5f;
for (int i = 0; i < 4; i++)
UiKit.RewardTile(c, "Tile" + i, tx0 + i * (Tile + Gap), 90f, Tile, i == 2 ? "gear_" + PatrolRules.GearGrade(db, d) + "_" : "gear_white_", rIcon[i], sn[i], i == 2);
// ④ 버튼
EconomyRules.RollDaily(d, S.Now);
var hb = UiKit.SkinBtn(p, "Harvest", h > 0f ? "btn_l_green" : "btn_gray", UiKit.White, "수확", 52, UiKit.Col("text_on_green", UiKit.White), (1080f - 600f) * 0.5f, 1004f, 600f, 128f, () => DoHarvest(false));
hb.interactable = h > 0f;
bool adOk = h > 0f && d.adsUsed.patrol < e.PatrolAdDaily;
var ad = UiKit.SkinBtn(p, "Ad", adOk ? "btn_sky" : "btn_gray", UiKit.White, "광고 2배", 38, UiKit.White, 24f, 1152f, 505f, 112f, () => DoHarvest(true));
ad.interactable = adOk;
UiKit.BtnCost(ad, "ad", d.adsUsed.patrol + "/" + e.PatrolAdDaily, 34f, 28);
var ins = UiKit.SkinBtn(p, "Instant", "btn_purple", UiKit.White, "즉시 " + FormatHours(e.PatrolInstantHours), 38, UiKit.White, 1056f - 505f, 1152f, 505f, 112f, () =>
{
string why;
var r = S.InstantHarvest(out why);
if (r == null) Root.Toast(why);
else ShowYield("즉시 수확", r);
});
UiKit.BtnCost(ins, "cur_gem", e.PatrolInstantGem.ToString(), 40f, 32);
}
void DoHarvest(bool ad)
{
string why;
var r = S.Harvest(ad, out why);
if (r == null) Root.Toast(why);
else ShowYield(ad ? "광고 2배 수확" : "수확", r);
}
void ShowYield(string title, PatrolYield y)
{
var sb = new System.Text.StringBuilder();
sb.Append(FormatHours(y.hours)).Append(y.mult > 1f ? " × " + y.mult : "").Append(" 분량\n\n");
sb.Append("골드 +").Append(y.gold.ToString("N0")).Append("\n병사 경험 +").Append(y.exp).Append(" (").Append(y.expTo.Count).Append("명)");
if (y.gearIds.Count > 0)
{
var names = new List<string>();
foreach (var g in y.gearIds) names.Add(S.Db.Gear.Get(g).nameKo);
sb.Append("\n장비 ").Append(string.Join(" · ", names.ToArray()));
}
if (y.chestIds.Count > 0) sb.Append("\n").Append(S.Db.Chests.Get(y.chestIds[0]).nameKo).Append(" ×").Append(y.chestIds.Count);
Root.Modal(title, sb.ToString(), new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
public static string FormatHours(float h)
{
int min = Mathf.FloorToInt(h * 60f + 1e-3f);
return (min / 60) + "시간" + (min % 60 > 0 ? " " + (min % 60) + "분" : "");
}
static RectTransform Panel(RectTransform p, string name, float top, float h)
{
var band = UiKit.TopBand(name, p, top, h, 24f, 24f);
var img = UiKit.Paint(band, UiKit.Paper);
UiKit.Border(img, UiKit.Line, 3f);
return band;
}
// ---------- ③ 개발 ----------
void BuildDev(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var branches = db.Dev.Branches;
if (branches.Count == 0) return;
int n = branches.Count;
string br = Branch;
UiKit.Btn("Prev", p, "◀", 44, UiKit.Paper, UiKit.Ink, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(-330f, -6f), new Vector2(100f, 90f), () => SetBranch(branchIdx - 1));
UiKit.Btn("Next", p, "▶", 44, UiKit.Paper, UiKit.Ink, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(330f, -6f), new Vector2(100f, 90f), () => SetBranch(branchIdx + 1));
UiKit.Label(p, BranchKo(branches[(branchIdx + n - 1) % n]), 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(-330f, -96f), new Vector2(140f, 34f), false);
UiKit.Label(p, BranchKo(branches[(branchIdx + 1) % n]), 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(330f, -96f), new Vector2(140f, 34f), false);
var title = UiKit.Box("Branch", p, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -6f), new Vector2(420f, 90f), UiKit.Red);
UiKit.FillLabel(title.transform, BranchKo(br), 48, UiKit.White);
for (int i = 0; i < n; i++) UiKit.Dot("Page" + i, p, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2((i - (n - 1) / 2f) * 30f, -106f), 16f, i == branchIdx ? UiKit.Red : UiKit.Slot);
UiKit.Label(p, BranchKo(br) + " — " + BranchGoal(br), 30, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -136f), new Vector2(900f, 44f), false);
// 깊이(선행 조건) = 줄 · 같은 줄 = 가로로
var nodes = db.Dev.InBranch(br);
var depth = new Dictionary<string, int>();
int maxDepth = 0;
foreach (var nd in nodes)
{
int dp = 0;
foreach (var r in nd.requires)
{
int x;
if (depth.TryGetValue(r.node, out x)) dp = Mathf.Max(dp, x + 1);
}
depth[nd.id] = dp;
maxDepth = Mathf.Max(maxDepth, dp);
}
var tree = UiKit.Fill("Tree", p, 24f, 24f, 190f, 190f);
float rowH = Mathf.Min(190f, 1050f / (maxDepth + 1));
var pos = new Dictionary<string, Vector2>();
for (int dp = 0; dp <= maxDepth; dp++)
{
var row = new List<DevTree.Node>();
foreach (var nd in nodes)
if (depth[nd.id] == dp) row.Add(nd);
for (int i = 0; i < row.Count; i++)
pos[row[i].id] = new Vector2((i - (row.Count - 1) / 2f) * (row.Count > 1 ? 840f / (row.Count - 1) : 0f), -rowH * (dp + 0.5f));
}
foreach (var nd in nodes)
foreach (var r in nd.requires)
if (pos.ContainsKey(r.node)) Line(tree, pos[r.node], pos[nd.id], DevRules.Level(d, r.node) >= r.lv ? new Color(0.45f, 0.36f, 0.3f) : UiKit.Slot);
NodeCount = 0;
foreach (var nd in nodes)
{
var node = nd;
int lv = DevRules.Level(d, nd.id);
bool open = DevRules.Unlocked(d, nd);
bool max = lv >= nd.lvMax;
float size = nd.Big ? Mathf.Min(150f, rowH * 0.78f) : Mathf.Min(110f, rowH * 0.6f);
var c = UiKit.Btn("Node_" + nd.id, tree, "", 30, max ? UiKit.Coin : open ? UiKit.Paper : new Color(0.55f, 0.51f, 0.48f), UiKit.Ink, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), pos[nd.id], new Vector2(size, size), () => NodePopup(node.id));
var img = c.GetComponent<Image>();
img.sprite = UiKit.Circle;
img.type = Image.Type.Simple;
UiKit.Border(img, nd.Big ? new Color(0.78f, 0.62f, 0.25f) : UiKit.Line, nd.Big ? 6f : 3f);
UiKit.LabelOf(c).text = open ? nd.nameKo.Substring(0, 1) : "잠";
UiKit.LabelOf(c).fontSize = nd.Big ? 60 : 44;
UiKit.LabelOf(c).color = open ? (max ? UiKit.White : UiKit.Ink) : UiKit.InkSoft;
var pill = UiKit.Box("Lv", c.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), new Vector2(0f, 0f), new Vector2(110f, 40f), UiKit.Dark);
UiKit.FillLabel(pill.transform, lv + "/" + nd.lvMax, 24, UiKit.White);
float side = pos[nd.id].x <= 0f ? 1f : -1f;
var lab = UiKit.Label(tree, nd.nameKo, 28, open ? UiKit.Ink : UiKit.InkSoft, side > 0 ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight, new Vector2(0.5f, 1f),
new Vector2(side > 0 ? 0f : 1f, 0.5f), pos[nd.id] + new Vector2(side * (size / 2f + 10f), 0f), new Vector2(240f, 50f));
lab.horizontalOverflow = HorizontalWrapMode.Overflow;
NodeCount++;
}
var foot = UiKit.Box("Foot", p, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 16f), new Vector2(1032f, 150f), UiKit.Paper);
UiKit.Border(foot, UiKit.Line, 3f);
UiKit.IconOrDot(foot.transform, "cur_blueprint", "도", new Color(0.33f, 0.45f, 0.62f), new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(20f, 0f), 76f);
UiKit.Label(foot.transform, "도면 ×" + d.inv.blueprint, 38, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(110f, 0f), new Vector2(260f, 60f));
UiKit.IconOrDot(foot.transform, "cur_gold", "골", UiKit.Coin, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(380f, 0f), 76f);
UiKit.Label(foot.transform, d.gold.ToString("N0"), 38, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(470f, 0f), new Vector2(220f, 60f));
UiKit.Btn("Reset", foot.transform, "트리 초기화\n<size=24>보옥 " + db.Economy.DevResetGem + " · 도면 환급</size>", 34, UiKit.Dark, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-16f, 0f), new Vector2(320f, 120f), () =>
{
Root.Modal("트리 초기화", "모든 개발 노드를 0 으로 되돌립니다.\n보옥 " + db.Economy.DevResetGem + " 소모 · 쓴 도면 " + S.Save.dev.blueprintSpent + " 환급 · 골드 환급 없음",
new OutGameRoot.ModalButton("초기화", UiKit.Red, () => Root.Toast(S.Do(() => DevRules.Reset(S.Db, S.Save)) ?? "개발 트리 초기화")),
new OutGameRoot.ModalButton("취소", UiKit.Dark, null));
});
}
static void Line(RectTransform parent, Vector2 a, Vector2 b, Color c)
{
var mid = (a + b) / 2f;
var dir = b - a;
var rt = UiKit.Rect("Link", parent, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), mid, new Vector2(dir.magnitude, 8f));
rt.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg);
UiKit.Paint(rt, c, false);
}
public void NodePopup(string id)
{
var db = S.Db;
var d = S.Save;
var n = db.Dev.Get(id);
if (n == null) return;
int lv = DevRules.Level(d, id);
var sb = new System.Text.StringBuilder();
sb.Append(BranchKo(n.branch)).Append(n.Big ? " · 큰 노드" : "").Append(" Lv ").Append(lv).Append(" / ").Append(n.lvMax).Append("\n\n");
foreach (var k in n.EffectKeys)
{
string what;
DevRules.Keys.TryGetValue(k, out what);
sb.Append(what).Append(" ").Append((n.pctPerLv * lv).ToString("0.##")).Append(" → ").Append((n.pctPerLv * Mathf.Min(n.lvMax, lv + 1)).ToString("0.##")).Append("\n");
}
if (n.requires.Count > 0)
{
sb.Append("\n선행: ");
for (int i = 0; i < n.requires.Count; i++)
{
var r = n.requires[i];
bool ok = DevRules.Level(d, r.node) >= r.lv;
sb.Append(i > 0 ? " · " : "").Append(ok ? "<color=#3a6a30>" : "<color=#a03a22>").Append(db.Dev.Get(r.node).nameKo).Append(" Lv ").Append(r.lv).Append("</color>");
}
sb.Append("\n");
}
if (lv < n.lvMax)
{
int gold, bp;
DevRules.NextCost(n, lv, out gold, out bp);
sb.Append("\n다음 비용 골드 ").Append(gold.ToString("N0")).Append(bp > 0 ? " · 도면 " + bp : "");
}
var why = DevRules.CanUpgrade(db, d, id);
Root.Modal(n.nameKo, sb.ToString(),
new OutGameRoot.ModalButton(why == null ? "개발 Lv+1" : why, why == null ? UiKit.Red : UiKit.Disabled, () =>
{
var w = S.Do(() => DevRules.Upgrade(S.Db, S.Save, id));
if (w == null) NodePopup(id);
else Root.Toast(w);
}, true),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
}
public static string BranchKo(string b)
{
switch (b)
{
case "commerce": return "상업";
case "agri": return "농업";
case "industry": return "공업";
}
return b;
}
static string BranchGoal(string b)
{
switch (b)
{
case "commerce": return "재화 획득 효율";
case "agri": return "자원 획득 효율";
case "industry": return "장비 획득";
}
return "";
}
public override bool OnBack()
{
return false;
}
}
}