2026-11-05 12:11:10 +00:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TKS.OutGame
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// 아웃게임 UI 도우미 — uGUI + Malgun Gothic(TextUtil) · 기준 1080×1920 · 배치 = 확정 목업(img03·05·06·07·08·11·13·14·16·17).
|
|
|
|
|
|
/// 부품·색 = UI 테마(PD #1100 · Layer Lab GUI Pro-SuperCasual 팩 · UISkin) · 테마가 없으면 기존 도형 와이어프레임.
|
2026-11-05 12:11:10 +00:00
|
|
|
|
/// 위치 인자는 전부 「부모 안 기준점(anchor) · 피벗 · 기준점에서의 거리 · 크기」(UI px).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class UiKit
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
// 팔레트 = 테마 색 id(ui_theme.csv) · 괄호 값 = 테마가 없을 때(구 와이어프레임)
|
|
|
|
|
|
public static Color Bg { get { return UISkin.Col("bg", new Color(0.95f, 0.92f, 0.85f)); } }
|
|
|
|
|
|
public static Color Paper { get { return UISkin.Col("paper", new Color(0.99f, 0.97f, 0.92f)); } }
|
|
|
|
|
|
public static Color Header { get { return UISkin.Col("header", new Color(0.92f, 0.87f, 0.78f)); } }
|
|
|
|
|
|
public static Color HeaderText { get { return UISkin.Col("header_text", Ink); } }
|
|
|
|
|
|
public static Color HeaderSub { get { return UISkin.Col("header_sub", InkSoft); } }
|
|
|
|
|
|
public static Color Ink { get { return UISkin.Col("ink", new Color(0.17f, 0.12f, 0.10f)); } }
|
|
|
|
|
|
public static Color InkSoft { get { return UISkin.Col("ink_soft", new Color(0.45f, 0.39f, 0.34f)); } }
|
|
|
|
|
|
public static Color Dark { get { return UISkin.Col("dark", new Color(0.21f, 0.16f, 0.14f)); } }
|
|
|
|
|
|
public static Color Red { get { return UISkin.Col("red", new Color(0.74f, 0.26f, 0.19f)); } }
|
|
|
|
|
|
public static Color Line { get { return UISkin.Col("line", new Color(0.33f, 0.26f, 0.22f)); } }
|
|
|
|
|
|
public static Color Slot { get { return UISkin.Col("slot", new Color(0.80f, 0.76f, 0.70f)); } }
|
|
|
|
|
|
public static Color Coin { get { return UISkin.Col("coin", new Color(0.88f, 0.67f, 0.20f)); } }
|
|
|
|
|
|
public static Color Jade { get { return UISkin.Col("jade", new Color(0.27f, 0.60f, 0.52f)); } }
|
|
|
|
|
|
public static Color Grain { get { return UISkin.Col("grain", new Color(0.76f, 0.58f, 0.38f)); } }
|
|
|
|
|
|
public static Color Ok { get { return UISkin.Col("ok", new Color(0.36f, 0.62f, 0.31f)); } }
|
|
|
|
|
|
public static Color Star { get { return UISkin.Col("star", new Color(0.93f, 0.68f, 0.13f)); } }
|
|
|
|
|
|
public static Color Disabled { get { return UISkin.Col("disabled", new Color(0.66f, 0.62f, 0.58f)); } }
|
|
|
|
|
|
public static Color Purple { get { return UISkin.Col("purple", new Color(0.58f, 0.36f, 0.70f)); } }
|
|
|
|
|
|
public static Color BarColor { get { return UISkin.Col("bar", new Color(0.15f, 0.11f, 0.09f)); } }
|
|
|
|
|
|
public static Color Stroke { get { return UISkin.Col("stroke", new Color(0.12f, 0.1f, 0.1f)); } }
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static readonly Color White = Color.white;
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
public static Color Col(string id, Color fallback)
|
|
|
|
|
|
{
|
|
|
|
|
|
return UISkin.Col(id, fallback);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
static Sprite round;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>둥근 모서리 9-slice(모서리 22 px).</summary>
|
|
|
|
|
|
public static Sprite Round
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (round == null) round = MakeRound(64, 22);
|
|
|
|
|
|
return round;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Sprite Circle
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return ProceduralSprites.Circle; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static Sprite MakeRound(int size, int radius)
|
|
|
|
|
|
{
|
|
|
|
|
|
var tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
|
|
|
|
|
|
tex.wrapMode = TextureWrapMode.Clamp;
|
|
|
|
|
|
tex.hideFlags = HideFlags.DontSave;
|
|
|
|
|
|
var px = new Color[size * size];
|
|
|
|
|
|
for (int y = 0; y < size; y++)
|
|
|
|
|
|
for (int x = 0; x < size; x++)
|
|
|
|
|
|
{
|
|
|
|
|
|
float cx = Mathf.Clamp(x + 0.5f, radius, size - radius);
|
|
|
|
|
|
float cy = Mathf.Clamp(y + 0.5f, radius, size - radius);
|
|
|
|
|
|
float d = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy));
|
|
|
|
|
|
px[y * size + x] = new Color(1f, 1f, 1f, Mathf.Clamp01(radius - d + 0.5f));
|
|
|
|
|
|
}
|
|
|
|
|
|
tex.SetPixels(px);
|
|
|
|
|
|
tex.Apply();
|
|
|
|
|
|
float b = radius + 2;
|
|
|
|
|
|
var sp = Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect, new Vector4(b, b, b, b));
|
|
|
|
|
|
sp.hideFlags = HideFlags.DontSave;
|
|
|
|
|
|
return sp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 기본 ----------
|
|
|
|
|
|
|
|
|
|
|
|
public static RectTransform Rect(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size)
|
|
|
|
|
|
{
|
|
|
|
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
|
|
|
|
go.transform.SetParent(parent, false);
|
|
|
|
|
|
var rt = (RectTransform)go.transform;
|
|
|
|
|
|
rt.anchorMin = rt.anchorMax = anchor;
|
|
|
|
|
|
rt.pivot = pivot;
|
|
|
|
|
|
rt.anchoredPosition = pos;
|
|
|
|
|
|
rt.sizeDelta = size;
|
|
|
|
|
|
return rt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>부모 전체에서 안쪽 여백(왼·오·위·아래)만 뺀 영역.</summary>
|
|
|
|
|
|
public static RectTransform Fill(string name, Transform parent, float l = 0f, float r = 0f, float t = 0f, float b = 0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
|
|
|
|
go.transform.SetParent(parent, false);
|
|
|
|
|
|
var rt = (RectTransform)go.transform;
|
|
|
|
|
|
rt.anchorMin = Vector2.zero;
|
|
|
|
|
|
rt.anchorMax = Vector2.one;
|
|
|
|
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
|
|
|
|
rt.offsetMin = new Vector2(l, b);
|
|
|
|
|
|
rt.offsetMax = new Vector2(-r, -t);
|
|
|
|
|
|
return rt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>위쪽 띠(부모 가로 전체 · 위에서 top 아래 · 높이 h).</summary>
|
|
|
|
|
|
public static RectTransform TopBand(string name, Transform parent, float top, float h, float l = 0f, float r = 0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
|
|
|
|
go.transform.SetParent(parent, false);
|
|
|
|
|
|
var rt = (RectTransform)go.transform;
|
|
|
|
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
|
|
|
|
rt.anchorMax = new Vector2(1f, 1f);
|
|
|
|
|
|
rt.pivot = new Vector2(0.5f, 1f);
|
|
|
|
|
|
rt.offsetMin = new Vector2(l, -top - h);
|
|
|
|
|
|
rt.offsetMax = new Vector2(-r, -top);
|
|
|
|
|
|
return rt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 칠하기 — rounded = 팩 카드(흰 바탕 + 굵은 외곽선 · 색 틴트 · 반투명이면 외곽선 없는 판) · false = 평면 띠.
|
|
|
|
|
|
/// </summary>
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static Image Paint(RectTransform rt, Color c, bool rounded = true, bool raycast = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|
|
|
|
|
img.color = c;
|
2026-11-06 14:20:57 +00:00
|
|
|
|
if (rounded && !UISkin.Apply(img, c.a >= 0.95f ? "card" : "soft"))
|
|
|
|
|
|
{
|
|
|
|
|
|
img.sprite = Round;
|
|
|
|
|
|
img.type = Image.Type.Sliced;
|
|
|
|
|
|
}
|
|
|
|
|
|
img.raycastTarget = raycast;
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>스킨 id 로 칠하기(없으면 둥근 도형).</summary>
|
|
|
|
|
|
public static Image Skin(RectTransform rt, string skin, Color c, bool raycast = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|
|
|
|
|
img.color = c;
|
|
|
|
|
|
if (!UISkin.Apply(img, skin))
|
2026-11-05 12:11:10 +00:00
|
|
|
|
{
|
|
|
|
|
|
img.sprite = Round;
|
|
|
|
|
|
img.type = Image.Type.Sliced;
|
|
|
|
|
|
}
|
|
|
|
|
|
img.raycastTarget = raycast;
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 03:20:32 +00:00
|
|
|
|
/// <summary>부모를 채우는 스킨 층(안쪽 여백 왼·위·오·아래 · 음수 = 바깥) · 스킨이 없으면 꺼 둔다(UI 상업용 재설계 #1104).</summary>
|
|
|
|
|
|
public static Image Layer(Transform p, string name, string skin, Color c, float l = 0f, float t = 0f, float r = 0f, float b = 0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = Skin(Fill(name, p, l, r, t, b), skin, c);
|
|
|
|
|
|
img.enabled = UISkin.Has(skin);
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 08:07:21 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 섹션 패널(Fable 제안서 §3-2 · §5-1) = 그림자(아래 6) → 프레임(#3F4262) → 필드(안쪽 6 · #262840 · 마스크) [→ 문양 타일 5 %] [→ 바닥 그라데이션] → 윗면 광택 h14.
|
|
|
|
|
|
/// 위치 = 부모 왼쪽 위 기준(x, y 아래로) · 반환 = 필드(내용은 필드 자식 · 필드 모양으로 잘림 · 잘리면 안 되는 내용은 패널 루트에).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static RectTransform SectionPanel(Transform parent, string name, float x, float y, float w, float h, bool pattern, bool ground, out RectTransform root)
|
|
|
|
|
|
{
|
|
|
|
|
|
root = Rect(name, parent, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x, -y), new Vector2(w, h));
|
|
|
|
|
|
Layer(root, "Shadow", "panel_frame", Col("panel_shadow", new Color(0f, 0f, 0f, 0.35f)), 0f, 6f, 0f, -6f);
|
|
|
|
|
|
Layer(root, "Frame", "panel_frame", Col("panel_frame", Paper));
|
|
|
|
|
|
var field = Layer(root, "Field", "panel_field", Col("panel_field", Dark), 6f, 6f, 6f, 6f);
|
|
|
|
|
|
field.gameObject.AddComponent<Mask>().showMaskGraphic = true;
|
|
|
|
|
|
if (pattern && UISkin.Has("bg_pattern"))
|
|
|
|
|
|
{
|
|
|
|
|
|
var pat = Layer(field.transform, "Pattern", "bg_pattern", Col("panel_pattern", Color.clear));
|
|
|
|
|
|
pat.type = Image.Type.Tiled;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ground && UISkin.Has("scroll_fade"))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 스프라이트 = 위 불투명 → 아래 투명(실측) → 뒤집어 바닥이 어둡게
|
2026-11-09 08:16:44 +00:00
|
|
|
|
var g = Skin(Rect("Ground", field.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(0f, 100f)), "scroll_fade", Col("panel_ground", Color.clear));
|
2026-11-09 08:07:21 +00:00
|
|
|
|
g.rectTransform.anchorMin = new Vector2(0f, 0f);
|
|
|
|
|
|
g.rectTransform.anchorMax = new Vector2(1f, 0f);
|
2026-11-09 08:16:44 +00:00
|
|
|
|
g.rectTransform.sizeDelta = new Vector2(0f, 100f);
|
2026-11-09 08:07:21 +00:00
|
|
|
|
g.rectTransform.localScale = new Vector3(1f, -1f, 1f);
|
|
|
|
|
|
g.rectTransform.pivot = new Vector2(0.5f, 1f);
|
|
|
|
|
|
g.rectTransform.anchoredPosition = Vector2.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
var lrt = Rect("Light", root, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -8f), new Vector2(0f, 14f));
|
|
|
|
|
|
lrt.anchorMin = new Vector2(0f, 1f);
|
|
|
|
|
|
lrt.anchorMax = new Vector2(1f, 1f);
|
|
|
|
|
|
lrt.sizeDelta = new Vector2(-24f, 14f);
|
|
|
|
|
|
lrt.anchoredPosition = new Vector2(0f, -8f);
|
|
|
|
|
|
Skin(lrt, "panel_light", Col("panel_light", Color.clear)).enabled = UISkin.Has("panel_light");
|
|
|
|
|
|
return (RectTransform)field.transform;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>섹션 제목 띠(팩 Title_Tapered02 · 패널 왼쪽 위 모서리 · 360×69 · 글자 36 흰 외곽선 · 왼쪽 여백 36).</summary>
|
|
|
|
|
|
public static RectTransform SectionTitle(Transform panelRoot, string text, float w = 360f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var band = Rect("Title", panelRoot, new Vector2(0f, 1f), new Vector2(0f, 1f), Vector2.zero, new Vector2(w, 69f));
|
|
|
|
|
|
if (UISkin.Has("band_title")) Skin(band, "band_title", Col("band_title", Dark));
|
|
|
|
|
|
var t = Title(Label(band, text, 36, White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(36f, 2f), new Vector2(w - 60f, 60f)), true);
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return band;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>스크롤 위·아래 페이드(필드색 → 투명) — 목록이 패널 안에서 잘리는 느낌을 지움.</summary>
|
|
|
|
|
|
public static void ScrollFades(Transform parent, float top, float bottom, float h = 40f, float side = 12f)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!UISkin.Has("scroll_fade")) return;
|
|
|
|
|
|
var c = Col("panel_field", Dark);
|
|
|
|
|
|
var a = Skin(Rect("FadeTop", parent, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -top), new Vector2(0f, h)), "scroll_fade", c);
|
|
|
|
|
|
a.rectTransform.anchorMin = new Vector2(0f, 1f);
|
|
|
|
|
|
a.rectTransform.anchorMax = new Vector2(1f, 1f);
|
|
|
|
|
|
a.rectTransform.sizeDelta = new Vector2(-2f * side, h);
|
|
|
|
|
|
var b = Skin(Rect("FadeBottom", parent, new Vector2(0.5f, 0f), new Vector2(0.5f, 1f), new Vector2(0f, bottom), new Vector2(0f, h)), "scroll_fade", c);
|
|
|
|
|
|
b.rectTransform.anchorMin = new Vector2(0f, 0f);
|
|
|
|
|
|
b.rectTransform.anchorMax = new Vector2(1f, 0f);
|
|
|
|
|
|
b.rectTransform.sizeDelta = new Vector2(-2f * side, h);
|
|
|
|
|
|
b.rectTransform.localScale = new Vector3(1f, -1f, 1f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 03:20:32 +00:00
|
|
|
|
/// <summary>아이템 칸 5층(팩 ItemFrame00~03 · 색 = prefix + bg · deco · light · glow · 테두리 gear_border) — 장비·보물·보상 칸 공용.</summary>
|
|
|
|
|
|
public static void ItemSlot(Transform cell, string prefix, bool lit)
|
|
|
|
|
|
{
|
|
|
|
|
|
Layer(cell, "Glow", "item_glow", lit ? Col(prefix + "glow", White) : Color.clear, -8f, -8f, -8f, -8f);
|
|
|
|
|
|
Layer(cell, "Bg", "item_bg", Col(prefix + "bg", White));
|
|
|
|
|
|
Layer(cell, "Deco", "item_deco", Col(prefix + "deco", White), 4f, 4f, 4f, 4f);
|
|
|
|
|
|
var light = Layer(cell, "Light", "item_light", Col(prefix + "light", White), 8f, 6f, 8f, 0f);
|
|
|
|
|
|
light.rectTransform.anchorMin = new Vector2(0f, 0.54f); // 윗면 빛 = 칸 높이 46 %(목업)
|
|
|
|
|
|
Layer(cell, "Border", "item_border", Col("gear_border", Line));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>아이콘·글자 그림자(테마 icon_shadow).</summary>
|
|
|
|
|
|
public static void DropShadow(Graphic g, float d)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (g == null) return;
|
|
|
|
|
|
var s = g.gameObject.AddComponent<Shadow>();
|
|
|
|
|
|
s.effectColor = Col("icon_shadow", new Color(0f, 0f, 0f, 0.35f));
|
|
|
|
|
|
s.effectDistance = new Vector2(0f, -d);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 09:03:43 +00:00
|
|
|
|
// ---------- 공용 부품 ②(Fable 제안서 §4~§5 · 나머지 화면 #1104) ----------
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>부모 왼쪽 위 기준 사각(x 오른쪽 · y 아래로).</summary>
|
|
|
|
|
|
public static RectTransform At(Transform p, string name, float x, float y, float w, float h)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Rect(name, p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x, -y), new Vector2(w, h));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 스킨 버튼(§5-5 버튼 위계 · Primary btn_l_yellow·btn_l_green·btn_l_red · Secondary btn_sky·btn_s_green·btn_purple · Tertiary round08) —
|
|
|
|
|
|
/// 틀 + 틴트 + 글자(제목 폰트 · 밝은 글자만 외곽선 · 팩 버튼 아래 턱만큼 올림) · 왼쪽 위 기준.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static Button SkinBtn(Transform p, string name, string skin, Color tint, string label, int size, Color fg, float x, float y, float w, float h, Action onClick)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = Skin(At(p, name, x, y, w, h), skin, tint, true);
|
|
|
|
|
|
var b = img.gameObject.AddComponent<Button>();
|
|
|
|
|
|
var cb = b.colors;
|
|
|
|
|
|
cb.disabledColor = new Color(0.75f, 0.75f, 0.75f, 0.85f);
|
|
|
|
|
|
b.colors = cb;
|
|
|
|
|
|
if (onClick != null) b.onClick.AddListener(() => onClick());
|
|
|
|
|
|
var t = Title(FillLabel(img.transform, label ?? "", size, fg, TextAnchor.MiddleCenter, true, 0f), fg.r + fg.g + fg.b > 2.4f);
|
|
|
|
|
|
t.rectTransform.offsetMin = new Vector2(14f, Mathf.Min(12f, h * 0.1f));
|
|
|
|
|
|
t.rectTransform.offsetMax = new Vector2(-14f, 0f);
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>버튼 글자 옆 비용(§5-5 「비용 = 아이콘 + 숫자 Sen」) — 글자 · 아이콘 · 숫자를 가운데 한 줄로 · 반환 = 숫자 글자.</summary>
|
|
|
|
|
|
public static Text BtnCost(Button b, string icon, string num, float iconSize, int numSize)
|
|
|
|
|
|
{
|
|
|
|
|
|
var label = LabelOf(b);
|
|
|
|
|
|
var lrt = label.rectTransform;
|
|
|
|
|
|
var row = Fill("Row", b.transform, lrt.offsetMin.x, -lrt.offsetMax.x, 0f, lrt.offsetMin.y);
|
|
|
|
|
|
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
|
|
|
|
hl.childAlignment = TextAnchor.MiddleCenter;
|
|
|
|
|
|
hl.spacing = 8f;
|
|
|
|
|
|
hl.childControlWidth = true; // 글자 = 선호 폭 · 아이콘 = LayoutElement 폭
|
|
|
|
|
|
hl.childControlHeight = false;
|
|
|
|
|
|
hl.childForceExpandWidth = hl.childForceExpandHeight = false;
|
|
|
|
|
|
label.transform.SetParent(row, false);
|
|
|
|
|
|
float rh = row.rect.height > 1f ? row.rect.height : 80f;
|
|
|
|
|
|
label.rectTransform.anchorMin = label.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
|
|
|
|
|
label.rectTransform.sizeDelta = new Vector2(Mathf.Ceil(label.preferredWidth), rh);
|
|
|
|
|
|
var ic = Icon(row, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, iconSize);
|
|
|
|
|
|
if (ic != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
DropShadow(ic, 3f);
|
|
|
|
|
|
var le = ic.gameObject.AddComponent<LayoutElement>();
|
|
|
|
|
|
le.minWidth = le.preferredWidth = iconSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
var n = Title(Label(row, num, numSize, label.color, TextAnchor.MiddleLeft, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(80f, rh)), label.color.r + label.color.g + label.color.b > 2.4f);
|
|
|
|
|
|
n.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>서브탭(§5-5 · Button_Round10 + 윗면 광택 · 선택 tab_focus / 꺼짐 subtab_off · 32 흰 외곽선) · 왼쪽 위 기준.</summary>
|
|
|
|
|
|
public static Button SubTab(Transform p, string name, string label, float x, float y, float w, float h, Action onClick)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = Skin(At(p, name, x, y, w, h), UISkin.Has("subtab") ? "subtab" : "button", Col("subtab_off", Paper), true);
|
|
|
|
|
|
var b = img.gameObject.AddComponent<Button>();
|
|
|
|
|
|
if (onClick != null) b.onClick.AddListener(() => onClick());
|
|
|
|
|
|
Title(FillLabel(img.transform, label, 32, White, TextAnchor.MiddleCenter, true, 8f), true);
|
|
|
|
|
|
return b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void SetSubTab(Button b, bool on)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.GetComponent<Image>().color = on ? Col("tab_focus", Red) : Col("subtab_off", Paper);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>레벨형 게이지(§4-4 · Slider_Level02 바탕 + 채움 틴트 + 왼쪽 배지 · 영웅 상세 수치) · 반환 = 채움(SetGauge) · 배지 글자 = out.</summary>
|
|
|
|
|
|
public static RectTransform Gauge(Transform p, string name, float x, float y, float w, string badge, string fillColor, out Text badgeText)
|
|
|
|
|
|
{
|
|
|
|
|
|
var root = At(p, name, x, y, w + 86f, 96f);
|
|
|
|
|
|
Skin(At(root, "Bg", 86f, 21f, w, 54f), "lv_bg", White);
|
|
|
|
|
|
var fill = At(root, "Fill", 96f, 27f, w - 20f, 38f);
|
|
|
|
|
|
Skin(fill, "lv_fill", Col(fillColor, Coin));
|
|
|
|
|
|
var bi = Icon(root, badge, new Vector2(0f, 1f), new Vector2(0f, 1f), Vector2.zero, 96f);
|
|
|
|
|
|
if (bi != null) DropShadow(bi, 3f);
|
|
|
|
|
|
badgeText = Title(Label(root, "", 34, White, TextAnchor.MiddleCenter, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(46f, -46f), new Vector2(92f, 60f)), true);
|
|
|
|
|
|
badgeText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return fill;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void SetGauge(RectTransform fill, float v, float w)
|
|
|
|
|
|
{
|
|
|
|
|
|
v = Mathf.Clamp01(v);
|
|
|
|
|
|
fill.sizeDelta = new Vector2(Mathf.Max(18f, (w - 20f) * v), fill.sizeDelta.y);
|
|
|
|
|
|
fill.gameObject.SetActive(v > 0.001f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>정보 칩(§5-6 · Oval46 chip_dark · 좌 아이콘 + 글자 Sen) · 왼쪽 위 기준 · 반환 = 글자.</summary>
|
|
|
|
|
|
public static Text InfoChip(Transform p, string name, float x, float y, float w, float h, string icon, string text, int size)
|
|
|
|
|
|
{
|
|
|
|
|
|
var chip = Skin(At(p, name, x, y, w, h), "chip", Col("chip_dark", Dark));
|
|
|
|
|
|
float lx = 20f;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(icon))
|
|
|
|
|
|
{
|
|
|
|
|
|
float s = Mathf.Min(h + 6f, 48f);
|
|
|
|
|
|
var ic = Icon(chip.transform, icon, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(h * 0.5f + 2f, 0f), s);
|
|
|
|
|
|
if (ic != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
DropShadow(ic, 3f);
|
|
|
|
|
|
lx = h + 8f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-09 09:23:18 +00:00
|
|
|
|
var t = Title(Label(chip.transform, text, size, White, lx > 20f ? TextAnchor.MiddleLeft : TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(lx, 2f), new Vector2(w - lx - 16f, h)), true);
|
2026-11-09 09:03:43 +00:00
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>수량 칩(§4-2 · 칸 오른쪽 아래에 걸친 Oval46 · 글자 Sen 흰 외곽선) · 폭 = 글자에 맞춤.</summary>
|
|
|
|
|
|
public static Text QtyChip(Transform cell, string qty, int size)
|
|
|
|
|
|
{
|
|
|
|
|
|
float h = size + 12f;
|
|
|
|
|
|
var chip = Skin(Rect("Qty", cell, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(8f, -8f), new Vector2(60f, h)), "chip", Col("chip_dark", Dark));
|
2026-11-09 09:12:53 +00:00
|
|
|
|
var t = Title(FillLabel(chip.transform, qty.Replace("×", "x"), size, White, TextAnchor.MiddleCenter, true, 0f), true); // Sen 에 × 글리프 없음
|
2026-11-09 09:03:43 +00:00
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
chip.rectTransform.sizeDelta = new Vector2(Mathf.Max(h + 8f, Mathf.Ceil(t.preferredWidth) + 30f), h);
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>보상 타일(§4-2 · ItemFrame 5층 + 아이콘 68 % + 수량 칩) · prefix = 칸 색(gear_white_ · gear_<등급>_ …) · 왼쪽 위 기준.</summary>
|
|
|
|
|
|
public static RectTransform RewardTile(Transform p, string name, float x, float y, float size, string prefix, string icon, string qty, bool lit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = At(p, name, x, y, size, size);
|
|
|
|
|
|
ItemSlot(cell, prefix, lit);
|
|
|
|
|
|
var ic = Icon(cell, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), size * 0.68f);
|
|
|
|
|
|
DropShadow(ic, 4f);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(qty)) QtyChip(cell, qty, size >= 120f ? 30 : 24);
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>조건·정보 행(§4-2 · BasicFrame_Round12 panel_field · 좌 아이콘(tint = 켜짐 ok · 꺼짐 disabled) + 글자) · 왼쪽 위 기준 · icon null = 아이콘 없음.</summary>
|
2026-11-09 09:19:06 +00:00
|
|
|
|
public static Text CondRow(Transform p, string name, float x, float y, float w, float h, string icon, bool on, string text, int size, bool tint = true, string plate = "row_on_field")
|
2026-11-09 09:03:43 +00:00
|
|
|
|
{
|
|
|
|
|
|
var row = Skin(At(p, name, x, y, w, h), "panel12", Col(plate, Dark));
|
|
|
|
|
|
float lx = 24f;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(icon))
|
|
|
|
|
|
{
|
|
|
|
|
|
var ic = Icon(row.transform, icon, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(h * 0.5f + 6f, 0f), Mathf.Min(40f, h - 16f));
|
|
|
|
|
|
if (ic != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (tint) ic.color = on ? Col("ok", Ok) : Col("disabled", Disabled);
|
|
|
|
|
|
DropShadow(ic, 2f);
|
|
|
|
|
|
lx = h + 12f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var t = Label(row.transform, text, size, on ? White : Col("text_soft", InkSoft), TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(lx, 0f), new Vector2(w - lx - 20f, h), false);
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>구역 소제목(§5-2 · Title_Line01_Divider 좌·우 + 글자 32 흰 외곽선) · x·y = 왼쪽 위 · w = 전체 폭.</summary>
|
|
|
|
|
|
public static Text SubTitle(Transform p, string text, float x, float y, float w, int size = 32)
|
|
|
|
|
|
{
|
|
|
|
|
|
var box = At(p, "Sub_" + text, x, y, w, 44f);
|
|
|
|
|
|
var t = Title(FillLabel(box, text, size, White, TextAnchor.MiddleCenter, true, 0f), true);
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
float tw = Mathf.Ceil(t.preferredWidth);
|
|
|
|
|
|
float lw = Mathf.Max(0f, (w - tw) * 0.5f - 18f);
|
|
|
|
|
|
if (lw > 0f && UISkin.Has("title_divider_l") && UISkin.Has("title_divider_r"))
|
|
|
|
|
|
{
|
|
|
|
|
|
Skin(Rect("L", box, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(lw, 30f)), "title_divider_l", Col("divider", White));
|
|
|
|
|
|
Skin(Rect("R", box, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(lw, 30f)), "title_divider_r", Col("divider", White));
|
|
|
|
|
|
}
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 10:30:58 +00:00
|
|
|
|
/// <summary>병과 단색 심볼 id(PD 2026-11-09 · 말 · 검 · 활 · 창 · 깃털부채) — 없으면 기존 병과 메달 아이콘.</summary>
|
|
|
|
|
|
public static string TypeSymbolId(UnitType t)
|
|
|
|
|
|
{
|
|
|
|
|
|
string id = "cls_sym_" + TypeKey(t);
|
|
|
|
|
|
return UISkin.Icon(id) != null ? id : TypeIconId(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>진영 글자(위 · 촉 · 오 · 군) · 키 = heroes.csv faction(shu · wei · wu · other).</summary>
|
|
|
|
|
|
public static string FactionKo(string f)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (f)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "shu": return "촉";
|
|
|
|
|
|
case "wei": return "위";
|
|
|
|
|
|
case "wu": return "오";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "군";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>진영 원 배지(PD 2026-11-09 「원형 프레임에 위·촉·오·군 · 촉 녹 · 위 청 · 오 적 · 군 짙은 회색」) = 원 틀(fbadge_*) + 안쪽 원(fbadge_*_in) + 흰 글자.</summary>
|
|
|
|
|
|
public static Image FactionBadge(Transform p, string faction, Vector2 anchor, Vector2 pivot, Vector2 pos, float d)
|
|
|
|
|
|
{
|
|
|
|
|
|
string f = faction == "shu" || faction == "wei" || faction == "wu" ? faction : "other";
|
|
|
|
|
|
var ring = Skin(Rect("Faction", p, anchor, pivot, pos, new Vector2(d, d)), "circle_frame", Col("fbadge_" + f, Dark));
|
|
|
|
|
|
ring.preserveAspect = true;
|
|
|
|
|
|
var inner = Rect("In", ring.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(d * 0.72f, d * 0.72f)).gameObject.AddComponent<Image>();
|
|
|
|
|
|
inner.sprite = Circle;
|
|
|
|
|
|
inner.color = Col("fbadge_" + f + "_in", Dark);
|
|
|
|
|
|
inner.raycastTarget = false;
|
|
|
|
|
|
var t = Title(FillLabel(ring.transform, FactionKo(f), Mathf.RoundToInt(d * 0.48f), White), true);
|
|
|
|
|
|
t.rectTransform.offsetMin = new Vector2(0f, d * 0.03f);
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
|
|
return ring;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>아이콘(테마 id · 없으면 null).</summary>
|
|
|
|
|
|
public static Image Icon(Transform parent, string id, Vector2 anchor, Vector2 pivot, Vector2 pos, float size)
|
|
|
|
|
|
{
|
|
|
|
|
|
return UISkin.IconImage(parent, id, anchor, pivot, pos, new Vector2(size, size));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>아이콘이 있으면 아이콘 · 없으면 원 + 글자(구 표시).</summary>
|
|
|
|
|
|
public static Image IconOrDot(Transform parent, string id, string mark, Color c, Vector2 anchor, Vector2 pivot, Vector2 pos, float size)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ic = Icon(parent, id, anchor, pivot, pos, size);
|
|
|
|
|
|
if (ic != null) return ic;
|
|
|
|
|
|
var dot = Dot("Icon", parent, anchor, pivot, pos, size, c);
|
|
|
|
|
|
FillLabel(dot.transform, mark, Mathf.RoundToInt(size * 0.5f), White);
|
|
|
|
|
|
return dot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>제목 폰트(Sen-ExtraBold + 한글 폴백) + 흰 글자 외곽선.</summary>
|
|
|
|
|
|
public static Text Title(Text t, bool stroke = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.font = UISkin.TitleFont;
|
|
|
|
|
|
if (stroke && UISkin.On) UISkin.Stroke(t, Stroke, 3f);
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static Image Box(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Color c, bool rounded = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Paint(Rect(name, parent, anchor, pivot, pos, size), c, rounded);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Image Dot(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, float d, Color c)
|
|
|
|
|
|
{
|
|
|
|
|
|
var img = Rect(name, parent, anchor, pivot, pos, new Vector2(d, d)).gameObject.AddComponent<Image>();
|
2026-11-06 14:20:57 +00:00
|
|
|
|
if (d > 280f || !UISkin.Apply(img, "dot")) img.sprite = Circle; // 큰 원(후광 등)은 팩 외곽선이 흐려져서 평면 원
|
|
|
|
|
|
img.preserveAspect = true;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
img.color = c;
|
|
|
|
|
|
img.raycastTarget = false;
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>테두리(외곽선 효과 · 둥근 상자에 굵은 선처럼 보임) · 테마 카드는 외곽선을 이미 가져서 기본 선 색이면 생략.</summary>
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static void Border(Graphic g, Color c, float w = 4f)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
if (UISkin.On && c == Line) return;
|
|
|
|
|
|
if (UISkin.On) w = Mathf.Min(w, 3f);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var o = g.gameObject.AddComponent<Outline>();
|
|
|
|
|
|
o.effectColor = c;
|
|
|
|
|
|
o.effectDistance = new Vector2(w, -w);
|
|
|
|
|
|
o.useGraphicAlpha = false;
|
|
|
|
|
|
var o2 = g.gameObject.AddComponent<Outline>();
|
|
|
|
|
|
o2.effectColor = c;
|
|
|
|
|
|
o2.effectDistance = new Vector2(-w, w);
|
|
|
|
|
|
o2.useGraphicAlpha = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Text Label(Transform parent, string s, int size, Color c, TextAnchor align, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 box, bool bold = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rt = Rect("Text", parent, anchor, pivot, pos, box);
|
|
|
|
|
|
return Style(rt.gameObject.AddComponent<Text>(), s, size, c, align, bold);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>부모 전체에 꽉 찬 글자(가운데 정렬 기본).</summary>
|
|
|
|
|
|
public static Text FillLabel(Transform parent, string s, int size, Color c, TextAnchor align = TextAnchor.MiddleCenter, bool bold = true, float pad = 0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rt = Fill("Text", parent, pad, pad, 0f, 0f);
|
|
|
|
|
|
return Style(rt.gameObject.AddComponent<Text>(), s, size, c, align, bold);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static Text Style(Text t, string s, int size, Color c, TextAnchor align, bool bold)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.font = TextUtil.Font;
|
|
|
|
|
|
t.fontSize = size;
|
|
|
|
|
|
t.fontStyle = bold ? FontStyle.Bold : FontStyle.Normal;
|
|
|
|
|
|
t.alignment = align;
|
|
|
|
|
|
t.color = c;
|
|
|
|
|
|
t.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
|
|
|
|
t.verticalOverflow = VerticalWrapMode.Overflow;
|
|
|
|
|
|
t.supportRichText = true;
|
|
|
|
|
|
t.raycastTarget = false;
|
|
|
|
|
|
t.text = s;
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Button Btn(string name, Transform parent, string label, int fontSize, Color bg, Color fg, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Action onClick)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
var img = Skin(Rect(name, parent, anchor, pivot, pos, size), "button", bg, true);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var b = img.gameObject.AddComponent<Button>();
|
|
|
|
|
|
var cb = b.colors;
|
|
|
|
|
|
cb.disabledColor = new Color(0.75f, 0.75f, 0.75f, 0.85f);
|
|
|
|
|
|
b.colors = cb;
|
|
|
|
|
|
if (onClick != null) b.onClick.AddListener(() => onClick());
|
2026-11-06 14:20:57 +00:00
|
|
|
|
var t = FillLabel(img.transform, label, fontSize, fg, TextAnchor.MiddleCenter, true, 6f);
|
|
|
|
|
|
if (UISkin.On)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 팩 버튼 = 아래 턱(약 16 px)이 있어 글자를 조금 올리고 흰 글자는 외곽선
|
|
|
|
|
|
var trt = t.rectTransform;
|
|
|
|
|
|
float side = Mathf.Min(16f, size.x * 0.08f); // 팩 버튼 외곽선 안쪽으로
|
|
|
|
|
|
trt.offsetMin = new Vector2(side, Mathf.Min(10f, size.y * 0.1f));
|
|
|
|
|
|
trt.offsetMax = new Vector2(-side, trt.offsetMax.y);
|
|
|
|
|
|
Title(t, fg.r + fg.g + fg.b > 2.4f);
|
|
|
|
|
|
}
|
2026-11-05 12:11:10 +00:00
|
|
|
|
return b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Text LabelOf(Button b)
|
|
|
|
|
|
{
|
|
|
|
|
|
return b.GetComponentInChildren<Text>(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>둥근 막대 게이지 · 반환 = 채움(SetBar 로 0~1).</summary>
|
|
|
|
|
|
public static RectTransform Bar(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Color bg, Color fg)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
if (UISkin.Has("bar_bg"))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 팩 슬라이더: 어두운 홈(스킨 원색) + 안쪽 홈에 채움(용도 색 틴트)
|
|
|
|
|
|
var back = Skin(Rect(name, parent, anchor, pivot, pos, size), "bar_bg", Col("bar_track", White));
|
|
|
|
|
|
float pad = Mathf.Clamp(size.y * 0.16f, 3f, 6f);
|
|
|
|
|
|
var track = Fill("Track", back.transform, pad, pad, pad, pad);
|
|
|
|
|
|
var f = Fill("Fill", track);
|
|
|
|
|
|
f.anchorMax = new Vector2(0f, 1f);
|
|
|
|
|
|
Skin(f, "bar_fill", fg);
|
|
|
|
|
|
return f;
|
|
|
|
|
|
}
|
|
|
|
|
|
var b0 = Box(name, parent, anchor, pivot, pos, size, bg);
|
|
|
|
|
|
var fill = Fill("Fill", b0.transform);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
fill.anchorMax = new Vector2(0f, 1f);
|
|
|
|
|
|
Paint(fill, fg);
|
|
|
|
|
|
return fill;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void SetBar(RectTransform fill, float v)
|
|
|
|
|
|
{
|
|
|
|
|
|
v = Mathf.Clamp01(v);
|
|
|
|
|
|
fill.anchorMax = new Vector2(v, 1f);
|
|
|
|
|
|
fill.gameObject.SetActive(v > 0.001f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>세로 스크롤 격자 목록(칸 크기 cell · 열 columns) · 반환 = 내용(자식 = 칸).</summary>
|
|
|
|
|
|
public static RectTransform ScrollGrid(string name, Transform parent, float l, float r, float t, float b, Vector2 cell, Vector2 spacing, int columns, out ScrollRect scroll)
|
|
|
|
|
|
{
|
|
|
|
|
|
var vp = Fill(name, parent, l, r, t, b);
|
|
|
|
|
|
var vimg = vp.gameObject.AddComponent<Image>();
|
|
|
|
|
|
vimg.color = new Color(1f, 1f, 1f, 0f);
|
|
|
|
|
|
vimg.raycastTarget = true;
|
|
|
|
|
|
vp.gameObject.AddComponent<RectMask2D>();
|
|
|
|
|
|
var content = new GameObject("Content", typeof(RectTransform)).GetComponent<RectTransform>();
|
|
|
|
|
|
content.SetParent(vp, false);
|
|
|
|
|
|
content.anchorMin = new Vector2(0f, 1f);
|
|
|
|
|
|
content.anchorMax = new Vector2(1f, 1f);
|
|
|
|
|
|
content.pivot = new Vector2(0.5f, 1f);
|
|
|
|
|
|
content.offsetMin = content.offsetMax = Vector2.zero;
|
2026-11-09 16:05:47 +00:00
|
|
|
|
// 스크롤 내용 = 자기 캔버스(Frame) — 끄는 동안 매 프레임 움직여도 화면 나머지를 다시 묶지 않게(PD 2026-11-10 · RectMask2D 자르기 유지)
|
|
|
|
|
|
UiLayer.Nest(content.gameObject, UiRate.Frame, true);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var grid = content.gameObject.AddComponent<GridLayoutGroup>();
|
|
|
|
|
|
grid.cellSize = cell;
|
|
|
|
|
|
grid.spacing = spacing;
|
|
|
|
|
|
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
|
|
|
|
|
grid.constraintCount = columns;
|
|
|
|
|
|
grid.childAlignment = TextAnchor.UpperCenter;
|
|
|
|
|
|
grid.padding = new RectOffset(0, 0, 6, 12);
|
|
|
|
|
|
var fit = content.gameObject.AddComponent<ContentSizeFitter>();
|
|
|
|
|
|
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
|
|
scroll = vp.gameObject.AddComponent<ScrollRect>();
|
|
|
|
|
|
scroll.viewport = vp;
|
|
|
|
|
|
scroll.content = content;
|
|
|
|
|
|
scroll.horizontal = false;
|
|
|
|
|
|
scroll.vertical = true;
|
|
|
|
|
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
|
|
|
|
|
scroll.scrollSensitivity = 60f;
|
|
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void Clear(Transform t)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = t.childCount - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var c = t.GetChild(i).gameObject;
|
|
|
|
|
|
c.SetActive(false);
|
|
|
|
|
|
UnityEngine.Object.Destroy(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>목록 칸(격자 자식) — 둥근 카드 + 클릭.</summary>
|
|
|
|
|
|
public static Image Cell(Transform content, string name, Color fill, Action onClick)
|
|
|
|
|
|
{
|
|
|
|
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
|
|
|
|
go.transform.SetParent(content, false);
|
|
|
|
|
|
var img = Paint((RectTransform)go.transform, fill);
|
|
|
|
|
|
img.raycastTarget = true;
|
|
|
|
|
|
if (onClick != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var b = go.AddComponent<Button>();
|
|
|
|
|
|
b.onClick.AddListener(() => onClick());
|
|
|
|
|
|
}
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 영웅 표시 ----------
|
|
|
|
|
|
|
|
|
|
|
|
public static Color TypeColor(UnitType t)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (t)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
case UnitType.Infantry: return Col("type_infantry", new Color(0.62f, 0.33f, 0.22f));
|
|
|
|
|
|
case UnitType.Spear: return Col("type_spear", new Color(0.30f, 0.50f, 0.30f));
|
|
|
|
|
|
case UnitType.Cavalry: return Col("type_cavalry", new Color(0.28f, 0.32f, 0.45f));
|
|
|
|
|
|
case UnitType.Archer: return Col("type_archer", new Color(0.70f, 0.52f, 0.20f));
|
|
|
|
|
|
case UnitType.Strategist: return Col("type_strategist", Purple);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
return Dark;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 02:11:20 +00:00
|
|
|
|
/// <summary>병종 키(아이콘 id 접미 · cls_<키> · skill_<키>_a|p).</summary>
|
|
|
|
|
|
public static string TypeKey(UnitType t)
|
2026-11-06 14:20:57 +00:00
|
|
|
|
{
|
|
|
|
|
|
switch (t)
|
|
|
|
|
|
{
|
2026-11-09 02:11:20 +00:00
|
|
|
|
case UnitType.Infantry: return "infantry";
|
|
|
|
|
|
case UnitType.Spear: return "spear";
|
|
|
|
|
|
case UnitType.Cavalry: return "cavalry";
|
|
|
|
|
|
case UnitType.Archer: return "archer";
|
|
|
|
|
|
case UnitType.Strategist: return "strategist";
|
2026-11-06 14:20:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-09 02:11:20 +00:00
|
|
|
|
/// <summary>병종 아이콘 id(테마 · 신규 제작 cls_*).</summary>
|
|
|
|
|
|
public static string TypeIconId(UnitType t)
|
|
|
|
|
|
{
|
|
|
|
|
|
var k = TypeKey(t);
|
|
|
|
|
|
return k != null ? "cls_" + k : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>신규 제작(B) 치환이 들어온 아이콘인가(임시 팩 아이콘이 뜻과 어긋나는 곳은 그때만 아이콘을 쓴다 · 예: 병종).</summary>
|
|
|
|
|
|
public static bool HasFinalIcon(string id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var e = UISkin.Theme != null ? UISkin.Theme.IconEntryOf(id) : null;
|
|
|
|
|
|
return e != null && e.samguk != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
/// <summary>등급(승급 green… · 장비 white…) 표시 이름.</summary>
|
|
|
|
|
|
public static string GradeKo(string g)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (g)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "white": return "흰";
|
|
|
|
|
|
case "green": return "녹";
|
|
|
|
|
|
case "blue": return "청";
|
|
|
|
|
|
case "purple": return "자";
|
|
|
|
|
|
case "orange": return "주";
|
|
|
|
|
|
case "gold": return "금";
|
|
|
|
|
|
}
|
|
|
|
|
|
return g;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Color GradeColor(string g)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (g)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
case "white": return Col("grade_white", new Color(0.80f, 0.79f, 0.76f));
|
|
|
|
|
|
case "green": return Col("grade_green", new Color(0.38f, 0.64f, 0.33f));
|
|
|
|
|
|
case "blue": return Col("grade_blue", new Color(0.29f, 0.48f, 0.78f));
|
|
|
|
|
|
case "purple": return Col("grade_purple", Purple);
|
|
|
|
|
|
case "orange": return Col("grade_orange", new Color(0.90f, 0.50f, 0.18f));
|
|
|
|
|
|
case "gold": return Col("grade_gold", new Color(0.92f, 0.74f, 0.16f));
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
return Slot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>등급 칸(팩 ItemFrame 등급 색 · 신규 제작 frame_* 오면 치환) — 스킨이 없으면 등급 색 카드.</summary>
|
|
|
|
|
|
public static Image GradeFrame(Image img, string grade)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrEmpty(grade) && UISkin.Apply(img, "frame_" + grade))
|
|
|
|
|
|
{
|
|
|
|
|
|
img.color = White;
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
UISkin.Apply(img, "card");
|
|
|
|
|
|
img.color = GradeColor(grade);
|
|
|
|
|
|
return img;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>별 줄(★ 아이콘 n개 · 테마 없으면 글자).</summary>
|
|
|
|
|
|
public static void StarRow(Transform parent, int n, int max, Vector2 anchor, Vector2 pivot, Vector2 pos, float size)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (UISkin.Icon("star_on") == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Label(parent, Stars(n, max), Mathf.RoundToInt(size), Star, TextAnchor.MiddleCenter, anchor, pivot, pos, new Vector2(size * max, size + 6f));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var row = Rect("Stars", parent, anchor, pivot, pos, new Vector2(size * max * 0.92f, size));
|
|
|
|
|
|
for (int i = 0; i < max; i++)
|
|
|
|
|
|
Icon(row, i < n ? "star_on" : "star_off", new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(i * size * 0.92f, 0f), size);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static string Stars(int n, int max)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new System.Text.StringBuilder();
|
|
|
|
|
|
for (int i = 0; i < max; i++) sb.Append(i < n ? "★" : "☆");
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static readonly Dictionary<string, Sprite> artCache = new Dictionary<string, Sprite>();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>일러 = Resources/Units/<art> front_idle 첫 프레임(없으면 다른 클립 첫 프레임 · 없으면 null = 도형).</summary>
|
|
|
|
|
|
public static Sprite ArtSprite(string art)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(art)) return null;
|
|
|
|
|
|
Sprite sp;
|
|
|
|
|
|
if (artCache.TryGetValue(art, out sp)) return sp;
|
|
|
|
|
|
var a = UnitArt.Load(art);
|
|
|
|
|
|
Sprite[] frames = null;
|
|
|
|
|
|
if (a != null && !a.Clips.TryGetValue("front_idle", out frames))
|
|
|
|
|
|
foreach (var kv in a.Clips)
|
|
|
|
|
|
{
|
|
|
|
|
|
frames = kv.Value;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
sp = frames != null && frames.Length > 0 ? frames[0] : null;
|
|
|
|
|
|
artCache[art] = sp;
|
|
|
|
|
|
return sp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-10 04:50:57 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 서 있는 유닛(발 = anchor + pos) — 크기 표 size_px 기준 배율(standardPx ÷ size_px = 머리 크기 통일 · 영웅 목록·전투와 같은 기준) ·
|
|
|
|
|
|
/// 기마 = 머리 끝이 standardPx × mountedMaxTop 을 넘지 않게(전투 FrameAnimator 와 같은 상한) · 일러·피벗이 없으면 null(호출 쪽이 초상으로 대체).
|
|
|
|
|
|
/// PD 2026-11-10 「로비에 배치된 관우의 크기만 작게 보임」 — 칸에 그림 틀을 맞추면 틀이 넓은 유닛(관우 언월도)만 작아짐.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static RectTransform UnitStand(string name, Transform parent, HeroDb.Hero h, Vector2 anchor, Vector2 pos, float standardPx, float mountedMaxTop)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sp = h != null ? ArtSprite(h.art) : null;
|
|
|
|
|
|
var a = h != null ? UnitArt.Load(h.art) : null;
|
|
|
|
|
|
if (sp == null || a == null || a.Meta.pivot == null || a.Meta.pivot.Length < 2) return null;
|
|
|
|
|
|
float size = a.SizePx > 0.5f ? a.SizePx : a.BodyPx;
|
|
|
|
|
|
if (size <= 0.5f) return null;
|
|
|
|
|
|
float k = standardPx / size;
|
|
|
|
|
|
float top = a.BodyPx * k;
|
|
|
|
|
|
if (a.Mounted && mountedMaxTop > 0.01f && top > standardPx * mountedMaxTop) k *= standardPx * mountedMaxTop / top;
|
|
|
|
|
|
var rt = Rect(name, parent, anchor, new Vector2(0.5f, a.Meta.pivot[1]), pos, sp.rect.size * k);
|
|
|
|
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|
|
|
|
|
img.sprite = sp;
|
|
|
|
|
|
img.raycastTarget = false;
|
|
|
|
|
|
return rt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 초상(틀 안을 마스크) — 일러가 있으면 그 그림(zoom > 1 = 위쪽 얼굴 부분 확대) · 없으면 병종 색 원 + 병종 글자.
|
|
|
|
|
|
/// circle = 원형 틀(편성 자리) · false = 둥근 사각.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static RectTransform Portrait(string name, Transform parent, HeroDb.Hero h, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, bool circle, float zoom, Color frame)
|
|
|
|
|
|
{
|
2026-11-06 14:20:57 +00:00
|
|
|
|
var outer = Rect(name, parent, anchor, pivot, pos, size);
|
|
|
|
|
|
var rt = outer;
|
|
|
|
|
|
bool framed = frame.a > 0.01f && UISkin.On;
|
|
|
|
|
|
if (framed)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 팩 틀(외곽선) 바깥 + 안쪽 마스크(외곽선 안으로 그림이 들어가게)
|
|
|
|
|
|
var fimg = outer.gameObject.AddComponent<Image>();
|
|
|
|
|
|
fimg.color = frame;
|
|
|
|
|
|
fimg.raycastTarget = false;
|
|
|
|
|
|
if (circle)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!UISkin.Apply(fimg, "circle_frame")) fimg.sprite = Circle;
|
|
|
|
|
|
fimg.preserveAspect = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else UISkin.Apply(fimg, "card");
|
|
|
|
|
|
float pad = Mathf.Max(5f, Mathf.Min(size.x, size.y) * 0.05f);
|
|
|
|
|
|
rt = Fill("Mask", outer, pad, pad, pad, pad + (circle ? 0f : 2f));
|
|
|
|
|
|
size = new Vector2(size.x - pad * 2f, size.y - pad * 2f);
|
|
|
|
|
|
}
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var bg = rt.gameObject.AddComponent<Image>();
|
2026-11-06 14:20:57 +00:00
|
|
|
|
bg.color = framed ? new Color(frame.r * 0.9f, frame.g * 0.9f, frame.b * 0.9f, 1f) : frame;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
bg.raycastTarget = false;
|
|
|
|
|
|
if (circle) bg.sprite = Circle;
|
2026-11-06 14:20:57 +00:00
|
|
|
|
else if (!framed || !UISkin.Apply(bg, "portrait_mask"))
|
2026-11-05 12:11:10 +00:00
|
|
|
|
{
|
|
|
|
|
|
bg.sprite = Round;
|
|
|
|
|
|
bg.type = Image.Type.Sliced;
|
|
|
|
|
|
}
|
|
|
|
|
|
var mask = rt.gameObject.AddComponent<Mask>();
|
|
|
|
|
|
mask.showMaskGraphic = frame.a > 0.01f;
|
|
|
|
|
|
if (frame.a <= 0.01f) bg.color = Color.white; // 투명 틀: 알파 0 그래픽은 스텐실을 안 써서 자식이 안 보임 → 불투명 + 그리지 않음
|
|
|
|
|
|
var sp = h != null ? ArtSprite(h.art) : null;
|
|
|
|
|
|
if (sp != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// zoom ≤ 1 = 전신 가운데 · zoom > 1 = 위쪽(얼굴) 확대
|
|
|
|
|
|
bool bust = zoom > 1f;
|
|
|
|
|
|
var a = bust
|
|
|
|
|
|
? Rect("Art", rt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, size.y * 0.06f * zoom), size * zoom)
|
|
|
|
|
|
: Rect("Art", rt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, size * zoom);
|
|
|
|
|
|
var img = a.gameObject.AddComponent<Image>();
|
|
|
|
|
|
img.sprite = sp;
|
|
|
|
|
|
img.preserveAspect = true;
|
|
|
|
|
|
img.raycastTarget = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (h != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
float d = Mathf.Min(size.x, size.y) * 0.72f;
|
|
|
|
|
|
var dot = Dot("Shape", rt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, d, TypeColor(h.type));
|
2026-11-06 14:20:57 +00:00
|
|
|
|
Title(FillLabel(dot.transform, UnitTypeInfo.Mark(h.type), Mathf.RoundToInt(d * 0.5f), White), true);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
2026-11-06 14:20:57 +00:00
|
|
|
|
return outer;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-06 14:20:57 +00:00
|
|
|
|
/// <summary>병종 배지(병종 색 칩 + 글자 · 신규 제작 cls_* 아이콘이 오면 아이콘).</summary>
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static Image TypeBadge(Transform parent, UnitType t, Vector2 anchor, Vector2 pivot, Vector2 pos, float h)
|
|
|
|
|
|
{
|
2026-11-09 10:59:56 +00:00
|
|
|
|
var b = Box("Type", parent, anchor, pivot, pos, new Vector2(h * 1.3f, h), UISkin.On ? Color.clear : Dark); // 테마 = 틀 없이 단색 심볼(PD 2026-11-09)
|
|
|
|
|
|
string icon = TypeSymbolId(t);
|
|
|
|
|
|
if (UISkin.Icon(icon) != null) Icon(b.transform, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 1f), h * 0.8f);
|
2026-11-06 14:20:57 +00:00
|
|
|
|
else Title(FillLabel(b.transform, UnitTypeInfo.Mark(t), Mathf.RoundToInt(h * 0.62f), White), true);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
return b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|