185 lines
11 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|