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

261 lines
17 KiB
C#
Raw 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 UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 본영 로비(Fable 제안서 §4-1 · PD 2026-11-09 「카피바라GO·탕탕특공대 참고 상업 수준」) — 키아트 + 위 비네트 · 진행 띠(지도 핀 · 장 이름 · 진행 바 n/m) ·
/// 좌우 세로 버튼 열(임무·우편·동맹 / 상점·상자·이벤트 · 미구현 = 잠금 자리) · 출전 편성 5인 · 아래 버튼 행(편성 원 · 출정 3층 · 연무장 원) + 뒤 어두운 띠 · 하단 탭 5.
/// 본문 좌표 = 절대 y − 224.
/// </summary>
public class LobbyScreen : OutGameScreen
{
Text progress;
Text progressNum;
RectTransform progressFill;
Text sortieSub;
Text chestBadge;
RectTransform field;
RectTransform scene; // 배경 그림 좌표 층(그림을 꽉 채움 · null = 배경 없음)
float artW;
float artH;
/// <summary>로비 출전 영웅 표준 키(px · 크기 표 size_px 가 이 높이) — 유비가 예전 칸 맞춤 때와 같은 크기(797×826 틀 → 196 칸 = ×0.246 · size 837 × 0.246 ≈ 206).</summary>
const float LobbyUnitPx = 206f;
static RectTransform At(Transform p, string name, float x, float y, float w, float h)
{
return UiKit.Rect(name, p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x, -y), new Vector2(w, h));
}
public override void Build()
{
Header("본영", "", false);
var outer = Body();
// 배경 = 테마 bg_lobby(관청 마당) · 위치·배율 = 표 lobby_scene.csv view 줄 · 없으면 땅 색 판
// PD 2026-11-11 「로비 화면을 조금 더 상단으로 · 캐릭터가 배경 이미지와 겹치지 않게 · 너무 어둡지 않게 · 배경에 움직이는 연출」
var art = UISkin.Icon("bg_lobby");
scene = null;
if (art != null)
{
var clip = UiKit.Fill("LobbyBg", outer);
clip.gameObject.AddComponent<RectMask2D>();
var view = LobbyScene.First("view");
// 표 좌표의 기준 = 원본 그림 px(view f_w × f_h) — 임포트 크기(최대 크기 제한으로 줄 수 있음)와 무관하게 같은 자리
artW = view != null && view.w > 0f ? view.w : art.rect.width;
artH = view != null && view.h > 0f ? view.h : art.rect.height;
float top = view != null ? view.y : 0f; // 그림의 이 줄(px)이 본문 맨 위
float zoom = view != null && view.scale > 0.01f ? view.scale : 1f; // 화면 폭 대비 배율(가운데 기준)
var art2 = UiKit.Rect("Art", clip, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f - top / Mathf.Max(1f, artH)), Vector2.zero, Vector2.zero);
float pad = (zoom - 1f) * 0.5f;
art2.anchorMin = new Vector2(-pad, 1f);
art2.anchorMax = new Vector2(1f + pad, 1f);
art2.sizeDelta = Vector2.zero;
art2.anchoredPosition = Vector2.zero;
var bimg = art2.gameObject.AddComponent<Image>();
bimg.sprite = art;
bimg.color = UISkin.Col("lobby_tint", Color.white);
bimg.raycastTarget = false;
var fit = bimg.gameObject.AddComponent<AspectRatioFitter>();
fit.aspectMode = AspectRatioFitter.AspectMode.WidthControlsHeight;
fit.aspectRatio = artW / Mathf.Max(1f, artH);
// 장면 층(Frame 캔버스 1장) = 뒤 연출(깃발·불빛) → 영웅(대기 프레임 재생) → 앞 연출(새·꽃잎)
scene = LobbyAmbient.Build(art2, artW, artH).Lineup;
}
// 위 비네트(Glow_Panel01 = 위 불투명 → 아래 투명 · 실측) · 아래 버튼 뒤 어두운 띠
var vg = UiKit.Rect("Vignette", outer, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(0f, 180f));
vg.anchorMin = new Vector2(0f, 1f);
vg.anchorMax = new Vector2(1f, 1f);
vg.sizeDelta = new Vector2(0f, 180f);
UiKit.Skin(vg, "scroll_fade", UiKit.Col("lobby_vignette", Color.clear)).enabled = UISkin.Has("scroll_fade");
var body = UiKit.Rect("Mock", outer, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(OutGameRoot.Reference.x, OutGameRoot.BodyH));
// 아래 버튼 뒤 띠 = 아래 진함 → 위 투명(위 비네트 그림을 뒤집어 씀) — 밝아진 배경에서 띠 위 경계가 직선으로 끊겨 보임(QA r1 M7)
var band = At(body, "BottomBand", -40f, 1180f, 1160f, 386f);
if (UISkin.Has("scroll_fade"))
{
UiKit.Skin(band, "scroll_fade", UiKit.Col("lobby_band", Color.clear));
// 위아래 뒤집기는 가운데 기준(At 의 피벗 = 왼쪽 위 → 그대로 뒤집으면 위로 올라감)
band.pivot = new Vector2(0f, 0.5f);
band.anchoredPosition = new Vector2(-40f, -(1180f + 386f * 0.5f));
band.localScale = new Vector3(1f, -1f, 1f);
}
else UiKit.Skin(band, "card", UiKit.Col("lobby_band", Color.clear));
// 진행 띠(절대 236~312) = 지도 핀 + 장 이름 + 진행 바 n/m
var pb = At(body, "Progress", 24f, 12f, 1032f, 76f);
UiKit.Skin(pb, "card", UiKit.Col("chip_dark", UiKit.Dark));
UiKit.DropShadow(UiKit.Icon(pb, "map_point", new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f, 2f), 50f), 3f);
progress = UiKit.Title(UiKit.Label(pb, "", 34, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(78f, 2f), new Vector2(560f, 60f)), true);
progress.horizontalOverflow = HorizontalWrapMode.Overflow;
var bar = UiKit.Rect("Bar", pb, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-104f, 0f), new Vector2(240f, 26f));
UiKit.Skin(bar, "lv_bar_bg", UiKit.White);
progressFill = UiKit.Rect("Fill", bar, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(4f, 0f), new Vector2(0f, 16f));
UiKit.Skin(progressFill, "lv_fill", UiKit.Col("lv_fill", UiKit.Coin));
progressNum = UiKit.Title(UiKit.Label(pb, "", 26, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-18f, 2f), new Vector2(80f, 50f)), true);
// 좌우 세로 버튼 열(166×150 · 미구현 = 잠금 자리)
SideButton(body, 24f, 106f, "lobby_mission", "임무", true, () => Root.ComingSoon("임무"));
SideButton(body, 24f, 272f, "lobby_mail", "우편", true, () => Root.ComingSoon("우편"));
SideButton(body, 24f, 438f, "lobby_alliance", "동맹", true, () => Root.ComingSoon("동맹", true));
SideButton(body, 890f, 106f, "lobby_shop", "상점", false, () => Root.Trade.OpenShop());
var chest = SideButton(body, 890f, 272f, "lobby_chest", "상자", false, () =>
{
Root.Show(Root.Treasures);
Root.Treasures.SetPage(2);
});
var cb = UiKit.Skin(UiKit.Rect("Alert", chest, new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-12f, -12f), new Vector2(44f, 46f)), "badge_circle", UiKit.Col("alert_red", UiKit.Red));
cb.preserveAspect = true;
chestBadge = UiKit.Title(UiKit.FillLabel(cb.transform, "", 24, UiKit.White), true);
SideButton(body, 890f, 438f, "lobby_event", "이벤트", true, () => Root.ComingSoon("이벤트"));
// 출전 편성 = 배경 마당 위 자리(표 slot 줄 · 그림 좌표) · 배경이 없으면 예전 한 줄 판
if (scene != null) field = scene;
else
{
var ground = UiKit.Fill("Field", body, 24f, 24f, 140f, 520f);
UiKit.Paint(ground, new Color(0.47f, 0.52f, 0.36f));
var shadow = UiKit.Box("Shadow", ground, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 70f), new Vector2(940f, 150f), new Color(0.2f, 0.18f, 0.14f, 0.55f), false);
shadow.sprite = UiKit.Circle;
shadow.type = Image.Type.Simple;
field = UiKit.Fill("Lineup", ground, 20f, 20f, 40f, 40f);
}
// 아래 버튼 행(절대 1500~1740) = 편성 원 · 출정 3층 · 연무장 원
RoundButton(body, 160f, "btn_formation", "편성", () => Root.Show(Root.Formation));
RoundButton(body, 920f, "btn_arena", "연무장", () =>
{
Root.Arena.SetPage(0);
Root.Show(Root.Arena); // 연무장(PD #1093 C)
});
var so = At(body, "Sortie", 310f, 1290f, 460f, 160f);
var soImg = UiKit.Skin(so, "tapered1", UiKit.Red, true);
so.gameObject.AddComponent<Button>().onClick.AddListener(() => Root.OpenWorldMap());
var gl = UiKit.Rect("Gloss", so, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), new Vector2(400f, 70f));
UiKit.Skin(gl, "tapered2", new Color(1f, 1f, 1f, 0.3f)).enabled = UISkin.Has("tapered2");
UiKit.Icon(so, "tapered3", new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(60f, -26f), 24f);
UiKit.Title(UiKit.Label(so, "출정", 64, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 18f), new Vector2(440f, 90f)), true);
var chip = UiKit.Skin(UiKit.Rect("Chip", so, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(300f, 44f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
sortieSub = UiKit.Title(UiKit.FillLabel(chip.transform, "", 26, UiKit.White), true);
}
/// <summary>측면 버튼(팩 Button_Round03_Dark 166×150 · 아이콘 96 · 라벨 28 · 잠금 = 흐림 + 자물쇠).</summary>
Transform SideButton(RectTransform body, float x, float y, string icon, string label, bool locked, System.Action onClick)
{
var b = At(body, "Side_" + label, x, y, 166f, 150f);
UiKit.Skin(b, "btn_round03", UiKit.White, true);
b.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
var ic = UiKit.Icon(b, icon, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), 92f);
if (ic != null)
{
UiKit.DropShadow(ic, 3f);
if (locked) ic.color = UiKit.Col("tab_locked", UiKit.InkSoft);
}
UiKit.Title(UiKit.Label(b, label, 28, locked ? UiKit.InkSoft : UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 4f), new Vector2(160f, 40f)), true);
if (locked) UiKit.Icon(b, "lock_s", new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-24f, -28f), 40f);
return b;
}
/// <summary>원형 버튼(팩 Button_Circle122 124 · 진남 틴트 · 아이콘 72 · 라벨 30 아래).</summary>
void RoundButton(RectTransform body, float cx, string icon, string label, System.Action onClick)
{
var b = At(body, "Round_" + label, cx - 62f, 1290f, 124f, 124f);
var img = UiKit.Skin(b, "circle122", UiKit.Col("menu_btn", UiKit.Dark), true);
img.preserveAspect = true;
b.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
UiKit.DropShadow(UiKit.Icon(b, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), 76f), 3f);
UiKit.Title(UiKit.Label(body, label, 30, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0f, 1f), new Vector2(0.5f, 1f), new Vector2(cx, -1420f), new Vector2(200f, 44f)), true);
}
public override void Refresh()
{
var map = Root.Map;
var cfg = Root.Battle.Config;
float pct = 0f;
if (map != null && map.HasData)
{
int ch = Root.CurrentChapter();
int nodes = map.NodesIn(ch);
int done = WorldProgress.Cleared(ch);
int node = Mathf.Min(done, Mathf.Max(0, nodes - 1));
int si = map.StageIndexOf(ch, node);
var def = cfg.chapters[ch];
progress.text = "제" + def.no + "장 " + def.title;
progressNum.text = Mathf.Min(done, nodes) + "/" + nodes;
pct = nodes > 0 ? Mathf.Clamp01(done / (float)nodes) : 0f;
sortieSub.text = si >= 0 ? "챕터 " + def.no + " · " + def.no + "-" + cfg.stages[si].stage : "챕터 " + def.no;
}
else
{
progress.text = "—";
progressNum.text = "";
sortieSub.text = "챕터 월드맵";
}
progressFill.sizeDelta = new Vector2(232f * pct, progressFill.sizeDelta.y);
progressFill.gameObject.SetActive(pct > 0.01f);
int chests = 0;
foreach (var c in S.Db.Chests.chests) chests += TreasureRules.ChestCount(S.Save, c.id);
chestBadge.text = chests.ToString();
chestBadge.transform.parent.gameObject.SetActive(chests > 0);
UiKit.Clear(field);
var lineup = S.BuildLineup();
if (scene != null)
{
PlaceLineup(lineup, cfg);
return;
}
int n = Mathf.Max(1, lineup.Count);
float w = Mathf.Min(200f, 980f / n);
for (int i = 0; i < lineup.Count; i++)
{
var h = S.Def(lineup[i].id);
float x = (i - (n - 1) / 2f) * w;
// 크기 표 기준으로 발 맞춰 세움(PD 2026-11-10 「로비 관우만 작게 보임」 = 칸에 그림 틀을 맞추면 틀이 넓은 관우만 작아짐) · 일러 없으면 초상
if (UiKit.UnitStand("Hero" + i, field, h, new Vector2(0.5f, 0f), new Vector2(x, 20f), LobbyUnitPx, cfg.mountedArtMaxTop) == null)
UiKit.Portrait("Hero" + i, field, h, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(x, 20f), new Vector2(w, 340f), false, 1f, Color.clear);
}
}
/// <summary>
/// 배경 마당 위 진형(표 slot 줄 · 발 = 그림 px) — 뒷줄(위)부터 그려 앞줄이 앞에 오게 · 자리마다 발밑 그림자(표 shadow 줄) · 뒷줄은 조금 작게(f_scale).
/// PD 2026-11-11 「나란히 일렬로 서 있으면 배경 이미지와 겹칠 수밖에 없음」 → 깃발·무기 거치대·북 사이 빈 돌바닥에 두 줄.
/// </summary>
void PlaceLineup(System.Collections.Generic.List<LineupEntry> lineup, BalanceConfig cfg)
{
var slots = LobbyScene.Slots(lineup.Count);
var order = new System.Collections.Generic.List<int>();
for (int i = 0; i < lineup.Count && i < slots.Count; i++) order.Add(i);
order.Sort((a, b) => slots[a].y.CompareTo(slots[b].y));
// 그림자(표 shadow 줄 여러 개 = 비스듬한 그늘 + 발 닿는 곳 접지 그림자 · QA r2)를 먼저 모두(한 아틀라스 = 한 묶음) → 영웅(뒷줄부터)
// 그림자·영웅을 번갈아 두면 묶음이 끊김(실측 로비 batches 18 → 21)
foreach (var sh in LobbyScene.All("shadow"))
{
var glow = UISkin.Icon(sh.sprite);
foreach (int i in order)
{
var r = slots[i];
float k = r.scale > 0.01f ? r.scale : 1f;
var at = new Vector2(r.x / Mathf.Max(1f, artW), 1f - r.y / Mathf.Max(1f, artH));
var s = UiKit.Rect("Shadow" + i, field, at, 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;
}
}
foreach (int i in order)
{
var r = slots[i];
var h = S.Def(lineup[i].id);
var at = new Vector2(r.x / Mathf.Max(1f, artW), 1f - r.y / Mathf.Max(1f, artH));
float k = r.scale > 0.01f ? r.scale : 1f;
var stand = UiKit.UnitStand("Hero" + i, field, h, at, Vector2.zero, LobbyUnitPx * k, cfg.mountedArtMaxTop);
if (stand == null)
UiKit.Portrait("Hero" + i, field, h, at, new Vector2(0.5f, 0f), Vector2.zero, new Vector2(196f * k, 340f * k), false, 1f, Color.clear);
else if (h != null)
UiFrameAnim.Attach(stand.GetComponent<Image>(), UnitArt.Load(h.art), "front_idle"); // 대기 숨 고르기(2~3 프레임 · 있으면)
}
}
}
}