Project_WL/Assets/WL/UI/Scripts/KillChainText.cs

240 lines
12 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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; }
[WL-813y] UI 핫픽스 묶음 (#813) Q3 `qa/W2-3_바퀴2.md` 결함 4·6·7·8 + 813tj 연결점 6줄 + 813k LevelUp 페이로드 + 813m 도달 747 px 대응. 재부팅으로 끊긴 이전 워커의 미커밋 작업을 인수해 이어서 완성했다. 1. 813i 연결 6줄 — WLSurvivalUiBridge(신규): PotionButton Provider 4 + ReviveDialog 2. Gameplay 소유 813i API 를 UI 쪽에서 호출만 한다(Assets/WL/Combat/** 수정 0). 2. 스탯 팝업 — StatPopup 이 813k GrowthEvents.LevelUp 을 구독(811b 코어 축과 택일 = 이중 팝업 0). 스탯 변화 0 이면 "+5 포인트(보유 N)" 줄로 떨어진다(813k 보고 ①). 3. 토스트 가시성 — 지속 1.2 → 2.0 s · 글자 40 → 52 px · 줄 뒤 반투명 패널 · 등급 1 회색(#a6a6a7) 밝기 보정(#F2F5FA) · 중심 오프셋 +120 → -120 px(스탯 +180 · 연쇄 +520 대역 회피). 4. 텍스트 아웃라인 — uGUI Outline/Shadow 는 TMP 에 안 먹으므로 TMP 머티리얼 아웃라인 + 언더레이 그림자를 쓴다. (원본 머티리얼 + 값) 조합마다 1장 캐시 공유 = 라벨별 인스턴스 0. 5. 보스 HP 바·배너 늦은 구독 보정 — 켜질 때 BossArena.Boss → 없으면 MobActor 1회 스캔. 6. 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 — HudCombatVisibility(신규 · 노드는 런타임 생성, 프리팹 diff 0) · fanOriginPx (0,260) → (0,0) · attackCenterPx (100,360) → (100,100). 엄지 도달 실측 746.8 → **490.0 px**(발주서 ≤ 490 PASS). 수정 중 실측으로 잡은 버그 2건: UnityEngine.Object 가짜 null 을 `??` 가 못 걸러 HUD_BottomMenu 에 CanvasGroup 이 붙지 않던 MissingComponentException · C8 롤백 (combatHideBottomMenu=false)이 모서리 원점을 돌려줘 메뉴와 다시 겹치던 문제. 값은 전부 SO(C8/C45) · 프리팹 0 · Gameplay/Systems 경로 0 · 핫스팟 0 · Play 0.
2026-09-08 22:58:46 +00:00
/// <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] UI 핫픽스 묶음 (#813) Q3 `qa/W2-3_바퀴2.md` 결함 4·6·7·8 + 813tj 연결점 6줄 + 813k LevelUp 페이로드 + 813m 도달 747 px 대응. 재부팅으로 끊긴 이전 워커의 미커밋 작업을 인수해 이어서 완성했다. 1. 813i 연결 6줄 — WLSurvivalUiBridge(신규): PotionButton Provider 4 + ReviveDialog 2. Gameplay 소유 813i API 를 UI 쪽에서 호출만 한다(Assets/WL/Combat/** 수정 0). 2. 스탯 팝업 — StatPopup 이 813k GrowthEvents.LevelUp 을 구독(811b 코어 축과 택일 = 이중 팝업 0). 스탯 변화 0 이면 "+5 포인트(보유 N)" 줄로 떨어진다(813k 보고 ①). 3. 토스트 가시성 — 지속 1.2 → 2.0 s · 글자 40 → 52 px · 줄 뒤 반투명 패널 · 등급 1 회색(#a6a6a7) 밝기 보정(#F2F5FA) · 중심 오프셋 +120 → -120 px(스탯 +180 · 연쇄 +520 대역 회피). 4. 텍스트 아웃라인 — uGUI Outline/Shadow 는 TMP 에 안 먹으므로 TMP 머티리얼 아웃라인 + 언더레이 그림자를 쓴다. (원본 머티리얼 + 값) 조합마다 1장 캐시 공유 = 라벨별 인스턴스 0. 5. 보스 HP 바·배너 늦은 구독 보정 — 켜질 때 BossArena.Boss → 없으면 MobActor 1회 스캔. 6. 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 — HudCombatVisibility(신규 · 노드는 런타임 생성, 프리팹 diff 0) · fanOriginPx (0,260) → (0,0) · attackCenterPx (100,360) → (100,100). 엄지 도달 실측 746.8 → **490.0 px**(발주서 ≤ 490 PASS). 수정 중 실측으로 잡은 버그 2건: UnityEngine.Object 가짜 null 을 `??` 가 못 걸러 HUD_BottomMenu 에 CanvasGroup 이 붙지 않던 MissingComponentException · C8 롤백 (combatHideBottomMenu=false)이 모서리 원점을 돌려줘 메뉴와 다시 겹치던 문제. 값은 전부 SO(C8/C45) · 프리팹 0 · Gameplay/Systems 경로 0 · 핫스팟 0 · Play 0.
2026-09-08 22:58:46 +00:00
// 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") +
[WL-813y] UI 핫픽스 묶음 (#813) Q3 `qa/W2-3_바퀴2.md` 결함 4·6·7·8 + 813tj 연결점 6줄 + 813k LevelUp 페이로드 + 813m 도달 747 px 대응. 재부팅으로 끊긴 이전 워커의 미커밋 작업을 인수해 이어서 완성했다. 1. 813i 연결 6줄 — WLSurvivalUiBridge(신규): PotionButton Provider 4 + ReviveDialog 2. Gameplay 소유 813i API 를 UI 쪽에서 호출만 한다(Assets/WL/Combat/** 수정 0). 2. 스탯 팝업 — StatPopup 이 813k GrowthEvents.LevelUp 을 구독(811b 코어 축과 택일 = 이중 팝업 0). 스탯 변화 0 이면 "+5 포인트(보유 N)" 줄로 떨어진다(813k 보고 ①). 3. 토스트 가시성 — 지속 1.2 → 2.0 s · 글자 40 → 52 px · 줄 뒤 반투명 패널 · 등급 1 회색(#a6a6a7) 밝기 보정(#F2F5FA) · 중심 오프셋 +120 → -120 px(스탯 +180 · 연쇄 +520 대역 회피). 4. 텍스트 아웃라인 — uGUI Outline/Shadow 는 TMP 에 안 먹으므로 TMP 머티리얼 아웃라인 + 언더레이 그림자를 쓴다. (원본 머티리얼 + 값) 조합마다 1장 캐시 공유 = 라벨별 인스턴스 0. 5. 보스 HP 바·배너 늦은 구독 보정 — 켜질 때 BossArena.Boss → 없으면 MobActor 1회 스캔. 6. 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 — HudCombatVisibility(신규 · 노드는 런타임 생성, 프리팹 diff 0) · fanOriginPx (0,260) → (0,0) · attackCenterPx (100,360) → (100,100). 엄지 도달 실측 746.8 → **490.0 px**(발주서 ≤ 490 PASS). 수정 중 실측으로 잡은 버그 2건: UnityEngine.Object 가짜 null 을 `??` 가 못 걸러 HUD_BottomMenu 에 CanvasGroup 이 붙지 않던 MissingComponentException · C8 롤백 (combatHideBottomMenu=false)이 모서리 원점을 돌려줘 메뉴와 다시 겹치던 문제. 값은 전부 SO(C8/C45) · 프리팹 0 · Gameplay/Systems 경로 0 · 핫스팟 0 · Play 0.
2026-09-08 22:58:46 +00:00
" 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) : "-"));
[WL-813y] UI 핫픽스 묶음 (#813) Q3 `qa/W2-3_바퀴2.md` 결함 4·6·7·8 + 813tj 연결점 6줄 + 813k LevelUp 페이로드 + 813m 도달 747 px 대응. 재부팅으로 끊긴 이전 워커의 미커밋 작업을 인수해 이어서 완성했다. 1. 813i 연결 6줄 — WLSurvivalUiBridge(신규): PotionButton Provider 4 + ReviveDialog 2. Gameplay 소유 813i API 를 UI 쪽에서 호출만 한다(Assets/WL/Combat/** 수정 0). 2. 스탯 팝업 — StatPopup 이 813k GrowthEvents.LevelUp 을 구독(811b 코어 축과 택일 = 이중 팝업 0). 스탯 변화 0 이면 "+5 포인트(보유 N)" 줄로 떨어진다(813k 보고 ①). 3. 토스트 가시성 — 지속 1.2 → 2.0 s · 글자 40 → 52 px · 줄 뒤 반투명 패널 · 등급 1 회색(#a6a6a7) 밝기 보정(#F2F5FA) · 중심 오프셋 +120 → -120 px(스탯 +180 · 연쇄 +520 대역 회피). 4. 텍스트 아웃라인 — uGUI Outline/Shadow 는 TMP 에 안 먹으므로 TMP 머티리얼 아웃라인 + 언더레이 그림자를 쓴다. (원본 머티리얼 + 값) 조합마다 1장 캐시 공유 = 라벨별 인스턴스 0. 5. 보스 HP 바·배너 늦은 구독 보정 — 켜질 때 BossArena.Boss → 없으면 MobActor 1회 스캔. 6. 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 — HudCombatVisibility(신규 · 노드는 런타임 생성, 프리팹 diff 0) · fanOriginPx (0,260) → (0,0) · attackCenterPx (100,360) → (100,100). 엄지 도달 실측 746.8 → **490.0 px**(발주서 ≤ 490 PASS). 수정 중 실측으로 잡은 버그 2건: UnityEngine.Object 가짜 null 을 `??` 가 못 걸러 HUD_BottomMenu 에 CanvasGroup 이 붙지 않던 MissingComponentException · C8 롤백 (combatHideBottomMenu=false)이 모서리 원점을 돌려줘 메뉴와 다시 겹치던 문제. 값은 전부 SO(C8/C45) · 프리팹 0 · Gameplay/Systems 경로 0 · 핫스팟 0 · Play 0.
2026-09-08 22:58:46 +00:00
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;
}
}
}