234 lines
12 KiB
C#
234 lines
12 KiB
C#
|
|
using System;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using System.Numerics;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.Events;
|
|||
|
|
using UnityEngine.EventSystems;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
using Vector2 = UnityEngine.Vector2;
|
|||
|
|
using Vector3 = UnityEngine.Vector3;
|
|||
|
|
using Vector4 = UnityEngine.Vector4;
|
|||
|
|
|
|||
|
|
// 코드로 만드는 uGUI 도우미 (기준 해상도 1080×1920 · 원작 GamePlayUI 좌표계와 같음)
|
|||
|
|
namespace VS.View
|
|||
|
|
{
|
|||
|
|
public static class Pal
|
|||
|
|
{
|
|||
|
|
public static readonly Color Ink = new Color32(44, 48, 80, 255); // 남색 잉크(GUI Pro-SuperCasual 테두리 계열)
|
|||
|
|
public static readonly Color Navy = new Color32(38, 44, 72, 255); // 어두운 판·탭 바
|
|||
|
|
public static readonly Color NavyLight = new Color32(78, 90, 136, 255); // 선택된 탭
|
|||
|
|
public static readonly Color Outline = new Color(0.14f, 0.16f, 0.28f, 0.95f);
|
|||
|
|
public static readonly Color White = Color.white;
|
|||
|
|
public static readonly Color Pink = new Color32(255, 105, 170, 255);
|
|||
|
|
public static readonly Color PinkDark = new Color32(214, 62, 132, 255);
|
|||
|
|
public static readonly Color Purple = new Color32(126, 87, 214, 255);
|
|||
|
|
public static readonly Color PurpleDark = new Color32(84, 56, 160, 255);
|
|||
|
|
public static readonly Color Yellow = new Color32(255, 206, 64, 255);
|
|||
|
|
public static readonly Color Green = new Color32(78, 200, 120, 255);
|
|||
|
|
public static readonly Color GreenDark = new Color32(42, 150, 84, 255);
|
|||
|
|
public static readonly Color Gray = new Color32(170, 170, 185, 255);
|
|||
|
|
public static readonly Color Panel = new Color32(255, 255, 255, 255);
|
|||
|
|
public static readonly Color PanelEdge = new Color32(236, 240, 252, 255);
|
|||
|
|
public static readonly Color Row = new Color32(241, 243, 252, 255); // 팝업 안 줄(행) 바탕
|
|||
|
|
public static readonly Color Dim = new Color(0.08f, 0.04f, 0.12f, 0.62f);
|
|||
|
|
public static readonly Color Blue = new Color32(84, 170, 255, 255);
|
|||
|
|
public static readonly Color Orange = new Color32(255, 150, 60, 255);
|
|||
|
|
public static readonly Color Red = new Color32(240, 72, 84, 255);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static class Fmt
|
|||
|
|
{
|
|||
|
|
// NSHTool.Utilities.NumberShortFormatter (.cctor RVA 0x252D820) 접미사 표 그대로 · 소수점 = '.'(원작은 vi-VN ',')
|
|||
|
|
static readonly string[] Suf = { "", "k", "M", "B", "T", "Qa", "Qi", "Sx", "Sp", "Oc" };
|
|||
|
|
|
|||
|
|
static decimal Trunc(decimal d, int n) { decimal p = 1m; for (int i = 0; i < n; i++) p *= 10m; return decimal.Truncate(d * p) / p; }
|
|||
|
|
|
|||
|
|
public static string Num(BigInteger v, int decimals = 1)
|
|||
|
|
{
|
|||
|
|
if (v < 0) return "-" + Num(-v, decimals);
|
|||
|
|
if (v < 1000) return v.ToString(CultureInfo.InvariantCulture);
|
|||
|
|
int e = 0;
|
|||
|
|
BigInteger t = v;
|
|||
|
|
while (t >= 1000 && e < Suf.Length - 1) { t /= 1000; e++; }
|
|||
|
|
decimal d = (decimal)(v / BigInteger.Pow(1000, Math.Max(0, e - 1))) / 1000m;
|
|||
|
|
if (d >= 1000m && e == Suf.Length - 1) return t.ToString(CultureInfo.InvariantCulture) + Suf[e];
|
|||
|
|
decimal r = Trunc(d, decimals);
|
|||
|
|
return r.ToString("0." + new string('#', decimals), CultureInfo.InvariantCulture) + Suf[e];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Num(decimal v, int decimals = 1)
|
|||
|
|
{
|
|||
|
|
if (v < 1000m)
|
|||
|
|
{
|
|||
|
|
if (v < 10m) return Trunc(v, 2).ToString("0.##", CultureInfo.InvariantCulture);
|
|||
|
|
return Trunc(v, 1).ToString("0.#", CultureInfo.InvariantCulture);
|
|||
|
|
}
|
|||
|
|
return Num(new BigInteger(decimal.Truncate(v)), decimals);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Time(double sec)
|
|||
|
|
{
|
|||
|
|
if (sec < 0) sec = 0;
|
|||
|
|
int s = (int)Math.Ceiling(sec);
|
|||
|
|
if (s >= 3600) return $"{s / 3600}:{(s / 60) % 60:00}:{s % 60:00}";
|
|||
|
|
return $"{s / 60:00}:{s % 60:00}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static class UI
|
|||
|
|
{
|
|||
|
|
static Font font;
|
|||
|
|
public static Font Font
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
if (font == null) font = Resources.Load<Font>("Fonts/MaplestoryBold");
|
|||
|
|
if (font == null) font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
|||
|
|
return font;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static RectTransform Rect(string name, Transform parent)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|||
|
|
go.layer = 5;
|
|||
|
|
var rt = (RectTransform)go.transform;
|
|||
|
|
rt.SetParent(parent, false);
|
|||
|
|
return rt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static RectTransform Place(this RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pos, Vector2 size, Vector2? pivot = null)
|
|||
|
|
{
|
|||
|
|
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot ?? new Vector2(0.5f, 0.5f);
|
|||
|
|
rt.anchoredPosition = pos; rt.sizeDelta = size;
|
|||
|
|
return rt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static RectTransform Fill(this RectTransform rt, float l = 0, float b = 0, float r = 0, float t = 0)
|
|||
|
|
{
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Image Img(Transform parent, string name, Sprite sp, Color c, bool raycast = false)
|
|||
|
|
{
|
|||
|
|
var rt = Rect(name, parent);
|
|||
|
|
var im = rt.gameObject.AddComponent<Image>();
|
|||
|
|
im.sprite = sp; im.color = c; im.raycastTarget = raycast;
|
|||
|
|
if (sp != null && sp.border != Vector4.zero) im.type = Image.Type.Sliced;
|
|||
|
|
return im;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Text Txt(Transform parent, string name, string s, int size, Color c, TextAnchor a = TextAnchor.MiddleCenter, bool outline = false)
|
|||
|
|
{
|
|||
|
|
var rt = Rect(name, parent);
|
|||
|
|
var t = rt.gameObject.AddComponent<Text>();
|
|||
|
|
t.font = Font; t.text = s; t.fontSize = size; t.color = c; t.alignment = a;
|
|||
|
|
t.raycastTarget = false; t.horizontalOverflow = HorizontalWrapMode.Wrap; t.verticalOverflow = VerticalWrapMode.Truncate;
|
|||
|
|
t.resizeTextForBestFit = true; t.resizeTextMinSize = Math.Max(10, size / 2); t.resizeTextMaxSize = size;
|
|||
|
|
if (outline) { var o = rt.gameObject.AddComponent<Outline>(); o.effectColor = Pal.Outline; float d = Mathf.Clamp(size / 15f, 1.5f, 3f); o.effectDistance = new Vector2(d, -d); } // 작은 글자는 얇게
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 아이콘 그림 (비율 유지)
|
|||
|
|
public static Image Icon(Transform parent, string name, Sprite sp, Color c)
|
|||
|
|
{
|
|||
|
|
var im = Img(parent, name, sp, c);
|
|||
|
|
im.preserveAspect = true;
|
|||
|
|
return im;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Button Btn(Transform parent, string name, Sprite sp, Color c, UnityAction onClick)
|
|||
|
|
{
|
|||
|
|
var im = Img(parent, name, sp, c, true);
|
|||
|
|
var b = im.gameObject.AddComponent<Button>();
|
|||
|
|
var cb = b.colors; cb.pressedColor = new Color(0.85f, 0.85f, 0.85f); cb.disabledColor = new Color(0.62f, 0.62f, 0.66f, 1f); b.colors = cb;
|
|||
|
|
if (onClick != null) b.onClick.AddListener(onClick);
|
|||
|
|
im.gameObject.AddComponent<PressScale>();
|
|||
|
|
return b;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 글자 버튼 (배경 + 글자)
|
|||
|
|
/// 팩이 있으면 색이 구워진 버튼 그림(흰 틴트) · 없으면 둥근 판 + 색
|
|||
|
|
public static Button TextBtn(Transform parent, string name, string label, Color bg, int size, UnityAction onClick, out Text text)
|
|||
|
|
{
|
|||
|
|
var sp = Skin.Button(bg);
|
|||
|
|
var b = Btn(parent, name, sp ?? Art.Round, sp != null ? Color.white : bg, onClick);
|
|||
|
|
text = Txt(b.transform, "Label", label, size, Color.white, TextAnchor.MiddleCenter, true);
|
|||
|
|
text.rectTransform.Fill(10, sp != null ? 16 : 6, 10, 6); // 팩 버튼은 아래 그림자 두께만큼 글자를 올림
|
|||
|
|
return b;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 버튼 색 바꾸기 (팩 = 그림 교체 · 코드 그림 = 색)
|
|||
|
|
public static void SetBtnColor(Button b, Color c)
|
|||
|
|
{
|
|||
|
|
var im = b.GetComponent<Image>();
|
|||
|
|
var sp = Skin.Button(c);
|
|||
|
|
if (sp != null) { im.sprite = sp; im.color = Color.white; } else { im.sprite = Art.Round; im.color = c; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 가로 막대 — small = 유닛 체력 같은 작은 막대(테두리 얇은 그림)
|
|||
|
|
public static Image Bar(Transform parent, string name, Color bg, Color fg, out Image fill, bool small = false)
|
|||
|
|
{
|
|||
|
|
var back = Img(parent, name, small ? Skin.HpBg : Skin.BarBg, bg);
|
|||
|
|
fill = Img(back.transform, "Fill", small ? Skin.HpFill : Skin.BarFill, fg);
|
|||
|
|
fill.type = Image.Type.Filled; fill.fillMethod = Image.FillMethod.Horizontal; fill.fillAmount = 0;
|
|||
|
|
fill.preserveAspect = false;
|
|||
|
|
int pad = Skin.On ? (small ? 3 : 5) : 4;
|
|||
|
|
fill.rectTransform.Fill(pad, pad, pad, pad);
|
|||
|
|
return back;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 빨간 알림 점 (흰 테두리) — 오른쪽 위
|
|||
|
|
public static Image Dot(Transform parent, string name, float size = 36f)
|
|||
|
|
{
|
|||
|
|
var d = Img(parent, name, Skin.CirclePlain, Pal.Red);
|
|||
|
|
d.rectTransform.Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-6, -6), new Vector2(size, size));
|
|||
|
|
if (Skin.DotBorder != null) Img(d.transform, "Border", Skin.DotBorder, Color.white).rectTransform.Fill(-3, -3, -3, -3);
|
|||
|
|
return d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static ScrollRect Scroll(Transform parent, string name, out RectTransform content, float spacing = 16f)
|
|||
|
|
{
|
|||
|
|
var root = Img(parent, name, Art.Square, new Color(1, 1, 1, 0), true);
|
|||
|
|
var sr = root.gameObject.AddComponent<ScrollRect>();
|
|||
|
|
var vp = Rect("Viewport", root.transform).Fill();
|
|||
|
|
vp.gameObject.AddComponent<RectMask2D>();
|
|||
|
|
content = Rect("Content", vp);
|
|||
|
|
content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1); content.pivot = new Vector2(0.5f, 1);
|
|||
|
|
content.anchoredPosition = Vector2.zero; content.sizeDelta = Vector2.zero;
|
|||
|
|
var vl = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
|||
|
|
vl.spacing = spacing; vl.childControlHeight = true; vl.childControlWidth = true; vl.childForceExpandHeight = false; vl.childForceExpandWidth = true;
|
|||
|
|
vl.padding = new RectOffset(8, 8, 8, 24);
|
|||
|
|
var fit = content.gameObject.AddComponent<ContentSizeFitter>();
|
|||
|
|
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|||
|
|
sr.viewport = vp; sr.content = content; sr.horizontal = false; sr.vertical = true;
|
|||
|
|
sr.movementType = ScrollRect.MovementType.Elastic; sr.scrollSensitivity = 30f;
|
|||
|
|
return sr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void SetLayoutHeight(Component c, float h)
|
|||
|
|
{
|
|||
|
|
var le = c.GetComponent<LayoutElement>() ?? c.gameObject.AddComponent<LayoutElement>();
|
|||
|
|
le.preferredHeight = h; le.minHeight = h;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 버튼 눌림 크기 반응
|
|||
|
|
public class PressScale : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler
|
|||
|
|
{
|
|||
|
|
Vector3 baseScale = Vector3.one; float target = 1f;
|
|||
|
|
void Awake() { baseScale = transform.localScale; }
|
|||
|
|
public void OnPointerDown(PointerEventData e) { target = 0.93f; }
|
|||
|
|
public void OnPointerUp(PointerEventData e) { target = 1f; }
|
|||
|
|
public void OnPointerExit(PointerEventData e) { target = 1f; }
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
float s = transform.localScale.x / Mathf.Max(0.0001f, baseScale.x);
|
|||
|
|
s = Mathf.MoveTowards(s, target, Time.unscaledDeltaTime * 2.5f);
|
|||
|
|
transform.localScale = baseScale * s;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|