Project_WL/Assets/WL/UI/Settings/WLCombatTextSettings.cs

476 lines
26 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLCombatTextSettings.cs — 전투 텍스트 연출 값의 단일 출처 (WL-813g · #813)
//
// 기준서 v1 §B 요소 2 「데미지 숫자 = 일반 0.6 s · 크리 1.0 s · 1.6배 · 합산 윈도우 0.25 s」
// 요소 2 「연쇄 처치 3 s 윈도우 2/3/5/8킬」(윈도우·티어 자체는 811e = WLReactionSettings)
// 요소 4 「토스트 1.2 s · 동시 3줄 · 등급 색」
// 요소 5 「레벨업 = 스탯 팝업 3개」
//
// ■ 왜 이 에셋인가 (C45 · 발주서 §2 ⓓ)
// 지속 시간·배율·상한·오프셋을 코드 상수로 두지 않는다. 이 파일에는 "필드와 기본값"만 있고,
// 게임이 실제로 읽는 값은 Resources/WL/WLCombatTextSettings.asset 이다.
//
// ■ 롤백(C8)
// 에셋이 없거나 enabled = false 면 네 연출(데미지 리듬 · 연쇄 문구 · 전리품 토스트 · 스탯 팝업)이
// 전부 무동작으로 떨어지고 HUDDMGUI 는 **원본 값(0.9 s · 스케일 1배 · 상한 없음)** 으로 돈다.
//
// ■ 좌표계 (px)
// px 값은 세로 1080×1920 기준 화면 픽셀이다(813c WLHudLayoutSettings 와 같은 규약).
// px → 캔버스 유닛 환산은 런타임에 캔버스 폭에서 구한다(802b 노트 · 세로에서 1 px = 1.7778 유닛).
//
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using UnityEngine.UI;
namespace WL.UI
{
[CreateAssetMenu(fileName = "WLCombatTextSettings", menuName = "WL/Combat Text Settings", order = 31)]
public sealed class WLCombatTextSettings : ScriptableObject
{
/// <summary>Resources 경로 — 런타임 로드 키.</summary>
public const string ResourcesPath = "WL/WLCombatTextSettings";
private static WLCombatTextSettings s_instance;
private static bool s_tried;
/// <summary>에셋이 없으면 null(호출부는 null 이면 원본 동작 유지 = C8 롤백).</summary>
public static WLCombatTextSettings Instance
{
get
{
if (s_instance == null && !s_tried)
{
s_tried = true;
s_instance = Resources.Load<WLCombatTextSettings>(ResourcesPath);
}
return s_instance;
}
}
/// <summary>에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다.</summary>
public static void ClearCache() { s_instance = null; s_tried = false; }
/// <summary>진단·A/B 전용 런타임 스위치(에셋을 건드리지 않고 끈다).</summary>
public static bool RuntimeDisabled;
/// <summary>이 웨이브의 텍스트 연출이 살아 있는가 = 에셋 존재 + enabled + 런타임 스위치.</summary>
public static bool Enabled { get { var i = Instance; return i != null && i.textEnabled && !RuntimeDisabled; } }
// ── 전체 스위치 ────────────────────────────────────────────────────────
[Header("전체 스위치")]
[Tooltip("끄면 데미지 리듬 · 연쇄 문구 · 전리품 토스트 · 스탯 팝업이 전부 무동작(원본 HUDDMGUI 동작으로 복귀). 롤백 1순위.")]
public bool textEnabled = true;
[Tooltip("표시 판정을 Console 에 남긴다(검증용 · GC 발생).")]
public bool verboseLog = false;
[Header("기준 해상도 (px · 813c 와 동일 규약)")]
[Tooltip("모든 px 값의 기준이 되는 화면 폭")]
public float designWidthPx = 1080f;
[Tooltip("모든 px 값의 기준이 되는 화면 높이")]
public float designHeightPx = 1920f;
// ── ① 데미지 숫자 리듬 (기준서 §B 요소 2) ──────────────────────────────
[Header("① 데미지 숫자 — 리듬 (HUDDMGUI · 원본 0.9 s · ±25 px 랜덤)")]
[Tooltip("리듬화 사용 여부. off 면 HUDDMGUI 가 원본 상수(0.9 s · 배율 1)로 돈다.")]
public bool damageRhythmEnabled = true;
[Tooltip("일반 타격 숫자 표시 시간(초). 기준서 §B = 0.6 s.")]
public float damageNormalSeconds = 0.6f;
[Tooltip("크리티컬 숫자 표시 시간(초). 기준서 §B = 1.0 s.")]
public float damageCritSeconds = 1.0f;
[Tooltip("크리티컬 글자 크기 배율. 기준서 §B = 1.6배(일반 = 1.0).")]
public float damageCritScale = 1.6f;
[Tooltip("일반 타격 글자 크기 배율(기준 1.0 · 숫자죽 방지로 낮출 수 있다).")]
public float damageNormalScale = 1.0f;
[Tooltip("화면에 동시에 떠 있을 수 있는 데미지 숫자 상한. 초과하면 가장 오래된 것부터 즉시 회수한다. 0 이하 = 상한 없음.")]
public int damageMaxConcurrent = 12;
[Tooltip("상한 초과로 회수할 때 크리티컬은 남기고 일반부터 회수한다.")]
public bool damageEvictNormalFirst = true;
[Tooltip("위로 떠오르는 속도(원본 1.0 · 캔버스 유닛/초).")]
public float damageRiseSpeed = 1.0f;
[Tooltip("크리티컬은 이 배율로 더 빨리 떠오른다(1 = 일반과 같음).")]
public float damageCritRiseMultiplier = 1.35f;
[Tooltip("표시 시간 대비 이 비율이 지난 뒤부터 알파가 빠지기 시작한다(0~1 · 원본은 처음부터 서서히).")]
public float damageFadeStartRatio = 0.55f;
[Tooltip("랜덤 산포 반경(px · 원본 ±25). 0 이면 정확히 대상 위. 겹침을 줄이려면 키운다.")]
public float damageScatterPx = 25f;
[Tooltip("크리티컬은 산포를 이 배율로 줄여 중앙에 가깝게 띄운다(눈에 잘 띄게).")]
public float damageCritScatterMultiplier = 0.4f;
[Tooltip("811e DamageAggregator.Flushed(합산 0.25 s)를 구독해 '합계 · N타' 를 함께 보여 준다. 데이터는 811e 소유 · 여기는 표시만.")]
public bool damageUseMergeWindow = true;
[Tooltip("합산 표시 형식 — {0} = 합계 · {1} = 타수. 빈 문자열이면 합계만 표시.")]
public string damageMergeFormat = "{0}";
[Tooltip("합산 타수가 이 값 이상일 때만 타수 꼬리표를 붙인다(damageMergeHitsSuffix).")]
public int damageMergeHitsMin = 2;
[Tooltip("타수 꼬리표 형식 — {0} = 타수. 예: \" x{0}\". 빈 문자열이면 붙이지 않는다.")]
public string damageMergeHitsSuffix = " <size=60%>x{0}</size>";
// ── ② 연쇄 처치 문구 (기준서 §B 요소 2 · 데이터는 811e KillChain) ──────
[Header("② 연쇄 처치 문구 (KillChain.TierReached 구독 · 화면 중앙 상단)")]
[Tooltip("연쇄 처치 문구 표시 여부.")]
public bool killChainTextEnabled = true;
[Tooltip("문구 표시 시간(초 · unscaled). 발주서 §1-1 = 0.8 s.")]
public float killChainSeconds = 0.8f;
[Tooltip("스케일 펀치 — 등장 순간의 배율(1.0 이면 펀치 없음).")]
public float killChainPunchScale = 1.45f;
[Tooltip("펀치가 1.0 으로 돌아오는 시간(초). 표시 시간보다 짧아야 한다.")]
public float killChainPunchSeconds = 0.18f;
[Tooltip("표시 시간 대비 이 비율이 지난 뒤부터 알파가 빠진다(0~1).")]
public float killChainFadeStartRatio = 0.6f;
[Tooltip("화면 중앙 기준 위쪽 오프셋(px · 세로 1080×1920 기준). Safe Area 안에서 잰다.")]
public float killChainTopOffsetPx = 520f;
[Tooltip("글자 크기(px · 세로 1080×1920 기준).")]
public float killChainFontPx = 96f;
[Tooltip("티어 인덱스별 글자 색(tiers 순서 = DOUBLE/TRIPLE/RAMPAGE/MASSACRE). 모자라면 마지막 색을 쓴다.")]
public Color[] killChainTierColors = new Color[]
{
new Color(1f, 0.94f, 0.72f, 1f),
new Color(1f, 0.80f, 0.35f, 1f),
new Color(1f, 0.52f, 0.22f, 1f),
new Color(1f, 0.27f, 0.28f, 1f),
};
[Tooltip("티어 인덱스별 글자 크기 배율(위 색과 같은 순서). 모자라면 마지막 값.")]
public float[] killChainTierScales = new float[] { 1f, 1.12f, 1.28f, 1.45f };
[Tooltip("문구 형식 — {0} = 티어 이름(811e 에셋의 tiers[i].name) · {1} = . PD Q5 ( §1-1).")]
public string killChainFormat = "{0}!";
[Tooltip("연쇄 킬 수를 아래 줄에 덧붙인다(빈 문자열이면 표시 안 함) — {0} = 킬 수.")]
public string killChainSubFormat = "<size=45%>{0} KILLS</size>";
// ── ③ 전리품 토스트 (기준서 §B 요소 4) ────────────────────────────────
[Header("③ 전리품 토스트 (획득 아이템 · 우측 · 등급 색)")]
[Tooltip("토스트 표시 여부.")]
public bool lootToastEnabled = true;
[Tooltip("한 줄이 남아 있는 시간(초). 기준서 §B 요소 4 = 1.2 s.")]
public float lootToastSeconds = 1.2f;
[Tooltip("동시에 보이는 줄 수. 기준서 §B 요소 4 = 3줄. 넘치면 가장 오래된 줄부터 밀려난다.")]
public int lootToastLines = 3;
[Tooltip("같은 아이템이 이 시간(초) 안에 또 들어오면 새 줄 대신 기존 줄의 수량을 올린다(줄 폭주 방지). 발주서 WL-813c2 §1-4 = 0.5 s.")]
public float lootToastMergeSeconds = 0.5f;
// ── WL-813c2 (Q6 D-31) — 획득 21건에 줄이 밀린다 ───────────────────────
// 실측: 화면 줄 수는 이미 `lootToastLines`(3)로 막혀 있고, Q6 의 「창 안 6줄」은
// 프로브가 **표시 시간 창 안의 Picked 이벤트 수**를 센 값이다(`WL813_ScoreProbe.cs:437~443`).
// 실물 결함은 넘칠 때 오래된 줄이 **페이드 없이 즉시 교체**돼 깜빡이는 것 → 큐 + 즉시 페이드.
[Tooltip("같은 아이템이 **아직 보이는 줄**로 남아 있으면 병합 창(lootToastMergeSeconds)과 무관하게 수량만 올린다. " +
"끄면 병합 창 안일 때만 병합한다(813g 동작).")]
public bool lootToastMergeWhileVisible = true;
[Tooltip("줄이 꽉 찼을 때 가장 오래된 줄을 이 시간(초) 안에 **페이드아웃**시키고 새 줄은 큐에서 기다린다. " +
"0 이면 813g 처럼 즉시 한 칸씩 당겨 교체한다(= C8 롤백).")]
public float lootToastOverflowFadeSec = 0.15f;
[Tooltip("넘쳐서 대기시킬 수 있는 최대 건수(고정 크기 링 · 할당 0). 넘으면 가장 오래 기다린 항목을 버린다.")]
public int lootToastQueueMax = 6;
[Tooltip("우측 여백(px · Safe Area 우측 기준).")]
public float lootToastRightMarginPx = 40f;
[Tooltip("첫 줄의 화면 세로 중앙 기준 오프셋(px · 양수 = 위).")]
public float lootToastCenterOffsetPx = 120f;
[Tooltip("줄 간격(px).")]
public float lootToastLineHeightPx = 54f;
[Tooltip("줄 글자 크기(px).")]
public float lootToastFontPx = 40f;
[Tooltip("줄 폭(px) — 오른쪽 정렬 기준의 상자 폭.")]
public float lootToastWidthPx = 520f;
[Tooltip("표시 시간 대비 이 비율이 지난 뒤부터 알파가 빠진다(0~1).")]
public float lootToastFadeStartRatio = 0.65f;
[Tooltip("등장할 때 오른쪽에서 밀려 들어오는 거리(px · 0 이면 슬라이드 없음).")]
public float lootToastSlideInPx = 60f;
[Tooltip("슬라이드가 끝나는 시간(초).")]
public float lootToastSlideSeconds = 0.14f;
[Tooltip("줄 형식 — {0} = 등급 색 태그 + 이름 · {1} = 개수. 개수 1 이면 lootToastFormatSingle 을 쓴다.")]
public string lootToastFormat = "{0}[-] x{1}";
[Tooltip("개수 1 일 때의 줄 형식 — {0} = 등급 색 태그 + 이름.")]
public string lootToastFormatSingle = "{0}[-]";
[Tooltip("이 등급 이상만 토스트로 띄운다(기준서 §B 요소 4 = 연출은 4 이상 · 토스트는 전부 보고 싶으면 1).")]
public int lootToastMinGrade = 1;
[Tooltip("골드(아이템 ID 2)도 토스트로 띄운다.")]
public bool lootToastIncludeGold = false;
[Tooltip("획득 집계를 초당 몇 번 확인할지(폴링 · DropItemInfo.m_StageDropData 델타). 813f 이벤트가 오면 이 폴링은 걷어낸다.")]
public float lootToastPollHz = 10f;
[Tooltip("등급 색 팔레트(index 0 = 등급 1). 비우면 MyValue.Get_GradeColor(코드 팔레트)를 그대로 쓴다.")]
public Color[] lootToastGradeColors = new Color[0];
// ─────────────────────────────────────── 전리품 토스트 Safe Area 클램프 (Q4-a D-7 · WL-813z)
// 실측 원인: 줄이 등장할 때 x 가 +lootToastSlideInPx(60) 에서 0 으로 온다. 줄 뿌리는 오른쪽 가장자리에서
// lootToastRightMarginPx(40) 안쪽에 있으므로 등장 프레임의 오른쪽 끝은 40 + 60 = **+20 px**,
// 뒤 패널(lootToastPanelPadXPx = 18)까지 더하면 **+38 px** 가 Safe Area 밖으로 나간다
// (0.14 s 동안 · Q4-a 캡처 `23_arena_12.png`·`29_levelup_01.png` 의 잘린 `[-]` 조각과 일치).
[Header("전리품 토스트 Safe Area 클램프 (Q4-a D-7 · WL-813z)")]
[Tooltip("끄면 813y 배치 100%(C8 롤백 = 0). 켜면 폭·오른쪽 여백·슬라이드 시작 x 를 Safe Area 안으로 자른다.")]
public bool lootToastClampToSafeArea = true;
[Tooltip("줄 폭 상한(px · 0 이면 상한 없음). 실제 폭 = min(lootToastWidthPx, 이 값, 부모 폭 좌우 여백).")]
public float lootToastMaxWidthPx = 560f;
[Tooltip("Safe Area 안쪽으로 더 확보할 여백(px). 뒤 패널 여백(lootToastPanelPadXPx)은 여기에 자동으로 더해진다.")]
public float lootToastSafePadPx = 8f;
[Tooltip("이름이 폭을 넘으면 줄바꿈 대신 말줄임(…)으로 자른다(TMP textWrappingMode=NoWrap · overflowMode=Ellipsis).")]
public bool lootToastEllipsis = true;
// ── ④ 레벨업 스탯 팝업 (기준서 §B 요소 5) ─────────────────────────────
[Header("④ 레벨업 스탯 팝업 (CombatEvents.LevelUp 구독 · 스탯 3줄)")]
[Tooltip("스탯 팝업 표시 여부.")]
public bool statPopupEnabled = true;
[Tooltip("팝업이 남아 있는 시간(초 · unscaled).")]
public float statPopupSeconds = 2.0f;
[Tooltip("등장 스케일 펀치(1.0 이면 없음).")]
public float statPopupPunchScale = 1.3f;
[Tooltip("펀치가 1.0 으로 돌아오는 시간(초).")]
public float statPopupPunchSeconds = 0.22f;
[Tooltip("표시 시간 대비 이 비율이 지난 뒤부터 알파가 빠진다(0~1).")]
public float statPopupFadeStartRatio = 0.7f;
[Tooltip("화면 중앙 기준 위쪽 오프셋(px).")]
public float statPopupCenterOffsetPx = 180f;
[Tooltip("제목 글자 크기(px).")]
public float statPopupTitleFontPx = 72f;
[Tooltip("스탯 줄 글자 크기(px).")]
public float statPopupRowFontPx = 46f;
[Tooltip("스탯 줄 간격(px).")]
public float statPopupRowHeightPx = 56f;
[Tooltip("보여 줄 스탯 줄 수 상한. 기준서 §B 요소 5 = 3개.")]
public int statPopupRows = 3;
[Tooltip("보여 줄 스탯 종류(순서대로 · statPopupRows 개까지). 813k 가 이전/새 값을 주면 그 값을, 아니면 이 스탯의 스냅샷 차이를 쓴다.")]
public eStat[] statPopupStats = new eStat[] { eStat.STR, eStat.DEX, eStat.INT };
[Tooltip("증가분이 0 인 스탯 줄도 보여 준다.")]
public bool statPopupShowZero = false;
[Tooltip("제목 형식 — {0} = 새 레벨.")]
public string statPopupTitleFormat = "LEVEL {0}";
[Tooltip("스탯 줄 형식 — {0} = 스탯 이름 · {1} = 증가분 · {2} = 새 값.")]
public string statPopupRowFormat = "{0} +{1}";
[Tooltip("제목 색.")]
public Color statPopupTitleColor = new Color(1f, 0.90f, 0.45f, 1f);
[Tooltip("스탯 줄 색.")]
public Color statPopupRowColor = new Color(0.55f, 0.95f, 0.62f, 1f);
[Tooltip("813k GrowthEvents.LevelUp(스탯 3종 + 잔여 포인트)을 구독한다. 끄면 811b 코어 LevelUp(레벨만)만 본다.")]
public bool statPopupUseGrowthEvents = true;
[Tooltip("스탯 변화가 0 일 때(원본은 레벨업으로 스탯을 안 올린다 · 813k ①) 대신 보여 줄 줄 — {0} = 잔여 스탯 포인트. 빈 문자열이면 표시 안 함.")]
public string statPopupPointFormat = "+5 포인트 <size=70%>(보유 {0})</size>";
[Tooltip("한 번의 Add_Exp 에서 여러 레벨이 오를 때(levelsGained ≥ 2) 마지막 레벨만 팝업한다.")]
public bool statPopupMergeMultiLevel = true;
[Header("⑤ 텍스트 아웃라인 · 그림자 (Q3 결함 D-6 · 밝은 배경 대비 · 포스트 0)")]
[Tooltip("끄면 아래 4종이 전부 원본 머티리얼로 돌아간다(C8 롤백).")]
public bool textEdgeEnabled = true;
[Tooltip("연쇄 처치 문구(KillChainText) 테두리.")]
public WLTextEdge killChainEdge = new WLTextEdge
{
enabled = true,
outlineWidth = 0.30f,
outlineColor = new Color(0.05f, 0.03f, 0.02f, 1f),
shadowOffset = new Vector2(0.55f, -0.55f),
shadowColor = new Color(0f, 0f, 0f, 0.8f),
shadowSoftness = 0.25f,
faceDilate = 0.1f,
};
[Tooltip("레벨업 스탯 팝업(StatPopup) 테두리.")]
public WLTextEdge statPopupEdge = new WLTextEdge
{
enabled = true,
outlineWidth = 0.26f,
outlineColor = new Color(0.05f, 0.03f, 0.02f, 1f),
shadowOffset = new Vector2(0.45f, -0.45f),
shadowColor = new Color(0f, 0f, 0f, 0.75f),
shadowSoftness = 0.2f,
faceDilate = 0.08f,
};
[Tooltip("데미지 숫자(HUDDMGUI) 테두리. 숫자가 작아 두께는 낮게.")]
public WLTextEdge damageEdge = new WLTextEdge
{
enabled = true,
outlineWidth = 0.22f,
outlineColor = new Color(0.03f, 0.02f, 0.02f, 1f),
shadowOffset = new Vector2(0.35f, -0.35f),
shadowColor = new Color(0f, 0f, 0f, 0.7f),
shadowSoftness = 0.15f,
faceDilate = 0.05f,
};
[Tooltip("전리품 토스트(LootToast) 테두리.")]
public WLTextEdge lootToastEdge = new WLTextEdge
{
enabled = true,
outlineWidth = 0.24f,
outlineColor = new Color(0.03f, 0.02f, 0.02f, 1f),
shadowOffset = new Vector2(0.4f, -0.4f),
shadowColor = new Color(0f, 0f, 0f, 0.7f),
shadowSoftness = 0.18f,
faceDilate = 0.06f,
};
[Header("⑥ 전리품 토스트 가시성 (Q3 결함 D-6 ② · 15회 발생 · 육안 실패)")]
[Tooltip("줄 뒤에 반투명 패널을 깐다(밝은 배경 대비 확보 · Image 1장/줄).")]
public bool lootToastPanelEnabled = true;
[Tooltip("패널 색(알파 포함).")]
public Color lootToastPanelColor = new Color(0.03f, 0.03f, 0.05f, 0.68f);
[Tooltip("패널이 글자 좌우로 넘치는 여백(px).")]
public float lootToastPanelPadXPx = 18f;
[Tooltip("패널이 글자 위아래로 넘치는 여백(px).")]
public float lootToastPanelPadYPx = 6f;
[Tooltip("등급 1(회색) 처럼 어두운 등급 색을 이 색으로 대체한다(알파 0 이면 원래 등급 색 유지).")]
public Color lootToastMinBrightColor = new Color(0.95f, 0.96f, 0.98f, 1f);
[Tooltip("등급 색의 밝기(V)가 이 값보다 어두우면 위 색으로 바꾼다(0~1 · 0 이면 대체 안 함).")]
[Range(0f, 1f)] public float lootToastMinBrightness = 0.72f;
// ── 파생값 헬퍼 (코드 상수 0) ──────────────────────────────────────────
/// <summary>
/// px → 캔버스 유닛 계수. 813c(WLHudLayoutSettings.UnitsPerPx)와 **같은 산식**을 쓴다 —
/// CanvasScaler 설정을 재현해 구하고(에디트 모드처럼 캔버스가 레이아웃 전이어도 값이 나온다),
/// 스케일러가 없으면 캔버스 rect 폭으로 떨어진다. 세로 1080×1920 · 참조 1920×1080 Expand = 1.7778(802b 실측).
/// </summary>
public float UnitsPerPx(Canvas canvas)
{
if (designWidthPx <= 0f || designHeightPx <= 0f) return 1f;
var scaler = canvas != null ? canvas.GetComponent<CanvasScaler>() : null;
if (scaler != null && scaler.uiScaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)
{
Vector2 r = scaler.referenceResolution;
if (r.x > 0f && r.y > 0f)
{
float w = designWidthPx, h = designHeightPx, scale;
switch (scaler.screenMatchMode)
{
case CanvasScaler.ScreenMatchMode.Expand: scale = Mathf.Min(w / r.x, h / r.y); break;
case CanvasScaler.ScreenMatchMode.Shrink: scale = Mathf.Max(w / r.x, h / r.y); break;
default: scale = Mathf.Pow(w / r.x, 1f - scaler.matchWidthOrHeight) *
Mathf.Pow(h / r.y, scaler.matchWidthOrHeight); break;
}
if (scale > 0f) return (w / scale) / designWidthPx;
}
}
var canvasRect = canvas != null ? canvas.transform as RectTransform : null;
if (canvasRect != null && canvasRect.rect.width > 0f) return canvasRect.rect.width / designWidthPx;
return 1f;
}
/// <summary>티어 인덱스의 색(배열이 짧으면 마지막 색 · 비어 있으면 흰색).</summary>
public Color TierColor(int tier)
{
if (killChainTierColors == null || killChainTierColors.Length == 0) return Color.white;
return killChainTierColors[Mathf.Clamp(tier, 0, killChainTierColors.Length - 1)];
}
/// <summary>티어 인덱스의 크기 배율(배열이 짧으면 마지막 값 · 비어 있으면 1).</summary>
public float TierScale(int tier)
{
if (killChainTierScales == null || killChainTierScales.Length == 0) return 1f;
return killChainTierScales[Mathf.Clamp(tier, 0, killChainTierScales.Length - 1)];
}
/// <summary>등급(1~9) 색. 팔레트가 비어 있으면 MyValue 코드 팔레트를 파싱해 쓴다.</summary>
public Color GradeColor(int grade)
{
if (lootToastGradeColors != null && lootToastGradeColors.Length > 0)
return lootToastGradeColors[Mathf.Clamp(grade - 1, 0, lootToastGradeColors.Length - 1)];
Color c;
var tag = MyValue.Get_GradeColor(grade); // "<color=#a6a6a7>"
int s = tag.IndexOf('#');
int e = tag.IndexOf('>');
if (s >= 0 && e > s && ColorUtility.TryParseHtmlString(tag.Substring(s, e - s), out c)) return c;
return Color.white;
}
/// <summary>등급의 리치텍스트 색 태그(원본 팔레트 그대로 · 기준서 §B 요소 4 「보유 팔레트 그대로」).</summary>
public string GradeColorTag(int grade) { return MyValue.Get_GradeColor(grade); }
/// <summary>
/// 밝은 배경에서도 읽히도록 보정한 등급 색 태그 (WL-813y · Q3 결함 D-6 ②).
/// 등급 1 은 원본 팔레트가 `#a6a6a7`(회색 · V≈0.65) 이라 모래 배경에서 사라진다 —
/// 밝기 V 가 <see cref="lootToastMinBrightness"/> 미만이면 색상(H)·채도(S)를 유지한 채 V 를 끌어올린다.
/// 값이 0 이거나 대체 색 알파가 0 이면 **원본 팔레트 그대로**(C8 롤백).
/// </summary>
public string GradeColorTagBright(int grade)
{
if (lootToastMinBrightness <= 0f || lootToastMinBrightColor.a <= 0f) return GradeColorTag(grade);
Color c = GradeColor(grade);
float h, s, v;
Color.RGBToHSV(c, out h, out s, out v);
if (v >= lootToastMinBrightness) return GradeColorTag(grade);
Color lift = s <= 0.05f ? lootToastMinBrightColor : Color.HSVToRGB(h, s, lootToastMinBrightness);
return "<color=#" + ColorUtility.ToHtmlStringRGB(lift) + ">";
}
/// <summary>전체 스위치(textEdgeEdge)를 반영한 테두리 값. 꺼져 있으면 enabled=false 로 준다.</summary>
public WLTextEdge Edge(WLTextEdge e)
{
if (!textEdgeEnabled) e.enabled = false;
return e;
}
}
}