Project_WL/Assets/WL/PartSkill/WLPartSkillHud.cs

185 lines
11 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLPartSkillHud.cs — 전투 중 파츠 스킬 표시 (PD 지시 #879 · 기획 v2 19장 「전투 중 표시」)
//
// 액티브 쿨 원(최대 4 · 시너지면 금테) + 패시브 아이콘(최대 2) + 첫 시너지 「시너지! <이름>」 배너.
// 코드로 만드는 uGUI(프리팹 0) · 위치 · 크기 · 색 = WLPartSkill.json hud(C45). 장비 화면이 열려 있으면 숨긴다.
// 모바일: 캔버스 1개 · 이미지 20 개 이하 · 갱신은 매 프레임 fillAmount 만.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace WL.PartSkill
{
public sealed class WLPartSkillHud : MonoBehaviour
{
public static WLPartSkillHud Instance { get; private set; }
public static int Banners;
public static string LastBanner = "";
sealed class Slot { public RectTransform root; public Image bg, icon, cool, ring; public TextMeshProUGUI label; }
readonly List<Slot> _act = new List<Slot>(), _pas = new List<Slot>();
Canvas _canvas; RectTransform _root; TextMeshProUGUI _banner; CanvasGroup _bannerGroup;
TMP_FontAsset _font;
int _builtFor = -1;
float _bannerUntil;
static Sprite s_circle;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (Instance != null || !WLPartSkillData.Enabled || WLPartSkillData.S.hud == null || WLPartSkillData.S.hud.enabled == 0) return;
var go = new GameObject("WL_PartSkillHud", typeof(RectTransform));
DontDestroyOnLoad(go);
Instance = go.AddComponent<WLPartSkillHud>();
}
void OnDestroy() { if (Instance == this) Instance = null; }
void Build()
{
var H = WLPartSkillData.S.hud;
gameObject.layer = 5;
_canvas = gameObject.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = H.sortingOrder;
var sc = gameObject.AddComponent<CanvasScaler>();
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; sc.referenceResolution = new Vector2(1080f, 1920f);
sc.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; sc.matchWidthOrHeight = 0.5f;
_root = (RectTransform)transform;
try { _font = WL.Equip.WLEquipUi.ResolveKoreanFont(WL.Equip.WLEquipSettings.Instance); } catch { }
var bgo = new GameObject("Banner", typeof(RectTransform)); bgo.layer = 5; bgo.transform.SetParent(_root, false);
var brt = (RectTransform)bgo.transform; brt.anchorMin = brt.anchorMax = new Vector2(0.5f, 1f); brt.pivot = new Vector2(0.5f, 0.5f);
brt.anchoredPosition = new Vector2(0f, H.bannerY); brt.sizeDelta = new Vector2(1000f, H.bannerFontPx * 2.2f);
_bannerGroup = bgo.AddComponent<CanvasGroup>(); _bannerGroup.alpha = 0f; _bannerGroup.blocksRaycasts = false;
_banner = bgo.AddComponent<TextMeshProUGUI>();
if (_font != null) _banner.font = _font;
_banner.fontSize = H.bannerFontPx; _banner.alignment = TextAlignmentOptions.Center; _banner.color = H.Col("banner", new Color(1f, 0.86f, 0.3f, 1f));
_banner.fontStyle = FontStyles.Bold; _banner.raycastTarget = false; _banner.enableWordWrapping = false;
}
/// <summary>원 스프라이트(절차 생성 1장 · 장비 화면 뱃지와 공유).</summary>
public static Sprite CircleSprite() { return Circle(); }
static Sprite Circle()
{
if (s_circle != null) return s_circle;
const int N = 128;
var tex = new Texture2D(N, N, TextureFormat.RGBA32, false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear };
var px = new Color32[N * N]; float c = (N - 1) * 0.5f;
for (int y = 0; y < N; y++) for (int x = 0; x < N; x++)
{
float d = Mathf.Sqrt((x - c) * (x - c) + (y - c) * (y - c));
float a = Mathf.Clamp01(c - d + 0.5f);
px[y * N + x] = new Color32(255, 255, 255, (byte)(a * 255f));
}
tex.SetPixels32(px); tex.Apply(false, true);
s_circle = Sprite.Create(tex, new Rect(0, 0, N, N), new Vector2(0.5f, 0.5f), 100f);
s_circle.name = "@circle";
return s_circle;
}
Slot NewSlot(float size, bool active)
{
var H = WLPartSkillData.S.hud;
var s = new Slot();
var go = new GameObject(active ? "Act" : "Pas", typeof(RectTransform)); go.layer = 5; go.transform.SetParent(_root, false);
s.root = (RectTransform)go.transform; s.root.sizeDelta = new Vector2(size, size);
s.root.anchorMin = s.root.anchorMax = new Vector2(H.anchorX, H.anchorY); s.root.pivot = new Vector2(0f, 0f);
s.ring = Img(s.root, "Ring", Circle(), H.Col("ring", new Color(0.95f, 0.85f, 0.35f, 1f)), -4f);
s.bg = Img(s.root, "Bg", Circle(), H.Col("bg", new Color(0.08f, 0.07f, 0.12f, 0.78f)), 0f);
s.icon = Img(s.root, "Icon", null, Color.white, size * 0.12f);
s.icon.preserveAspect = true;
s.cool = Img(s.root, "Cool", Circle(), H.Col("cool", new Color(0f, 0f, 0f, 0.62f)), 0f);
s.cool.type = Image.Type.Filled; s.cool.fillMethod = Image.FillMethod.Radial360; s.cool.fillOrigin = (int)Image.Origin360.Top; s.cool.fillClockwise = false;
var lgo = new GameObject("Lv", typeof(RectTransform)); lgo.layer = 5; lgo.transform.SetParent(s.root, false);
var lrt = (RectTransform)lgo.transform; lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 0f); lrt.pivot = new Vector2(0.5f, 1f);
lrt.anchoredPosition = new Vector2(0f, -2f); lrt.sizeDelta = new Vector2(40f, H.fontPx + 6f);
s.label = lgo.AddComponent<TextMeshProUGUI>(); if (_font != null) s.label.font = _font;
s.label.fontSize = H.fontPx * (active ? 1f : 0.85f); s.label.alignment = TextAlignmentOptions.Top; s.label.color = H.Col("text", Color.white); s.label.raycastTarget = false;
s.label.fontStyle = FontStyles.Bold; s.label.enableWordWrapping = false;
return s;
}
static Image Img(RectTransform p, string n, Sprite sp, Color c, float inset)
{
var go = new GameObject(n, typeof(RectTransform)); go.layer = 5; go.transform.SetParent(p, false);
var rt = (RectTransform)go.transform; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = new Vector2(inset, inset); rt.offsetMax = new Vector2(-inset, -inset);
var img = go.AddComponent<Image>(); img.sprite = sp; img.color = c; img.raycastTarget = false;
return img;
}
void Rebuild(WLPartSkillRunner r)
{
var H = WLPartSkillData.S.hud;
foreach (var s in _act) Destroy(s.root.gameObject);
foreach (var s in _pas) Destroy(s.root.gameObject);
_act.Clear(); _pas.Clear();
float x = H.x, y = H.y;
int n = 0;
for (int i = 0; i < r.Skills.Count; i++)
{
var sk = r.Skills[i];
if (sk.def.kind == WLPartKind.WeaponSyn && !sk.syn) continue;
var s = NewSlot(H.size, true);
s.root.anchoredPosition = new Vector2(x + n * (H.size + H.gap), y);
s.icon.sprite = WLPartSkillData.Icon(sk.def.icon);
s.icon.color = s.icon.sprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
s.ring.color = sk.syn ? H.Col("synergy", new Color(1f, 0.78f, 0.2f, 1f)) : H.Col("ring", new Color(0.95f, 0.85f, 0.35f, 1f));
s.ring.gameObject.SetActive(true);
s.label.text = sk.syn ? "★" : (sk.row != null ? "Lv" + Mathf.Max(1, sk.slot.level) : "");
_act.Add(s); n++;
}
var lo = r.L; int k = 0;
for (int i = 0; i < lo.passives.Count; i++)
{
var p = lo.passives[i]; if (!p.Open) continue;
var s = NewSlot(H.passiveSize, false);
s.root.anchoredPosition = new Vector2(x + k * (H.passiveSize + H.gap * 0.6f), y + H.size + H.gap + H.fontPx);
s.icon.sprite = WLPartSkillData.Icon(p.def.icon);
s.icon.color = s.icon.sprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
s.ring.color = H.Col("passive", new Color(0.55f, 0.85f, 1f, 1f));
s.cool.fillAmount = 0f;
s.label.text = "Lv" + p.level;
_pas.Add(s); k++;
}
}
void Update()
{
var r = WLPartSkillRunner.Instance;
bool show = r != null && WLPartSkillCombat.PC != null && !WL.Equip.WLEquipGui.IsOpen && r.Skills.Count + r.L.passives.Count > 0;
if (_canvas == null) { if (!show) return; Build(); }
if (_canvas.enabled != show) _canvas.enabled = show;
if (!show) return;
int ver = WLPartSkillState.Version * 1000 + WLPartSkillRunner.Builds;
if (ver != _builtFor) { _builtFor = ver; Rebuild(r); }
int n = 0;
for (int i = 0; i < r.Skills.Count && n < _act.Count; i++)
{
var sk = r.Skills[i];
if (sk.def.kind == WLPartKind.WeaponSyn && !sk.syn) continue;
_act[n].cool.fillAmount = sk.CoolFrac;
n++;
}
// 「시너지!」 배너 — 전투 중 처음 성립한 시너지마다 1번
if (Time.time >= _bannerUntil && r.InCombat)
{
var name = r.TakeBanner();
if (name != null)
{
_banner.text = "시너지! " + name;
_bannerUntil = Time.time + WLPartSkillData.S.synergyBannerSec;
Banners++; LastBanner = name;
}
}
float left = _bannerUntil - Time.time, dur = Mathf.Max(0.1f, WLPartSkillData.S.synergyBannerSec);
_bannerGroup.alpha = left <= 0f ? 0f : Mathf.Clamp01(Mathf.Min((dur - left) / 0.15f, left / 0.4f));
if (left > 0f) _banner.rectTransform.localScale = Vector3.one * (1f + 0.15f * Mathf.Clamp01(1f - (dur - left) / 0.25f));
}
}
}