240 lines
12 KiB
C#
240 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// KillChainText.cs — 연쇄 처치 문구 표시 (WL-813g §1-1 · #813)
|
|
//
|
|
// 기준서 v1 §B 요소 2 「처치 연출 · 연쇄 처치 3 s 윈도우 2/3/5/8킬」 · 발주서 §1-1
|
|
// 「811e KillChain 티어 이벤트(DOUBLE/TRIPLE/RAMPAGE) 구독 → 화면 중앙 상단 0.8 s 팝(스케일 펀치)」.
|
|
//
|
|
// ■ 데이터/표시 분리
|
|
// 카운터·윈도우·티어 표는 전부 811e(`WL.Combat.Reaction.KillChain` + `WLReactionSettings.tiers`)가 갖는다.
|
|
// 이 파일은 `KillChain.TierReached` 를 **구독만** 하고 문구를 띄운다(원본 훅 0 · Gameplay 코드 0).
|
|
//
|
|
// ■ 문구 톤 — PD Q5 전까지 임시 영문(발주서 §1-1). 형식·색·크기는 전부 WLCombatTextSettings 에셋.
|
|
//
|
|
// ■ Safe Area (813c 규칙)
|
|
// 이 컴포넌트는 이미 SafeAreaFitter 가 붙은 `IngameUIs/WL_HUD` 아래에 놓인다 →
|
|
// 부모가 안전 영역이므로 여기서는 **부모 기준 중앙 상단 오프셋**만 계산한다(Screen.safeArea 재계산 0).
|
|
//
|
|
// ■ 롤백(C8)
|
|
// WLCombatTextSettings 가 없거나 textEnabled/killChainTextEnabled = false 면 구독조차 하지 않는다(표시 0).
|
|
//
|
|
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System.Text;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using WL.Combat.Reaction;
|
|
|
|
namespace WL.UI
|
|
{
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class KillChainText : MonoBehaviour
|
|
{
|
|
[Header("구성 요소 (BuildIfNeeded 가 만든다)")]
|
|
[SerializeField] private CanvasGroup group;
|
|
[SerializeField] private RectTransform box;
|
|
[SerializeField] private TextMeshProUGUI mainLabel;
|
|
[SerializeField] private TextMeshProUGUI subLabel;
|
|
[SerializeField] private TMP_FontAsset font;
|
|
|
|
private RectTransform _rt;
|
|
private float _life, _lifeMax, _elapsed;
|
|
private bool _subscribed;
|
|
|
|
// ── 진단(프로브가 읽는다 · 실측만)
|
|
public int TierSeen { get; private set; }
|
|
public int LastTier { get; private set; }
|
|
public int LastKills { get; private set; }
|
|
public string LastText { get; private set; }
|
|
public bool Visible { get { return group != null && group.alpha > 0f; } }
|
|
public bool Subscribed { get { return _subscribed; } }
|
|
public float LastScale { get; private set; }
|
|
public Vector2 LastAnchoredPos { get; private set; }
|
|
/// <summary>마지막으로 적용한 아웃라인/그림자 실측 문자열(WL-813y).</summary>
|
|
public string LastEdge { get; private set; } = "";
|
|
|
|
private void OnEnable() { Initialize(); }
|
|
private void OnDisable() { Subscribe(false); }
|
|
|
|
/// <summary>에디트 모드에서도 런타임과 같은 경로를 타게 OnEnable 본체를 분리한다(813c 교훈).</summary>
|
|
public string Initialize()
|
|
{
|
|
_rt = GetComponent<RectTransform>();
|
|
BuildIfNeeded();
|
|
string layout = ApplyLayout();
|
|
Hide();
|
|
var s = WLCombatTextSettings.Instance;
|
|
bool on = WLCombatTextSettings.Enabled && s != null && s.killChainTextEnabled;
|
|
Subscribe(on);
|
|
return (on ? "구독 ON · " : "설정 off — 구독 0 · ") + layout;
|
|
}
|
|
|
|
public void Subscribe(bool on)
|
|
{
|
|
if (on == _subscribed) return;
|
|
if (on) KillChain.TierReached.Add(OnTier);
|
|
else KillChain.TierReached.Remove(OnTier);
|
|
_subscribed = on;
|
|
}
|
|
|
|
/// <summary>필요한 자식(박스 · 본문 · 부제)을 만든다. 이미 있으면 그대로 쓴다.</summary>
|
|
public bool BuildIfNeeded()
|
|
{
|
|
if (_rt == null) _rt = GetComponent<RectTransform>();
|
|
bool made = false;
|
|
if (group == null)
|
|
{
|
|
group = GetComponent<CanvasGroup>();
|
|
if (group == null) { group = gameObject.AddComponent<CanvasGroup>(); made = true; }
|
|
group.interactable = false; group.blocksRaycasts = false;
|
|
}
|
|
if (box == null) { box = WLTextFxUtil.NewChild(_rt, "Box"); made = true; }
|
|
if (mainLabel == null) { mainLabel = WLTextFxUtil.NewText(box, "Main", font, TextAlignmentOptions.Center); made = true; }
|
|
if (subLabel == null) { subLabel = WLTextFxUtil.NewText(box, "Sub", font, TextAlignmentOptions.Center); made = true; }
|
|
return made;
|
|
}
|
|
|
|
/// <summary>설정 값대로 위치·크기를 잡는다(px → 유닛 환산은 캔버스에서 구한다 · 상수 0).</summary>
|
|
public string ApplyLayout()
|
|
{
|
|
var s = WLCombatTextSettings.Instance;
|
|
if (s == null) return "WLCombatTextSettings 없음 — 배치 건너뜀";
|
|
if (_rt == null) _rt = GetComponent<RectTransform>();
|
|
if (box == null) return "구성 요소 없음 — BuildIfNeeded 먼저";
|
|
|
|
float u = WLTextFxUtil.UnitsPerPx(this, s);
|
|
|
|
// 루트는 부모(Safe Area 레이어)를 가득 채운다 — 좌표 계산의 기준이 안전 영역이 되게.
|
|
WLTextFxUtil.Stretch(_rt);
|
|
|
|
// 박스 = 화면 중앙 기준으로 위로 killChainTopOffsetPx.
|
|
box.anchorMin = box.anchorMax = new Vector2(0.5f, 0.5f);
|
|
box.pivot = new Vector2(0.5f, 0.5f);
|
|
box.sizeDelta = new Vector2(s.designWidthPx * u, (s.killChainFontPx * 2.4f) * u);
|
|
box.anchoredPosition = new Vector2(0f, s.killChainTopOffsetPx * u);
|
|
LastAnchoredPos = box.anchoredPosition;
|
|
|
|
mainLabel.rectTransform.anchorMin = new Vector2(0f, 0.5f);
|
|
mainLabel.rectTransform.anchorMax = new Vector2(1f, 1f);
|
|
mainLabel.rectTransform.offsetMin = Vector2.zero;
|
|
mainLabel.rectTransform.offsetMax = Vector2.zero;
|
|
mainLabel.fontSize = s.killChainFontPx * u;
|
|
mainLabel.alignment = TextAlignmentOptions.Bottom;
|
|
|
|
subLabel.rectTransform.anchorMin = new Vector2(0f, 0f);
|
|
subLabel.rectTransform.anchorMax = new Vector2(1f, 0.5f);
|
|
subLabel.rectTransform.offsetMin = Vector2.zero;
|
|
subLabel.rectTransform.offsetMax = Vector2.zero;
|
|
subLabel.fontSize = s.killChainFontPx * u;
|
|
subLabel.alignment = TextAlignmentOptions.Top;
|
|
|
|
// WL-813y — Q3 결함 D-6 ①: 밝은 모래 배경에서 금색 글자가 사라진다 → 아웃라인 + 드롭섀도.
|
|
var edge = s.Edge(s.killChainEdge);
|
|
LastEdge = WLTextFxUtil.ApplyEdge(mainLabel, edge);
|
|
WLTextFxUtil.ApplyEdge(subLabel, edge);
|
|
|
|
return "box pos=" + box.anchoredPosition + " size=" + box.sizeDelta + " unitsPerPx=" + u.ToString("F4") +
|
|
" fontPx=" + s.killChainFontPx + "→" + mainLabel.fontSize.ToString("F1") +
|
|
" edge[" + LastEdge + "]";
|
|
}
|
|
|
|
// ───────────────────────────────────────────── 이벤트
|
|
|
|
private void OnTier(in KillChainTierEvent e)
|
|
{
|
|
var s = WLCombatTextSettings.Instance;
|
|
if (s == null || !WLCombatTextSettings.Enabled || !s.killChainTextEnabled) return;
|
|
Show(e.tier, e.name, e.kills, s);
|
|
}
|
|
|
|
/// <summary>티어 문구를 띄운다(합성 이벤트·프로브도 이 경로를 쓴다).</summary>
|
|
public string Show(int tier, string tierName, int kills, WLCombatTextSettings s)
|
|
{
|
|
if (s == null) s = WLCombatTextSettings.Instance;
|
|
if (s == null) return "설정 없음";
|
|
BuildIfNeeded();
|
|
ApplyLayout();
|
|
|
|
TierSeen++; LastTier = tier; LastKills = kills;
|
|
string main = string.IsNullOrEmpty(s.killChainFormat) ? tierName : DSUtil.Format(s.killChainFormat, tierName, kills);
|
|
string sub = string.IsNullOrEmpty(s.killChainSubFormat) ? "" : DSUtil.Format(s.killChainSubFormat, kills);
|
|
mainLabel.text = main;
|
|
subLabel.text = sub;
|
|
LastText = main + (string.IsNullOrEmpty(sub) ? "" : " / " + sub);
|
|
|
|
var c = s.TierColor(tier);
|
|
mainLabel.color = c;
|
|
subLabel.color = new Color(c.r, c.g, c.b, c.a * 0.8f);
|
|
|
|
LastScale = s.TierScale(tier);
|
|
_lifeMax = _life = Mathf.Max(0.01f, s.killChainSeconds);
|
|
_elapsed = 0f;
|
|
group.alpha = 1f;
|
|
box.localScale = Vector3.one * (LastScale * WLTextFxUtil.PunchScale(0f, s.killChainPunchScale, s.killChainPunchSeconds));
|
|
enabled = true;
|
|
return "tier=" + tier + " name=" + tierName + " kills=" + kills + " text=\"" + LastText +
|
|
"\" scale=" + box.localScale.x.ToString("F3") + " pos=" + box.anchoredPosition + " alpha=" + group.alpha;
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
if (group != null) group.alpha = 0f;
|
|
if (box != null) box.localScale = Vector3.one;
|
|
_life = 0f;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (_life <= 0f) return;
|
|
var s = WLCombatTextSettings.Instance;
|
|
if (s == null) { Hide(); return; }
|
|
|
|
float dt = Time.unscaledDeltaTime; // 히트스톱(TimeScaleArbiter) 중에도 문구는 흐른다
|
|
_life -= dt; _elapsed += dt;
|
|
|
|
float passed = WLTextFxUtil.Passed(_life, _lifeMax);
|
|
group.alpha = WLTextFxUtil.FadeAlpha(passed, s.killChainFadeStartRatio);
|
|
box.localScale = Vector3.one * (LastScale * WLTextFxUtil.PunchScale(_elapsed, s.killChainPunchScale, s.killChainPunchSeconds));
|
|
if (_life <= 0f) Hide();
|
|
}
|
|
|
|
// ───────────────────────────────────────────── 프로브(에디트 모드 · Play 0)
|
|
|
|
/// <summary>합성 티어 이벤트 — 811e 의 실제 Dispatch 경로로 보낸다(구독 배선까지 함께 검증).</summary>
|
|
public static string RaiseFakeTier(int tier, string name, int kills)
|
|
{
|
|
var e = new KillChainTierEvent
|
|
{
|
|
tier = tier, name = name, kills = kills, centroid = Vector3.zero,
|
|
lastVictim = null, burstSpawned = false, time = Time.unscaledTime, frame = Time.frameCount
|
|
};
|
|
KillChain.TierReached.Dispatch(in e);
|
|
return "dispatch tier=" + tier + " name=" + name + " kills=" + kills +
|
|
" 구독자=" + KillChain.TierReached.Count;
|
|
}
|
|
|
|
public string Dump()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("KillChainText subscribed=" + _subscribed + " tierSeen=" + TierSeen +
|
|
" lastTier=" + LastTier + " lastKills=" + LastKills);
|
|
sb.AppendLine(" text=\"" + LastText + "\" alpha=" + (group != null ? group.alpha.ToString("F2") : "-") +
|
|
" scale=" + (box != null ? box.localScale.x.ToString("F3") : "-") +
|
|
" pos=" + (box != null ? box.anchoredPosition.ToString() : "-") +
|
|
" size=" + (box != null ? box.sizeDelta.ToString() : "-"));
|
|
sb.AppendLine(" fontSize=" + (mainLabel != null ? mainLabel.fontSize.ToString("F1") : "-") +
|
|
" color=" + (mainLabel != null ? ColorUtility.ToHtmlStringRGB(mainLabel.color) : "-"));
|
|
sb.AppendLine(" edge = " + LastEdge);
|
|
return sb.ToString();
|
|
}
|
|
|
|
public void SetFont(TMP_FontAsset f)
|
|
{
|
|
font = f;
|
|
if (mainLabel != null && f != null) mainLabel.font = f;
|
|
if (subLabel != null && f != null) subLabel.font = f;
|
|
}
|
|
}
|
|
}
|