// ───────────────────────────────────────────────────────────────────────────── // 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 { /// Resources 경로 — 런타임 로드 키. public const string ResourcesPath = "WL/WLCombatTextSettings"; private static WLCombatTextSettings s_instance; private static bool s_tried; /// 에셋이 없으면 null(호출부는 null 이면 원본 동작 유지 = C8 롤백). public static WLCombatTextSettings Instance { get { if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load(ResourcesPath); } return s_instance; } } /// 에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다. public static void ClearCache() { s_instance = null; s_tried = false; } /// 진단·A/B 전용 런타임 스위치(에셋을 건드리지 않고 끈다). public static bool RuntimeDisabled; /// 이 웨이브의 텍스트 연출이 살아 있는가 = 에셋 존재 + enabled + 런타임 스위치. 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 = " x{0}"; // ── ② 연쇄 처치 문구 (기준서 §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 = "{0} KILLS"; // ── ③ 전리품 토스트 (기준서 §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) ────────────────────────────────────────── /// /// px → 캔버스 유닛 계수. 813c(WLHudLayoutSettings.UnitsPerPx)와 **같은 산식**을 쓴다 — /// CanvasScaler 설정을 재현해 구하고(에디트 모드처럼 캔버스가 레이아웃 전이어도 값이 나온다), /// 스케일러가 없으면 캔버스 rect 폭으로 떨어진다. 세로 1080×1920 · 참조 1920×1080 Expand = 1.7778(802b 실측). /// public float UnitsPerPx(Canvas canvas) { if (designWidthPx <= 0f || designHeightPx <= 0f) return 1f; var scaler = canvas != null ? canvas.GetComponent() : 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; } /// 티어 인덱스의 색(배열이 짧으면 마지막 색 · 비어 있으면 흰색). public Color TierColor(int tier) { if (killChainTierColors == null || killChainTierColors.Length == 0) return Color.white; return killChainTierColors[Mathf.Clamp(tier, 0, killChainTierColors.Length - 1)]; } /// 티어 인덱스의 크기 배율(배열이 짧으면 마지막 값 · 비어 있으면 1). public float TierScale(int tier) { if (killChainTierScales == null || killChainTierScales.Length == 0) return 1f; return killChainTierScales[Mathf.Clamp(tier, 0, killChainTierScales.Length - 1)]; } /// 등급(1~9) 색. 팔레트가 비어 있으면 MyValue 코드 팔레트를 파싱해 쓴다. 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); // "" 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; } /// 등급의 리치텍스트 색 태그(원본 팔레트 그대로 · 기준서 §B 요소 4 「보유 팔레트 그대로」). public string GradeColorTag(int grade) { return MyValue.Get_GradeColor(grade); } } }