[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.
This commit is contained in:
parent
f408618b31
commit
3d4936cf36
|
|
@ -0,0 +1,117 @@
|
|||
// WL-813y 적용 — 설정 에셋 값 갱신(C8/C45 · 코드 상수 0). 프리팹은 건드리지 않는다.
|
||||
// run_script --file AgentScripts/WL813y_Apply.cs --entry WL813y_Apply.Assets
|
||||
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using WL.UI;
|
||||
|
||||
public static class WL813y_Apply
|
||||
{
|
||||
public static string Assets()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// ── ① 전투 패드 원점 복귀 + 하단 메뉴 숨김 (발주서 §1-6) ─────────────
|
||||
var hud = Resources.Load<WLHudLayoutSettings>(WLHudLayoutSettings.ResourcesPath);
|
||||
if (hud == null) sb.AppendLine("WLHudLayoutSettings 없음");
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[hud] 전 fanOrigin" + hud.fanOriginPx + " attackCenter" + hud.attackCenterPx);
|
||||
hud.fanOriginPx = new Vector2(0f, 0f); // Q3 D-5: Lead 핫픽스 260 → 0
|
||||
hud.attackCenterPx = new Vector2(100f, 100f); // 360 → 100
|
||||
hud.idleFanOriginPx = new Vector2(0f, 260f); // 메뉴가 보이는 동안만 쓰던 자리
|
||||
hud.idleAttackCenterPx = new Vector2(100f, 360f);
|
||||
hud.combatHideBottomMenu = true;
|
||||
hud.bottomMenuPath = "IngameUIs/WL_HUD/HUD_BottomMenu"; // 813y 실측 경로
|
||||
hud.bottomMenuUseCanvasGroup = true;
|
||||
hud.bottomMenuFadeSeconds = 0.18f;
|
||||
hud.combatExitSeconds = 6f;
|
||||
hud.combatEnterDelaySeconds = 0f;
|
||||
hud.combatUseTargetAsSignal = true;
|
||||
hud.bottomMenuRestoreButton = true;
|
||||
hud.restoreButtonCenterPx = new Vector2(-380f, 62f);
|
||||
hud.restoreButtonDiameterPx = 88f;
|
||||
hud.restoreButtonLabel = "≡";
|
||||
hud.restoreButtonFontPx = 46f;
|
||||
hud.restoreHoldSeconds = 8f;
|
||||
hud.bossBarBindExisting = true; // Q3 D-3
|
||||
EditorUtility.SetDirty(hud);
|
||||
sb.AppendLine("[hud] 후 fanOrigin" + hud.fanOriginPx + " attackCenter" + hud.attackCenterPx +
|
||||
" idleFanOrigin" + hud.idleFanOriginPx + " idleAttack" + hud.idleAttackCenterPx +
|
||||
" menu=" + hud.bottomMenuPath + " exit=" + hud.combatExitSeconds + "s");
|
||||
}
|
||||
|
||||
// ── ② 토스트 가시성 + 텍스트 아웃라인 (발주서 §1-3 · §1-4) ───────────
|
||||
var txt = Resources.Load<WLCombatTextSettings>(WLCombatTextSettings.ResourcesPath);
|
||||
if (txt == null) sb.AppendLine("WLCombatTextSettings 없음");
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[text] 전 toast(sec=" + txt.lootToastSeconds + " font=" + txt.lootToastFontPx +
|
||||
" w=" + txt.lootToastWidthPx + " lh=" + txt.lootToastLineHeightPx +
|
||||
" centerOff=" + txt.lootToastCenterOffsetPx + ")");
|
||||
txt.lootToastSeconds = 2.0f; // 발주서 §1-3: 1.2 → 2.0 s
|
||||
txt.lootToastFontPx = 52f; // Q3 D-6 ②: 40 px 는 세로 캡처에서 육안 실패
|
||||
txt.lootToastLineHeightPx = 68f; // 폰트 + 패널 여백이 들어갈 줄 간격
|
||||
txt.lootToastWidthPx = 560f;
|
||||
txt.lootToastCenterOffsetPx = -120f; // 스탯 팝업(+180) · 연쇄 문구(+520) 대역을 피한다
|
||||
txt.lootToastRightMarginPx = 40f;
|
||||
txt.lootToastPanelEnabled = true;
|
||||
txt.lootToastPanelColor = new Color(0.03f, 0.03f, 0.05f, 0.68f);
|
||||
txt.lootToastPanelPadXPx = 18f;
|
||||
txt.lootToastPanelPadYPx = 6f;
|
||||
txt.lootToastMinBrightness = 0.72f; // 등급 1 회색(#a6a6a7 · V 0.65)을 끌어올린다
|
||||
txt.lootToastMinBrightColor = new Color(0.95f, 0.96f, 0.98f, 1f);
|
||||
|
||||
txt.textEdgeEnabled = true;
|
||||
txt.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,
|
||||
};
|
||||
txt.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,
|
||||
};
|
||||
txt.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,
|
||||
};
|
||||
txt.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,
|
||||
};
|
||||
|
||||
txt.statPopupUseGrowthEvents = true; // 발주서 §1-2 · 813k 축
|
||||
txt.statPopupPointFormat = "+5 포인트 <size=70%>(보유 {0})</size>";
|
||||
txt.statPopupMergeMultiLevel = true;
|
||||
EditorUtility.SetDirty(txt);
|
||||
sb.AppendLine("[text] 후 toast(sec=" + txt.lootToastSeconds + " font=" + txt.lootToastFontPx +
|
||||
" w=" + txt.lootToastWidthPx + " lh=" + txt.lootToastLineHeightPx +
|
||||
" centerOff=" + txt.lootToastCenterOffsetPx + " panel=" + txt.lootToastPanelEnabled + ")");
|
||||
sb.AppendLine("[text] edge kill=" + txt.killChainEdge + " / stat=" + txt.statPopupEdge +
|
||||
" / dmg=" + txt.damageEdge + " / loot=" + txt.lootToastEdge);
|
||||
}
|
||||
|
||||
// ── ③ 보스 배너 늦은 구독 (발주서 §1-5) ──────────────────────────────
|
||||
var boss = Resources.Load<WLBossUiSettings>(WLBossUiSettings.ResourcesPath);
|
||||
if (boss == null) sb.AppendLine("WLBossUiSettings 없음");
|
||||
else
|
||||
{
|
||||
boss.bannerTriggerExisting = true;
|
||||
EditorUtility.SetDirty(boss);
|
||||
sb.AppendLine("[boss] bannerTriggerExisting=" + boss.bannerTriggerExisting);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
// WL-813y 프로브 — UI 핫픽스 묶음 실측(에디트 모드 · Play 0).
|
||||
// run_script --file AgentScripts/WL813y_Probe.cs --entry WL813y_Probe.<Entry>
|
||||
// Nodes · Bridge · Stat · Text · BossLate · Hud
|
||||
// 로그는 <worktree>/Logs/WL813y_*.txt (Assets 밖 · 미추적).
|
||||
// 🔴 프리팹은 LoadPrefabContents 로 열고 **저장하지 않는다**. 구독·임시 노드는 전부 되돌린다.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using WL.UI;
|
||||
|
||||
public static class WL813y_Probe
|
||||
{
|
||||
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||||
const string kHud = "IngameUIs/WL_HUD";
|
||||
const string kPad = "IngameUIs/BattleUI";
|
||||
const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
|
||||
static string LogDir
|
||||
{
|
||||
get
|
||||
{
|
||||
string d = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Logs");
|
||||
if (!Directory.Exists(d)) Directory.CreateDirectory(d);
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
static string Write(string name, string body)
|
||||
{
|
||||
string p = Path.Combine(LogDir, name);
|
||||
File.WriteAllText(p, body, new UTF8Encoding(false));
|
||||
return p;
|
||||
}
|
||||
|
||||
static Transform Find(Transform root, string path)
|
||||
{
|
||||
var cur = root;
|
||||
foreach (var part in path.Split('/'))
|
||||
{
|
||||
Transform next = null;
|
||||
for (int i = 0; i < cur.childCount; i++)
|
||||
if (cur.GetChild(i).name == part) { next = cur.GetChild(i); break; }
|
||||
if (next == null) return null;
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
static string PathOf(Transform t, Transform root)
|
||||
{
|
||||
var sb = new StringBuilder(t.name);
|
||||
var c = t.parent;
|
||||
while (c != null && c != root) { sb.Insert(0, c.name + "/"); c = c.parent; }
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// ── ① 프리팹 노드 탐색 — HUD_BottomMenu 경로 · WL_HUD 자식 · 패드 좌표 ──────
|
||||
public static string Nodes()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
try
|
||||
{
|
||||
var rt = root.transform;
|
||||
sb.AppendLine("[1] 루트 자식 " + rt.childCount + "개");
|
||||
foreach (Transform c in rt) sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] 이름에 'Bottom' / 'Menu' 가 든 노드 (전수 탐색)");
|
||||
var all = root.GetComponentsInChildren<Transform>(true);
|
||||
sb.AppendLine(" 총 Transform = " + all.Length);
|
||||
foreach (var t in all)
|
||||
{
|
||||
if (t.name.IndexOf("Bottom", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||
t.name.IndexOf("Menu", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
||||
var r = t as RectTransform;
|
||||
sb.AppendLine(" - " + PathOf(t, rt) + " active=" + t.gameObject.activeSelf +
|
||||
" children=" + t.childCount +
|
||||
(r != null ? " pos" + r.anchoredPosition + " size" + r.sizeDelta +
|
||||
" aMin" + r.anchorMin + " aMax" + r.anchorMax + " pivot" + r.pivot : ""));
|
||||
foreach (Transform cc in t) sb.AppendLine(" · " + cc.name + " active=" + cc.gameObject.activeSelf);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] IngameUIs 아래 1단계");
|
||||
var ing = WLVignetteUtil.FindUiPath(rt, "IngameUIs");
|
||||
if (ing == null) sb.AppendLine(" IngameUIs 없음");
|
||||
else foreach (Transform c in ing) sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf + " children=" + c.childCount);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[4] WL_HUD 자식");
|
||||
var hud = WLVignetteUtil.FindUiPath(rt, kHud);
|
||||
if (hud == null) sb.AppendLine(" WL_HUD 없음");
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" WL_HUD SafeAreaFitter=" + (hud.GetComponent<SafeAreaFitter>() != null));
|
||||
foreach (Transform c in hud)
|
||||
{
|
||||
var comps = c.GetComponents<Component>();
|
||||
var names = new List<string>();
|
||||
foreach (var cp in comps) names.Add(cp == null ? "(missing)" : cp.GetType().Name);
|
||||
sb.AppendLine(" - " + c.name + " [" + string.Join(", ", names.ToArray()) + "] children=" + c.childCount);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[5] BattleUI 자식");
|
||||
var bui = WLVignetteUtil.FindUiPath(rt, kPad);
|
||||
if (bui == null) sb.AppendLine(" BattleUI 없음");
|
||||
else foreach (Transform c in bui)
|
||||
{
|
||||
var r = c as RectTransform;
|
||||
sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf +
|
||||
(r != null ? " pos" + r.anchoredPosition + " size" + r.sizeDelta + " aMin" + r.anchorMin : ""));
|
||||
}
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
|
||||
string p = Write("WL813y_nodes.txt", sb.ToString());
|
||||
return sb.ToString() + "\n→ " + p;
|
||||
}
|
||||
|
||||
// ═══ ⓑ ① 813i 연결 6줄 (발주서 §1-1) ═══════════════════════════════════
|
||||
public static string Bridge()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
int ov0 = WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride;
|
||||
float ts0 = Time.timeScale;
|
||||
try
|
||||
{
|
||||
var hud = Find(root.transform, kHud);
|
||||
var pad = Find(root.transform, kPad);
|
||||
var dlgT = hud != null ? hud.Find("WL_ReviveDialog") : null;
|
||||
var dlg = dlgT != null ? dlgT.GetComponent<ReviveDialog>() : null;
|
||||
var potion = pad != null ? pad.GetComponentInChildren<PotionButton>(true) : null;
|
||||
if (dlg == null) return "🔴 WL_ReviveDialog 없음";
|
||||
if (potion == null) return "🔴 WL_PotionButton 없음";
|
||||
dlg.Initialize();
|
||||
potion.Initialize();
|
||||
|
||||
sb.AppendLine("[0] 연결 전 (813tj 상태)");
|
||||
WLSurvivalUiBridge.Uninstall();
|
||||
WLSurvivalUiBridge.ResetDiagnostics();
|
||||
sb.AppendLine(" " + WLSurvivalUiBridge.Dump().TrimEnd());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[1] Install() — 6줄 연결");
|
||||
sb.AppendLine(" " + WLSurvivalUiBridge.Install());
|
||||
sb.Append(WLSurvivalUiBridge.Dump());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] 물약 4줄 — 스위치 off(에셋 enabled=false · 병합 전) → 813tj 표시용 폴백");
|
||||
WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = -1;
|
||||
sb.AppendLine(" survivalEnabled=" + WL.Combat.Survival.WLSurvivalSettings.Enabled +
|
||||
" · UI 가 읽는 count=" + PotionButton.CountProvider() +
|
||||
" cdRemain=" + PotionButton.CooldownRemainProvider().ToString("F2") +
|
||||
" cdTotal=" + PotionButton.CooldownTotalProvider().ToString("F2") +
|
||||
" · 버튼 ReadCount=" + potion.ReadCount() + " fill=" + potion.ReadCooldownFill().ToString("F3"));
|
||||
sb.AppendLine(" 클릭 → " + potion.OnClickPotion());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] 물약 4줄 — 스위치 on(RuntimeOverride=1 · Lead 가 병합 시 켜는 상태) → 813i PotionUse");
|
||||
WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = 1;
|
||||
WL.Combat.Survival.PotionUse.ResetRun("probe");
|
||||
WL.Combat.Survival.PotionUse.ResetDiagnostics();
|
||||
sb.AppendLine(" survivalEnabled=" + WL.Combat.Survival.WLSurvivalSettings.Enabled +
|
||||
" · 813i Remaining=" + WL.Combat.Survival.PotionUse.Remaining + "/" + WL.Combat.Survival.PotionUse.Max +
|
||||
" cdTotal=" + WL.Combat.Survival.PotionUse.CooldownTotal.ToString("F2") + "s");
|
||||
sb.AppendLine(" UI 가 읽는 count=" + PotionButton.CountProvider() +
|
||||
" cdRemain=" + PotionButton.CooldownRemainProvider().ToString("F2") +
|
||||
" cdTotal=" + PotionButton.CooldownTotalProvider().ToString("F2") +
|
||||
" ← ① ② ③ 이 813i 값을 그대로 읽는다");
|
||||
int chg0 = WLSurvivalUiBridge.PotionChangedSeen;
|
||||
sb.AppendLine(" ④ UseHandler() → " + PotionButton.UseHandler() +
|
||||
" (PC 없는 에디트 모드라 813i 가 " + WL.Combat.Survival.PotionUse.LastResult +
|
||||
" 로 거절 · 호출은 도달) · Changed 수신 " + chg0 + "→" + WLSurvivalUiBridge.PotionChangedSeen);
|
||||
sb.AppendLine(" 버튼 클릭 경로 → " + potion.OnClickPotion());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[4] 부활 2줄 — 813i DeathFlow 왕복(가짜 메인 PC · ReviveOverride 로 원본 회복 경로 차단)");
|
||||
var deathFlowOverride = WL.Combat.Survival.DeathFlow.ReviveOverride;
|
||||
int overrideCalls = 0;
|
||||
WL.Combat.Survival.DeathFlow.ReviveOverride = (p, r) => { overrideCalls++; };
|
||||
var pc = MakePC();
|
||||
try
|
||||
{
|
||||
WL.Combat.Survival.DeathFlow.ResetDiagnostics();
|
||||
WLSurvivalUiBridge.ResetDiagnostics();
|
||||
dlg.HideNow();
|
||||
float t = 100f;
|
||||
WL.Combat.Survival.DeathFlow.Tick(t);
|
||||
sb.AppendLine(" ⑤ OnPCDied → " + "state=" + WL.Combat.Survival.DeathFlow.State);
|
||||
WL.Combat.Survival.DeathFlow.OnPCDied(pc);
|
||||
sb.AppendLine(" Died 수신=" + WLSurvivalUiBridge.DiedSeen + " state=" + WL.Combat.Survival.DeathFlow.State +
|
||||
" · 팝업 표시=" + dlg.FallbackVisible + "(사망 연출 대기 · 예약=" + dlg.Pending + ")");
|
||||
var st = WL.Combat.Survival.WLSurvivalSettings.Instance;
|
||||
float disp = st != null ? st.deathDisplaySeconds : 2f;
|
||||
WL.Combat.Survival.DeathFlow.Tick(t + disp + 0.01f);
|
||||
sb.AppendLine(" +" + disp.ToString("F2") + "s → ReviveRequested 수신=" + WLSurvivalUiBridge.ReviveRequestedSeen +
|
||||
" state=" + WL.Combat.Survival.DeathFlow.State +
|
||||
" · 팝업 표시=" + dlg.FallbackVisible + " shows=" + dlg.ShownCount +
|
||||
" 본문=\"" + dlg.LastMessage.Replace("\n", " / ") + "\"");
|
||||
sb.AppendLine(" autoReviveWhenNoListener 무력화 = ReviveRequested 구독자 " +
|
||||
WL.Combat.Survival.DeathFlow.ReviveRequested.Count + "(0 이면 813i 가 자동 부활한다)");
|
||||
sb.AppendLine(" ⑥ 팝업 확인 버튼 → " + dlg.OnConfirm());
|
||||
sb.AppendLine(" DeathFlow.Revive() 호출=" + WLSurvivalUiBridge.ReviveCalls +
|
||||
" state=" + WL.Combat.Survival.DeathFlow.State +
|
||||
" reviveCount=" + WL.Combat.Survival.DeathFlow.ReviveCount +
|
||||
" (ReviveOverride 호출 " + overrideCalls + ")");
|
||||
sb.AppendLine(" Revived 수신=" + WLSurvivalUiBridge.RevivedSeen + " → 팝업 닫힘=" + !dlg.FallbackVisible);
|
||||
sb.AppendLine(" 최종 " + WLSurvivalUiBridge.Dump().Split('\n')[0].Trim());
|
||||
}
|
||||
finally
|
||||
{
|
||||
WL.Combat.Survival.DeathFlow.ReviveOverride = deathFlowOverride;
|
||||
if (pc != null) UnityEngine.Object.DestroyImmediate(pc.gameObject);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
WLSurvivalUiBridge.Uninstall();
|
||||
WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = ov0;
|
||||
WL.Combat.Survival.DeathFlow.ResetDiagnostics();
|
||||
Time.timeScale = ts0;
|
||||
KillRunners();
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("정리: 브리지 해제=" + !WLSurvivalUiBridge.Installed +
|
||||
" · DeathFlow 구독(Died/Req/Revived)=" + WL.Combat.Survival.DeathFlow.Died.Count + "/" +
|
||||
WL.Combat.Survival.DeathFlow.ReviveRequested.Count + "/" + WL.Combat.Survival.DeathFlow.Revived.Count +
|
||||
" · PotionButton.HasProvider=" + PotionButton.HasProvider +
|
||||
" · timeScale=" + Time.timeScale + " · SurvivalRunner=" + WL.Combat.Survival.SurvivalRunner.Exists);
|
||||
var body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813y_bridge.txt", body);
|
||||
}
|
||||
|
||||
static PCActor MakePC()
|
||||
{
|
||||
var go = new GameObject("__WL813y_PC");
|
||||
var pc = go.AddComponent<PCActor>();
|
||||
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
|
||||
var stat = new ActorStatInfo(eRole.PC);
|
||||
stat.Set_Stat(eStat.MaxHP, 1000d); stat.Set_Stat(eStat.HP, 0d);
|
||||
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
|
||||
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
|
||||
typeof(Actor).GetField("DeadStatus", NP).SetValue(pc, true);
|
||||
return pc;
|
||||
}
|
||||
|
||||
static void KillRunners()
|
||||
{
|
||||
var all = UnityEngine.Object.FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
foreach (var m in all)
|
||||
{
|
||||
if (m == null) continue;
|
||||
string n = m.GetType().Name;
|
||||
if (n == "SurvivalRunner" || n == "HudCombatVisibilityRunner")
|
||||
UnityEngine.Object.DestroyImmediate(m.gameObject);
|
||||
}
|
||||
var leftovers = UnityEngine.Object.FindObjectsByType<Actor>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
foreach (var a in leftovers)
|
||||
if (a != null && a.name.StartsWith("__WL813y_")) UnityEngine.Object.DestroyImmediate(a.gameObject);
|
||||
}
|
||||
|
||||
// ═══ ⓑ ② 스탯 팝업 3줄 (발주서 §1-2) ═══════════════════════════════════
|
||||
public static string Stat()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
StatPopup sp = null;
|
||||
try
|
||||
{
|
||||
var hud = Find(root.transform, kHud);
|
||||
var node = hud != null ? hud.Find("WL_StatPopup") : null;
|
||||
sp = node != null ? node.GetComponent<StatPopup>() : null;
|
||||
if (sp == null) return "🔴 WL_StatPopup 없음";
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null) return "🔴 WLCombatTextSettings 에셋 없음";
|
||||
|
||||
sb.AppendLine("[0] 구독 축 선택 (statPopupUseGrowthEvents=" + s.statPopupUseGrowthEvents + ")");
|
||||
sb.AppendLine(" 구독 전 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count +
|
||||
" core=" + WL.Combat.Core.CombatEvents.LevelUp.Count);
|
||||
sb.AppendLine(" " + sp.Initialize());
|
||||
sb.AppendLine(" 구독 후 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count +
|
||||
" core=" + WL.Combat.Core.CombatEvents.LevelUp.Count +
|
||||
" · UsingGrowthEvents=" + sp.UsingGrowthEvents +
|
||||
" ← 813k 축 하나만 문다(이중 팝업 방지)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[1] 813k 실제 Dispatch — 스탯이 오른 경우(statDelta=+12) → 3줄");
|
||||
sb.AppendLine(" " + StatPopup.RaiseFakeGrowthLevelUp(7, 30, 12d));
|
||||
sb.AppendLine(" rows=" + sp.LastRowCount + " 포인트줄=" + sp.UsedPointLine +
|
||||
" visible=" + sp.Visible + " level=" + sp.LastLevel);
|
||||
sb.AppendLine(" text = " + sp.LastText);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] 813k 실제 Dispatch — 스탯 변화 0(원본 기본값 · 813k 보고 ①) → \"포인트\" 줄");
|
||||
sb.AppendLine(" " + StatPopup.RaiseFakeGrowthLevelUp(8, 35, 0d));
|
||||
sb.AppendLine(" rows=" + sp.LastRowCount + " 포인트줄=" + sp.UsedPointLine +
|
||||
" statPointTotal=" + sp.LastStatPointTotal + " levelsGained=" + sp.LastLevelsGained);
|
||||
sb.AppendLine(" text = " + sp.LastText);
|
||||
sb.AppendLine(" 포맷 = \"" + s.statPopupPointFormat + "\" (SO · C8)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] 811b 코어 축은 이제 안 문다 — CombatEvents.LevelUp 을 쏴도 무반응(이중 팝업 0)");
|
||||
int before = sp.LevelUpSeen;
|
||||
sb.AppendLine(" " + StatPopup.RaiseFakeLevelUp(9));
|
||||
sb.AppendLine(" levelUpSeen " + before + "→" + sp.LevelUpSeen + " lastLevel=" + sp.LastLevel + "(8 이어야 한다)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[4] 최종 덤프");
|
||||
sb.Append(sp.Dump());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (sp != null) sp.Subscribe(false);
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("해제 후 구독자 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count +
|
||||
" core=" + WL.Combat.Core.CombatEvents.LevelUp.Count + " (0/0 이어야 한다)");
|
||||
var body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813y_stat.txt", body);
|
||||
}
|
||||
|
||||
// ═══ ⓑ ③④ 토스트 가시성 · 텍스트 아웃라인 (발주서 §1-3 · §1-4) ═════════
|
||||
public static string Text()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
LootToast lt = null; KillChainText kc = null; StatPopup sp = null;
|
||||
try
|
||||
{
|
||||
var hud = Find(root.transform, kHud);
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null) return "🔴 WLCombatTextSettings 에셋 없음";
|
||||
lt = hud.Find("WL_LootToast").GetComponent<LootToast>();
|
||||
kc = hud.Find("WL_KillChainText").GetComponent<KillChainText>();
|
||||
sp = hud.Find("WL_StatPopup").GetComponent<StatPopup>();
|
||||
|
||||
float u = WLVignetteUtil.UnitsPerPx(hud.GetComponentInParent<Canvas>());
|
||||
sb.AppendLine("[0] 기준 — 설계 " + s.designWidthPx + "×" + WLHudLayoutSettings.Instance.designHeightPx +
|
||||
" · unitsPerPx=" + u.ToString("F4") + " · textEdgeEnabled=" + s.textEdgeEnabled);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[1] 토스트 배치 (Q3 결함 D-6 ② · 15회 발생 · 육안 실패)");
|
||||
sb.AppendLine(" 지속 " + s.lootToastSeconds + "s (발주서 1.2 → 2.0) · 글자 " + s.lootToastFontPx +
|
||||
"px · 줄높이 " + s.lootToastLineHeightPx + "px · 폭 " + s.lootToastWidthPx +
|
||||
"px · 우측여백 " + s.lootToastRightMarginPx + "px · 중심오프셋 " + s.lootToastCenterOffsetPx + "px");
|
||||
sb.AppendLine(" " + lt.Initialize());
|
||||
sb.AppendLine(" " + lt.ApplyLayout());
|
||||
var ltRt = lt.GetComponent<RectTransform>();
|
||||
sb.AppendLine(" 루트 pos" + ltRt.anchoredPosition + " size" + ltRt.sizeDelta +
|
||||
" aMin" + ltRt.anchorMin + " aMax" + ltRt.anchorMax +
|
||||
" · SafeArea 안(부모 WL_HUD SafeAreaFitter=" +
|
||||
(hud.GetComponent<SafeAreaFitter>() != null) + ")");
|
||||
sb.AppendLine(" 등급 색 보정: 등급1 " + s.GradeColorTag(1) + " → " + s.GradeColorTagBright(1) +
|
||||
" · 등급2 " + s.GradeColorTag(2) + " → " + s.GradeColorTagBright(2) +
|
||||
" (밝기하한 " + s.lootToastMinBrightness + ")");
|
||||
sb.AppendLine(" " + lt.Push(1, 3));
|
||||
sb.AppendLine(" " + lt.Push(2, 1));
|
||||
sb.Append(lt.Dump());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] 아웃라인 · 그림자 — TMP 머티리얼 실측값(uGUI Outline/Shadow 는 TMP 에 안 먹는다)");
|
||||
sb.AppendLine(" [연쇄] 값 " + s.Edge(s.killChainEdge));
|
||||
sb.AppendLine(" " + kc.Initialize());
|
||||
sb.AppendLine(" 실측 " + kc.LastEdge);
|
||||
sb.AppendLine(" [스탯] 값 " + s.Edge(s.statPopupEdge));
|
||||
sb.AppendLine(" " + sp.Initialize());
|
||||
sb.AppendLine(" 실측 " + sp.LastEdge);
|
||||
sb.AppendLine(" [토스트] 값 " + s.Edge(s.lootToastEdge));
|
||||
sb.AppendLine(" 실측 " + lt.LastEdge);
|
||||
sb.AppendLine(" [데미지] 값 " + s.Edge(s.damageEdge));
|
||||
sb.AppendLine(" 풀 인스턴스 없음: " + HUDDMGUI.RefreshEdges());
|
||||
// 데미지 숫자는 InGameInfo.cs:110 이 Addressables 로 Res_Addr/Ingame/HUDDMGUI.prefab 을 12개 복제한다
|
||||
// (Systems 소유 · **읽기만** — 프로브에서 1개만 인스턴스화해 머티리얼 경로를 확인하고 즉시 파기한다).
|
||||
var dmgSrc = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Ingame/HUDDMGUI.prefab");
|
||||
if (dmgSrc == null) sb.AppendLine(" 🔴 HUDDMGUI.prefab 없음");
|
||||
else
|
||||
{
|
||||
var inst = (GameObject)PrefabUtility.InstantiatePrefab(dmgSrc);
|
||||
try
|
||||
{
|
||||
var d = inst.GetComponent<HUDDMGUI>();
|
||||
sb.AppendLine(" prefab 인스턴스 1개(dmgs=" + (d != null && d.dmgs != null ? d.dmgs.Length : 0) + "라벨) → " + HUDDMGUI.RefreshEdges());
|
||||
if (d != null && d.dmgs != null)
|
||||
for (int i = 0; i < d.dmgs.Length; i++)
|
||||
if (d.dmgs[i] != null)
|
||||
sb.AppendLine(" dmgs[" + i + "] mat=" + d.dmgs[i].fontSharedMaterial.name +
|
||||
" outlineW=" + d.dmgs[i].fontSharedMaterial.GetFloat("_OutlineWidth").ToString("F3") +
|
||||
" UNDERLAY_ON=" + d.dmgs[i].fontSharedMaterial.IsKeywordEnabled("UNDERLAY_ON"));
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(inst); }
|
||||
sb.AppendLine(" 🔴 HUDDMGUI.prefab 은 **읽기 전용** — 인스턴스만 만들고 파기(에셋 저장 0 · Systems 소유)");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] 배칭 — 변형 머티리얼은 (원본 머티리얼 + 값) 조합마다 1장 공유(라벨별 인스턴스 0)");
|
||||
var mats = new Dictionary<int, int>();
|
||||
var matName = new Dictionary<int, string>();
|
||||
foreach (var t in root.GetComponentsInChildren<TMP_Text>(true))
|
||||
{
|
||||
var m = t.fontSharedMaterial;
|
||||
if (m == null || m.name.IndexOf("WL813y") < 0) continue;
|
||||
int id = m.GetInstanceID();
|
||||
int n; mats.TryGetValue(id, out n); mats[id] = n + 1;
|
||||
matName[id] = m.name + " outlineW=" + m.GetFloat("_OutlineWidth").ToString("F3") +
|
||||
" underlayOff(" + m.GetFloat("_UnderlayOffsetX").ToString("F2") + ")";
|
||||
}
|
||||
if (mats.Count == 0) sb.AppendLine(" 변형 머티리얼 0 (edge off)");
|
||||
sb.AppendLine(" 서로 다른 머티리얼 " + mats.Count + "장 (값 조합 3종 = 연쇄 0.30 / 스탯 0.26 / 토스트 0.24)");
|
||||
foreach (var kv in mats) sb.AppendLine(" · " + matName[kv.Key] + " ← 라벨 " + kv.Value + "개 공유");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lt != null) lt.Subscribe(false);
|
||||
if (kc != null) kc.Subscribe(false);
|
||||
if (sp != null) sp.Subscribe(false);
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
var body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813y_text.txt", body);
|
||||
}
|
||||
|
||||
// ═══ ⓑ ⑤ 보스 HP 바 · 배너 늦은 구독 보정 (발주서 §1-5) ════════════════
|
||||
public static string BossLate()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
BossHpBar bar = null; BossBanner ban = null; MobActor boss = null;
|
||||
try
|
||||
{
|
||||
var hud = Find(root.transform, kHud);
|
||||
bar = hud.Find("WL_BossHpBar").GetComponent<BossHpBar>();
|
||||
ban = hud.Find("WL_BossBanner").GetComponent<BossBanner>();
|
||||
|
||||
sb.AppendLine("[0] Q3 결함 D-3 재현 — 보스가 **먼저** 스폰되고 HUD 가 **나중에** 켜진다");
|
||||
sb.AppendLine(" (Q3 실측: SpawnedSeen=232 · _bound=False — Spawned(isBoss) 가 구독 전에 지나갔다)");
|
||||
boss = MakeBoss();
|
||||
sb.AppendLine(" 가짜 보스 = " + boss.name + " m_SubRole=" + boss.m_SubRole +
|
||||
" IsSubRole(Boss)=" + boss.IsSubRole(eSubRol.Boss) + " IsDead=" + boss.IsDead() +
|
||||
" activeInHierarchy=" + boss.gameObject.activeInHierarchy);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[1] 경로 ① BossArena.Boss (813d 게이트가 잡아 둔 보스)");
|
||||
WL.Combat.Boss.BossArena.SetBossForProbe(boss);
|
||||
sb.AppendLine(" BossArena.Boss=" + (WL.Combat.Boss.BossArena.Boss != null ? WL.Combat.Boss.BossArena.Boss.name : "없음"));
|
||||
sb.AppendLine(" 보스바 " + bar.Initialize());
|
||||
sb.AppendLine(" → visible=" + bar.Visible + " ratio=" + bar.LastRatio.ToString("F3") +
|
||||
" 늦은구독=" + bar.LateBound + " (기대 True)");
|
||||
sb.AppendLine(" 배너 " + ban.Initialize());
|
||||
ban.Tick(Time.unscaledTime + 0.2f);
|
||||
sb.AppendLine(" → visible=" + ban.Visible + " shows=" + ban.ShowCount +
|
||||
" 늦은구독=" + ban.LateTriggered + " α=" + ban.BannerAlpha.ToString("F3") + " (기대 True)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] 경로 ② 씬 1회 스캔 (게이트 이전 = 맵 로드 시 스폰 · BossArena 비어 있음)");
|
||||
bar.Subscribe(false); ban.Subscribe(false);
|
||||
WL.Combat.Boss.BossArena.SetBossForProbe(null);
|
||||
var bar2 = hud.Find("WL_BossHpBar").GetComponent<BossHpBar>();
|
||||
typeof(BossHpBar).GetField("_bound", NP).SetValue(bar2, false);
|
||||
sb.AppendLine(" BossArena.Boss=없음 · " + bar2.BindExistingBoss());
|
||||
sb.AppendLine(" → visible=" + bar2.Visible + " 늦은구독=" + bar2.LateBound + " 스캔=" + bar2.LateScanCount);
|
||||
sb.AppendLine(" 배너 " + ban.TriggerExistingBoss());
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] 살아 있는 보스가 없으면 아무 일도 없어야 한다(오탐 0)");
|
||||
boss.gameObject.SetActive(false);
|
||||
var bar3 = hud.Find("WL_BossHpBar").GetComponent<BossHpBar>();
|
||||
typeof(BossHpBar).GetField("_bound", NP).SetValue(bar3, false);
|
||||
sb.AppendLine(" 보스 비활성 → " + bar3.BindExistingBoss());
|
||||
boss.gameObject.SetActive(true);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[4] C8 롤백 — bossBarBindExisting / bannerTriggerExisting = false 면 생략");
|
||||
var hs = WLHudLayoutSettings.Instance; var bs = WLBossUiSettings.Instance;
|
||||
bool h0 = hs.bossBarBindExisting, b0 = bs.bannerTriggerExisting;
|
||||
hs.bossBarBindExisting = false; bs.bannerTriggerExisting = false;
|
||||
var bar4 = hud.Find("WL_BossHpBar").GetComponent<BossHpBar>();
|
||||
typeof(BossHpBar).GetField("_bound", NP).SetValue(bar4, false);
|
||||
sb.AppendLine(" " + bar4.BindExistingBoss());
|
||||
sb.AppendLine(" " + ban.TriggerExistingBoss());
|
||||
hs.bossBarBindExisting = h0; bs.bannerTriggerExisting = b0;
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[5] 최종 덤프");
|
||||
sb.Append(bar.Dump());
|
||||
sb.Append(ban.Dump());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bar != null) bar.Subscribe(false);
|
||||
if (ban != null) ban.Subscribe(false);
|
||||
WL.Combat.Boss.BossArena.SetBossForProbe(null);
|
||||
if (boss != null) UnityEngine.Object.DestroyImmediate(boss.gameObject);
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("해제 후 구독자 Spawned=" + WL.Combat.Core.CombatEvents.Spawned.Count +
|
||||
" BossPhase=" + WL.Combat.Boss.BossEvents.BossPhase.Count + " (0/0 이어야 한다)" +
|
||||
" · BossArena.Boss=" + (WL.Combat.Boss.BossArena.Boss != null ? "남음🔴" : "없음"));
|
||||
var body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813y_bosslate.txt", body);
|
||||
}
|
||||
|
||||
static string MenuBlocks(Transform root, WLHudLayoutSettings s)
|
||||
{
|
||||
var m = WLVignetteUtil.FindUiPath(root, s.bottomMenuPath);
|
||||
if (m == null) return "메뉴 없음";
|
||||
var cg = m.GetComponent<CanvasGroup>();
|
||||
return cg == null ? "CanvasGroup 없음🔴" : cg.blocksRaycasts.ToString();
|
||||
}
|
||||
|
||||
static MobActor MakeBoss()
|
||||
{
|
||||
var go = new GameObject("__WL813y_Boss");
|
||||
var m = go.AddComponent<MobActor>();
|
||||
m.m_Role = eRole.Mob; m.m_SubRole = eSubRol.Boss;
|
||||
var stat = new ActorStatInfo(eRole.Mob);
|
||||
stat.Set_Stat(eStat.MaxHP, 50000d); stat.Set_Stat(eStat.HP, 50000d);
|
||||
typeof(Actor).GetField("m_Stat", NP).SetValue(m, stat);
|
||||
typeof(Actor).GetField("m_Enemy", NP).SetValue(m, true);
|
||||
return m;
|
||||
}
|
||||
|
||||
// ═══ ⓑ ⑥ 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 (발주서 §1-6) ══════════
|
||||
public static string Hud()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
try
|
||||
{
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "🔴 WLHudLayoutSettings 에셋 없음";
|
||||
var padT = Find(root.transform, kPad);
|
||||
var pad = padT != null ? padT.GetComponent<WLBattlePadLayout>() : null;
|
||||
if (pad == null) pad = root.GetComponentInChildren<WLBattlePadLayout>(true);
|
||||
if (pad == null) return "🔴 WLBattlePadLayout 없음";
|
||||
|
||||
sb.AppendLine("[0] 에셋 값 (C8/C45 · 코드 상수 0)");
|
||||
sb.AppendLine(" 전투(메뉴 숨김) fanOrigin=" + s.fanOriginPx + " attackCenter=" + s.attackCenterPx);
|
||||
sb.AppendLine(" idle(메뉴 보임) fanOrigin=" + s.idleFanOriginPx + " attackCenter=" + s.idleAttackCenterPx);
|
||||
sb.AppendLine(" fanRadius=" + s.fanRadiusPx + " slot⌀=" + s.slotDiameterPx +
|
||||
" reserveRadius=" + s.reserveRadiusPx + " reserve⌀=" + s.reserveDiameterPx +
|
||||
" attack⌀=" + s.attackDiameterPx + " 목표반경=" + s.thumbReachRadiusPx);
|
||||
sb.AppendLine(" combatHideBottomMenu=" + s.combatHideBottomMenu + " path=\"" + s.bottomMenuPath +
|
||||
"\" exit=" + s.combatExitSeconds + "s enter=" + s.combatEnterDelaySeconds +
|
||||
"s fade=" + s.bottomMenuFadeSeconds + "s canvasGroup=" + s.bottomMenuUseCanvasGroup);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[1] 하단 메뉴 연결 + 복귀 버튼 생성(런타임 · 프리팹 diff 0)");
|
||||
HudCombatVisibility.Teardown();
|
||||
HudCombatVisibility.ResetState();
|
||||
sb.AppendLine(" " + HudCombatVisibility.Bind(root.transform));
|
||||
sb.AppendLine(" " + HudCombatVisibility.Subscribe(true));
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[2] idle(메뉴 보임) → 전투(메뉴 숨김) → 다시 idle · 도달 px 실측");
|
||||
// 🔴 Signal() 은 Time.unscaledTime 을 쓴다 — 프로브 클록도 같은 축이어야 한다(임의 t 를 넣으면 since 가 수천 초로 뜬다).
|
||||
float t = Time.unscaledTime;
|
||||
sb.AppendLine(" ⓪ idle " + HudCombatVisibility.Tick(t));
|
||||
sb.AppendLine(" pad " + pad.Apply());
|
||||
float idleReach = pad.LastActionReachPx, idleMax = pad.LastMaxReachPx;
|
||||
sb.AppendLine(" ① 전투 신호 " + HudCombatVisibility.Signal("probe:attack"));
|
||||
t = Time.unscaledTime;
|
||||
sb.AppendLine(" tick " + HudCombatVisibility.Tick(t));
|
||||
sb.AppendLine(" pad " + pad.Apply());
|
||||
float fightReach = pad.LastActionReachPx, fightMax = pad.LastMaxReachPx;
|
||||
sb.AppendLine(" 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + "(fade " + s.bottomMenuFadeSeconds + "s 시작)" +
|
||||
" 복귀버튼 표시=" + HudCombatVisibility.RestoreButtonVisible +
|
||||
" 메뉴 blocksRaycasts=" + MenuBlocks(root.transform, s));
|
||||
HudCombatVisibility.Tick(t + s.bottomMenuFadeSeconds + 0.01f);
|
||||
sb.AppendLine(" +" + s.bottomMenuFadeSeconds + "s → 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") +
|
||||
"(기대 0.00) blocksRaycasts=" + MenuBlocks(root.transform, s));
|
||||
sb.AppendLine(" ② 전투 종료(" + s.combatExitSeconds + "s 무신호) " + HudCombatVisibility.Tick(t + s.combatExitSeconds + 0.01f));
|
||||
HudCombatVisibility.Tick(t + s.combatExitSeconds + s.bottomMenuFadeSeconds + 0.02f);
|
||||
sb.AppendLine(" 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + "(기대 1.00) menuVisible=" + HudCombatVisibility.BottomMenuVisible);
|
||||
sb.AppendLine(" pad " + pad.Apply());
|
||||
sb.AppendLine(" ③ 복귀 버튼 클릭(전투 중이어도 되돌린다)");
|
||||
HudCombatVisibility.Signal("probe:attack2");
|
||||
t = Time.unscaledTime;
|
||||
HudCombatVisibility.Tick(t);
|
||||
sb.AppendLine(" 숨김 확인 menuVisible=" + HudCombatVisibility.BottomMenuVisible + "(기대 False) 복귀버튼 표시=" + HudCombatVisibility.RestoreButtonVisible);
|
||||
sb.AppendLine(" " + HudCombatVisibility.OnRestoreClicked());
|
||||
sb.AppendLine(" menuVisible=" + HudCombatVisibility.BottomMenuVisible + " (기대 True)");
|
||||
HudCombatVisibility.Tick(t + 0.01f);
|
||||
sb.AppendLine(" hold " + s.restoreHoldSeconds + "s 안에는 전투여도 유지 → menuVisible=" + HudCombatVisibility.BottomMenuVisible);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[3] 엄지 도달 실측 (발주서 §1-6 목표 ≤ 490 px · 813m 오버레이 747 px 대비)");
|
||||
sb.AppendLine(" idle(메뉴 보임) actionReach=" + idleReach.ToString("F1") + " px · maxReach(자동토글 포함)=" + idleMax.ToString("F1") + " px");
|
||||
sb.AppendLine(" 전투(메뉴 숨김) actionReach=" + fightReach.ToString("F1") + " px · maxReach(자동토글 포함)=" + fightMax.ToString("F1") + " px");
|
||||
sb.AppendLine(" 판정: 전투 중 " + fightReach.ToString("F1") + " ≤ 490 → " + (fightReach <= 490f ? "PASS" : "FAIL") +
|
||||
" (813m 실측 747 px 대비 " + (747f - fightReach).ToString("F1") + " px 감소)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[4] C8 롤백 — combatHideBottomMenu=false 면 메뉴를 건드리지 않고 패드도 idle 자리(813c+Lead 핫픽스 상태)");
|
||||
bool k0 = s.combatHideBottomMenu;
|
||||
s.combatHideBottomMenu = false;
|
||||
HudCombatVisibility.Signal("probe:rollback");
|
||||
sb.AppendLine(" " + HudCombatVisibility.Tick(Time.unscaledTime));
|
||||
sb.AppendLine(" " + pad.Apply());
|
||||
sb.AppendLine(" menuVisible=" + HudCombatVisibility.BottomMenuVisible + "(기대 True · 전투 신호가 있어도 안 숨긴다)" +
|
||||
" 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") +
|
||||
" actionReach=" + pad.LastActionReachPx.ToString("F1") + " px(= 롤백 전 746.8)");
|
||||
s.combatHideBottomMenu = k0;
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[5] 최종 덤프");
|
||||
sb.Append(HudCombatVisibility.Dump());
|
||||
sb.Append(pad.Dump());
|
||||
}
|
||||
finally
|
||||
{
|
||||
HudCombatVisibility.Teardown();
|
||||
KillRunners();
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("정리: 구독=" + HudCombatVisibility.Subscribed + " 연결=" + HudCombatVisibility.Bound +
|
||||
" · AttackStarted 구독자=" + WL.Combat.Core.CombatEvents.AttackStarted.Count +
|
||||
" Damaged=" + WL.Combat.Core.CombatEvents.Damaged.Count + " (0 이어야 한다)");
|
||||
var body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813y_hud.txt", body);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
// WL-813g (#813) — 데미지 숫자 리듬화. 원본(힘민지) HUDDMGUI 에 표시 규칙만 얹는다.
|
||||
// WL-813y (#813) — Q3 결함 D-6: 밝은 배경에서 숫자 대비 부족 → TMP 머티리얼 아웃라인 + 그림자.
|
||||
// · uGUI Outline/Shadow(BaseMeshEffect)는 TMP 에 적용되지 않으므로 WLTextFxUtil.ApplyEdge 를 쓴다.
|
||||
// · 변형 머티리얼은 (원본 머티리얼 + 값) 조합마다 **1장만** 캐시해 공유한다 = 배칭 유지 · 인스턴스 0.
|
||||
// · 인스턴스당 1회만 적용(m_edgeDone)해 표시 경로(SetCore)에 GC 를 넣지 않는다.
|
||||
// 기준서 v1 §B 요소 2: 일반 0.6 s · 크리 1.0 s · 크리 1.6배 · 합산 윈도우 0.25 s · 동시 상한(숫자죽 방지).
|
||||
// 값은 전부 Assets/WL/UI/Settings/Resources/WL/WLCombatTextSettings.asset (C45 · 코드 상수 0).
|
||||
// 🔴 C8 롤백: 에셋이 없거나 textEnabled/damageRhythmEnabled = false 면 **아래 원본 상수**(0.9 s · 배율 1 · 상한 없음)
|
||||
|
|
@ -27,6 +31,11 @@ public class HUDDMGUI : HUDBase
|
|||
public static int EvictedCount, MergedCount;
|
||||
public static void ResetDiagnostics() { EvictedCount = MergedCount = 0; }
|
||||
|
||||
bool m_edgeDone; // WL-813y 아웃라인을 이 인스턴스에 이미 입혔는가
|
||||
/// <summary>프로브가 읽는다 — 아웃라인을 입힌 인스턴스 수 · 마지막 실측 문자열.</summary>
|
||||
public static int EdgeAppliedCount;
|
||||
public static string LastEdgeDump = "";
|
||||
|
||||
bool m_wl; // 이번 표시에 813g 규칙이 적용됐는가
|
||||
bool m_crit; // 크리티컬로 표시 중인가
|
||||
float m_life, m_lifeMax; // 남은/전체 표시 시간(unscaled 아님 — 원본과 같은 Time.deltaTime 계열)
|
||||
|
|
@ -139,10 +148,32 @@ public class HUDDMGUI : HUDBase
|
|||
Register(null);
|
||||
}
|
||||
|
||||
ApplyEdge(st);
|
||||
Redraw(TextColor, st);
|
||||
isUpdate = false;
|
||||
}
|
||||
|
||||
/// <summary>WL-813y — TMP 아웃라인/그림자(값은 WLCombatTextSettings.damageEdge · 인스턴스당 1회).</summary>
|
||||
void ApplyEdge(WL.UI.WLCombatTextSettings st)
|
||||
{
|
||||
if (m_edgeDone || dmgs == null) return;
|
||||
if (st == null) { m_edgeDone = true; return; } // 설정 off = 원본 머티리얼 그대로
|
||||
var e = st.Edge(st.damageEdge);
|
||||
for (int i = 0; i < dmgs.Length; i++)
|
||||
if (dmgs[i] != null) LastEdgeDump = WL.UI.WLTextFxUtil.ApplyEdge(dmgs[i], e);
|
||||
m_edgeDone = true;
|
||||
EdgeAppliedCount++;
|
||||
}
|
||||
|
||||
/// <summary>프로브 — 설정을 바꾼 뒤 살아 있는 인스턴스 전체에 다시 입힌다.</summary>
|
||||
public static string RefreshEdges()
|
||||
{
|
||||
var all = Object.FindObjectsByType<HUDDMGUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
EdgeAppliedCount = 0;
|
||||
for (int i = 0; i < all.Length; i++) { all[i].m_edgeDone = false; all[i].ApplyEdge(St); }
|
||||
return "HUDDMGUI 인스턴스 " + all.Length + "개 중 적용 " + EdgeAppliedCount + " · " + LastEdgeDump;
|
||||
}
|
||||
|
||||
/// <summary>합산 윈도우 안의 추가 타격 — 숫자를 더하고, 크리가 섞이면 크리 규격으로 승격한다.</summary>
|
||||
void Accumulate(double Damage, eStat Critical, WL.UI.WLCombatTextSettings st)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -79,9 +79,56 @@ namespace WL.UI
|
|||
HideNow();
|
||||
var s = WLBossUiSettings.Instance;
|
||||
Subscribe(s == null || s.bannerEnabled);
|
||||
return "initialized subscribed=" + _subscribed + " · " + layout;
|
||||
string late = TriggerExistingBoss(); // WL-813y — 늦은 구독 보정(Q3 결함 D-3 · 813c 보스 바와 동일)
|
||||
return "initialized subscribed=" + _subscribed + " · " + layout + " · " + late;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배너가 켜질 때 **이미 살아 있는 보스**가 있으면 배너를 띄운다 (WL-813y).
|
||||
/// 필드 보스는 맵 로드 시(`FieldBossData.Start()`) 스폰돼 `Spawned(isBoss)` 가 구독 전에 지나간다
|
||||
/// → 813c 보스 HP 바(`BossHpBar.BindExistingBoss`)와 **같은 조회**를 쓴다.
|
||||
/// </summary>
|
||||
public string TriggerExistingBoss()
|
||||
{
|
||||
var s = WLBossUiSettings.Instance;
|
||||
if (s != null && !s.bannerEnabled) return "늦은구독: bannerEnabled=false — 생략";
|
||||
if (s != null && !s.bannerTriggerExisting) return "늦은구독: bannerTriggerExisting=false — 생략";
|
||||
|
||||
Actor found = null;
|
||||
string how = "";
|
||||
var arenaBoss = WL.Combat.Boss.BossArena.Boss;
|
||||
if (IsAliveBoss(arenaBoss)) { found = arenaBoss; how = "BossArena.Boss"; }
|
||||
if (found == null)
|
||||
{
|
||||
var mobs = Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
LateScanCount = mobs != null ? mobs.Length : 0;
|
||||
for (int i = 0; mobs != null && i < mobs.Length; i++)
|
||||
{
|
||||
if (!IsAliveBoss(mobs[i])) continue;
|
||||
found = mobs[i]; how = "FindObjectsByType<MobActor> " + LateScanCount + "개 스캔";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == null) return "늦은구독: 살아 있는 보스 없음(스캔 " + LateScanCount + ")";
|
||||
|
||||
LateTriggered = true;
|
||||
// 이름은 813h BossPhase 가 오면 승격된다(813tj 실측) — 여기서는 오브젝트 이름으로 먼저 띄운다.
|
||||
return "늦은구독 " + how + " → " + Trigger(found.name, false);
|
||||
}
|
||||
|
||||
private static bool IsAliveBoss(Actor a)
|
||||
{
|
||||
if (a == null) return false;
|
||||
if (!a.IsSubRole(eSubRol.Boss)) return false;
|
||||
if (a.IsDead()) return false;
|
||||
return a.gameObject != null && a.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
/// <summary>늦은 구독 보정으로 배너를 띄웠는가(진단).</summary>
|
||||
public bool LateTriggered { get; private set; }
|
||||
/// <summary>마지막 씬 스캔에서 본 MobActor 수(진단).</summary>
|
||||
public int LateScanCount { get; private set; }
|
||||
|
||||
public void Subscribe(bool on)
|
||||
{
|
||||
if (on == _subscribed) return;
|
||||
|
|
@ -420,7 +467,7 @@ namespace WL.UI
|
|||
" scale=" + BannerScale.ToString("F3") +
|
||||
" name=\"" + _shownName + "\"(known=" + _nameKnown + ")" +
|
||||
" seen(Spawned/Boss/Phase)=" + SpawnedSeen + "/" + BossSpawnSeen + "/" + PhaseSeen +
|
||||
" shows=" + ShowCount);
|
||||
" shows=" + ShowCount + " 늦은구독=" + LateTriggered + "(스캔 " + LateScanCount + ")");
|
||||
sb.AppendLine(" 구독자 Spawned=" + CombatEvents.Spawned.Count + " BossPhase=" + BossEvents.BossPhase.Count +
|
||||
" CombatEvents.Enabled=" + CombatEvents.Enabled + " BossEvents.Enabled=" + BossEvents.Enabled);
|
||||
if (bannerBox != null)
|
||||
|
|
|
|||
|
|
@ -78,9 +78,63 @@ namespace WL.UI
|
|||
string layout = ApplyLayout();
|
||||
HideNow();
|
||||
Subscribe(true);
|
||||
return "initialized subscribed=" + _subscribed + " · " + layout;
|
||||
string late = BindExistingBoss(); // WL-813y — 늦은 구독 보정(Q3 결함 D-3)
|
||||
return "initialized subscribed=" + _subscribed + " · " + layout + " · " + late;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 바가 켜질 때 **이미 살아 있는 보스**가 있으면 즉시 물린다 (WL-813y · Q3 결함 D-3).
|
||||
///
|
||||
/// 필드 보스는 `FieldBossData.Start()` 로 **맵 로드 시** 스폰돼 `Spawned(isBoss)` 가
|
||||
/// HUD 가 구독하기 **전에** 지나간다(Q3 실측: `SpawnedSeen=232 · _bound=False`).
|
||||
/// 813d 가 만든 `BossArena.Boss`(게이트가 잡아 둔 현재 보스)를 1순위로 보고,
|
||||
/// 없으면 씬에서 `MobActor` 를 1회만 훑어 `IsSubRole(eSubRol.Boss)` 인 살아 있는 개체를 찾는다.
|
||||
/// </summary>
|
||||
public string BindExistingBoss()
|
||||
{
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s != null && !s.bossBarEnabled) return "늦은구독: bossBarEnabled=false — 생략";
|
||||
if (s != null && !s.bossBarBindExisting) return "늦은구독: bossBarBindExisting=false — 생략";
|
||||
if (_bound) return "늦은구독: 이미 물림";
|
||||
|
||||
Actor found = null;
|
||||
string how = "";
|
||||
|
||||
// ① 813d 보스 아레나가 잡고 있는 보스(게이트 통과 시점에 채워진다)
|
||||
var arenaBoss = WL.Combat.Boss.BossArena.Boss;
|
||||
if (IsAliveBoss(arenaBoss)) { found = arenaBoss; how = "BossArena.Boss"; }
|
||||
|
||||
// ② 씬 1회 스캔 — 게이트 이전(맵 로드 시) 스폰까지 잡는다
|
||||
if (found == null)
|
||||
{
|
||||
var mobs = Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
LateScanCount = mobs != null ? mobs.Length : 0;
|
||||
for (int i = 0; mobs != null && i < mobs.Length; i++)
|
||||
{
|
||||
if (!IsAliveBoss(mobs[i])) continue;
|
||||
found = mobs[i]; how = "FindObjectsByType<MobActor> " + LateScanCount + "개 스캔";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == null) return "늦은구독: 살아 있는 보스 없음(스캔 " + LateScanCount + ")";
|
||||
LateBound = true;
|
||||
return "늦은구독 " + how + " → " + Bind(found, null, DebugFallbackMaxHp);
|
||||
}
|
||||
|
||||
private static bool IsAliveBoss(Actor a)
|
||||
{
|
||||
if (a == null) return false;
|
||||
if (!a.IsSubRole(eSubRol.Boss)) return false;
|
||||
if (a.IsDead()) return false;
|
||||
return a.gameObject != null && a.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
/// <summary>늦은 구독 보정으로 물었는가(진단).</summary>
|
||||
public bool LateBound { get; private set; }
|
||||
/// <summary>마지막 씬 스캔에서 본 MobActor 수(진단).</summary>
|
||||
public int LateScanCount { get; private set; }
|
||||
|
||||
/// <summary>구독 등록/해제. OnEnable/OnDisable 이 부르고, 에디터 검증도 같은 것을 쓴다.</summary>
|
||||
public void Subscribe(bool on)
|
||||
{
|
||||
|
|
@ -409,7 +463,9 @@ namespace WL.UI
|
|||
sb.AppendLine("[BossHpBar] subscribed=" + _subscribed +
|
||||
" visible=" + Visible + " ratio=" + LastRatio.ToString("F3") +
|
||||
" hp=" + _curHp.ToString("F0") + "/" + _maxHp.ToString("F0") +
|
||||
" seen(Spawned/Hit/Killed)=" + SpawnedSeen + "/" + HitSeen + "/" + KilledSeen);
|
||||
" seen(Spawned/Hit/Killed)=" + SpawnedSeen + "/" + HitSeen + "/" + KilledSeen +
|
||||
" 늦은구독=" + LateBound + "(스캔 " + LateScanCount + " · BossArena.Boss=" +
|
||||
(WL.Combat.Boss.BossArena.Boss != null ? WL.Combat.Boss.BossArena.Boss.name : "없음") + ")");
|
||||
sb.AppendLine(" CombatEvents subscribers Spawned=" + CombatEvents.Spawned.Count +
|
||||
" HitConfirmed=" + CombatEvents.HitConfirmed.Count +
|
||||
" Killed=" + CombatEvents.Killed.Count + " Enabled=" + CombatEvents.Enabled);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,367 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// HudCombatVisibility.cs — 전투 중 하단 메뉴 숨김 + 전투 패드 원점 복귀 (WL-813y · #813)
|
||||
//
|
||||
// 근거 = Q3 `qa/W2-3_바퀴2.md` 결함 D-5:
|
||||
// Lead 핫픽스가 하단 메뉴(`IngameUIs/WL_HUD/HUD_BottomMenu`) 겹침을 피하려고
|
||||
// `fanOriginPx.y 0 → 260` 을 넣어 **엄지 도달이 565 → 746.8 px 로 더 나빠졌다**(목표 320).
|
||||
//
|
||||
// ■ 해법 = 겹칠 상대를 전투 중에는 치운다
|
||||
// · 전투 신호(적 인지 · 피격 · 공격 · 스킬 · 처치)가 오면 하단 메뉴를 접고(CanvasGroup α 0)
|
||||
// 전투 패드를 **모서리 원점**(`fanOriginPx` = 에셋 0,0 / `attackCenterPx` = 100,100)으로 되돌린다.
|
||||
// · 마지막 신호 뒤 `combatExitSeconds` 가 지나면 메뉴가 돌아오고 패드는 idle 자리로 올라간다.
|
||||
// · 숨은 동안 **복귀 버튼 1개**(런타임 생성)를 띄워 언제든 메뉴를 되돌릴 수 있다.
|
||||
//
|
||||
// ■ 프리팹 diff 0
|
||||
// 노드를 굽지 않는다 — 러너도 복귀 버튼도 **런타임 생성**(`HideFlags.DontSave`).
|
||||
// `NewGameUI.prefab` 은 이 기능 때문에 1줄도 바뀌지 않는다(813c/813g/813tj 와 다른 점).
|
||||
//
|
||||
// ■ 값 = WLHudLayoutSettings.asset (C45 · 코드 상수 0) · C8 롤백 = `combatHideBottomMenu = 0`
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
public static class HudCombatVisibility
|
||||
{
|
||||
// ── 상태 ──────────────────────────────────────────────────────────────
|
||||
private static bool s_subscribed;
|
||||
private static float s_lastCombatAt = -9999f;
|
||||
private static float s_firstSignalAt = -9999f;
|
||||
private static float s_restoreHeldUntil = -9999f;
|
||||
private static bool s_menuVisible = true; // 초기값 = 원본(보임)
|
||||
private static float s_alpha = 1f;
|
||||
private static Transform s_menu;
|
||||
private static CanvasGroup s_menuGroup;
|
||||
private static Transform s_uiRoot;
|
||||
private static RectTransform s_restore;
|
||||
private static TextMeshProUGUI s_restoreLabel;
|
||||
private static Image s_restoreBg;
|
||||
|
||||
/// <summary>하단 메뉴가 지금 보이는가 — 전투 패드 원점(WLHudLayoutSettings)이 이 값을 본다.</summary>
|
||||
public static bool BottomMenuVisible { get { return s_menuVisible; } }
|
||||
public static bool InCombat { get; private set; }
|
||||
public static bool Subscribed { get { return s_subscribed; } }
|
||||
public static float LastCombatAt { get { return s_lastCombatAt; } }
|
||||
public static int HideCount, ShowCount, RestoreClickCount, SignalCount;
|
||||
public static string LastSignal = "";
|
||||
public static bool Bound { get { return s_menu != null; } }
|
||||
public static float MenuAlpha { get { return s_alpha; } }
|
||||
public static bool RestoreButtonVisible { get { return s_restore != null && s_restore.gameObject.activeSelf; } }
|
||||
|
||||
private static WLHudLayoutSettings St { get { return WLHudLayoutSettings.Instance; } }
|
||||
|
||||
// ── 부팅 (노드 0 · 프리팹 diff 0) ─────────────────────────────────────
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Boot()
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || !s.combatHideBottomMenu) return;
|
||||
Subscribe(true);
|
||||
HudCombatVisibilityRunner.Ensure();
|
||||
}
|
||||
|
||||
// ── 전투 신호 구독 ────────────────────────────────────────────────────
|
||||
public static string Subscribe(bool on)
|
||||
{
|
||||
if (on == s_subscribed) return "구독 변화 없음(" + s_subscribed + ")";
|
||||
if (on)
|
||||
{
|
||||
CombatEvents.AttackStarted.Add(OnAttack);
|
||||
CombatEvents.SkillCast.Add(OnSkill);
|
||||
CombatEvents.Damaged.Add(OnDamaged);
|
||||
CombatEvents.Killed.Add(OnKilled);
|
||||
}
|
||||
else
|
||||
{
|
||||
CombatEvents.AttackStarted.Remove(OnAttack);
|
||||
CombatEvents.SkillCast.Remove(OnSkill);
|
||||
CombatEvents.Damaged.Remove(OnDamaged);
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
}
|
||||
s_subscribed = on;
|
||||
return "구독=" + on + " (AttackStarted/SkillCast/Damaged/Killed)";
|
||||
}
|
||||
|
||||
private static void OnAttack(in AttackStartedEvent e) { if (e.isMainPC) Signal("attack"); }
|
||||
private static void OnSkill(in SkillCastEvent e) { if (IsMine(e.actor)) Signal("skill"); }
|
||||
private static void OnDamaged(in DamagedEvent e) { if (IsMine(e.victim) || IsMine(e.attacker)) Signal("damaged"); }
|
||||
private static void OnKilled(in KilledEvent e) { if (IsMine(e.killer)) Signal("killed"); }
|
||||
|
||||
private static bool IsMine(Actor a)
|
||||
{
|
||||
if (a == null) return false;
|
||||
if (!MyValue.bMyPC) return false;
|
||||
return ReferenceEquals(a, MyValue.MyPC);
|
||||
}
|
||||
|
||||
/// <summary>전투 신호 1건(프로브도 이 경로를 쓴다).</summary>
|
||||
public static string Signal(string reason)
|
||||
{
|
||||
float now = Time.unscaledTime;
|
||||
if (s_lastCombatAt < 0f || now - s_lastCombatAt > 0.5f) s_firstSignalAt = now;
|
||||
s_lastCombatAt = now;
|
||||
SignalCount++;
|
||||
LastSignal = reason;
|
||||
return "signal=" + reason + " t=" + now.ToString("F2");
|
||||
}
|
||||
|
||||
/// <summary>현재 타깃이 살아 있으면 전투로 본다(적 인지).</summary>
|
||||
private static bool HasLiveTarget()
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || !s.combatUseTargetAsSignal) return false;
|
||||
if (!MyValue.bMyPC || MyValue.MyPC == null) return false;
|
||||
var t = MyValue.MyPC.Get_Target();
|
||||
return t != null && !t.IsDead();
|
||||
}
|
||||
|
||||
// ── 매 프레임 판정 (러너가 부른다 · 프로브는 now 를 직접 준다) ────────
|
||||
public static string Tick(float now)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "WLHudLayoutSettings 에셋 없음";
|
||||
if (!s.combatHideBottomMenu)
|
||||
{
|
||||
if (!s_menuVisible) SetMenuVisible(true, now, true);
|
||||
return "combatHideBottomMenu=false — 무동작";
|
||||
}
|
||||
|
||||
if (HasLiveTarget()) { s_lastCombatAt = now; if (s_firstSignalAt < 0f) s_firstSignalAt = now; LastSignal = "target"; }
|
||||
|
||||
float since = now - s_lastCombatAt;
|
||||
bool combat = since <= Mathf.Max(0f, s.combatExitSeconds) &&
|
||||
(now - s_firstSignalAt) >= Mathf.Max(0f, s.combatEnterDelaySeconds);
|
||||
InCombat = combat;
|
||||
|
||||
bool wantVisible = !combat;
|
||||
if (now < s_restoreHeldUntil) wantVisible = true; // 복귀 버튼을 누른 직후에는 전투여도 보인다
|
||||
|
||||
if (wantVisible != s_menuVisible) SetMenuVisible(wantVisible, now, false);
|
||||
StepFade(s, now);
|
||||
return "combat=" + combat + " since=" + since.ToString("F2") + "s menuVisible=" + s_menuVisible +
|
||||
" alpha=" + s_alpha.ToString("F2");
|
||||
}
|
||||
|
||||
private static float s_fadeFrom, s_fadeAt;
|
||||
|
||||
private static void StepFade(WLHudLayoutSettings s, float now)
|
||||
{
|
||||
float want = s_menuVisible ? 1f : 0f;
|
||||
float dur = Mathf.Max(0f, s.bottomMenuFadeSeconds);
|
||||
float a = dur <= 0f ? want : Mathf.Lerp(s_fadeFrom, want, Mathf.Clamp01((now - s_fadeAt) / dur));
|
||||
if (!Mathf.Approximately(a, s_alpha)) { s_alpha = a; PushAlpha(s); }
|
||||
}
|
||||
|
||||
private static void PushAlpha(WLHudLayoutSettings s)
|
||||
{
|
||||
if (s_menu == null) return;
|
||||
if (s.bottomMenuUseCanvasGroup)
|
||||
{
|
||||
// 🔴 `??` 는 UnityEngine.Object 의 "가짜 null"(파괴됨 · 컴포넌트 없음)을 걸러 내지 못한다 —
|
||||
// GetComponent 결과는 반드시 오버로드된 `==` 로 검사한다(813y 프로브가 잡은 MissingComponentException).
|
||||
if (s_menuGroup == null)
|
||||
{
|
||||
var cg = s_menu.GetComponent<CanvasGroup>();
|
||||
s_menuGroup = cg != null ? cg : s_menu.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
s_menuGroup.alpha = s_alpha;
|
||||
s_menuGroup.blocksRaycasts = s_alpha > 0.05f;
|
||||
s_menuGroup.interactable = s_alpha > 0.05f;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool on = s_alpha > 0.05f;
|
||||
if (s_menu.gameObject.activeSelf != on) s_menu.gameObject.SetActive(on);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>메뉴 표시 상태를 바꾸고 전투 패드를 다시 앉힌다.</summary>
|
||||
public static string SetMenuVisible(bool visible, float now, bool instant)
|
||||
{
|
||||
var s = St;
|
||||
s_fadeFrom = s_alpha;
|
||||
s_fadeAt = now;
|
||||
s_menuVisible = visible;
|
||||
if (visible) ShowCount++; else HideCount++;
|
||||
if (instant || s == null || s.bottomMenuFadeSeconds <= 0f) { s_alpha = visible ? 1f : 0f; if (s != null) PushAlpha(s); }
|
||||
|
||||
SetRestoreVisible(!visible);
|
||||
string pad = ReapplyPad();
|
||||
return "menuVisible=" + visible + " · " + pad;
|
||||
}
|
||||
|
||||
/// <summary>패드 재배치 — 원점이 메뉴 상태에 따라 바뀐다(WLHudLayoutSettings.FanOriginPxFor).</summary>
|
||||
public static string ReapplyPad()
|
||||
{
|
||||
var pads = Object.FindObjectsByType<WLBattlePadLayout>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
if (pads == null || pads.Length == 0) return "WLBattlePadLayout 없음(패드 재배치 생략)";
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < pads.Length; i++) sb.Append(i > 0 ? " | " : "").Append(pads[i].Apply());
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// ── 노드 연결 · 복귀 버튼(런타임 생성) ────────────────────────────────
|
||||
/// <summary>NewGameUI 루트에서 하단 메뉴를 찾고 복귀 버튼을 만든다. 이미 물려 있으면 그대로.</summary>
|
||||
public static string Bind(Transform uiRoot)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "WLHudLayoutSettings 에셋 없음";
|
||||
if (uiRoot == null) return "uiRoot=null";
|
||||
if (s_menu != null && s_uiRoot == uiRoot) return "이미 연결됨 — " + s_menu.name;
|
||||
|
||||
s_uiRoot = uiRoot;
|
||||
s_menu = WLVignetteUtil.FindUiPath(uiRoot, s.bottomMenuPath);
|
||||
s_menuGroup = null;
|
||||
if (s_menu == null) return "하단 메뉴 없음 — 경로 \"" + s.bottomMenuPath + "\"";
|
||||
|
||||
s_alpha = s_menuVisible ? 1f : 0f;
|
||||
PushAlpha(s);
|
||||
string btn = BuildRestore(s);
|
||||
SetRestoreVisible(!s_menuVisible);
|
||||
return "연결 " + s.bottomMenuPath + " (자식 " + s_menu.childCount + ") · " + btn;
|
||||
}
|
||||
|
||||
private static string BuildRestore(WLHudLayoutSettings s)
|
||||
{
|
||||
if (!s.bottomMenuRestoreButton) { DestroyRestore(); return "복귀 버튼 off"; }
|
||||
if (s_menu == null) return "복귀 버튼 — 부모 없음";
|
||||
|
||||
var parent = s_menu.parent as RectTransform; // WL_HUD (SafeAreaFitter 아래 = Safe Area 안)
|
||||
if (parent == null) return "복귀 버튼 — 부모가 RectTransform 이 아님";
|
||||
|
||||
if (s_restore == null)
|
||||
{
|
||||
var go = new GameObject("WL_BottomMenuRestore", typeof(RectTransform));
|
||||
go.layer = WLTextFxUtil.UILayer;
|
||||
go.hideFlags = HideFlags.DontSave; // 씬·프리팹에 굽지 않는다
|
||||
s_restore = (RectTransform)go.transform;
|
||||
s_restore.SetParent(parent, false);
|
||||
s_restoreBg = go.AddComponent<Image>();
|
||||
s_restoreBg.raycastTarget = true;
|
||||
var b = go.AddComponent<Button>();
|
||||
b.targetGraphic = s_restoreBg;
|
||||
b.onClick.AddListener(() => OnRestoreClicked()); // UnityAction = void() — 반환값 있는 메서드는 람다로 감싼다
|
||||
s_restoreLabel = WLTextFxUtil.NewText(s_restore, "Label", null, TextAlignmentOptions.Center);
|
||||
WLTextFxUtil.Stretch(s_restoreLabel.rectTransform);
|
||||
}
|
||||
|
||||
float u = WLVignetteUtil.UnitsPerPx(parent.GetComponentInParent<Canvas>());
|
||||
s_restore.anchorMin = s_restore.anchorMax = new Vector2(0.5f, 0f);
|
||||
s_restore.pivot = new Vector2(0.5f, 0.5f);
|
||||
s_restore.anchoredPosition = new Vector2(s.restoreButtonCenterPx.x * u, s.restoreButtonCenterPx.y * u);
|
||||
s_restore.sizeDelta = new Vector2(s.restoreButtonDiameterPx * u, s.restoreButtonDiameterPx * u);
|
||||
s_restore.localScale = Vector3.one;
|
||||
s_restoreBg.color = s.restoreButtonBgColor;
|
||||
s_restoreLabel.text = s.restoreButtonLabel;
|
||||
s_restoreLabel.color = s.restoreButtonLabelColor;
|
||||
s_restoreLabel.fontSize = s.restoreButtonFontPx * u;
|
||||
return "복귀 버튼 pos" + s_restore.anchoredPosition + " size" + s_restore.sizeDelta +
|
||||
" (px " + s.restoreButtonCenterPx + " 지름 " + s.restoreButtonDiameterPx + " u=" + u.ToString("F4") + ")";
|
||||
}
|
||||
|
||||
private static void SetRestoreVisible(bool on)
|
||||
{
|
||||
var s = St;
|
||||
if (s_restore == null) return;
|
||||
bool want = on && s != null && s.bottomMenuRestoreButton;
|
||||
if (s_restore.gameObject.activeSelf != want) s_restore.gameObject.SetActive(want);
|
||||
}
|
||||
|
||||
/// <summary>복귀 버튼 클릭 — 메뉴를 되돌리고 잠시 전투 판정을 무시한다.</summary>
|
||||
public static string OnRestoreClicked()
|
||||
{
|
||||
var s = St;
|
||||
RestoreClickCount++;
|
||||
float now = Time.unscaledTime;
|
||||
s_restoreHeldUntil = now + (s != null ? Mathf.Max(0f, s.restoreHoldSeconds) : 0f);
|
||||
return SetMenuVisible(true, now, false) + " · hold " + (s != null ? s.restoreHoldSeconds : 0f) + "s";
|
||||
}
|
||||
|
||||
private static void DestroyRestore()
|
||||
{
|
||||
if (s_restore == null) return;
|
||||
if (Application.isPlaying) Object.Destroy(s_restore.gameObject); else Object.DestroyImmediate(s_restore.gameObject);
|
||||
s_restore = null; s_restoreLabel = null; s_restoreBg = null;
|
||||
}
|
||||
|
||||
// ── 프로브 · 진단 ─────────────────────────────────────────────────────
|
||||
/// <summary>상태를 초기값으로(에디트 모드 반복 실행 대비 · 노드는 남긴다).</summary>
|
||||
public static string ResetState()
|
||||
{
|
||||
InCombat = false;
|
||||
s_lastCombatAt = -9999f; s_firstSignalAt = -9999f; s_restoreHeldUntil = -9999f;
|
||||
HideCount = ShowCount = RestoreClickCount = SignalCount = 0;
|
||||
LastSignal = "";
|
||||
s_menuVisible = true; s_alpha = 1f;
|
||||
var s = St; if (s != null) PushAlpha(s);
|
||||
SetRestoreVisible(false);
|
||||
return "상태 초기화(메뉴 보임 · 패드 idle)";
|
||||
}
|
||||
|
||||
/// <summary>연결·노드까지 되돌린다(프로브 종료용).</summary>
|
||||
public static string Teardown()
|
||||
{
|
||||
Subscribe(false);
|
||||
ResetState();
|
||||
DestroyRestore();
|
||||
s_menu = null; s_menuGroup = null; s_uiRoot = null;
|
||||
return "teardown 완료";
|
||||
}
|
||||
|
||||
public static string Dump()
|
||||
{
|
||||
var s = St;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("[HudCombatVisibility] 구독=" + s_subscribed + " inCombat=" + InCombat +
|
||||
" menuVisible=" + s_menuVisible + " alpha=" + s_alpha.ToString("F2") +
|
||||
" hide=" + HideCount + " show=" + ShowCount + " restoreClick=" + RestoreClickCount +
|
||||
" signals=" + SignalCount + "(last=" + LastSignal + ")");
|
||||
sb.AppendLine(" 메뉴 = " + (s_menu == null ? "미연결" :
|
||||
s_menu.name + " active=" + s_menu.gameObject.activeSelf + " 자식=" + s_menu.childCount +
|
||||
" group=" + (s_menuGroup != null ? s_menuGroup.alpha.ToString("F2") : "-")));
|
||||
sb.AppendLine(" 복귀버튼 = " + (s_restore == null ? "없음" :
|
||||
"active=" + s_restore.gameObject.activeSelf + " pos" + s_restore.anchoredPosition +
|
||||
" size" + s_restore.sizeDelta + " text=\"" + (s_restoreLabel != null ? s_restoreLabel.text : "") + "\""));
|
||||
if (s != null)
|
||||
sb.AppendLine(" 설정 enabled=" + s.combatHideBottomMenu + " exit=" + s.combatExitSeconds + "s enter=" +
|
||||
s.combatEnterDelaySeconds + "s fade=" + s.bottomMenuFadeSeconds + "s canvasGroup=" +
|
||||
s.bottomMenuUseCanvasGroup + " target신호=" + s.combatUseTargetAsSignal +
|
||||
" · 원점 전투" + s.fanOriginPx + " idle" + s.idleFanOriginPx +
|
||||
" 공격 전투" + s.attackCenterPx + " idle" + s.idleAttackCenterPx);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>매 프레임 <see cref="HudCombatVisibility.Tick"/> 을 부르는 런타임 전용 러너(씬에 굽지 않는다).</summary>
|
||||
internal sealed class HudCombatVisibilityRunner : MonoBehaviour
|
||||
{
|
||||
private static HudCombatVisibilityRunner s_ins;
|
||||
private NewGameUI _lastUi;
|
||||
|
||||
internal static void Ensure()
|
||||
{
|
||||
if (s_ins != null) return;
|
||||
var go = new GameObject("WL_HudCombatVisibilityRunner");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
Object.DontDestroyOnLoad(go);
|
||||
s_ins = go.AddComponent<HudCombatVisibilityRunner>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var ui = NewGameUI.Ins;
|
||||
if (ui != null && !ReferenceEquals(ui, _lastUi))
|
||||
{
|
||||
_lastUi = ui;
|
||||
HudCombatVisibility.Bind(ui.transform);
|
||||
}
|
||||
HudCombatVisibility.Tick(Time.unscaledTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac5c1e39203b8f94f875435d6b356b67
|
||||
|
|
@ -51,6 +51,8 @@ namespace WL.UI
|
|||
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); }
|
||||
|
|
@ -127,8 +129,14 @@ namespace WL.UI
|
|||
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");
|
||||
" fontPx=" + s.killChainFontPx + "→" + mainLabel.fontSize.ToString("F1") +
|
||||
" edge[" + LastEdge + "]";
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────── 이벤트
|
||||
|
|
@ -217,6 +225,7 @@ namespace WL.UI
|
|||
" 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ using System.Collections.Generic;
|
|||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.UI
|
||||
|
|
@ -40,6 +41,7 @@ namespace WL.UI
|
|||
public RectTransform rt;
|
||||
public TextMeshProUGUI label;
|
||||
public CanvasGroup group;
|
||||
public Image panel; // WL-813y — 밝은 배경 대비용 반투명 패널
|
||||
public int itemId, count;
|
||||
public float life, lifeMax, elapsed;
|
||||
public bool used;
|
||||
|
|
@ -58,6 +60,8 @@ namespace WL.UI
|
|||
public int MergeCount { get; private set; }
|
||||
public int DropCount { get; private set; } // 줄 상한 초과로 밀려난 수
|
||||
public bool Subscribed { get { return _subscribed; } }
|
||||
/// <summary>마지막으로 적용한 아웃라인/그림자 실측 문자열(WL-813y).</summary>
|
||||
public string LastEdge { get; private set; } = "";
|
||||
public int VisibleLines
|
||||
{
|
||||
get { int n = 0; for (int i = 0; i < _lines.Count; i++) if (_lines[i].used) n++; return n; }
|
||||
|
|
@ -100,11 +104,16 @@ namespace WL.UI
|
|||
{
|
||||
int idx = _lines.Count;
|
||||
var lrt = WLTextFxUtil.NewChild(lineRoot, "Line" + idx);
|
||||
// 패널을 먼저 만든다 — uGUI 는 형제 순서가 곧 그리는 순서라 라벨이 위로 온다(813tj 교훈).
|
||||
var prt = WLTextFxUtil.NewChild(lrt, "Panel");
|
||||
var panel = prt.GetComponent<Image>();
|
||||
if (panel == null) panel = prt.gameObject.AddComponent<Image>();
|
||||
panel.raycastTarget = false;
|
||||
var label = WLTextFxUtil.NewText(lrt, "Label", font, TextAlignmentOptions.Right);
|
||||
var cg = lrt.GetComponent<CanvasGroup>();
|
||||
if (cg == null) cg = lrt.gameObject.AddComponent<CanvasGroup>();
|
||||
cg.interactable = false; cg.blocksRaycasts = false; cg.alpha = 0f;
|
||||
_lines.Add(new Line { rt = lrt, label = label, group = cg });
|
||||
_lines.Add(new Line { rt = lrt, label = label, group = cg, panel = panel });
|
||||
made = true;
|
||||
}
|
||||
for (int i = want; i < _lines.Count; i++)
|
||||
|
|
@ -141,6 +150,19 @@ namespace WL.UI
|
|||
WLTextFxUtil.Stretch(l.label.rectTransform);
|
||||
l.label.fontSize = s.lootToastFontPx * u;
|
||||
l.label.alignment = TextAlignmentOptions.MidlineRight;
|
||||
|
||||
// WL-813y — 줄 뒤 패널(대비) + 글자 아웃라인. 값은 전부 SO.
|
||||
if (l.panel != null)
|
||||
{
|
||||
bool on = s.lootToastPanelEnabled;
|
||||
if (l.panel.enabled != on) l.panel.enabled = on;
|
||||
l.panel.color = s.lootToastPanelColor;
|
||||
var prt = l.panel.rectTransform;
|
||||
prt.anchorMin = Vector2.zero; prt.anchorMax = Vector2.one; prt.pivot = new Vector2(0.5f, 0.5f);
|
||||
prt.offsetMin = new Vector2(-s.lootToastPanelPadXPx * u, -s.lootToastPanelPadYPx * u);
|
||||
prt.offsetMax = new Vector2(s.lootToastPanelPadXPx * u, s.lootToastPanelPadYPx * u);
|
||||
}
|
||||
LastEdge = WLTextFxUtil.ApplyEdge(l.label, s.Edge(s.lootToastEdge));
|
||||
sb.Append(" · [").Append(i).Append("] pos=").Append(l.rt.anchoredPosition);
|
||||
}
|
||||
return sb.ToString();
|
||||
|
|
@ -223,7 +245,7 @@ namespace WL.UI
|
|||
|
||||
private void Render(Line l, string name, int grade, WLCombatTextSettings s)
|
||||
{
|
||||
string colored = s.GradeColorTag(grade) + name;
|
||||
string colored = s.GradeColorTagBright(grade) + name; // WL-813y — 등급 1 회색이 모래 배경에서 사라진다(Q3 D-6 ②)
|
||||
l.label.text = l.count > 1
|
||||
? DSUtil.Format(s.lootToastFormat, colored, l.count)
|
||||
: DSUtil.Format(s.lootToastFormatSingle, colored);
|
||||
|
|
@ -336,8 +358,18 @@ namespace WL.UI
|
|||
sb.AppendLine(" [" + i + "] used=" + l.used + " item=" + l.itemId + " x" + l.count +
|
||||
" alpha=" + l.group.alpha.ToString("F2") + " pos=" + l.rt.anchoredPosition +
|
||||
" size=" + l.rt.sizeDelta + " font=" + l.label.fontSize.ToString("F1") +
|
||||
" panel=" + (l.panel != null ? (l.panel.enabled ? "#" + ColorUtility.ToHtmlStringRGBA(l.panel.color) +
|
||||
" off" + l.panel.rectTransform.offsetMin + l.panel.rectTransform.offsetMax : "off") : "없음") +
|
||||
" text=\"" + l.label.text + "\"");
|
||||
}
|
||||
var st = WLCombatTextSettings.Instance;
|
||||
if (st != null)
|
||||
sb.AppendLine(" 설정 seconds=" + st.lootToastSeconds + "s fontPx=" + st.lootToastFontPx +
|
||||
" widthPx=" + st.lootToastWidthPx + " lineHeightPx=" + st.lootToastLineHeightPx +
|
||||
" rightMarginPx=" + st.lootToastRightMarginPx + " centerOffsetPx=" + st.lootToastCenterOffsetPx +
|
||||
" panel=" + st.lootToastPanelEnabled + " 밝기하한=" + st.lootToastMinBrightness +
|
||||
" · 등급1 태그 " + st.GradeColorTag(1) + " → " + st.GradeColorTagBright(1));
|
||||
sb.AppendLine(" edge = " + LastEdge);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,25 @@ namespace WL.UI
|
|||
return Active.QueueShow(Time.unscaledTime);
|
||||
}
|
||||
|
||||
// ── WL-813y — 813i DeathFlow 와 이어 붙인 진입점 2개 ──────────────────
|
||||
/// <summary>
|
||||
/// `DeathFlow.ReviveRequested` 진입점 — 813i 가 **사망 연출을 이미 기다렸으므로** 지연 없이 연다.
|
||||
/// (`NotifyDeath()` 의 UI 지연과 겹쳐 두 번 뜨지 않도록 예약을 지우고 1회만 연다.)
|
||||
/// </summary>
|
||||
public static string NotifyReviveRequested()
|
||||
{
|
||||
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
|
||||
return Active.ShowNowOnce();
|
||||
}
|
||||
|
||||
/// <summary>`DeathFlow.Revived` 진입점 — 부활이 끝났으니 팝업을 닫는다(자동/외부 부활 포함).</summary>
|
||||
public static string NotifyRevived()
|
||||
{
|
||||
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
|
||||
Active.HideNow();
|
||||
return "부활 완료 — 팝업 닫음";
|
||||
}
|
||||
|
||||
[Header("폴백 팝업 구성 요소 (공용 Popup 이 없을 때만 쓴다)")]
|
||||
[SerializeField] private CanvasGroup group;
|
||||
[SerializeField] private RectTransform box;
|
||||
|
|
@ -66,6 +85,7 @@ namespace WL.UI
|
|||
private RectTransform _rt;
|
||||
private Canvas _canvas;
|
||||
private float _showAt = -1f;
|
||||
private bool _open; // 팝업이 열려 있는가(공용 Popup 경로 포함 · WL-813y)
|
||||
|
||||
// ── 진단 ──────────────────────────────────────────────────────────────
|
||||
public int DeathSeen { get; private set; }
|
||||
|
|
@ -112,11 +132,20 @@ namespace WL.UI
|
|||
ShowNow();
|
||||
}
|
||||
|
||||
/// <summary>예약을 지우고, 아직 안 떠 있을 때만 연다(WL-813y · 813i 이중 통지 방지).</summary>
|
||||
public string ShowNowOnce()
|
||||
{
|
||||
_showAt = -1f;
|
||||
if (_open) return "이미 표시 중 — 중복 무시(shows=" + ShownCount + ")";
|
||||
return ShowNow();
|
||||
}
|
||||
|
||||
/// <summary>지금 바로 팝업을 연다(공용 Popup 우선 · 실패하면 폴백 노드).</summary>
|
||||
public string ShowNow()
|
||||
{
|
||||
var s = WLSurvivalUiSettings.Instance;
|
||||
if (s == null) return "WLSurvivalUiSettings 에셋 없음";
|
||||
_open = true;
|
||||
|
||||
string msg = BuildMessage(s);
|
||||
LastMessage = msg;
|
||||
|
|
@ -191,6 +220,7 @@ namespace WL.UI
|
|||
public void HideNow()
|
||||
{
|
||||
_showAt = -1f;
|
||||
_open = false;
|
||||
if (group == null) return;
|
||||
group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false;
|
||||
}
|
||||
|
|
@ -327,7 +357,9 @@ namespace WL.UI
|
|||
var s = WLSurvivalUiSettings.Instance;
|
||||
sb.AppendLine("[ReviveDialog] deaths=" + DeathSeen + " shows=" + ShownCount + " confirms=" + ConfirmCount +
|
||||
" 대기중=" + Pending + " 공용팝업사용=" + UsedCommonPopup +
|
||||
" 폴백표시=" + FallbackVisible + " 813i(ReviveRequested)=" + (ReviveRequested != null));
|
||||
" 폴백표시=" + FallbackVisible + " open=" + _open +
|
||||
" 813i(ReviveRequested)=" + (ReviveRequested != null) +
|
||||
" 브리지=" + WLSurvivalUiBridge.Installed);
|
||||
sb.AppendLine(" 본문=\"" + LastMessage.Replace("\n", " / ") + "\"");
|
||||
sb.AppendLine(" 공용 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음(에디트 모드/씬 미로드)"));
|
||||
if (box != null)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using System.Text;
|
|||
using TMPro;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Growth;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
|
|
@ -47,7 +48,9 @@ namespace WL.UI
|
|||
private readonly Dictionary<int, double> _snapshot = new Dictionary<int, double>();
|
||||
private RectTransform _rt;
|
||||
private bool _subscribed;
|
||||
private bool _usingGrowth;
|
||||
private float _life, _lifeMax, _elapsed;
|
||||
private string _lastEdge = "";
|
||||
|
||||
/// <summary>813k(또는 다른 발생원)가 찾아 쓸 수 있는 현재 활성 팝업.</summary>
|
||||
public static StatPopup Active { get; private set; }
|
||||
|
|
@ -59,6 +62,13 @@ namespace WL.UI
|
|||
public string LastText { get; private set; }
|
||||
public bool Subscribed { get { return _subscribed; } }
|
||||
public bool Visible { get { return group != null && group.alpha > 0f; } }
|
||||
/// <summary>813k 축(GrowthEvents)을 물었는가. false = 811b 코어 축.</summary>
|
||||
public bool UsingGrowthEvents { get { return _usingGrowth; } }
|
||||
/// <summary>마지막 표시가 "포인트" 줄로 떨어졌는가(스탯 변화 0).</summary>
|
||||
public bool UsedPointLine { get; private set; }
|
||||
public int LastLevelsGained { get; private set; }
|
||||
public int LastStatPointTotal { get; private set; }
|
||||
public string LastEdge { get { return _lastEdge; } }
|
||||
|
||||
private void OnEnable() { Initialize(); }
|
||||
private void OnDisable() { Subscribe(false); if (Active == this) Active = null; }
|
||||
|
|
@ -81,8 +91,20 @@ namespace WL.UI
|
|||
public void Subscribe(bool on)
|
||||
{
|
||||
if (on == _subscribed) return;
|
||||
if (on) CombatEvents.LevelUp.Add(OnLevelUp);
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
bool growth = s == null || s.statPopupUseGrowthEvents;
|
||||
if (on)
|
||||
{
|
||||
// 813k 축(스탯 3종 + 잔여 포인트)이 우선. 811b 코어 축은 813k 가 forwardToCombatEvents 로
|
||||
// **같은 프레임에 또** 쏘므로, 둘 다 구독하면 팝업이 두 번 뜬다 → 하나만 문다.
|
||||
if (growth) { GrowthEvents.LevelUp.Add(OnGrowthLevelUp); _usingGrowth = true; }
|
||||
else { CombatEvents.LevelUp.Add(OnLevelUp); _usingGrowth = false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_usingGrowth) GrowthEvents.LevelUp.Remove(OnGrowthLevelUp);
|
||||
else CombatEvents.LevelUp.Remove(OnLevelUp);
|
||||
}
|
||||
_subscribed = on;
|
||||
}
|
||||
|
||||
|
|
@ -142,8 +164,15 @@ namespace WL.UI
|
|||
_rows[i].fontSize = s.statPopupRowFontPx * u;
|
||||
_rows[i].color = s.statPopupRowColor;
|
||||
}
|
||||
|
||||
// WL-813y — 밝은 배경 대비(Q3 D-6 ①). 값은 SO · 포스트 0.
|
||||
var edge = s.Edge(s.statPopupEdge);
|
||||
_lastEdge = WLTextFxUtil.ApplyEdge(titleLabel, edge);
|
||||
for (int i = 0; i < _rows.Count; i++) WLTextFxUtil.ApplyEdge(_rows[i], edge);
|
||||
|
||||
return "box pos=" + box.anchoredPosition + " size=" + box.sizeDelta + " rows=" + _rows.Count +
|
||||
" unitsPerPx=" + u.ToString("F4") + " titleFont=" + titleLabel.fontSize.ToString("F1");
|
||||
" unitsPerPx=" + u.ToString("F4") + " titleFont=" + titleLabel.fontSize.ToString("F1") +
|
||||
" edge[" + _lastEdge + "]";
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────── 이벤트 · 공개 API
|
||||
|
|
@ -155,6 +184,38 @@ namespace WL.UI
|
|||
Report(e.newLevel, BuildDeltasFromSnapshot(e.pc, s));
|
||||
}
|
||||
|
||||
// ── 813k 축 (WL-813y §1-2 · Q3 결함 D-7) ──────────────────────────────
|
||||
/// <summary>
|
||||
/// 813k `GrowthEvents.LevelUp` 구독. 페이로드가 스탯 3종의 prev/new 와 잔여 스탯 포인트를 실어 온다
|
||||
/// (813k 완료보고 §3). 원본은 레벨업으로 스탯을 올리지 않으므로 **prev == new 가 기본** —
|
||||
/// 그때는 "포인트" 줄로 떨어진다(813k 보고 ①).
|
||||
/// </summary>
|
||||
private void OnGrowthLevelUp(in PlayerLevelUpEvent e)
|
||||
{
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null || !WLCombatTextSettings.Enabled || !s.statPopupEnabled) return;
|
||||
// 다단 레벨업(같은 Add_Exp 에서 여러 번)은 마지막 것만 남긴다 — 팝업이 겹쳐 깜빡이지 않도록.
|
||||
LastLevelsGained = e.levelsGained;
|
||||
LastStatPointTotal = e.statPointTotal;
|
||||
Show(e.newLevel, BuildDeltasFromGrowth(in e, s), PointLine(s, e.statPointTotal));
|
||||
}
|
||||
|
||||
private static WLStatDelta[] BuildDeltasFromGrowth(in PlayerLevelUpEvent e, WLCombatTextSettings s)
|
||||
{
|
||||
int n = Mathf.Clamp(s.statPopupRows, 1, 3);
|
||||
var arr = new WLStatDelta[n];
|
||||
if (n > 0) arr[0] = new WLStatDelta { name = e.statAKind.ToString(), prev = e.prevStatA, next = e.newStatA };
|
||||
if (n > 1) arr[1] = new WLStatDelta { name = e.statBKind.ToString(), prev = e.prevStatB, next = e.newStatB };
|
||||
if (n > 2) arr[2] = new WLStatDelta { name = e.statCKind.ToString(), prev = e.prevStatC, next = e.newStatC };
|
||||
return arr;
|
||||
}
|
||||
|
||||
private static string PointLine(WLCombatTextSettings s, int statPointTotal)
|
||||
{
|
||||
if (s == null || string.IsNullOrEmpty(s.statPopupPointFormat)) return null;
|
||||
return DSUtil.Format(s.statPopupPointFormat, statPointTotal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 🔵 813k 연결점 — 이전/새 스탯을 그대로 실어 보내면 이 메서드가 팝업을 띄운다.
|
||||
/// deltas 가 null 이면 컴포넌트가 스스로 스냅샷 차이를 만든다.
|
||||
|
|
@ -166,7 +227,13 @@ namespace WL.UI
|
|||
}
|
||||
|
||||
/// <summary>팝업을 띄운다(프로브도 이 경로를 쓴다).</summary>
|
||||
public string Show(int newLevel, WLStatDelta[] deltas)
|
||||
public string Show(int newLevel, WLStatDelta[] deltas) { return Show(newLevel, deltas, null); }
|
||||
|
||||
/// <summary>
|
||||
/// 팝업을 띄운다. <paramref name="fallbackLine"/> 은 **스탯 변화가 하나도 없을 때** 대신 채우는 줄
|
||||
/// (WL-813y — 원본은 레벨업으로 스탯을 안 올리고 포인트만 준다 · 813k 보고 ①).
|
||||
/// </summary>
|
||||
public string Show(int newLevel, WLStatDelta[] deltas, string fallbackLine)
|
||||
{
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null) return "설정 없음";
|
||||
|
|
@ -187,6 +254,14 @@ namespace WL.UI
|
|||
sb.Append(" / ").Append(_rows[i].text);
|
||||
shown++;
|
||||
}
|
||||
UsedPointLine = false;
|
||||
if (shown == 0 && !string.IsNullOrEmpty(fallbackLine) && _rows.Count > 0)
|
||||
{
|
||||
_rows[0].text = fallbackLine;
|
||||
sb.Append(" / ").Append(fallbackLine);
|
||||
shown = 1;
|
||||
UsedPointLine = true;
|
||||
}
|
||||
LastRowCount = shown;
|
||||
LastText = sb.ToString();
|
||||
|
||||
|
|
@ -262,6 +337,34 @@ namespace WL.UI
|
|||
return "dispatch LevelUp level=" + newLevel + " · 구독자=" + CombatEvents.LevelUp.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 합성 813k 레벨업 — GrowthEvents 의 **실제 Dispatch 경로**로 쏜다(구독 배선까지 검증).
|
||||
/// statDelta 가 0 이면 원본 기본값(prev == new) 상황 = "포인트" 줄로 떨어지는지 확인한다.
|
||||
/// </summary>
|
||||
public static string RaiseFakeGrowthLevelUp(int newLevel, int statPointTotal, double statDelta)
|
||||
{
|
||||
var e = new PlayerLevelUpEvent
|
||||
{
|
||||
data = null,
|
||||
pc = null,
|
||||
prevLevel = newLevel - 1,
|
||||
newLevel = newLevel,
|
||||
levelsGained = 1,
|
||||
statPointTotal = statPointTotal,
|
||||
statAKind = eStat.MaxHP, statBKind = eStat.MaxMP, statCKind = eStat.ATK,
|
||||
prevStatA = 1000d, newStatA = 1000d + statDelta,
|
||||
prevStatB = 200d, newStatB = 200d + statDelta,
|
||||
prevStatC = 50d, newStatC = 50d + statDelta,
|
||||
hpBefore = 370d, maxHp = 1000d, mpBefore = 120d, maxMp = 200d,
|
||||
position = Vector3.zero,
|
||||
time = Time.unscaledTime,
|
||||
frame = Time.frameCount,
|
||||
};
|
||||
GrowthEvents.LevelUp.Dispatch(in e);
|
||||
return "dispatch GrowthEvents.LevelUp level=" + newLevel + " point=" + statPointTotal +
|
||||
" statDelta=" + statDelta + " · 구독자=" + GrowthEvents.LevelUp.Count;
|
||||
}
|
||||
|
||||
/// <summary>합성 스탯 델타로 직접 표시(813k 페이로드가 오면 이 모양이 된다).</summary>
|
||||
public string ShowFake(int newLevel, string[] names, double[] prev, double[] next)
|
||||
{
|
||||
|
|
@ -274,9 +377,13 @@ namespace WL.UI
|
|||
public string Dump()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("StatPopup subscribed=" + _subscribed + " levelUpSeen=" + LevelUpSeen +
|
||||
sb.AppendLine("StatPopup subscribed=" + _subscribed + " 축=" + (_usingGrowth ? "813k GrowthEvents" : "811b CombatEvents") +
|
||||
"(구독자 growth=" + GrowthEvents.LevelUp.Count + " core=" + CombatEvents.LevelUp.Count + ")" +
|
||||
" levelUpSeen=" + LevelUpSeen +
|
||||
" lastLevel=" + LastLevel + " rows=" + LastRowCount +
|
||||
" 포인트줄=" + UsedPointLine + " statPoint=" + LastStatPointTotal + " levelsGained=" + LastLevelsGained +
|
||||
" alpha=" + (group != null ? group.alpha.ToString("F2") : "-"));
|
||||
sb.AppendLine(" edge = " + _lastEdge);
|
||||
sb.AppendLine(" title=\"" + (titleLabel != null ? titleLabel.text : "") + "\" pos=" +
|
||||
(box != null ? box.anchoredPosition.ToString() : "-") +
|
||||
" size=" + (box != null ? box.sizeDelta.ToString() : "-") +
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ namespace WL.UI
|
|||
public float LastMinGapPx { get; private set; }
|
||||
public int LastActiveSlots { get; private set; }
|
||||
public int LastReserveSlots { get; private set; }
|
||||
/// <summary>전투 액션 버튼(스킬 4 + 공격 + 물약)만의 도달 반경 — 자동전투 토글 제외(WL-813y).</summary>
|
||||
public float LastActionReachPx { get; private set; }
|
||||
/// <summary>마지막 배치 때의 하단 메뉴 상태(true = 메뉴 보임 → 패드가 idle 자리로 올라감).</summary>
|
||||
public bool LastBottomMenuVisible { get; private set; }
|
||||
|
||||
private void Awake() { _rt = GetComponent<RectTransform>(); }
|
||||
private void OnEnable() { Apply(); }
|
||||
|
|
@ -105,9 +109,13 @@ namespace WL.UI
|
|||
LastActiveSlots = active;
|
||||
LastReserveSlots = Mathf.Max(0, total - active);
|
||||
|
||||
float maxReach = 0f;
|
||||
float maxReach = 0f, actionReach = 0f;
|
||||
var centers = new Vector2[total];
|
||||
|
||||
// WL-813y — 하단 메뉴가 보이는 동안만 패드를 위로 올린다(전투 중에는 모서리 원점).
|
||||
bool menuVisible = HudCombatVisibility.BottomMenuVisible;
|
||||
LastBottomMenuVisible = menuVisible;
|
||||
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var card = cards[i];
|
||||
|
|
@ -116,12 +124,13 @@ namespace WL.UI
|
|||
if (crt == null) continue;
|
||||
|
||||
bool isSkill = i < active;
|
||||
Vector2 px = isSkill ? s.FanSlotPx(i) : s.ReserveSlotPx(i - active);
|
||||
Vector2 px = isSkill ? s.FanSlotPx(i, menuVisible) : s.ReserveSlotPx(i - active, menuVisible);
|
||||
float dia = isSkill ? s.slotDiameterPx : s.reserveDiameterPx;
|
||||
|
||||
PlaceFromCorner(crt, px, dia, u, s.mirrorLeftHanded);
|
||||
centers[i] = px;
|
||||
maxReach = Mathf.Max(maxReach, px.magnitude + dia * 0.5f);
|
||||
if (isSkill) actionReach = Mathf.Max(actionReach, px.magnitude + dia * 0.5f);
|
||||
|
||||
// 4 활성 · 나머지는 물약/회피(813j/813r) 예약 자리 = 비활성
|
||||
if (card.gameObject.activeSelf != isSkill) card.gameObject.SetActive(isSkill);
|
||||
|
|
@ -131,8 +140,10 @@ namespace WL.UI
|
|||
var attack = FindChild(attackButtonName);
|
||||
if (attack != null)
|
||||
{
|
||||
PlaceFromCorner(attack, s.attackCenterPx, s.attackDiameterPx, u, s.mirrorLeftHanded);
|
||||
maxReach = Mathf.Max(maxReach, s.attackCenterPx.magnitude + s.attackDiameterPx * 0.5f);
|
||||
Vector2 apx = s.AttackCenterPxFor(menuVisible);
|
||||
PlaceFromCorner(attack, apx, s.attackDiameterPx, u, s.mirrorLeftHanded);
|
||||
maxReach = Mathf.Max(maxReach, apx.magnitude + s.attackDiameterPx * 0.5f);
|
||||
actionReach = Mathf.Max(actionReach, apx.magnitude + s.attackDiameterPx * 0.5f);
|
||||
}
|
||||
var auto = FindChild(autoButtonName);
|
||||
if (auto != null)
|
||||
|
|
@ -146,13 +157,28 @@ namespace WL.UI
|
|||
|
||||
// 813tj 물약 버튼 — 예약 슬롯 자리에 있으므로 미러·해상도가 바뀌면 같이 다시 앉힌다.
|
||||
var potion = GetComponentInChildren<PotionButton>(true);
|
||||
if (potion != null) potion.ApplyLayout();
|
||||
if (potion != null)
|
||||
{
|
||||
potion.ApplyLayout();
|
||||
Vector2 ppx = s.ReserveSlotPx(Mathf.Max(0, PotionSlotIndex()), menuVisible);
|
||||
actionReach = Mathf.Max(actionReach, ppx.magnitude + s.reserveDiameterPx * 0.5f);
|
||||
}
|
||||
LastActionReachPx = actionReach;
|
||||
|
||||
return "applied slots=" + active + "/" + total + " unitsPerPx=" + u.ToString("F4") +
|
||||
" maxReachPx=" + maxReach.ToString("F1") + " minGapPx=" + LastMinGapPx.ToString("F1") +
|
||||
" maxReachPx=" + maxReach.ToString("F1") +
|
||||
" actionReachPx=" + actionReach.ToString("F1") + "(스킬+공격+물약 · 자동토글 제외)" +
|
||||
" minGapPx=" + LastMinGapPx.ToString("F1") +
|
||||
" menuVisible=" + menuVisible + " fanOrigin=" + s.FanOriginPxFor(menuVisible) +
|
||||
" mirror=" + s.mirrorLeftHanded;
|
||||
}
|
||||
|
||||
private static int PotionSlotIndex()
|
||||
{
|
||||
var su = WLSurvivalUiSettings.Instance;
|
||||
return su != null ? su.potionReserveSlotIndex : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모서리(우하단 · 미러면 좌하단) 기준 (dx, dy) px 로 배치한다.
|
||||
/// 813tj 물약 버튼(PotionButton)이 예약 슬롯 자리에 앉을 때 **같은 산식**을 쓰도록 public 이다.
|
||||
|
|
@ -225,6 +251,10 @@ namespace WL.UI
|
|||
" activeSlots=" + LastActiveSlots + "/" + (LastActiveSlots + LastReserveSlots) +
|
||||
" maxReachPx=" + LastMaxReachPx.ToString("F1") +
|
||||
" (목표 " + (s != null ? s.thumbReachRadiusPx.ToString("F0") : "?") + ")" +
|
||||
" actionReachPx=" + LastActionReachPx.ToString("F1") + "(자동토글 제외)" +
|
||||
" menuVisible=" + LastBottomMenuVisible +
|
||||
" fanOrigin=" + (s != null ? s.FanOriginPxFor(LastBottomMenuVisible).ToString() : "?") +
|
||||
" attackCenter=" + (s != null ? s.AttackCenterPxFor(LastBottomMenuVisible).ToString() : "?") +
|
||||
" minGapPx=" + LastMinGapPx.ToString("F1"));
|
||||
sb.AppendLine(" root " + RectLine(_rt != null ? _rt : GetComponent<RectTransform>()) +
|
||||
" safeAreaFitter=" + (GetComponent<SafeAreaFitter>() != null));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSurvivalUiBridge.cs — 813tj 가 열어 둔 6줄을 813i API 에 연결한다 (WL-813y §1-1 · #813)
|
||||
//
|
||||
// 813tj 완료보고 §4 ②:
|
||||
// "813i 병합 시 PotionButton.CountProvider/CooldownRemainProvider/CooldownTotalProvider/UseHandler 4줄 +
|
||||
// ReviveDialog.NotifyDeath()/ReviveRequested 2줄만 연결하면 끝(UI 수정 0)"
|
||||
//
|
||||
// ■ 왜 Gameplay 가 아니라 여기인가
|
||||
// 813i(`Assets/WL/Combat/Survival/**`)는 **Gameplay 소유**라 이 세션이 손대지 못한다(발주서 §2 금지).
|
||||
// UI 쪽에서 813i 의 **public API 만 호출**하는 얇은 브리지를 두면 소유 경계를 넘지 않고 6줄이 붙는다.
|
||||
//
|
||||
// ■ 연결 6줄 (813i 완료보고 §4 · 전부 `WL.Combat.Survival`)
|
||||
// ① PotionButton.CountProvider ← PotionUse.Remaining
|
||||
// ② PotionButton.CooldownRemainProvider ← PotionUse.CooldownRemaining
|
||||
// ③ PotionButton.CooldownTotalProvider ← PotionUse.CooldownTotal
|
||||
// ④ PotionButton.UseHandler ← PotionUse.TryUse() == PotionResult.Used
|
||||
// ⑤ ReviveDialog.NotifyDeath() ← DeathFlow.Died / DeathFlow.ReviveRequested 구독
|
||||
// ⑥ ReviveDialog.ReviveRequested ← DeathFlow.Revive()
|
||||
//
|
||||
// ■ 스위치 = 813i 소유 `WLSurvivalSettings.Enabled`(에셋 `enabled` · **Lead 가 병합 시 켠다**)
|
||||
// 꺼져 있으면 물약 표시는 813tj 의 표시용 기본값으로 떨어지고, 사망/부활 이벤트는 애초에 안 온다
|
||||
// → 병합 전후 어느 쪽이든 화면이 깨지지 않는다(C8).
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Survival;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
public static class WLSurvivalUiBridge
|
||||
{
|
||||
public static bool Installed { get; private set; }
|
||||
|
||||
// 진단(프로브가 읽는다 · 실측만)
|
||||
public static int DiedSeen, ReviveRequestedSeen, RevivedSeen, PotionChangedSeen;
|
||||
public static int ReviveCalls, PotionUseCalls, PotionUseOk;
|
||||
public static string LastEvent = "";
|
||||
public static float LastEventTime;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Boot() { Install(); }
|
||||
|
||||
/// <summary>6줄을 연결한다(중복 호출 안전). 프로브도 이 경로를 쓴다.</summary>
|
||||
public static string Install()
|
||||
{
|
||||
if (Installed) return "이미 연결됨";
|
||||
|
||||
// ── 물약 4줄 ──────────────────────────────────────────────────────
|
||||
// 스위치가 꺼져 있으면 813tj 의 표시용 기본값으로 떨어진다(연결 전 화면과 동일).
|
||||
PotionButton.CountProvider = ReadCount;
|
||||
PotionButton.CooldownRemainProvider = ReadCooldownRemain;
|
||||
PotionButton.CooldownTotalProvider = ReadCooldownTotal;
|
||||
PotionButton.UseHandler = Use;
|
||||
|
||||
// ── 부활 2줄 ──────────────────────────────────────────────────────
|
||||
ReviveDialog.ReviveRequested = RequestRevive;
|
||||
DeathFlow.Died.Add(OnDied);
|
||||
DeathFlow.ReviveRequested.Add(OnReviveRequested);
|
||||
DeathFlow.Revived.Add(OnRevived);
|
||||
PotionUse.Changed.Add(OnPotionChanged);
|
||||
|
||||
Installed = true;
|
||||
return "연결 6줄 완료 · survivalEnabled=" + WLSurvivalSettings.Enabled;
|
||||
}
|
||||
|
||||
/// <summary>연결을 끊는다(프로브 종료 · 813tj 원래 상태로 복귀).</summary>
|
||||
public static string Uninstall()
|
||||
{
|
||||
if (!Installed) return "연결 안 돼 있음";
|
||||
PotionButton.ClearProvider();
|
||||
ReviveDialog.ReviveRequested = null;
|
||||
DeathFlow.Died.Remove(OnDied);
|
||||
DeathFlow.ReviveRequested.Remove(OnReviveRequested);
|
||||
DeathFlow.Revived.Remove(OnRevived);
|
||||
PotionUse.Changed.Remove(OnPotionChanged);
|
||||
Installed = false;
|
||||
return "연결 해제(813tj 미연결 상태로 복귀)";
|
||||
}
|
||||
|
||||
// ── 물약 (813i PotionUse) ─────────────────────────────────────────────
|
||||
private static int ReadCount()
|
||||
{
|
||||
if (!WLSurvivalSettings.Enabled) return FallbackCount();
|
||||
return PotionUse.Remaining;
|
||||
}
|
||||
|
||||
private static float ReadCooldownRemain()
|
||||
{
|
||||
return WLSurvivalSettings.Enabled ? PotionUse.CooldownRemaining : 0f;
|
||||
}
|
||||
|
||||
private static float ReadCooldownTotal()
|
||||
{
|
||||
if (!WLSurvivalSettings.Enabled) return FallbackCooldown();
|
||||
float t = PotionUse.CooldownTotal;
|
||||
return t > 0f ? t : FallbackCooldown();
|
||||
}
|
||||
|
||||
private static bool Use()
|
||||
{
|
||||
PotionUseCalls++;
|
||||
if (!WLSurvivalSettings.Enabled) { Note("potion:disabled"); return false; }
|
||||
var r = PotionUse.TryUse();
|
||||
bool ok = r == PotionResult.Used;
|
||||
if (ok) PotionUseOk++;
|
||||
Note("potion:" + r);
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static int FallbackCount()
|
||||
{
|
||||
var s = WLSurvivalUiSettings.Instance;
|
||||
return s != null ? s.potionFallbackCount : 0;
|
||||
}
|
||||
|
||||
private static float FallbackCooldown()
|
||||
{
|
||||
var s = WLSurvivalUiSettings.Instance;
|
||||
return s != null ? s.potionFallbackCooldownSeconds : 0f;
|
||||
}
|
||||
|
||||
private static void OnPotionChanged(in PotionEvent e)
|
||||
{
|
||||
PotionChangedSeen++;
|
||||
Note("changed:" + e.change + "(" + e.remaining + "/" + e.max + ")");
|
||||
}
|
||||
|
||||
// ── 사망 · 부활 (813i DeathFlow) ──────────────────────────────────────
|
||||
/// <summary>사망 = 팝업 예약(813tj 는 자체 지연으로 사망 연출 시간을 기다린다).</summary>
|
||||
private static void OnDied(in DeathEvent e)
|
||||
{
|
||||
DiedSeen++;
|
||||
Note("died#" + e.deathCount + " display=" + e.displaySeconds.ToString("F2") + "s · " + ReviveDialog.NotifyDeath());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813i 가 사망 연출을 끝내고 부활 대기에 들어간 시점 — 지연 없이 지금 연다.
|
||||
/// **이 구독자가 있어야** 813i 의 `autoReviveWhenNoListener`(구독 0 이면 자동 부활)가 꺼진다.
|
||||
/// </summary>
|
||||
private static void OnReviveRequested(in ReviveRequestEvent e)
|
||||
{
|
||||
ReviveRequestedSeen++;
|
||||
Note("reviveRequested#" + e.deathCount + " worldHeld=" + e.worldHeld + " · " + ReviveDialog.NotifyReviveRequested());
|
||||
}
|
||||
|
||||
private static void OnRevived(in RevivedEvent e)
|
||||
{
|
||||
RevivedSeen++;
|
||||
Note("revived#" + e.deathCount + " external=" + e.external + " auto=" + e.auto + " · " + ReviveDialog.NotifyRevived());
|
||||
}
|
||||
|
||||
/// <summary>팝업 "존 시작점에서 부활" 버튼 → 813i.</summary>
|
||||
private static void RequestRevive()
|
||||
{
|
||||
ReviveCalls++;
|
||||
bool ok = false;
|
||||
if (WLSurvivalSettings.Enabled) ok = DeathFlow.Revive();
|
||||
Note("revive() → " + ok);
|
||||
}
|
||||
|
||||
private static void Note(string s)
|
||||
{
|
||||
LastEvent = s;
|
||||
LastEventTime = Time.unscaledTime;
|
||||
}
|
||||
|
||||
// ── 진단 ──────────────────────────────────────────────────────────────
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
DiedSeen = ReviveRequestedSeen = RevivedSeen = PotionChangedSeen = 0;
|
||||
ReviveCalls = PotionUseCalls = PotionUseOk = 0;
|
||||
LastEvent = ""; LastEventTime = 0f;
|
||||
}
|
||||
|
||||
public static string Dump()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("[WLSurvivalUiBridge] installed=" + Installed +
|
||||
" survivalEnabled=" + WLSurvivalSettings.Enabled +
|
||||
" · died=" + DiedSeen + " reviveReq=" + ReviveRequestedSeen + " revived=" + RevivedSeen +
|
||||
" potionChanged=" + PotionChangedSeen +
|
||||
" reviveCalls=" + ReviveCalls + " potionUse=" + PotionUseOk + "/" + PotionUseCalls +
|
||||
" last=\"" + LastEvent + "\"");
|
||||
sb.AppendLine(" 연결 6줄 = count:" + (PotionButton.CountProvider != null) +
|
||||
" cdRemain:" + (PotionButton.CooldownRemainProvider != null) +
|
||||
" cdTotal:" + (PotionButton.CooldownTotalProvider != null) +
|
||||
" use:" + (PotionButton.UseHandler != null) +
|
||||
" reviveReq:" + (ReviveDialog.ReviveRequested != null) +
|
||||
" deathFlow구독(Died/ReviveRequested/Revived)=" +
|
||||
DeathFlow.Died.Count + "/" + DeathFlow.ReviveRequested.Count + "/" + DeathFlow.Revived.Count);
|
||||
sb.AppendLine(" 813i 현재값 = remaining " + PotionUse.Remaining + "/" + PotionUse.Max +
|
||||
" cd " + PotionUse.CooldownRemaining.ToString("F2") + "/" + PotionUse.CooldownTotal.ToString("F2") + "s" +
|
||||
" ready=" + PotionUse.IsReady +
|
||||
" · DeathFlow state=" + DeathFlow.State + " deaths=" + DeathFlow.DeathCount +
|
||||
" revives=" + DeathFlow.ReviveCount + " auto=" + DeathFlow.AutoReviveCount);
|
||||
sb.AppendLine(" UI 가 읽는 값 = count " + ReadCount() + " cdRemain " + ReadCooldownRemain().ToString("F2") +
|
||||
"s cdTotal " + ReadCooldownTotal().ToString("F2") + "s");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3da24e17a6c2d0b42acbced8fd585620
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
// 값은 하나도 들고 있지 않다(전부 WLCombatTextSettings). 여기 있는 숫자는 Unity 기본값(앵커 0/1)뿐이다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
|
|
@ -82,5 +83,143 @@ namespace WL.UI
|
|||
float k = Mathf.Clamp01(elapsed / punchSeconds);
|
||||
return Mathf.Lerp(punchScale, 1f, k * k);
|
||||
}
|
||||
|
||||
// ── 아웃라인 · 그림자 (WL-813y · Q3 결함 D-6) ─────────────────────────
|
||||
// uGUI `Outline`/`Shadow`(BaseMeshEffect)는 **TextMeshPro 에 적용되지 않는다** —
|
||||
// TMP_Text 는 IMeshModifier 를 부르지 않고 자체 메시를 만든다(실측 근거는 완료보고).
|
||||
// 그래서 TMP 의 정식 경로인 **머티리얼 아웃라인(_OutlineWidth/_OutlineColor)** +
|
||||
// **언더레이 그림자(UNDERLAY_ON · _UnderlayOffsetX/Y · _UnderlaySoftness)** 를 쓴다. 포스트 0.
|
||||
// 라벨마다 인스턴스 머티리얼을 만들면 배칭이 깨지므로, (원본 머티리얼 + 값) 조합마다
|
||||
// **변형 머티리얼 1장**을 캐시해 `fontSharedMaterial` 로 공유한다.
|
||||
|
||||
private static readonly Dictionary<string, Material> s_edgeMats = new Dictionary<string, Material>();
|
||||
private static readonly Dictionary<Material, Material> s_edgeOrigin = new Dictionary<Material, Material>();
|
||||
|
||||
/// <summary>지금까지 만든 아웃라인 변형 머티리얼 수(진단).</summary>
|
||||
internal static int EdgeMaterialCount { get { return s_edgeMats.Count; } }
|
||||
|
||||
/// <summary>TMP 라벨에 아웃라인 + 언더레이 그림자를 입힌다. 적용 결과 문자열(실측)을 돌려준다.</summary>
|
||||
internal static string ApplyEdge(TMP_Text label, WLTextEdge edge)
|
||||
{
|
||||
if (label == null) return "label=null";
|
||||
|
||||
Material src = label.fontSharedMaterial;
|
||||
if (src == null) return "sharedMaterial=null";
|
||||
Material origin;
|
||||
if (s_edgeOrigin.TryGetValue(src, out origin)) src = origin; // 이미 우리 변형이면 원본으로 되돌아가서 다시 만든다
|
||||
|
||||
if (!edge.enabled || (edge.outlineWidth <= 0f && edge.shadowColor.a <= 0f))
|
||||
{
|
||||
if (!ReferenceEquals(label.fontSharedMaterial, src)) { label.fontSharedMaterial = src; label.UpdateMeshPadding(); }
|
||||
return "edge off → 원본 머티리얼(" + src.name + ")";
|
||||
}
|
||||
|
||||
string key = src.GetInstanceID() + "|" + edge.Key();
|
||||
Material mat;
|
||||
if (!s_edgeMats.TryGetValue(key, out mat) || mat == null)
|
||||
{
|
||||
mat = new Material(src);
|
||||
mat.name = src.name + " (WL813y Edge)";
|
||||
mat.hideFlags = HideFlags.DontSave;
|
||||
Write(mat, edge);
|
||||
s_edgeMats[key] = mat;
|
||||
s_edgeOrigin[mat] = src;
|
||||
}
|
||||
if (!ReferenceEquals(label.fontSharedMaterial, mat))
|
||||
{
|
||||
label.fontSharedMaterial = mat;
|
||||
label.UpdateMeshPadding(); // 아웃라인이 글리프 밖으로 나가므로 패딩을 다시 잡는다
|
||||
}
|
||||
return Describe(mat);
|
||||
}
|
||||
|
||||
private static void Write(Material m, WLTextEdge e)
|
||||
{
|
||||
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth)) m.SetFloat(ShaderUtilities.ID_OutlineWidth, Mathf.Clamp01(e.outlineWidth));
|
||||
if (m.HasProperty(ShaderUtilities.ID_OutlineColor)) m.SetColor(ShaderUtilities.ID_OutlineColor, e.outlineColor);
|
||||
if (m.HasProperty(ShaderUtilities.ID_FaceDilate)) m.SetFloat(ShaderUtilities.ID_FaceDilate, Mathf.Clamp(e.faceDilate, -1f, 1f));
|
||||
|
||||
bool shadow = e.shadowColor.a > 0f && (Mathf.Abs(e.shadowOffset.x) > 0f || Mathf.Abs(e.shadowOffset.y) > 0f || e.shadowSoftness > 0f);
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
|
||||
{
|
||||
m.SetColor(ShaderUtilities.ID_UnderlayColor, shadow ? e.shadowColor : new Color(0f, 0f, 0f, 0f));
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetX)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, e.shadowOffset.x);
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetY)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, e.shadowOffset.y);
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlaySoftness)) m.SetFloat(ShaderUtilities.ID_UnderlaySoftness, Mathf.Clamp01(e.shadowSoftness));
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlayDilate)) m.SetFloat(ShaderUtilities.ID_UnderlayDilate, 0f);
|
||||
if (shadow) m.EnableKeyword("UNDERLAY_ON"); else m.DisableKeyword("UNDERLAY_ON");
|
||||
}
|
||||
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth) && e.outlineWidth > 0f) m.EnableKeyword("OUTLINE_ON");
|
||||
}
|
||||
|
||||
/// <summary>머티리얼에 실제로 들어간 값(실측 덤프용).</summary>
|
||||
internal static string Describe(Material m)
|
||||
{
|
||||
if (m == null) return "(머티리얼 없음)";
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append(m.name).Append(" shader=").Append(m.shader != null ? m.shader.name : "-");
|
||||
sb.Append(" outlineW=").Append(m.HasProperty(ShaderUtilities.ID_OutlineWidth) ? m.GetFloat(ShaderUtilities.ID_OutlineWidth).ToString("F3") : "없음");
|
||||
sb.Append(" outlineC=").Append(m.HasProperty(ShaderUtilities.ID_OutlineColor) ? ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_OutlineColor)) : "없음");
|
||||
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
|
||||
{
|
||||
sb.Append(" underlay=").Append(ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_UnderlayColor)));
|
||||
sb.Append(" off(").Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetX).ToString("F2")).Append(',')
|
||||
.Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetY).ToString("F2")).Append(')');
|
||||
sb.Append(" soft=").Append(m.GetFloat(ShaderUtilities.ID_UnderlaySoftness).ToString("F2"));
|
||||
sb.Append(" UNDERLAY_ON=").Append(m.IsKeywordEnabled("UNDERLAY_ON"));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>라벨을 원본 머티리얼로 되돌린다(프로브·롤백).</summary>
|
||||
internal static string ClearEdge(TMP_Text label)
|
||||
{
|
||||
if (label == null) return "label=null";
|
||||
Material origin;
|
||||
if (label.fontSharedMaterial != null && s_edgeOrigin.TryGetValue(label.fontSharedMaterial, out origin))
|
||||
{
|
||||
label.fontSharedMaterial = origin;
|
||||
label.UpdateMeshPadding();
|
||||
return "원복 → " + origin.name;
|
||||
}
|
||||
return "변형 아님 — 그대로";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트 테두리·그림자 값 묶음 (WL-813y · C8/C45 — 코드 상수 0 · 전부 SO).
|
||||
/// TMP 머티리얼 규격 그대로다: outlineWidth 0~1 · shadowOffset 은 폰트 단위(±1) · softness 0~1.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct WLTextEdge
|
||||
{
|
||||
[Tooltip("이 텍스트에 아웃라인/그림자를 입힐지")]
|
||||
public bool enabled;
|
||||
[Tooltip("아웃라인 두께 (TMP _OutlineWidth · 0~1 · 0.2~0.3 이 육안으로 또렷)")]
|
||||
[Range(0f, 1f)] public float outlineWidth;
|
||||
[Tooltip("아웃라인 색")]
|
||||
public Color outlineColor;
|
||||
[Tooltip("그림자(언더레이) 오프셋 — 폰트 단위(±1). 예 (0.6, -0.6) = 우하단")]
|
||||
public Vector2 shadowOffset;
|
||||
[Tooltip("그림자 색 — 알파 0 이면 그림자 없음")]
|
||||
public Color shadowColor;
|
||||
[Tooltip("그림자 번짐 (TMP _UnderlaySoftness · 0~1)")]
|
||||
[Range(0f, 1f)] public float shadowSoftness;
|
||||
[Tooltip("글자 두께 보정 (TMP _FaceDilate · -1~1). 아웃라인이 글자를 먹으면 살짝 올린다")]
|
||||
[Range(-1f, 1f)] public float faceDilate;
|
||||
|
||||
internal string Key()
|
||||
{
|
||||
return (enabled ? "1" : "0") + outlineWidth.ToString("F3") + outlineColor + shadowOffset + shadowColor +
|
||||
shadowSoftness.ToString("F3") + faceDilate.ToString("F3");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "on=" + enabled + " w=" + outlineWidth.ToString("F2") +
|
||||
" oc=#" + ColorUtility.ToHtmlStringRGBA(outlineColor) +
|
||||
" sh" + shadowOffset + " sc=#" + ColorUtility.ToHtmlStringRGBA(shadowColor) +
|
||||
" soft=" + shadowSoftness.ToString("F2") + " dilate=" + faceDilate.ToString("F2");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLBossUiSettings
|
||||
bannerEnabled: 1
|
||||
retriggerGuardSeconds: 3
|
||||
bannerTriggerExisting: 1
|
||||
bannerSeconds: 1.2
|
||||
bannerFadeInSeconds: 0.15
|
||||
bannerFadeOutSeconds: 0.25
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ MonoBehaviour:
|
|||
killChainFormat: '{0}!'
|
||||
killChainSubFormat: <size=45%>{0} KILLS</size>
|
||||
lootToastEnabled: 1
|
||||
lootToastSeconds: 1.2
|
||||
lootToastSeconds: 2
|
||||
lootToastLines: 3
|
||||
lootToastMergeSeconds: 1
|
||||
lootToastRightMarginPx: 40
|
||||
lootToastCenterOffsetPx: 120
|
||||
lootToastLineHeightPx: 54
|
||||
lootToastFontPx: 40
|
||||
lootToastWidthPx: 520
|
||||
lootToastCenterOffsetPx: -120
|
||||
lootToastLineHeightPx: 68
|
||||
lootToastFontPx: 52
|
||||
lootToastWidthPx: 560
|
||||
lootToastFadeStartRatio: 0.65
|
||||
lootToastSlideInPx: 60
|
||||
lootToastSlideSeconds: 0.14
|
||||
|
|
@ -85,3 +85,45 @@ MonoBehaviour:
|
|||
statPopupRowFormat: '{0} +{1}'
|
||||
statPopupTitleColor: {r: 1, g: 0.9, b: 0.45, a: 1}
|
||||
statPopupRowColor: {r: 0.55, g: 0.95, b: 0.62, a: 1}
|
||||
statPopupUseGrowthEvents: 1
|
||||
statPopupPointFormat: "+5 \uD3EC\uC778\uD2B8 <size=70%>(\uBCF4\uC720 {0})</size>"
|
||||
statPopupMergeMultiLevel: 1
|
||||
textEdgeEnabled: 1
|
||||
killChainEdge:
|
||||
enabled: 1
|
||||
outlineWidth: 0.3
|
||||
outlineColor: {r: 0.05, g: 0.03, b: 0.02, a: 1}
|
||||
shadowOffset: {x: 0.55, y: -0.55}
|
||||
shadowColor: {r: 0, g: 0, b: 0, a: 0.8}
|
||||
shadowSoftness: 0.25
|
||||
faceDilate: 0.1
|
||||
statPopupEdge:
|
||||
enabled: 1
|
||||
outlineWidth: 0.26
|
||||
outlineColor: {r: 0.05, g: 0.03, b: 0.02, a: 1}
|
||||
shadowOffset: {x: 0.45, y: -0.45}
|
||||
shadowColor: {r: 0, g: 0, b: 0, a: 0.75}
|
||||
shadowSoftness: 0.2
|
||||
faceDilate: 0.08
|
||||
damageEdge:
|
||||
enabled: 1
|
||||
outlineWidth: 0.22
|
||||
outlineColor: {r: 0.03, g: 0.02, b: 0.02, a: 1}
|
||||
shadowOffset: {x: 0.35, y: -0.35}
|
||||
shadowColor: {r: 0, g: 0, b: 0, a: 0.7}
|
||||
shadowSoftness: 0.15
|
||||
faceDilate: 0.05
|
||||
lootToastEdge:
|
||||
enabled: 1
|
||||
outlineWidth: 0.24
|
||||
outlineColor: {r: 0.03, g: 0.02, b: 0.02, a: 1}
|
||||
shadowOffset: {x: 0.4, y: -0.4}
|
||||
shadowColor: {r: 0, g: 0, b: 0, a: 0.7}
|
||||
shadowSoftness: 0.18
|
||||
faceDilate: 0.06
|
||||
lootToastPanelEnabled: 1
|
||||
lootToastPanelColor: {r: 0.03, g: 0.03, b: 0.05, a: 0.68}
|
||||
lootToastPanelPadXPx: 18
|
||||
lootToastPanelPadYPx: 6
|
||||
lootToastMinBrightColor: {r: 0.95, g: 0.96, b: 0.98, a: 1}
|
||||
lootToastMinBrightness: 0.72
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ MonoBehaviour:
|
|||
slotDiameterPx: 140
|
||||
slotMinGapPx: 16
|
||||
thumbReachRadiusPx: 320
|
||||
fanOriginPx: {x: 0, y: 260}
|
||||
fanOriginPx: {x: 0, y: 0}
|
||||
fanRadiusPx: 420
|
||||
fanStartAngleDeg: 12.5
|
||||
fanStepAngleDeg: 22
|
||||
|
|
@ -28,7 +28,7 @@ MonoBehaviour:
|
|||
reserveRadiusPx: 270
|
||||
reserveStartAngleDeg: 25
|
||||
reserveStepAngleDeg: 40
|
||||
attackCenterPx: {x: 100, y: 360}
|
||||
attackCenterPx: {x: 100, y: 100}
|
||||
attackDiameterPx: 132
|
||||
autoButtonCenterPx: {x: 56, y: 545}
|
||||
autoButtonDiameterPx: 34.3
|
||||
|
|
@ -48,3 +48,21 @@ MonoBehaviour:
|
|||
bossBarBgColor: {r: 0.05, g: 0.05, b: 0.06, a: 0.78}
|
||||
bossBarTickColor: {r: 1, g: 1, b: 1, a: 0.85}
|
||||
bossBarNameColor: {r: 1, g: 0.92, b: 0.6, a: 1}
|
||||
bossBarBindExisting: 1
|
||||
combatHideBottomMenu: 1
|
||||
bottomMenuPath: IngameUIs/WL_HUD/HUD_BottomMenu
|
||||
bottomMenuUseCanvasGroup: 1
|
||||
bottomMenuFadeSeconds: 0.18
|
||||
combatExitSeconds: 6
|
||||
combatEnterDelaySeconds: 0
|
||||
combatUseTargetAsSignal: 1
|
||||
bottomMenuRestoreButton: 1
|
||||
restoreButtonCenterPx: {x: -380, y: 62}
|
||||
restoreButtonDiameterPx: 88
|
||||
restoreButtonLabel: "\u2261"
|
||||
restoreButtonFontPx: 46
|
||||
restoreButtonBgColor: {r: 0.06, g: 0.07, b: 0.1, a: 0.72}
|
||||
restoreButtonLabelColor: {r: 0.96, g: 0.94, b: 0.82, a: 1}
|
||||
restoreHoldSeconds: 8
|
||||
idleFanOriginPx: {x: 0, y: 260}
|
||||
idleAttackCenterPx: {x: 100, y: 360}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ namespace WL.UI
|
|||
[Tooltip("같은 보스가 다시 Spawned 를 내도 이 시간 안이면 배너를 다시 띄우지 않는다(중복 방지)")]
|
||||
public float retriggerGuardSeconds = 3f;
|
||||
|
||||
[Tooltip("배너가 켜질 때 이미 살아 있는 보스가 있으면 즉시 배너를 띄운다(맵 로드 시 스폰 = Q3 결함 D-3 · WL-813y). 끄면 Spawned/BossPhase 이벤트만 본다.")]
|
||||
public bool bannerTriggerExisting = true;
|
||||
|
||||
// ── 배너 ──────────────────────────────────────────────────────────────
|
||||
[Header("배너 (기준서 §D-1 813t · 1.2 s)")]
|
||||
[Tooltip("배너 총 표시 시간 — 기준서 1.2")]
|
||||
|
|
|
|||
|
|
@ -269,6 +269,88 @@ namespace WL.UI
|
|||
[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>
|
||||
|
|
@ -332,5 +414,29 @@ namespace WL.UI
|
|||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,62 @@ namespace WL.UI
|
|||
[Tooltip("이름 라벨 색")]
|
||||
public Color bossBarNameColor = new Color(1f, 0.92f, 0.6f, 1f);
|
||||
|
||||
[Tooltip("바가 켜질 때 이미 살아 있는 보스가 있으면 즉시 물린다(813d 게이트 이전 스폰 = Q3 결함 D-3). 끄면 Spawned 이벤트만 본다.")]
|
||||
public bool bossBarBindExisting = true;
|
||||
|
||||
|
||||
[Header("전투 중 하단 메뉴 숨김 · 패드 원점 복귀 (WL-813y · Q3 결함 D-5)")]
|
||||
[Tooltip("끄면 하단 메뉴를 건드리지 않고 패드도 항상 idle 원점(아래 값)을 쓴다 — 813c+Lead 핫픽스 상태로 복귀(C8 롤백).")]
|
||||
public bool combatHideBottomMenu = true;
|
||||
|
||||
[Tooltip("숨길 하단 메뉴 노드의 루트 기준 경로(813y 실측 = IngameUIs/WL_HUD/HUD_BottomMenu).")]
|
||||
public string bottomMenuPath = "IngameUIs/WL_HUD/HUD_BottomMenu";
|
||||
|
||||
[Tooltip("SetActive 대신 CanvasGroup 으로 숨긴다(하위 스크립트의 Update 가 계속 돌아야 하는 경우 · #796 선례).")]
|
||||
public bool bottomMenuUseCanvasGroup = true;
|
||||
|
||||
[Tooltip("숨김/복귀 페이드 시간(초 · unscaled). 0 이면 즉시.")]
|
||||
public float bottomMenuFadeSeconds = 0.18f;
|
||||
|
||||
[Tooltip("마지막 전투 신호(적 인지 · 피격 · 공격 · 스킬 · 처치) 뒤 이 시간이 지나면 메뉴가 돌아온다.")]
|
||||
public float combatExitSeconds = 6f;
|
||||
|
||||
[Tooltip("전투 신호가 들어오고 이 시간이 지나야 숨긴다(0 = 즉시).")]
|
||||
public float combatEnterDelaySeconds = 0f;
|
||||
|
||||
[Tooltip("현재 타깃이 살아 있으면 전투로 본다(적 인지). 끄면 공격/피격/스킬/처치 이벤트만 본다.")]
|
||||
public bool combatUseTargetAsSignal = true;
|
||||
|
||||
[Tooltip("메뉴가 숨은 동안 되돌릴 버튼 1개를 만든다(런타임 생성 · 프리팹 diff 0).")]
|
||||
public bool bottomMenuRestoreButton = true;
|
||||
|
||||
[Tooltip("복귀 버튼 중심 — x = 화면 가로 중앙 기준(양수 = 오른쪽) · y = 화면 아래 기준(px).")]
|
||||
public Vector2 restoreButtonCenterPx = new Vector2(-380f, 62f);
|
||||
|
||||
[Tooltip("복귀 버튼 지름(px).")]
|
||||
public float restoreButtonDiameterPx = 88f;
|
||||
|
||||
[Tooltip("복귀 버튼 글자(TMP · 아트 에셋 의존 0).")]
|
||||
public string restoreButtonLabel = "≡";
|
||||
|
||||
[Tooltip("복귀 버튼 글자 크기(px).")]
|
||||
public float restoreButtonFontPx = 46f;
|
||||
|
||||
[Tooltip("복귀 버튼 배경 색.")]
|
||||
public Color restoreButtonBgColor = new Color(0.06f, 0.07f, 0.10f, 0.72f);
|
||||
|
||||
[Tooltip("복귀 버튼 글자 색.")]
|
||||
public Color restoreButtonLabelColor = new Color(0.96f, 0.94f, 0.82f, 1f);
|
||||
|
||||
[Tooltip("복귀 버튼으로 메뉴를 되돌린 뒤, 이 시간이 지나면 다시 전투 판정을 따른다(초).")]
|
||||
public float restoreHoldSeconds = 8f;
|
||||
|
||||
[Tooltip("하단 메뉴가 **보이는 동안** 쓰는 부채꼴 중심 — 메뉴(상단 y ≈ 163 px)를 피해 위로 올린 813c+Lead 핫픽스 값.")]
|
||||
public Vector2 idleFanOriginPx = new Vector2(0f, 260f);
|
||||
|
||||
[Tooltip("하단 메뉴가 **보이는 동안** 쓰는 공격 버튼 중심.")]
|
||||
public Vector2 idleAttackCenterPx = new Vector2(100f, 360f);
|
||||
|
||||
// ── 유틸 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// px → 캔버스 유닛 환산 계수.
|
||||
|
|
@ -180,20 +236,43 @@ namespace WL.UI
|
|||
return 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지금 써야 할 부채꼴 중심. 하단 메뉴가 **보이는 동안**은 메뉴를 피해 올린 자리(idleFanOriginPx),
|
||||
/// 전투 중(메뉴 숨김)은 모서리 원점(fanOriginPx) — WL-813y §1-6.
|
||||
/// </summary>
|
||||
public Vector2 FanOriginPxFor(bool bottomMenuVisible)
|
||||
{
|
||||
// C8 롤백: 기능을 끄면 하단 메뉴가 **늘 보이므로** 메뉴를 피한 자리(idle) 를 써야 한다
|
||||
// = 813c + Lead 핫픽스 상태 그대로. (모서리 원점을 쓰면 메뉴와 다시 겹친다 · Q3 결함 D-5)
|
||||
if (!combatHideBottomMenu) return idleFanOriginPx;
|
||||
return bottomMenuVisible ? idleFanOriginPx : fanOriginPx;
|
||||
}
|
||||
|
||||
/// <summary>지금 써야 할 공격 버튼 중심(위와 같은 규칙).</summary>
|
||||
public Vector2 AttackCenterPxFor(bool bottomMenuVisible)
|
||||
{
|
||||
if (!combatHideBottomMenu) return idleAttackCenterPx;
|
||||
return bottomMenuVisible ? idleAttackCenterPx : attackCenterPx;
|
||||
}
|
||||
|
||||
/// <summary>부채꼴 각도 → 모서리 기준 (dx, dy) px.</summary>
|
||||
public Vector2 FanSlotPx(int index)
|
||||
public Vector2 FanSlotPx(int index) { return FanSlotPx(index, HudCombatVisibility.BottomMenuVisible); }
|
||||
|
||||
public Vector2 FanSlotPx(int index, bool bottomMenuVisible)
|
||||
{
|
||||
float deg = fanStartAngleDeg + fanStepAngleDeg * index;
|
||||
float rad = deg * Mathf.Deg2Rad;
|
||||
return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * fanRadiusPx;
|
||||
return FanOriginPxFor(bottomMenuVisible) + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * fanRadiusPx;
|
||||
}
|
||||
|
||||
/// <summary>예약(물약·회피) 슬롯 각도 → 모서리 기준 (dx, dy) px.</summary>
|
||||
public Vector2 ReserveSlotPx(int index)
|
||||
public Vector2 ReserveSlotPx(int index) { return ReserveSlotPx(index, HudCombatVisibility.BottomMenuVisible); }
|
||||
|
||||
public Vector2 ReserveSlotPx(int index, bool bottomMenuVisible)
|
||||
{
|
||||
float deg = reserveStartAngleDeg + reserveStepAngleDeg * index;
|
||||
float rad = deg * Mathf.Deg2Rad;
|
||||
return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * reserveRadiusPx;
|
||||
return FanOriginPxFor(bottomMenuVisible) + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * reserveRadiusPx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue