337 lines
18 KiB
C#
337 lines
18 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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("같은 아이템이 이 시간(초) 안에 또 들어오면 새 줄 대신 기존 줄의 수량을 올린다(줄 폭주 방지).")]
|
||
public float lootToastMergeSeconds = 1.0f;
|
||
|
||
[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];
|
||
|
||
// ── ④ 레벨업 스탯 팝업 (기준서 §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);
|
||
|
||
// ── 파생값 헬퍼 (코드 상수 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); }
|
||
}
|
||
}
|