// ───────────────────────────────────────────────────────────────────────────── // 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 _act = new List(), _pas = new List(); 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(); } void OnDestroy() { if (Instance == this) Instance = null; } void Build() { var H = WLPartSkillData.S.hud; gameObject.layer = 5; _canvas = gameObject.AddComponent(); _canvas.renderMode = RenderMode.ScreenSpaceOverlay; _canvas.sortingOrder = H.sortingOrder; var sc = gameObject.AddComponent(); 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(); _bannerGroup.alpha = 0f; _bannerGroup.blocksRaycasts = false; _banner = bgo.AddComponent(); 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; } /// 원 스프라이트(절차 생성 1장 · 장비 화면 뱃지와 공유). 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(); 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(); 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)); } } }