1000 lines
60 KiB
C#
1000 lines
60 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace TKS.OutGame
|
||
{
|
||
/// <summary>
|
||
/// 내정(하단 탭 · 기획 S9~S11 · PD #1093 C) — 상단 탭 3(한 항목 = 한 페이지) · **PD 2026-11-12 내정 메뉴 개편 = 로비처럼 탭마다 일러 배경 장면 + 화풍 통일**:
|
||
/// ① 훈련 = 연병장 배경 + 고른 병종의 범용 병사 5명이 한 구령에 훈련(원화 대기·자세·타격 클립 · UiDrillAnim) · 병종 탭 5 · 랭크 진행 · 카드 3(HP·공·방) · 3항목 Max → 랭크 승급
|
||
/// ② 순찰 = 저잣거리 배경이 보이게 위(순찰 상태 · 「시간당 수확」 = 누르면 펼치는 드롭다운) · 아래(쌓인 보상 · 버튼 「수확」 「즉시 수확」 2개만) · 광고 2배 = 수확을 누른 뒤 고름
|
||
/// ③ 개발 = 규장각 배경 + 양피지 두루마리 위 성장 트리(갈래 ◀ 상업·농업·공업 ▶ · 선행 조건 깊이로 줄 · 노드 탭 = 강화 팝업) · 도면·골드 · 트리 초기화.
|
||
/// 배경·연출 = 표 Resources/UI/domestic_scene.csv(DomesticScene) · 연출 층 = LobbyAmbient(Frame 1장 · P41).
|
||
/// </summary>
|
||
public class DomesticScreen : OutGameScreen
|
||
{
|
||
public static readonly string[] PageNames = { "훈련", "순찰", "개발" };
|
||
|
||
class Scene
|
||
{
|
||
public List<LobbyScene.Row> rows;
|
||
public LobbyAmbient amb;
|
||
public RectTransform art;
|
||
public float artW;
|
||
public float artH;
|
||
}
|
||
|
||
int page;
|
||
int typeIdx;
|
||
int branchIdx;
|
||
RectTransform backdrop;
|
||
readonly Scene[] scenes = new Scene[3];
|
||
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; }
|
||
|
||
/// <summary>그림 아이콘으로 그린 노드 수(테스트 · 나머지 = 첫 글자).</summary>
|
||
public int IconNodeCount { get; private set; }
|
||
|
||
/// <summary>훈련 장면에 선 병사 수(테스트).</summary>
|
||
public int SoldierCount { get; private set; }
|
||
|
||
/// <summary>그 탭의 배경 장면 층(없으면 null · 테스트).</summary>
|
||
public LobbyAmbient SceneOf(int p)
|
||
{
|
||
return p >= 0 && p < scenes.Length && scenes[p] != null ? scenes[p].amb : null;
|
||
}
|
||
|
||
public override void Build()
|
||
{
|
||
Tab = 2;
|
||
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(배경 그림이 없을 때)
|
||
Header("내정", "훈련 · 순찰 · 개발", true);
|
||
var body = Body();
|
||
backdrop = UiKit.Fill("Backdrop", 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();
|
||
}
|
||
|
||
Scene EnsureScene(int p)
|
||
{
|
||
if (scenes[p] != null) return scenes[p];
|
||
var s = new Scene { rows = DomesticScene.Page(DomesticScene.Pages[p]) };
|
||
s.amb = DomesticScene.Backdrop(backdrop, "Scene_" + DomesticScene.Pages[p], s.rows, out s.art, out s.artW, out s.artH);
|
||
scenes[p] = s;
|
||
return s;
|
||
}
|
||
|
||
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);
|
||
}
|
||
var sc = EnsureScene(page);
|
||
for (int i = 0; i < scenes.Length; i++)
|
||
if (scenes[i] != null && scenes[i].art != null)
|
||
scenes[i].art.parent.gameObject.SetActive(i == page);
|
||
UiKit.Clear(pages[page]);
|
||
if (page == 0) BuildTraining(pages[0], sc);
|
||
else if (page == 1) BuildPatrol(pages[1], sc);
|
||
else BuildDev(pages[2]);
|
||
}
|
||
|
||
/// <summary>배경 위 반투명 판(배경이 비침 · 테마 dom_panel).</summary>
|
||
static RectTransform Glass(Transform p, string name, float x, float y, float w, float h)
|
||
{
|
||
var rt = UiKit.At(p, name, x, y, w, h);
|
||
UiKit.Paint(rt, UiKit.Col("dom_panel", new Color(0.12f, 0.14f, 0.21f, 0.78f)));
|
||
return rt;
|
||
}
|
||
|
||
static Text Stroke(Text t)
|
||
{
|
||
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||
return UiKit.Title(t, true);
|
||
}
|
||
|
||
// ---------- ① 훈련 ----------
|
||
|
||
void BuildTraining(RectTransform p, Scene sc)
|
||
{
|
||
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;
|
||
if (UISkin.On)
|
||
{
|
||
var tb = UiKit.SubTab(p, "Type" + i, UnitTypeInfo.Korean(ut), 24f + i * 208f, 6f, 196f, 78f, () => SetTrainType(ut));
|
||
UiKit.SetSubTab(tb, on);
|
||
// 병종 아이콘(PD 2026-11-12 「보병 = 검 · 창병 = 창 · 기병 = 말 · 궁병 = 활 · 책사 = 깃털 부채」 · 테마 dom_tab_<병종 키>) — 아이콘 왼쪽 · 글자 오른쪽
|
||
var ti = UiKit.Icon(tb.transform, "dom_tab_" + UnitTypeInfo.Key(ut), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-32f, 3f), 62f);
|
||
if (ti != null)
|
||
{
|
||
ti.raycastTarget = false;
|
||
UiKit.DropShadow(ti, 2f);
|
||
var tl = UiKit.LabelOf(tb).rectTransform;
|
||
tl.offsetMin = new Vector2(tl.offsetMin.x + 56f, tl.offsetMin.y);
|
||
}
|
||
}
|
||
else UiKit.Btn("Type" + i, p, UnitTypeInfo.Korean(ut), 36, 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 chip = UiKit.Rect("Rank", p, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -100f), new Vector2(620f, 68f));
|
||
UiKit.Skin(chip, "chip", UiKit.Col("chip_dark", UiKit.Dark));
|
||
Stroke(UiKit.FillLabel(chip, TrainingRules.RankName(db, rank) + " <size=30>랭크 " + rank + "/" + db.Training.MaxRank + "</size>", 40, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
Stroke(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.#") + "%", 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -172f), new Vector2(1000f, 40f), false));
|
||
|
||
// 지휘관 파견 칸(오른쪽 위 · PD 2026-11-12 「훈련에 사용할 영웅을 지정(파견) · 병종 별 지휘관 1명」)
|
||
DispatchSlot(p, DispatchRules.TrainSlot(t), 1056f - 140f, 92f, "지휘관");
|
||
|
||
// 연병장 병사 + 지휘관(배경 장면 · 고른 병종) — 배경이 없으면 예전 병종 원
|
||
if (sc != null && sc.amb != null) PlaceSoldiers(sc, t);
|
||
else
|
||
{
|
||
SoldierCount = 0;
|
||
var face = UiKit.Dot("Face", p, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -460f), 220f, UiKit.TypeColor(t));
|
||
UiKit.FillLabel(face.transform, UnitTypeInfo.Mark(t), 110, UiKit.White);
|
||
}
|
||
|
||
// 랭크 진행(반투명 판): 현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 비용 · 남은 레벨
|
||
// 아래 UI = 페이지 아래끝 쪽으로(PD 2026-11-12 「하단 UI를 좀 더 아래쪽으로」 · 랭크 판 872 · 카드 1016 · 줄 1330)
|
||
const float BarY = 872f, CardY = 1016f, FootY = 1330f;
|
||
var bar = Glass(p, "RankBar", 24f, BarY, 1032f, 132f);
|
||
Stroke(UiKit.Label(bar, "랭크 진행", 28, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -8f), new Vector2(300f, 44f)));
|
||
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, 0f), new Vector2(700f, 26f), new Color(0.32f, 0.28f, 0.35f), UiKit.Coin);
|
||
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 + 700f * v, 0f), 54f, UiKit.Coin);
|
||
UiKit.FillLabel(marker.transform, "▲", 26, UiKit.Dark);
|
||
var r = db.Training.RankOf(rank);
|
||
Stroke(UiKit.Label(bar, TrainingRules.RankName(db, rank) + " · 랭크 보너스 +" + (r != null ? r.rankPct : 0f).ToString("0.#") + "%", 28, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 6f), new Vector2(700f, 44f)));
|
||
string promo = rank >= db.Training.MaxRank ? "최종 랭크" : "다음 랭크 승급 <color=#FFD36A>골드 " + TrainingRules.PromoteCost(db, d, t).ToString("N0") + "</color>";
|
||
Stroke(UiKit.Label(bar, promo, 24, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -8f), new Vector2(440f, 40f), false));
|
||
Stroke(UiKit.Label(bar, left + " <size=22>남은 레벨</size>", 36, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-24f, 6f), new Vector2(260f, 50f)));
|
||
|
||
// 카드 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), -CardY), new Vector2(cw, 300f), max ? UiKit.Purple : UiKit.Paper);
|
||
UiKit.Border(card, UiKit.Line, 3f);
|
||
var head = UiKit.TopBand("Head", card.transform, 0f, 60f);
|
||
UiKit.Paint(head, max ? UiKit.Purple : UiKit.Dark);
|
||
UiKit.FillLabel(head, max ? "Max" : "LV." + lv, 32, max ? UiKit.White : UiKit.Ink);
|
||
// 항목 아이콘 = 하트 · 검 · 갑옷(PD 2026-11-12 「H = 하트 · 공 = 검 · 방 = 갑옷」 · 테마 dom_stat_*) · 없으면 예전 글자 원
|
||
// 받침 = 외곽선 없는 둥근 판 · 세 카드 같은 단색(PD 2026-11-12 「둥근 원형 아웃라인을 쓰지 말고 동일한 색상으로 채워진 백판」 · 테마 dom_icon_plate)
|
||
var dot = UiKit.Rect("Icon", card.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -66f), new Vector2(90f, 76f));
|
||
UiKit.Skin(dot, "soft", UiKit.Col("dom_icon_plate", UiKit.Dark));
|
||
var sic = UiKit.Icon(dot, StatIcons[i], new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 62f); // 받침 안 여백 ≥ 7 px(QA r6 M)
|
||
if (sic != null) UiKit.DropShadow(sic, 2f);
|
||
else UiKit.FillLabel(dot, marks[i], 36, UiKit.Ink);
|
||
// 이름 + 지금 레벨 수치 한 줄(PD 2026-11-12 「레벨 정보가 중복 → 현재 레벨의 수치만」 · 레벨은 위 띠 LV.n 하나)
|
||
var val = UiKit.Label(card.transform, names[i] + " <color=#FFD36A>+" + TrainingRules.Bonus(db, d, t, TrainingTable.Stats[i]).ToString("0.#") + "%</color>", 36, UiKit.Ink, TextAnchor.MiddleCenter,
|
||
new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -168f), new Vector2(316f, 52f));
|
||
val.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||
if (max) UiKit.Label(card.transform, "<color=#3a8a30>✔</color>", 44, UiKit.Ok, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(300f, 62f));
|
||
else
|
||
{
|
||
var b = UiKit.Btn("Up", card.transform, TrainingRules.Cost(db, d, t, i).ToString("N0") + " 골드", 28, UiKit.Coin, UiKit.Ink, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(280f, 62f), () =>
|
||
{
|
||
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"), 34, UiKit.Red, UiKit.White, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -FootY + 8f), new Vector2(900f, 84f), () =>
|
||
{
|
||
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;
|
||
}
|
||
else
|
||
Stroke(UiKit.Label(p, "3항목 Max → 랭크 승급 · 레벨 리셋 · 랭크 보너스 유지", 24, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -FootY - 20f), new Vector2(1030f, 40f), false));
|
||
}
|
||
|
||
/// <summary>훈련 카드 항목 아이콘(HP · 공격 · 방어 = 테마 아이콘 id).</summary>
|
||
static readonly string[] StatIcons = { "dom_stat_hp", "dom_stat_atk", "dom_stat_def" };
|
||
|
||
/// <summary>병종의 범용 병사(heroes.csv is_soldier · 첫 줄 = 그 병종 일러).</summary>
|
||
HeroDb.Hero SoldierOf(UnitType t)
|
||
{
|
||
foreach (var h in S.Db.Heroes.heroes)
|
||
if (h.isSoldier && h.type == t)
|
||
return h;
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 연병장 병사(표 train_slot · 발 = 그림 px) — 그림자·흙먼지(한 아틀라스) 먼저 → 병사(뒷줄부터 · 앞줄이 앞) · 동작 = UiDrillAnim(한 구령 · 뒷줄 f_phase 늦게).
|
||
/// 병사는 장면 층(Frame · LobbyAmbient.Lineup)에 둔다 — 매 프레임 그림이 바뀜(P41).
|
||
/// </summary>
|
||
void PlaceSoldiers(Scene sc, UnitType t)
|
||
{
|
||
var line = sc.amb.Lineup;
|
||
UiKit.Clear(line);
|
||
SoldierCount = 0;
|
||
CommanderShown = false;
|
||
var h = SoldierOf(t);
|
||
var drill = DomesticScene.First(sc.rows, "drill"); // 공통 줄(맨 앞 · 표준 키 f_h)
|
||
var dustRow = DomesticScene.First(sc.rows, "dust");
|
||
var slots = DomesticScene.All(sc.rows, "slot");
|
||
if (h == null || drill == null || slots.Count == 0) return;
|
||
slots.Sort((a, b) => a.y.CompareTo(b.y)); // 뒷줄(위) 먼저
|
||
float stdPx = drill.h > 0f ? drill.h : 190f;
|
||
// 지휘관(파견 영웅 · 병사 뒷줄 위 · PD 2026-11-12) — 그림자는 병사와 한 번에
|
||
var cmdRow = DomesticScene.First(sc.rows, "cmd");
|
||
var cmd = S.Def(DispatchRules.Get(S.Save, DispatchRules.TrainSlot(t)));
|
||
var feet = new List<LobbyScene.Row>(slots);
|
||
if (cmd != null && cmdRow != null) feet.Add(cmdRow);
|
||
Shadows(sc, line, feet);
|
||
if (cmd != null && cmdRow != null)
|
||
CommanderShown = Stand(sc, line, "Commander", cmd, cmdRow, stdPx) != null;
|
||
var dusts = new Image[slots.Count];
|
||
if (dustRow != null)
|
||
{
|
||
var dsp = UISkin.Icon(dustRow.sprite);
|
||
for (int i = 0; i < slots.Count; i++)
|
||
{
|
||
var r = slots[i];
|
||
float k = r.scale > 0.01f ? r.scale : 1f;
|
||
var s = UiKit.Rect("Dust" + i, line, DomesticScene.Anchor(r.x, r.y, sc.artW, sc.artH), new Vector2(0.5f, 0.5f), new Vector2(dustRow.x * k, -dustRow.y * k), new Vector2(dustRow.w * k, dustRow.h * k));
|
||
dusts[i] = s.gameObject.AddComponent<Image>();
|
||
dusts[i].sprite = dsp != null ? dsp : UiKit.Circle;
|
||
dusts[i].color = dustRow.color;
|
||
dusts[i].raycastTarget = false;
|
||
}
|
||
}
|
||
var data = UnitArt.Load(h.art);
|
||
LobbyScene.Row footFix = null; // 그 병종 그림의 뒷발 맞춤(표 train_feet)
|
||
foreach (var fr in DomesticScene.All(sc.rows, "feet"))
|
||
if (fr.sprite == h.art) footFix = fr;
|
||
// 병종 그림별 박자(표 train_drill 중 s_sprite = 그 그림 · 없으면 공통 줄) — PD 2026-11-12 기병 「말을 세운 상태에서 돌진 그림은 빠르게 재생되며 원래 상태로」
|
||
foreach (var dr in DomesticScene.All(sc.rows, "drill"))
|
||
if (dr.sprite == h.art) drill = dr;
|
||
// 타격 때 날아가는 화살(궁병 · 표 train_shot s_sprite = 「아트 id|화살 아이콘」)
|
||
LobbyScene.Row shot = null;
|
||
Sprite shotSp = null;
|
||
foreach (var sr in DomesticScene.All(sc.rows, "shot"))
|
||
{
|
||
var ss = sr.Sprites;
|
||
if (ss.Length >= 2 && ss[0] == h.art)
|
||
{
|
||
shot = sr;
|
||
shotSp = UISkin.Icon(ss[1]);
|
||
}
|
||
}
|
||
for (int i = 0; i < slots.Count; i++)
|
||
{
|
||
var r = slots[i];
|
||
float k = r.scale > 0.01f ? r.scale : 1f;
|
||
var stand = UiKit.UnitStand("Soldier" + i, line, h, DomesticScene.Anchor(r.x, r.y, sc.artW, sc.artH), Vector2.zero, stdPx * k, MountedTop);
|
||
if (stand == null) continue;
|
||
var anim = UiDrillAnim.Attach(stand.GetComponent<Image>(), data, drill, r.phase, k, sc.artW, dusts[i], dustRow, footFix);
|
||
if (anim != null && shot != null) anim.AttachShot(shotSp, shot);
|
||
SoldierCount++;
|
||
}
|
||
}
|
||
|
||
/// <summary>훈련 장면에 지휘관이 섰는지(테스트).</summary>
|
||
public bool CommanderShown { get; private set; }
|
||
|
||
float MountedTop
|
||
{
|
||
get { return Root.Battle != null && Root.Battle.Config != null ? Root.Battle.Config.mountedArtMaxTop : 0f; }
|
||
}
|
||
|
||
/// <summary>발 자리마다 그림자(표 shadow 줄 · 한 아틀라스 = 한 묶음이라 사람보다 먼저 모두).</summary>
|
||
static void Shadows(Scene sc, RectTransform line, List<LobbyScene.Row> feet)
|
||
{
|
||
foreach (var sh in DomesticScene.All(sc.rows, "shadow"))
|
||
{
|
||
var glow = UISkin.Icon(sh.sprite);
|
||
for (int i = 0; i < feet.Count; i++)
|
||
{
|
||
var r = feet[i];
|
||
float k = r.scale > 0.01f ? r.scale : 1f;
|
||
var s = UiKit.Rect("Shadow" + i, line, DomesticScene.Anchor(r.x, r.y, sc.artW, sc.artH), new Vector2(0.5f, 0.5f), new Vector2(sh.x * k, -sh.y * k), new Vector2(sh.w * k, sh.h * k));
|
||
var simg = s.gameObject.AddComponent<Image>();
|
||
simg.sprite = glow != null ? glow : UiKit.Circle;
|
||
simg.color = sh.color;
|
||
simg.raycastTarget = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>장면에 영웅·병사 1명(발 = 표 줄 · 키 = 표준 × f_scale · 대기 숨 고르기 클립).</summary>
|
||
RectTransform Stand(Scene sc, RectTransform line, string name, HeroDb.Hero h, LobbyScene.Row r, float stdPx)
|
||
{
|
||
float k = r.scale > 0.01f ? r.scale : 1f;
|
||
var stand = UiKit.UnitStand(name, line, h, DomesticScene.Anchor(r.x, r.y, sc.artW, sc.artH), Vector2.zero, stdPx * k, MountedTop);
|
||
if (stand != null) UiFrameAnim.Attach(stand.GetComponent<Image>(), UnitArt.Load(h.art), "front_idle");
|
||
return stand;
|
||
}
|
||
|
||
// ---------- ② 순찰 ----------
|
||
|
||
/// <summary>
|
||
/// 순찰(PD 2026-11-12) — 저잣거리 배경이 잘 보이게: 위 = 순찰 상태(게이지) + 「시간당 수확 ▼」 드롭다운(누르면 펼침) · 가운데 = 배경 · 아래 = 쌓인 보상 + 「수확」 「즉시 수확」.
|
||
/// 광고 2배는 수확을 누른 뒤 고르는 팝업(OpenHarvest).
|
||
/// </summary>
|
||
void BuildPatrol(RectTransform p, Scene sc)
|
||
{
|
||
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);
|
||
// 문 앞 순찰 영웅 + 창병 2 + 상인·주민(장면) · 영웅 파견 칸(오른쪽 위) — PD 2026-11-12
|
||
if (sc != null && sc.amb != null) PlacePatrol(sc);
|
||
DispatchSlot(p, DispatchRules.Patrol, 1056f - 140f, 6f, "순찰 영웅");
|
||
// 배치(PD 「저잣거리가 잘 보이게 · 남은 공간 활용」) = UI 를 모두 아래 한 판에 모음 · 위·가운데는 패루·상점·길
|
||
// PD 2026-11-12 예시(정보 판) = 왼쪽 「순찰 · 시계 + 7시간 / 12시간 · 게이지 · 기준」 | 오른쪽 시간당 수확 3줄(늘 보임 · 드롭다운 없앰 · 아이템 = 「전리품」 하나로) / 아래 = 프레임 안 「획득 보상」 + 칸 4
|
||
// 판 아래끝 1228 = 즉시 수확 「N시간 분량」 배지(≈1247) 와 띄움
|
||
const float W = 1032f, PanelH = 384f, PanelY = 1228f - PanelH, LeftW = 560f, GaugeX = 12f, GaugeY = 42f, FrameY = 176f, FrameH = 198f;
|
||
const float GaugeBar = LeftW - GaugeX - 86f - 12f; // 게이지 틀 = 배지 86 + 막대 · 왼쪽 칸 안
|
||
var a = Glass(p, "Patrol", 24f, PanelY, W, PanelH);
|
||
|
||
// ① 왼쪽 = 순찰 상태 — 배지(쌓인 시간) · 「순찰」 · 시계 + 「7시간 / 12시간」 · 게이지 · 기준 줄
|
||
Text badge;
|
||
var fill = UiKit.Gauge(a, "Gauge", GaugeX, GaugeY, GaugeBar, "lv_badge", "grain", out badge);
|
||
UiKit.SetGauge(fill, cap > 0f ? h / cap : 0f, GaugeBar);
|
||
badge.text = Mathf.FloorToInt(h) + "h";
|
||
badge.fontSize = 30;
|
||
float barX = GaugeX + 86f; // 막대 왼쪽 끝 = 제목 시작
|
||
Stroke(UiKit.Label(a, "순찰", 34, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(barX + 8f, -4f), new Vector2(160f, 50f)));
|
||
var tt = Stroke(UiKit.Label(a, FormatHours(h) + " / " + FormatHours(cap), 30, UiKit.White, TextAnchor.MiddleRight, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(LeftW - 12f, -4f), new Vector2(300f, 50f)));
|
||
// 시간 앞 시계(PD 2026-11-12 「시간 정보 앞에는 시계 아이콘」 · 테마 clock · 없으면 글자만)
|
||
var ck = UiKit.Icon(a, "clock", new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(LeftW - 12f - Mathf.Ceil(tt.preferredWidth) - 26f, -29f), 42f);
|
||
if (ck != null)
|
||
{
|
||
ck.name = "Clock";
|
||
UiKit.DropShadow(ck, 3f);
|
||
}
|
||
var basis = UiKit.Label(a, "기준 = 마지막 클리어 " + (row != null ? row.chapter : 0) + "장 · 오프라인 누적", 22, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(26f, -(GaugeY + 98f)), new Vector2(LeftW - 30f, 34f), false);
|
||
basis.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||
Stroke(basis);
|
||
|
||
// ② 오른쪽 = 시간당 수확(간략 · 3줄) — 용어 = 골드 · 경험치 · 전리품(장비·상자 구분 없이 합) · 값 = 「획득량 / 시간」만 오른쪽 정렬(PD 2026-11-12)
|
||
var div = UiKit.At(a, "Divider", LeftW + 6f, 16f, 2f, FrameY - 30f);
|
||
UiKit.Paint(div, new Color(1f, 1f, 1f, 0.18f));
|
||
float infoW = W - LeftW - 36f;
|
||
var info = UiKit.At(a, "RateInfo", LeftW + 22f, 10f, infoW, FrameY - 18f);
|
||
string[] rIcon = { "cur_gold", "cur_exp", "cur_gear" };
|
||
string[] rn = { "골드", "경험치", "전리품" };
|
||
string[] rv =
|
||
{
|
||
PatrolRules.GoldPerHour(db, d).ToString("N0"),
|
||
PatrolRules.ExpPerHour(db, d).ToString("N0"),
|
||
(PatrolRules.GearPerHour(db, d) + PatrolRules.ChestPerHour(db, d)).ToString("0.##"),
|
||
};
|
||
for (int i = 0; i < rn.Length; i++)
|
||
{
|
||
var r = UiKit.At(info, "Rate" + i, 0f, i * 52f, infoW, 50f);
|
||
var ic = UiKit.Icon(r, rIcon[i], new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(24f, 0f), 44f);
|
||
if (ic != null) UiKit.DropShadow(ic, 3f);
|
||
Stroke(UiKit.Label(r, rn[i], 27, UiKit.Col("dom_rate_name", new Color(0.91f, 0.86f, 0.77f)), TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(56f, 0f), new Vector2(150f, 50f)));
|
||
var v = Stroke(UiKit.Label(r, rv[i] + "<size=20> / 시간</size>", 28, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-6f, 0f), new Vector2(infoW - 210f, 50f)));
|
||
v.name = "PerHour";
|
||
}
|
||
|
||
// ③ 아래 = 획득 보상 — 프레임(테마 dom_reward_frame · 9-slice = 네 모서리 장식 그대로 · 없으면 프레임 없이) 안 왼쪽 글자 + 칸 4
|
||
var y = PatrolRules.Preview(db, d, h, 1f);
|
||
var fsp = UISkin.Icon("dom_reward_frame");
|
||
if (fsp != null)
|
||
{
|
||
var fr = UiKit.At(a, "RewardFrame", 10f, FrameY, W - 20f, FrameH);
|
||
var fimg = fr.gameObject.AddComponent<Image>();
|
||
fimg.sprite = fsp;
|
||
fimg.raycastTarget = false;
|
||
if (fsp.border != Vector4.zero)
|
||
{
|
||
fimg.type = Image.Type.Sliced;
|
||
fimg.pixelsPerUnitMultiplier = 2f; // 테두리(모서리 매듭·막대)를 그림의 절반 크기로 = 얇은 금테(PD 예시)
|
||
}
|
||
}
|
||
Stroke(UiKit.Label(a, "획득\n보상", 30, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(50f, -(FrameY + FrameH * 0.5f)), new Vector2(104f, 90f)));
|
||
RewardRow(a, y, db, d, 164f, W - 10f - 36f - 164f, FrameY + 30f, 128f); // 칸 위 30 · 수량 칩 아래 ≈ 8 = 금테 막대 안쪽(QA r9 M⑥ 칩이 막대에 붙음)
|
||
|
||
// ④ 버튼 2개(PD 2026-11-12 「순찰 메뉴에는 수확과 즉시 수확만」)
|
||
var hb = UiKit.SkinBtn(p, "Harvest", h > 0f ? "btn_l_green" : "btn_gray", UiKit.White, "수확", 52, UiKit.Col("text_on_green", UiKit.White), 24f, 1270f, 505f, 128f, OpenHarvest);
|
||
hb.interactable = h > 0f;
|
||
var ins = UiKit.SkinBtn(p, "Instant", "btn_purple", UiKit.White, "즉시 수확", 44, UiKit.White, 1056f - 505f, 1270f, 505f, 128f, () =>
|
||
{
|
||
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);
|
||
// 즉시 수확 분량 = 버튼 위끝에 걸친 배지(칩)
|
||
var hc = UiKit.Skin(UiKit.Rect("Hours", ins.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -2f), new Vector2(200f, 42f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
|
||
Stroke(UiKit.FillLabel(hc.transform, FormatHours(e.PatrolInstantHours) + " 분량", 24, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
}
|
||
|
||
/// <summary>순찰 장면에 선 NPC 수 · 창병 수 · 영웅 여부(테스트).</summary>
|
||
public int NpcCount { get; private set; }
|
||
public int GuardCount { get; private set; }
|
||
public bool PatrolHeroShown { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 저잣거리 장면(PD 2026-11-12 「순찰 영웅은 문 앞에서 창병 2기를 양쪽에 두고 대기 자세」 · 「상인 및 주민 NPC 등이 보이게」) —
|
||
/// 표 patrol_hero · patrol_guard · patrol_npc 줄(그림 px) · 그림자 먼저 → 위(뒤)에서 아래(앞) 순서. NPC 는 움직이지 않는 그림.
|
||
/// </summary>
|
||
void PlacePatrol(Scene sc)
|
||
{
|
||
var line = sc.amb.Lineup;
|
||
UiKit.Clear(line);
|
||
NpcCount = GuardCount = 0;
|
||
PatrolHeroShown = false;
|
||
var unit = DomesticScene.First(sc.rows, "unit");
|
||
float stdPx = unit != null && unit.h > 0f ? unit.h : 180f;
|
||
var heroRow = DomesticScene.First(sc.rows, "hero");
|
||
var hero = S.Def(DispatchRules.Get(S.Save, DispatchRules.Patrol));
|
||
var guards = DomesticScene.All(sc.rows, "guard");
|
||
var npcs = DomesticScene.All(sc.rows, "npc");
|
||
var people = new List<LobbyScene.Row>(guards);
|
||
if (hero != null && heroRow != null) people.Add(heroRow);
|
||
people.AddRange(npcs);
|
||
Shadows(sc, line, people);
|
||
people.Sort((a, b) => a.y.CompareTo(b.y)); // 위(뒤) 먼저
|
||
foreach (var r in people)
|
||
{
|
||
if (r.kind == "npc")
|
||
{
|
||
// 상인·주민 = 유닛 아트(Resources/Units/npc_*) 대기 숨 고르기(유닛과 같은 R22 클립 · PD 2026-11-12 「NPC 들이 유닛 대기 모션처럼」) · 키 = 표 f_h(그림 몸 높이 기준)
|
||
var data = UnitArt.Load(r.sprite);
|
||
Sprite[] fr;
|
||
if (data == null || data.Meta.pivot == null || data.Meta.pivot.Length < 2 || !data.Clips.TryGetValue("front_idle", out fr) || fr.Length == 0) continue;
|
||
float hh = r.h > 0f ? r.h : 150f;
|
||
float k = hh / Mathf.Max(1f, data.BodyPx);
|
||
var rt = UiKit.Rect("Npc_" + r.sprite, line, DomesticScene.Anchor(r.x, r.y, sc.artW, sc.artH), new Vector2(0.5f, data.Meta.pivot[1]), Vector2.zero, fr[0].rect.size * k);
|
||
var img = rt.gameObject.AddComponent<Image>();
|
||
img.sprite = fr[0];
|
||
img.raycastTarget = false;
|
||
if (r.n == 1) rt.localScale = new Vector3(-1f, 1f, 1f);
|
||
UiFrameAnim.Attach(img, data, "front_idle");
|
||
NpcCount++;
|
||
}
|
||
else if (r.kind == "guard")
|
||
{
|
||
var g = S.Def(r.sprite);
|
||
if (g != null && Stand(sc, line, "Guard" + r.slot, g, r, stdPx) != null) GuardCount++;
|
||
}
|
||
else if (hero != null && Stand(sc, line, "PatrolHero", hero, r, stdPx) != null) PatrolHeroShown = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>쌓인 보상 타일 4(골드 · 병사 경험 · 승급 장비 · 청동 상자) — x ~ x+w 안 가운데 정렬.</summary>
|
||
static void RewardRow(Transform c, PatrolYield y, OutGameData db, SaveData d, float x, float w, float top, float tile)
|
||
{
|
||
string[] rIcon = { "cur_gold", "cur_exp", "cur_gear", "chest_bronze" };
|
||
string[] sn = { y.gold.ToString("N0"), y.exp.ToString("N0"), y.gear.ToString(), y.chests.ToString() };
|
||
float gap = Mathf.Min(72f, (w - 4f * tile - 140f) / 3f);
|
||
float x0 = x + (w - 4f * tile - 3f * gap) * 0.5f;
|
||
for (int i = 0; i < 4; i++)
|
||
UiKit.RewardTile(c, "Tile" + i, x0 + i * (tile + gap), top, tile, i == 2 ? "gear_" + PatrolRules.GearGrade(db, d) + "_" : "gear_white_", rIcon[i], sn[i], i == 2);
|
||
}
|
||
|
||
/// <summary>수확 팝업(PD 2026-11-12 「광고 2배 보상은 수확 버튼을 눌렀을 때 선택」) — 쌓인 보상 + 「수확」 · 「광고 2배」(하루 n/m회).</summary>
|
||
public void OpenHarvest()
|
||
{
|
||
var db = S.Db;
|
||
var d = S.Save;
|
||
var e = db.Economy;
|
||
float h = PatrolRules.Hours(db, d, S.Now);
|
||
if (h <= 0f)
|
||
{
|
||
Root.Toast("쌓인 보상이 없습니다");
|
||
return;
|
||
}
|
||
EconomyRules.RollDaily(d, S.Now);
|
||
var content = Root.ModalCustom("수확", 700f);
|
||
var panel = content.parent;
|
||
UiKit.Label(content, FormatHours(h) + " 분량", 36, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -4f), new Vector2(800f, 54f));
|
||
RewardRow(content, PatrolRules.Preview(db, d, h, 1f), db, d, 0f, 860f, 76f, 132f);
|
||
bool adOk = d.adsUsed.patrol < e.PatrolAdDaily;
|
||
UiKit.Label(content, "광고를 보면 보상 2배 · 오늘 " + d.adsUsed.patrol + "/" + e.PatrolAdDaily + "회", 28, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -250f), new Vector2(800f, 44f), false);
|
||
var hb = UiKit.SkinBtn(panel, "Take", "btn_l_green", UiKit.White, "수확", 44, UiKit.Col("text_on_green", UiKit.White), 40f, 700f - 150f, 420f, 116f, () =>
|
||
{
|
||
Root.CloseModal();
|
||
DoHarvest(false);
|
||
});
|
||
var ad = UiKit.SkinBtn(panel, "Ad", adOk ? "btn_sky" : "btn_gray", UiKit.White, "광고 2배", 40, UiKit.White, 940f - 40f - 420f, 700f - 150f, 420f, 116f, () =>
|
||
{
|
||
Root.CloseModal();
|
||
DoHarvest(true);
|
||
});
|
||
ad.interactable = adOk;
|
||
UiKit.BtnCost(ad, "ad", d.adsUsed.patrol + "/" + e.PatrolAdDaily, 34f, 28);
|
||
hb.name = "Take";
|
||
}
|
||
|
||
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");
|
||
// 용어 = 골드 · 경험치 · 전리품(PD 2026-11-12 「3가지로 용어를 통일」) · 전리품 = 받은 장비·상자 이름
|
||
sb.Append("골드 +").Append(y.gold.ToString("N0")).Append("\n경험치 +").Append(y.exp).Append(" (").Append(y.expTo.Count).Append("명)");
|
||
var loot = new List<string>();
|
||
foreach (var g in y.gearIds) loot.Add(S.Db.Gear.Get(g).nameKo);
|
||
if (y.chestIds.Count > 0) loot.Add(S.Db.Chests.Get(y.chestIds[0]).nameKo + " ×" + y.chestIds.Count);
|
||
if (loot.Count > 0) sb.Append("\n전리품 ").Append(string.Join(" · ", loot.ToArray()));
|
||
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) + "분" : "");
|
||
}
|
||
|
||
// ---------- 영웅 파견(훈련 지휘관 · 순찰 · 개발 · PD 2026-11-12) ----------
|
||
|
||
/// <summary>파견 칸(왼쪽 위 기준 140×150) — 파견됨 = 영웅 초상 원 + 이름표 · 비어 있음 = 「+」 · 누르면 고르기 창.</summary>
|
||
void DispatchSlot(Transform p, string slot, float x, float y, string caption)
|
||
{
|
||
var box = UiKit.At(p, "Dispatch", x, y, 140f, 150f);
|
||
var hit = box.gameObject.AddComponent<Image>();
|
||
hit.color = new Color(1f, 1f, 1f, 0f);
|
||
hit.raycastTarget = true;
|
||
box.gameObject.AddComponent<Button>().onClick.AddListener(() => OpenDispatch(slot));
|
||
var h = S.Def(DispatchRules.Get(S.Save, slot));
|
||
if (h != null) UiKit.Portrait("Face", box, h, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(116f, 116f), true, 2.1f, UiKit.Coin);
|
||
else
|
||
{
|
||
var c = UiKit.Rect("Empty", box, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(116f, 116f));
|
||
var ci = UiKit.Skin(c, "circle122", UiKit.Col("menu_btn", UiKit.Dark));
|
||
ci.preserveAspect = true;
|
||
Stroke(UiKit.FillLabel(c, "+", 64, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
}
|
||
var tag = UiKit.Skin(UiKit.Rect("Tag", box, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(150f, 40f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
|
||
Stroke(UiKit.FillLabel(tag.transform, h != null ? h.nameKo : caption, 24, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
}
|
||
|
||
/// <summary>파견 고르기 창 — 보유 영웅(훈련 = 그 병종만) 초상 격자 · 다른 자리에 있으면 「○○ 중」 표시(고르면 옮김) · 해제 · 닫기.</summary>
|
||
public void OpenDispatch(string slot)
|
||
{
|
||
var db = S.Db;
|
||
var d = S.Save;
|
||
var list = DispatchRules.Candidates(db, d, slot);
|
||
string cur = DispatchRules.Get(d, slot);
|
||
string title = slot == DispatchRules.Patrol ? "순찰 영웅 파견" : slot == DispatchRules.Dev ? "개발 영웅 배치" : DispatchRules.SlotKo(slot) + " 지휘관 파견";
|
||
var content = Root.ModalCustom(title, 1180f);
|
||
var panel = content.parent;
|
||
DispatchChoices = list.Count;
|
||
if (list.Count == 0)
|
||
UiKit.FillLabel(content, slot.StartsWith("train:") ? "이 병종의 보유 영웅이 없습니다" : "보유 영웅이 없습니다", 34, UiKit.Ink, TextAnchor.MiddleCenter, true, 0f);
|
||
else
|
||
{
|
||
ScrollRect sr;
|
||
var grid = UiKit.ScrollGrid("Pick", content, 0f, 0f, 0f, 0f, new Vector2(196f, 236f), new Vector2(16f, 16f), 4, out sr);
|
||
foreach (var hh in list)
|
||
{
|
||
var h = hh;
|
||
var cell = new GameObject("Hero_" + h.id, typeof(RectTransform)).GetComponent<RectTransform>();
|
||
cell.SetParent(grid, false);
|
||
var ci = cell.gameObject.AddComponent<Image>();
|
||
ci.color = new Color(1f, 1f, 1f, 0f);
|
||
ci.raycastTarget = true;
|
||
cell.gameObject.AddComponent<Button>().onClick.AddListener(() =>
|
||
{
|
||
var why = S.Do(() => DispatchRules.Assign(S.Db, S.Save, slot, h.id));
|
||
if (why != null) Root.Toast(why);
|
||
else Root.CloseModal();
|
||
});
|
||
UiKit.Portrait("Face", cell, h, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -4f), new Vector2(172f, 172f), true, 2.1f, h.id == cur ? UiKit.Coin : UiKit.Line);
|
||
Stroke(UiKit.Label(cell, h.nameKo, 28, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(196f, 40f)));
|
||
var at = DispatchRules.Where(d, h.id);
|
||
if (at != null)
|
||
{
|
||
var chip = UiKit.Skin(UiKit.Rect("Where", cell, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -150f), new Vector2(170f, 36f)), "chip", at == slot ? UiKit.Coin : UiKit.Col("chip_dark", UiKit.Dark));
|
||
Stroke(UiKit.FillLabel(chip.transform, at == slot ? "배치 중" : DispatchRules.SlotKo(at) + " 중", 22, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
}
|
||
}
|
||
}
|
||
if (!string.IsNullOrEmpty(cur))
|
||
UiKit.SkinBtn(panel, "Clear", "btn_gray", UiKit.White, "해제", 38, UiKit.White, 40f, 1180f - 142f, 420f, 108f, () =>
|
||
{
|
||
S.Do(() => DispatchRules.Clear(S.Save, slot));
|
||
Root.CloseModal();
|
||
});
|
||
UiKit.SkinBtn(panel, "Close", "btn_sky", UiKit.White, "닫기", 38, UiKit.White, 940f - 40f - 420f, 1180f - 142f, 420f, 108f, Root.CloseModal);
|
||
}
|
||
|
||
/// <summary>마지막으로 연 고르기 창의 후보 수(테스트).</summary>
|
||
public int DispatchChoices { get; private set; }
|
||
|
||
// ---------- ③ 개발 ----------
|
||
|
||
/// <summary>
|
||
/// 개발(PD 2026-11-12 「규장각 같은 연구 장소를 배경 · 양피지나 서적 내용에 성장 트리」 + 같은 날 개편 「재화 정보 판·글자 줄 제거 → 그 자리에 아이콘·재화 · 양피지 좌우 넓힘 · 트리 초기화 = 아래 두루마리」 · 「개발 메뉴에도 영웅 배치」) —
|
||
/// 두루마리(dom_scroll · 좌우 9-slice = 금 마개 그대로 가운데만 늘림) 위 막대 = 갈래 이름 ◀ ▶ · 종이 = 쪽 점 · 도면·골드 · 트리(먹 선 = 노드 가운데끼리 · 이름 = 노드 옆) · 아래 막대 = 트리 초기화 · 왼쪽 아래 = 배치 영웅.
|
||
/// 종이 사각 = 표 dev_paper(원본 px · 실측) · 그림이 없으면 같은 자리에 종이색 판.
|
||
/// </summary>
|
||
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;
|
||
var ink = UiKit.Col("dom_ink", new Color(0.35f, 0.23f, 0.13f));
|
||
var inkSoft = UiKit.Col("dom_ink_soft", new Color(0.54f, 0.42f, 0.30f));
|
||
|
||
// 두루마리(표 dev_scroll · 화면 폭 f_x × 높이 f_y · 좌우 테두리 = 원본 px 그대로)
|
||
var sc = scenes[2] != null ? scenes[2].rows : DomesticScene.Page("dev");
|
||
var scrollRow = DomesticScene.First(sc, "scroll");
|
||
var paperRow = DomesticScene.First(sc, "paper");
|
||
var sp = scrollRow != null ? UISkin.Icon(scrollRow.sprite) : null;
|
||
float SH = scrollRow != null && scrollRow.y > 0f ? scrollRow.y : 1290f;
|
||
float bw = scrollRow != null && scrollRow.w > 0f ? scrollRow.w : (sp != null ? sp.rect.width : 1f);
|
||
float bh = scrollRow != null && scrollRow.h > 0f ? scrollRow.h : (sp != null ? sp.rect.height : 1f);
|
||
float ry = SH / Mathf.Max(1f, bh);
|
||
float sw = scrollRow != null && scrollRow.x > 0f ? scrollRow.x : SH * bw / Mathf.Max(1f, bh);
|
||
var scroll = UiKit.Rect("Scroll", p, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(sw, SH));
|
||
Rect paper; // 두루마리 안 종이(왼쪽 위 기준 · 아래로 +)
|
||
if (sp != null && paperRow != null)
|
||
{
|
||
var simg = scroll.gameObject.AddComponent<Image>();
|
||
simg.sprite = sp;
|
||
simg.raycastTarget = false;
|
||
// 좌우 테두리(spriteBorder) 가 있으면 가운데만 늘림 — 종이 왼끝 = 원본 px 그대로 · 오른끝 = 화면 폭 − (원본 폭 − 원본 오른끝)
|
||
float bl = 0f, brd = 0f;
|
||
if (sp.border.x > 0f || sp.border.z > 0f)
|
||
{
|
||
simg.type = Image.Type.Sliced;
|
||
float k = sp.rect.height / Mathf.Max(1f, bh); // 임포트 px ÷ 원본 px
|
||
simg.pixelsPerUnitMultiplier = k / Mathf.Max(0.01f, ry); // 테두리 = 원본 px × 세로 배율
|
||
bl = sp.border.x / k * ry;
|
||
brd = sp.border.z / k * ry;
|
||
}
|
||
float px0 = paperRow.x * ry, px1 = sw - (bw - (paperRow.x + paperRow.w)) * ry;
|
||
if (bl <= 0f) // 테두리 없음 = 비율대로
|
||
{
|
||
float rx = sw / Mathf.Max(1f, bw);
|
||
px0 = paperRow.x * rx;
|
||
px1 = (paperRow.x + paperRow.w) * rx;
|
||
}
|
||
paper = new Rect(px0, paperRow.y * ry, px1 - px0, paperRow.h * ry);
|
||
}
|
||
else
|
||
{
|
||
UiKit.Paint(scroll, new Color(0.97f, 0.91f, 0.78f));
|
||
paper = new Rect(40f, 140f, sw - 80f, SH - 200f);
|
||
}
|
||
|
||
// 위 막대 = 갈래 이름 + ◀ ▶ · 아래 막대 = 트리 초기화
|
||
float rollY = Mathf.Max(60f, paper.y * 0.46f);
|
||
Stroke(UiKit.Label(scroll, BranchKo(br), 50, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -rollY), new Vector2(360f, 70f)));
|
||
ArrowBtn(scroll, "Prev", "◀", -250f, rollY, () => SetBranch(branchIdx - 1));
|
||
ArrowBtn(scroll, "Next", "▶", 250f, rollY, () => SetBranch(branchIdx + 1));
|
||
float rollB = paper.yMax + (SH - paper.yMax) * 0.5f; // 아래 막대 가운데
|
||
// 아래 막대 = [영웅 배치 칸(다른 탭과 같은 파견 칸 · PD 2026-11-12)] [트리 초기화] — 둘을 막대 가운데에 나란히(왼쪽 아래 영웅 상반신과 안 겹치게 조금 오른쪽)
|
||
// PD 2026-11-12 「배치 영웅의 위치를 좌측으로 조금 더 · 초기화 버튼은 중앙 정렬」 = 버튼 = 막대 가운데 · 파견 칸 = 왼쪽(금 마개 안쪽)
|
||
const float ResetW = 320f;
|
||
DispatchSlot(scroll, DispatchRules.Dev, sw * 0.15f, rollB - 75f, "영웅 배치");
|
||
DevHeroShown = S.Def(DispatchRules.Get(d, DispatchRules.Dev)) != null;
|
||
var rb = UiKit.SkinBtn(scroll, "Reset", "btn_purple", UiKit.White, "트리 초기화", 30, UiKit.White, (sw - ResetW) * 0.5f, rollB - 46f, ResetW, 92f, () =>
|
||
{
|
||
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));
|
||
});
|
||
UiKit.BtnCost(rb, "cur_gem", db.Economy.DevResetGem.ToString(), 34f, 26);
|
||
|
||
// 종이 머리: 쪽 점 + 도면·골드(예전 갈래 설명 글자 줄 자리 · PD 2026-11-12)
|
||
var pr = UiKit.At(scroll, "Paper", paper.x, paper.y, paper.width, paper.height);
|
||
for (int i = 0; i < n; i++) UiKit.Dot("Page" + i, pr, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2((i - (n - 1) / 2f) * 30f, -22f), 16f, i == branchIdx ? UiKit.Red : inkSoft);
|
||
// 재화 = 양피지 색 둥근 받침(단색 · 짙은 색 금지 · PD 2026-11-12) 2개 · 받침 사이 넓게
|
||
var cur = UiKit.Rect("Currency", pr, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -36f), new Vector2(paper.width, 58f));
|
||
var hl = cur.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.childAlignment = TextAnchor.MiddleCenter;
|
||
hl.spacing = 170f; // PD 2026-11-12 「재화 간 간격을 더 넓히고」 2회(56 → 120 → 170)
|
||
hl.childControlWidth = true;
|
||
hl.childControlHeight = false;
|
||
hl.childForceExpandWidth = hl.childForceExpandHeight = false;
|
||
CurrencyItem(cur, "cur_blueprint", "도", new Color(0.33f, 0.45f, 0.62f), "도면 ×" + d.inv.blueprint, ink);
|
||
CurrencyItem(cur, "cur_gold", "골", UiKit.Coin, d.gold.ToString("N0"), ink);
|
||
|
||
// 깊이(선행 조건) = 줄 · 같은 줄 = 가로로
|
||
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);
|
||
}
|
||
const float TreeTop = 100f, TreeBottom = 12f;
|
||
var tree = UiKit.Fill("Tree", pr, 0f, 0f, TreeTop, TreeBottom);
|
||
float th = paper.height - TreeTop - TreeBottom;
|
||
float rowH = th / (maxDepth + 1);
|
||
// 이름 글자 없음(PD 2026-11-12 「텍스트는 제거 · 대신 아이콘 크기를 키우고 가로 1열에 아이콘을 놓을 수 있게」 · 이름 = 노드를 누르면 팝업) —
|
||
// 한 줄 = 종이 폭에 고르게(2개 = 가운데 ± 종이 28 % · 더 많으면 종이 88 % 안에 나눔)
|
||
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);
|
||
float step = row.Count > 1 ? Mathf.Min(paper.width * 0.56f, paper.width * 0.88f / (row.Count - 1)) : 0f;
|
||
for (int i = 0; i < row.Count; i++)
|
||
pos[row[i].id] = new Vector2((i - (row.Count - 1) / 2f) * step, -rowH * (dp + 0.5f));
|
||
}
|
||
float big = Mathf.Min(132f, rowH * 0.80f), small = Mathf.Min(112f, rowH * 0.70f);
|
||
// 선 = 노드 가운데끼리(끝은 노드 원이 덮음 · 줄 사이에 글자가 없어 끊기지 않음 · QA r2 B2)
|
||
// 두 줄 이상 건너는 선 = 꺾은선(위 노드에서 곧장 아래 → 아래 노드 높이에서 옆으로 진입) — 사선이 가운데 줄 노드·알약을 스치지 않게(QA r3 F1)
|
||
foreach (var nd in nodes)
|
||
foreach (var r in nd.requires)
|
||
{
|
||
if (!pos.ContainsKey(r.node)) continue;
|
||
var a = pos[r.node];
|
||
var b = pos[nd.id];
|
||
var col = DevRules.Level(d, r.node) >= r.lv ? ink : new Color(inkSoft.r, inkSoft.g, inkSoft.b, 0.55f);
|
||
if (depth[nd.id] - depth[r.node] > 1 && Mathf.Abs(a.x - b.x) > 1f)
|
||
{
|
||
var corner = new Vector2(a.x, b.y);
|
||
Line(tree, a, corner + new Vector2(0f, -3f), col);
|
||
Line(tree, corner + new Vector2(a.x > b.x ? 3f : -3f, 0f), b, col);
|
||
}
|
||
else Line(tree, a, b, col);
|
||
}
|
||
NodeCount = 0;
|
||
IconNodeCount = 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 ? big : small;
|
||
var face = max ? UiKit.Coin : open ? new Color(1f, 0.97f, 0.88f) : new Color(0.80f, 0.74f, 0.63f);
|
||
var c = UiKit.Btn("Node_" + nd.id, tree, "", 30, face, 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.55f, 0.18f) : ink, nd.Big ? 6f : 4f);
|
||
var lab = UiKit.LabelOf(c);
|
||
// 노드 그림 아이콘(테마 dev_<node_id> · PD 2026-11-12 「노드마다 그림 아이콘」) · 잠김 = 흐리게 + 자물쇠 · 그림이 없으면 예전 첫 글자
|
||
var ic = UiKit.Icon(c.transform, "dev_" + nd.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), size * 0.74f);
|
||
if (ic != null)
|
||
{
|
||
lab.text = "";
|
||
ic.raycastTarget = false;
|
||
if (!open)
|
||
{
|
||
ic.color = UiKit.Col("dom_node_locked", new Color(0.62f, 0.58f, 0.52f, 0.85f));
|
||
var lk = UiKit.Icon(c.transform, "lock_s", new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-size * 0.12f, -size * 0.12f), size * 0.34f);
|
||
if (lk != null) lk.raycastTarget = false;
|
||
}
|
||
IconNodeCount++;
|
||
}
|
||
else
|
||
{
|
||
lab.text = open ? nd.nameKo.Substring(0, 1) : "잠";
|
||
lab.fontSize = nd.Big ? 56 : 46;
|
||
lab.color = open ? ink : inkSoft;
|
||
}
|
||
var pill = UiKit.Box("Lv", c.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(100f, 32f), max ? UiKit.Purple : UiKit.Dark);
|
||
Stroke(UiKit.FillLabel(pill.transform, lv + "/" + nd.lvMax, 22, UiKit.White));
|
||
NodeCount++;
|
||
}
|
||
|
||
// 개발 배치 영웅 = 아래 두루마리 파견 칸에만(왼쪽 아래 큰 상반신은 PD 2026-11-12 「거대한 유닛 얼굴은 제거」)
|
||
}
|
||
|
||
/// <summary>재화 받침 1개(테마 chip 스킨 · 색 dom_chip = 양피지보다 조금 짙은 황갈 · 아이콘 + 먹 글자 · 폭 = 내용에 맞춤).</summary>
|
||
static void CurrencyItem(RectTransform row, string icon, string mark, Color dot, string text, Color ink)
|
||
{
|
||
var pill = UiKit.Rect("Pill_" + icon, row, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(10f, 58f));
|
||
UiKit.Skin(pill, "soft", UiKit.Col("dom_chip", new Color(0.90f, 0.80f, 0.60f))); // 외곽선 없는 둥근 판(PD 2026-11-12 「백판 아웃라인 제거」 · 예전 chip = 테두리 있음)
|
||
var hl = pill.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.childAlignment = TextAnchor.MiddleCenter;
|
||
hl.spacing = 10f;
|
||
hl.padding = new RectOffset(14, 26, 0, 0);
|
||
hl.childControlWidth = true;
|
||
hl.childControlHeight = false;
|
||
hl.childForceExpandWidth = hl.childForceExpandHeight = false;
|
||
var ic = UiKit.IconOrDot(pill, icon, mark, dot, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 46f);
|
||
if (ic != null)
|
||
{
|
||
var le = ic.gameObject.AddComponent<LayoutElement>();
|
||
le.minWidth = le.preferredWidth = 46f;
|
||
}
|
||
var t = UiKit.Label(pill, text, 32, ink, TextAnchor.MiddleLeft, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(10f, 50f));
|
||
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||
}
|
||
|
||
/// <summary>개발 파견 칸에 배치 영웅이 보이는지(테스트 · 왼쪽 아래 큰 상반신은 PD 2026-11-12 지시로 없앰).</summary>
|
||
public bool DevHeroShown { get; private set; }
|
||
|
||
static void ArrowBtn(RectTransform scroll, string name, string mark, float x, float y, System.Action onClick)
|
||
{
|
||
var b = UiKit.Rect(name, scroll, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(x, -y), new Vector2(84f, 84f));
|
||
var img = UiKit.Skin(b, "circle122", UiKit.Col("menu_btn", UiKit.Dark), true);
|
||
img.preserveAspect = true;
|
||
b.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
|
||
Stroke(UiKit.FillLabel(b, mark, 36, UiKit.White, TextAnchor.MiddleCenter, true, 0f));
|
||
}
|
||
|
||
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, 6f));
|
||
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 "";
|
||
}
|
||
}
|
||
}
|