From 87fdb49648449a14024df9618ea64fdbf95626ad Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 15 Sep 2026 00:49:56 +0900 Subject: [PATCH] =?UTF-8?q?[WL-816m]=20=EB=8D=98=EC=A0=84=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=B0=BD=20=3D=20=EB=8D=98=EC=A0=84=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20+=20=ED=9A=8D=EB=93=9D=20=EA=B3=A8=EB=93=9C=20(#816?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PD 원문(2026-09-15) 「클리어 던전은 무조건 11로 나오고 획득한 재화 정보를 알 수 없어」. - 결과창 제목 = 「던전 N 클리어!」 (WLDungeonDef.displayName) · 행에 「획득 골드 +96」 추가 - HUD 단계 라벨 = 「던전 N」 - 판정 = WLDungeonSceneSetup.Current 하나 · 골드 = WLIslandBridge.ComputeGold 재사용(식 1곳) - 문구·형식 전부 SO(WLStageUiSettings 던전 절 7필드 · C45) · C8 롤백 = dungeonModeEnabled 0 - #815 스테이지 모드 무변경(구 4인자 경로와 캡처 MD5 동일) · Assets/WL/Island/** 0줄 - 게이트 PASS(resident · console errors 0) · 프로브 28/28 PASS Co-Authored-By: Claude Opus 5 (1M context) --- AgentScripts/WL816m_CAPTURE.txt | 19 + AgentScripts/WL816m_PROBE.txt | 96 ++++ AgentScripts/WL816m_Probe.cs | 504 ++++++++++++++++++ Assets/WL/UI/Scripts/RunHud.cs | 33 ++ Assets/WL/UI/Scripts/RunResultUI.cs | 106 +++- Assets/WL/UI/Scripts/StageUiBridge.cs | 62 ++- .../Resources/WL/WLStageUiSettings.asset | 23 +- Assets/WL/UI/Settings/WLStageUiSettings.cs | 27 + 8 files changed, 839 insertions(+), 31 deletions(-) create mode 100644 AgentScripts/WL816m_CAPTURE.txt create mode 100644 AgentScripts/WL816m_PROBE.txt create mode 100644 AgentScripts/WL816m_Probe.cs diff --git a/AgentScripts/WL816m_CAPTURE.txt b/AgentScripts/WL816m_CAPTURE.txt new file mode 100644 index 000000000..6e6d909d9 --- /dev/null +++ b/AgentScripts/WL816m_CAPTURE.txt @@ -0,0 +1,19 @@ +ⓒ HUD 던전1 "던전 1" "00:42" "남은 적 3 / 3" + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\c_hud_dungeon1.png (62853 B · 1080×1920) +ⓐ 던전1 "던전 1 클리어!" 행=7 골드=96 + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\a_dungeon1_clear.png (145037 B · 1080×1920) +ⓑ 던전3 "던전 3 클리어!" 골드=192 + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\b_dungeon3_clear.png (145609 B · 1080×1920) +ⓓ HUD 스테이지 "스테이지 3" "00:08" "남은 적 5 / 12" + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\d1_hud_stage_new.png (66700 B · 1080×1920) +ⓓ 스테이지 클리어(새 경로) "스테이지 3 클리어!" 행=6 + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\d1_stage_clear_new.png (146352 B · 1080×1920) + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\d2_hud_stage_legacy.png (66700 B · 1080×1920) +ⓓ 스테이지 클리어(구 4인자) "스테이지 3 클리어!" 행=6 + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\d2_stage_clear_legacy.png (146352 B · 1080×1920) +ⓔ 실패 "시간 초과! / 남은 적 4마리" 버튼=[다시 도전] [섬으로] + → E:\NerdNavis\WL_wt\ui\Screenshots_WL\WL816m\e_fail.png (84370 B · 1080×1920) + +픽셀 대조 (MD5) — 새 경로 vs 816m 이전 경로 + HUD 동일 ae76d1c543ba / ae76d1c543ba + 결과창 동일 f77b6ffa7382 / f77b6ffa7382 diff --git a/AgentScripts/WL816m_PROBE.txt b/AgentScripts/WL816m_PROBE.txt new file mode 100644 index 000000000..383996fe0 --- /dev/null +++ b/AgentScripts/WL816m_PROBE.txt @@ -0,0 +1,96 @@ +# WL816m Probe 2026-09-15 00:47:59 (edit mode · Play 0 · 로그인 0) +playMode=False + +── ① 설정 실값 (SO · 하드코딩 0) + [PASS] WLStageUiSettings 에셋 로드 + 던전모드=True · 제목 "{0} 클리어!" · HUD "{0}" · 대체이름 "던전 {0}" + 골드행=True · 라벨 "획득 골드" · 값 "+{0}" + 스테이지 제목(불변) "스테이지 {0} 클리어!" · 스테이지 HUD(불변) "스테이지 {0}" + [PASS] 던전 표 3행 이상 + 던전 표[0] id=1 "던전 1" stageIndex=10 → 스테이지 표 id=11(지금 화면에 나오던 숫자) · goldMultiplier=1.00 + 던전 표[1] id=2 "던전 2" stageIndex=11 → 스테이지 표 id=12(지금 화면에 나오던 숫자) · goldMultiplier=1.50 + 던전 표[2] id=3 "던전 3" stageIndex=12 → 스테이지 표 id=13(지금 화면에 나오던 숫자) · goldMultiplier=2.00 + 던전 표[3] id=4 "던전 4" stageIndex=13 → 스테이지 표 id=14(지금 화면에 나오던 숫자) · goldMultiplier=3.00 + 보상 값 — clearBonus=60 perKill=12 bossBonus=120 배수적용=1 rewardEnabled=1 + +── ② 던전 판정 = WLDungeonSceneSetup.Current 하나뿐 + [PASS] Current=null → 던전 아님(#815 스테이지 모드) + [PASS] 이름 null + [PASS] Current=던전1 행 → 던전으로 판정 + 이름 = "던전 1" + [PASS] 이름 = 표의 displayName + displayName 빈 경우 대체 이름 = "던전 1" + [PASS] 대체 이름 = dungeonNameFallbackFormat + id + +── ③ 획득 골드 = 섬 축 WLIslandBridge.ComputeGold 재사용(식 1곳) + 던전 1 (index 10 · 처치 3) → 96 / 섬 축 직접 = 96 + [PASS] 던전 1 = 96 (816g §5 실측과 같음) + 던전 3 (index 12 · 처치 3) → 192 + [PASS] 던전 3 = 192 (816i2 §3 실측과 같음) + [PASS] UI 표시값 == 섬 축 지급 계산값(두 곳 일치) + +── ④ 결과창 — 던전 1 / 던전 3 (StageEvents 실제 Dispatch 경로) + 제목 "던전 1 클리어!" · 행 7개 · 던전=True "던전 1" 골드=96 + 본문 ⤵ + 던전 1 클리어! + 클리어 시간 : 00:35 + 처치 : 3 / 3 + 엘리트 처치 : 0 + 최대 연쇄 : 0 + 피격 / 물약 : 0회 / 0개 + 재도전 : 0회 + 획득 골드 : +96 + 무피격 클리어! + [PASS] 제목 = "던전 1 클리어!" (지금까지는 "스테이지 11 클리어!") + [PASS] 행 = "획득 골드 : +96" + [PASS] 행 6 + 획득 골드 1 = 7 + 제목 "던전 3 클리어!" · 골드=192 + [PASS] 제목 = "던전 3 클리어!" + [PASS] 행 = "획득 골드 : +192" + +── ⑤ HUD 단계 라벨 — 던전이면 「던전 N」 + 던전 1 → 단계 "던전 1" · 남은 적 "남은 적 3 / 3" + [PASS] HUD 라벨 = "던전 1" (지금까지는 "스테이지 11") + 던전 3 → 단계 "던전 3" + [PASS] HUD 라벨 = "던전 3" + [PASS] 같은 이름 재입력 시 쓰기 0회(GC 0) + +── ⑥ #815 스테이지 모드 무변경 (Current = null) + 단계 "스테이지 3" · 시간 "00:08" · 남은 적 "남은 적 5 / 12" + [PASS] HUD = "스테이지 3" 그대로 + 제목 "스테이지 3 클리어!" · 행 6개 · 던전=False + 본문 ⤵ + 스테이지 3 클리어! + 클리어 시간 : 00:35 + 처치 : 12 / 12 + 엘리트 처치 : 0 + 최대 연쇄 : 0 + 피격 / 물약 : 0회 / 0개 + 재도전 : 0회 + 무피격 클리어! + [PASS] 제목 = "스테이지 3 클리어!" 그대로 + [PASS] 행 6개 그대로(획득 골드 없음) + [PASS] 본문에 「획득 골드」 0건 + [PASS] 던전 문맥 없음 + 구 경로 본문 ⤵ + 스테이지 3 클리어! + 클리어 시간 : 00:35 + 처치 : 12 / 12 + 엘리트 처치 : 0 + 최대 연쇄 : 0 + 피격 / 물약 : 0회 / 0개 + 재도전 : 0회 + 무피격 클리어! + [PASS] 구 4인자 경로(=816m 이전) == 새 경로 본문 문자열 동일 + +── ⑦ C8 롤백 — dungeonModeEnabled = 0 이면 던전도 지금까지 그대로 + 롤백 제목 "스테이지 11 클리어!" · HUD "스테이지 11" · 행 6 + [PASS] 롤백 = "스테이지 11 클리어!"(PD 가 본 화면) + [PASS] 롤백 HUD = "스테이지 11" + [PASS] 롤백 행 6개 + +── ⑦ PlayerPrefs 무변경 (섬 축은 에디트 모드에 설치되지 않는다) + 키 "WL_Island_PendingGold" 전=0 후=0 + [PASS] 보류 보상 PlayerPrefs 무변경 + +RESULT PASS (pass=28 fail=0) diff --git a/AgentScripts/WL816m_Probe.cs b/AgentScripts/WL816m_Probe.cs new file mode 100644 index 000000000..2b7b4a218 --- /dev/null +++ b/AgentScripts/WL816m_Probe.cs @@ -0,0 +1,504 @@ +// WL-816m 프로브 — 던전 결과창(던전 이름 + 획득 골드) 실측 (에디트 모드 · Play 0 · 로그인 0). +// unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.SaveAsset +// unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.RunAll +// unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.Capture +// 산출물: /AgentScripts/WL816m_PROBE.txt · Screenshots_WL/WL816m/*.png +// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 저장하지 않는다(UnloadPrefabContents). +// 🔴 PlayerPrefs 는 건드리지 않는다 — 에디트 모드라 WLIslandBridge 는 설치되지 않는다(전후 값 실측 대조). + +using System; +using System.IO; +using System.Text; +using UnityEngine; +using UnityEditor; +using WL.UI; +using WL.Island; + +public static class WL816m_Probe +{ + const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; + + static StringBuilder _o; + static int _pass, _fail; + + static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } } + static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); } + static void N(string s) { _o.AppendLine(" " + s); } + static bool Chk(bool ok, string what) + { + if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); } + else { _fail++; _o.AppendLine(" [FAIL] " + what); } + return ok; + } + + // ═════════════════════════════════════════════════════════════════════ + // ① SO 에 새 필드를 직렬화한다(에셋 YAML 에 값이 보이도록 · C45) + // ═════════════════════════════════════════════════════════════════════ + public static string SaveAsset() + { + var g = WLStageUiSettings.Instance; + if (g == null) return "WLStageUiSettings 에셋 없음"; + EditorUtility.SetDirty(g); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return "저장 — 던전모드=" + g.dungeonModeEnabled + " 제목=\"" + g.dungeonClearTitleFormat + + "\" 라벨=\"" + g.dungeonLabelFormat + "\" 골드행=" + g.dungeonGoldRowEnabled + + " \"" + g.rowDungeonGoldLabel + "\" \"" + g.rowDungeonGoldFormat + "\""; + } + + // ═════════════════════════════════════════════════════════════════════ + // ② 실측 + // ═════════════════════════════════════════════════════════════════════ + public static string RunAll() + { + _o = new StringBuilder(); + _pass = _fail = 0; + _o.AppendLine("# WL816m Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)"); + _o.AppendLine("playMode=" + Application.isPlaying); + + var cfg = WLIslandSettings.Instance; + string prefsKey = cfg != null ? cfg.pendingPrefsKey : "WL_Island_PendingGold"; + int prefsBefore = PlayerPrefs.GetInt(prefsKey, -1); + var savedCurrent = WLDungeonSceneSetup.Current; + + GameObject prefabRoot = null; + try + { + Step0_Settings(cfg); + prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab); + BuildRig(prefabRoot); + Step1_Detect(cfg); + Step2_Gold(cfg); + Step3_Result(cfg, prefabRoot); + Step4_Hud(cfg, prefabRoot); + Step5_StageUnchanged(prefabRoot); + Step6_Rollback(cfg, prefabRoot); + } + catch (Exception e) + { + _fail++; + _o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message); + _o.AppendLine(e.StackTrace); + } + finally + { + try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { } + DestroyRig(); + WLDungeonSceneSetup.Current = savedCurrent; + if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot); + } + + int prefsAfter = PlayerPrefs.GetInt(prefsKey, -1); + H("⑦ PlayerPrefs 무변경 (섬 축은 에디트 모드에 설치되지 않는다)"); + N("키 \"" + prefsKey + "\" 전=" + prefsBefore + " 후=" + prefsAfter); + Chk(prefsBefore == prefsAfter, "보류 보상 PlayerPrefs 무변경"); + + _o.AppendLine(); + _o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")"); + string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL816m_PROBE.txt"); + File.WriteAllText(path, _o.ToString(), new UTF8Encoding(true)); + return _o.ToString(); + } + + static void Step0_Settings(WLIslandSettings cfg) + { + H("① 설정 실값 (SO · 하드코딩 0)"); + var g = WLStageUiSettings.Instance; + Chk(g != null, "WLStageUiSettings 에셋 로드"); + if (g == null) return; + N("던전모드=" + g.dungeonModeEnabled + " · 제목 \"" + g.dungeonClearTitleFormat + "\" · HUD \"" + + g.dungeonLabelFormat + "\" · 대체이름 \"" + g.dungeonNameFallbackFormat + "\""); + N("골드행=" + g.dungeonGoldRowEnabled + " · 라벨 \"" + g.rowDungeonGoldLabel + "\" · 값 \"" + g.rowDungeonGoldFormat + "\""); + N("스테이지 제목(불변) \"" + g.stageClearTitleFormat + "\" · 스테이지 HUD(불변) \"" + g.stageLabelFormat + "\""); + Chk(cfg != null && cfg.dungeons != null && cfg.dungeons.Length >= 3, "던전 표 3행 이상"); + if (cfg == null || cfg.dungeons == null) return; + for (int i = 0; i < cfg.dungeons.Length; i++) + { + var d = cfg.dungeons[i]; + int id = StageIdOf(d.stageIndex); + N("던전 표[" + i + "] id=" + d.id + " \"" + d.displayName + "\" stageIndex=" + d.stageIndex + + " → 스테이지 표 id=" + id + "(지금 화면에 나오던 숫자) · goldMultiplier=" + + WLIslandBridge.MultiplierOf(d.stageIndex).ToString("F2")); + } + N("보상 값 — clearBonus=" + cfg.goldClearBonus + " perKill=" + cfg.goldPerKill + " bossBonus=" + + cfg.goldBossBonus + " 배수적용=" + cfg.applyGoldMultiplier + " rewardEnabled=" + cfg.rewardEnabled); + } + + static int StageIdOf(int index) + { + var t = WL.Combat.Stage.WLStageTable.Instance; + if (t == null) return -1; + var def = t.Get(index); + return def != null ? def.id : -1; + } + + static void Step1_Detect(WLIslandSettings cfg) + { + H("② 던전 판정 = WLDungeonSceneSetup.Current 하나뿐"); + WLDungeonSceneSetup.Current = null; + Chk(StageUiBridge.CurrentDungeon == null, "Current=null → 던전 아님(#815 스테이지 모드)"); + Chk(StageUiBridge.DungeonNameOf(null) == null, "이름 null"); + + WLDungeonSceneSetup.Current = cfg.dungeons[0]; + var d1 = StageUiBridge.CurrentDungeon; + Chk(d1 != null, "Current=던전1 행 → 던전으로 판정"); + N("이름 = \"" + StageUiBridge.DungeonNameOf(d1) + "\""); + Chk(StageUiBridge.DungeonNameOf(d1) == cfg.dungeons[0].displayName, "이름 = 표의 displayName"); + + // displayName 이 비었을 때의 대체 이름 + string saved = cfg.dungeons[0].displayName; + cfg.dungeons[0].displayName = ""; + string fb = StageUiBridge.DungeonNameOf(StageUiBridge.CurrentDungeon); + cfg.dungeons[0].displayName = saved; + N("displayName 빈 경우 대체 이름 = \"" + fb + "\""); + Chk(fb == "던전 " + cfg.dungeons[0].id, "대체 이름 = dungeonNameFallbackFormat + id"); + } + + static void Step2_Gold(WLIslandSettings cfg) + { + H("③ 획득 골드 = 섬 축 WLIslandBridge.ComputeGold 재사용(식 1곳)"); + var r1 = Cleared(cfg.dungeons[0].stageIndex, 3, false); + int g1 = StageUiBridge.DungeonGoldOf(in r1); + N("던전 1 (index " + cfg.dungeons[0].stageIndex + " · 처치 3) → " + g1 + + " / 섬 축 직접 = " + WLIslandBridge.ComputeGold(cfg, in r1)); + Chk(g1 == 96, "던전 1 = 96 (816g §5 실측과 같음)"); + + var r3 = Cleared(cfg.dungeons[2].stageIndex, 3, false); + int g3 = StageUiBridge.DungeonGoldOf(in r3); + N("던전 3 (index " + cfg.dungeons[2].stageIndex + " · 처치 3) → " + g3); + Chk(g3 == 192, "던전 3 = 192 (816i2 §3 실측과 같음)"); + Chk(g1 == WLIslandBridge.ComputeGold(cfg, in r1) && g3 == WLIslandBridge.ComputeGold(cfg, in r3), + "UI 표시값 == 섬 축 지급 계산값(두 곳 일치)"); + } + + static WL.Combat.Stage.StageClearResult Cleared(int index, int killed, bool isBoss) + { + return new WL.Combat.Stage.StageClearResult + { + index = index, stageId = StageIdOf(index), win = true, + failReason = WL.Combat.Stage.StageFailReason.None, + elapsedSec = 34.2f, timeLimit = 60f, killed = killed, total = killed, remaining = 0, + retries = 0, isLastStage = false, isBoss = isBoss, + time = Time.unscaledTime, frame = Time.frameCount, + }; + } + + // ── 목업 캔버스(815d 와 같은 방식) — TitleText·PhaseText 를 읽으려면 노드가 있어야 한다 ── + static GameObject _rig; + static Camera _cam; + static RectTransform _crt; + static ReviveDialog _revive; + + static void BuildRig(GameObject prefabRoot) + { + _rig = new GameObject("WL816m_Rig"); + _rig.hideFlags = HideFlags.HideAndDontSave; + var camGo = new GameObject("Cam"); + camGo.transform.SetParent(_rig.transform, false); + _cam = camGo.AddComponent(); + _cam.clearFlags = CameraClearFlags.SolidColor; + _cam.backgroundColor = new Color(0.09f, 0.10f, 0.12f, 1f); + _cam.orthographic = true; _cam.orthographicSize = 960f; + _cam.nearClipPlane = 0.1f; _cam.farClipPlane = 5000f; + camGo.transform.position = new Vector3(0f, 0f, -1000f); + + var canvasGo = new GameObject("Canvas", typeof(RectTransform)); + canvasGo.transform.SetParent(_rig.transform, false); + canvasGo.layer = 5; + var canvas = canvasGo.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceCamera; + canvas.worldCamera = _cam; + canvas.planeDistance = 500f; + var scaler = canvasGo.AddComponent(); + scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1080f, 1920f); + scaler.matchWidthOrHeight = 0.5f; + canvasGo.AddComponent(); + _crt = (RectTransform)canvasGo.transform; + _crt.sizeDelta = new Vector2(1080f, 1920f); + + var ingame = NewRect(_crt, "IngameUIs"); + var wlHud = NewRect(ingame, "WL_HUD"); + + var font = RunHud.BorrowFont(prefabRoot.transform); + RunHud.SetFont(font); RunResultUI.SetFont(font); + StageUiBridge.SetUiRootForProbe(prefabRoot.transform); + RunHud.Subscribe(true); RunResultUI.Subscribe(true); StageUiBridge.Subscribe(true); + RunHud.Bind(_crt); RunResultUI.Bind(_crt); + + var dlgGo = new GameObject("WL_ReviveDialog", typeof(RectTransform)); + dlgGo.layer = 5; + dlgGo.transform.SetParent(wlHud, false); + _revive = dlgGo.AddComponent(); + _revive.SetFont(font); + _revive.Initialize(); + } + + static void DestroyRig() + { + if (_rig != null) UnityEngine.Object.DestroyImmediate(_rig); + _rig = null; _cam = null; _crt = null; _revive = null; + } + + static void Step3_Result(WLIslandSettings cfg, GameObject prefabRoot) + { + H("④ 결과창 — 던전 1 / 던전 3 (StageEvents 실제 Dispatch 경로)"); + // 던전 1 + WLDungeonSceneSetup.Current = cfg.dungeons[0]; + int idx1 = cfg.dungeons[0].stageIndex; + WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); + N("제목 \"" + RunResultUI.TitleText + "\" · 행 " + RunResultUI.VisibleRows + "개 · 던전=" + + RunResultUI.IsDungeonResult + " \"" + RunResultUI.DungeonName + "\" 골드=" + RunResultUI.DungeonGold); + N("본문 ⤵\n" + Indent(RunResultUI.BodyText)); + Chk(RunResultUI.TitleText == "던전 1 클리어!", "제목 = \"던전 1 클리어!\" (지금까지는 \"스테이지 11 클리어!\")"); + Chk(RunResultUI.BodyText.Contains("획득 골드 : +96"), "행 = \"획득 골드 : +96\""); + Chk(RunResultUI.VisibleRows == 7, "행 6 + 획득 골드 1 = 7"); + + // 던전 3 + RunResultUI.Hide(); + WLDungeonSceneSetup.Current = cfg.dungeons[2]; + int idx3 = cfg.dungeons[2].stageIndex; + WLStageUiProbeHooks.RaiseEntered(idx3, StageIdOf(idx3), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(idx3, StageIdOf(idx3), 41.5f, 60f, 3, 3, 0, false, false); + N("제목 \"" + RunResultUI.TitleText + "\" · 골드=" + RunResultUI.DungeonGold); + Chk(RunResultUI.TitleText == "던전 3 클리어!", "제목 = \"던전 3 클리어!\""); + Chk(RunResultUI.BodyText.Contains("획득 골드 : +192"), "행 = \"획득 골드 : +192\""); + } + + static string Indent(string s) + { + if (string.IsNullOrEmpty(s)) return " (빈 본문)"; + var lines = s.Replace("\r", "").Split('\n'); + var sb = new StringBuilder(); + for (int i = 0; i < lines.Length; i++) + if (lines[i].Length > 0) sb.AppendLine(" " + lines[i]); + return sb.ToString().TrimEnd(); + } + + static void Step4_Hud(WLIslandSettings cfg, GameObject prefabRoot) + { + H("⑤ HUD 단계 라벨 — 던전이면 「던전 N」"); + WLDungeonSceneSetup.Current = cfg.dungeons[0]; + int idx1 = cfg.dungeons[0].stageIndex; + WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + N("던전 1 → 단계 \"" + RunHud.PhaseText + "\" · 남은 적 \"" + RunHud.EnemyText + "\""); + Chk(RunHud.PhaseText == "던전 1", "HUD 라벨 = \"던전 1\" (지금까지는 \"스테이지 11\")"); + + WLDungeonSceneSetup.Current = cfg.dungeons[2]; + int idx3 = cfg.dungeons[2].stageIndex; + WLStageUiProbeHooks.RaiseStarted(idx3, StageIdOf(idx3), 3, 60f, 0); + N("던전 3 → 단계 \"" + RunHud.PhaseText + "\""); + Chk(RunHud.PhaseText == "던전 3", "HUD 라벨 = \"던전 3\""); + + int w0 = RunHud.StageLabelWrites; + WLStageUiProbeHooks.RaiseStarted(idx3, StageIdOf(idx3), 3, 60f, 0); + Chk(RunHud.StageLabelWrites == w0, "같은 이름 재입력 시 쓰기 0회(GC 0)"); + } + + static void Step5_StageUnchanged(GameObject prefabRoot) + { + H("⑥ #815 스테이지 모드 무변경 (Current = null)"); + WLDungeonSceneSetup.Current = null; + RunResultUI.Hide(); + WLStageUiProbeHooks.RaiseEntered(2, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0); + WLStageUiProbeHooks.RaiseEnemyCount(2, 5, 12); + WLStageUiProbeHooks.RaiseTime(2, 8f, 60f); + N("단계 \"" + RunHud.PhaseText + "\" · 시간 \"" + RunHud.TimerText + "\" · 남은 적 \"" + RunHud.EnemyText + "\""); + Chk(RunHud.PhaseText == "스테이지 3", "HUD = \"스테이지 3\" 그대로"); + + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(2, 3, 34.2f, 60f, 12, 12, 0, false, false); + N("제목 \"" + RunResultUI.TitleText + "\" · 행 " + RunResultUI.VisibleRows + "개 · 던전=" + RunResultUI.IsDungeonResult); + N("본문 ⤵\n" + Indent(RunResultUI.BodyText)); + Chk(RunResultUI.TitleText == "스테이지 3 클리어!", "제목 = \"스테이지 3 클리어!\" 그대로"); + Chk(RunResultUI.VisibleRows == 6, "행 6개 그대로(획득 골드 없음)"); + Chk(!RunResultUI.BodyText.Contains("획득 골드"), "본문에 「획득 골드」 0건"); + Chk(!RunResultUI.IsDungeonResult && RunResultUI.DungeonGold == -1, "던전 문맥 없음"); + + // 구 경로(4인자 = 816m 이전 동작)와 같은 본문인가 — 진짜 before/after 대조 + string after = RunResultUI.BodyText; + var r = Cleared(2, 12, false); + RunResultUI.Hide(); + RunResultUI.SetStageMode(true); + RunResultUI.ShowStage(in r, 0, 0, 0); // 816m 이전과 같은 진입점 + RunResultUI.BuildBody(WLRunUiSettings.Instance); + N("구 경로 본문 ⤵\n" + Indent(RunResultUI.BodyText)); + Chk(RunResultUI.BodyText == after, "구 4인자 경로(=816m 이전) == 새 경로 본문 문자열 동일"); + } + + static void Step6_Rollback(WLIslandSettings cfg, GameObject prefabRoot) + { + H("⑦ C8 롤백 — dungeonModeEnabled = 0 이면 던전도 지금까지 그대로"); + var g = WLStageUiSettings.Instance; + bool saved = g.dungeonModeEnabled; + try + { + g.dungeonModeEnabled = false; + WLDungeonSceneSetup.Current = cfg.dungeons[0]; + int idx1 = cfg.dungeons[0].stageIndex; + RunResultUI.Hide(); + WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); + N("롤백 제목 \"" + RunResultUI.TitleText + "\" · HUD \"" + RunHud.PhaseText + "\" · 행 " + RunResultUI.VisibleRows); + Chk(RunResultUI.TitleText == "스테이지 11 클리어!", "롤백 = \"스테이지 11 클리어!\"(PD 가 본 화면)"); + Chk(RunHud.PhaseText == "스테이지 11", "롤백 HUD = \"스테이지 11\""); + Chk(RunResultUI.VisibleRows == 6, "롤백 행 6개"); + } + finally { g.dungeonModeEnabled = saved; } + } + + // ═════════════════════════════════════════════════════════════════════ + // ③ 캡처 — 1080×1920 목업(실 노드 · 실 값 · RenderTexture · 815d 와 같은 방식) + // ═════════════════════════════════════════════════════════════════════ + public static string Capture() + { + var sb = new StringBuilder(); + string dir = Path.Combine(Path.Combine(Root, "Screenshots_WL"), "WL816m"); + Directory.CreateDirectory(dir); + + var cfg = WLIslandSettings.Instance; + var savedCurrent = WLDungeonSceneSetup.Current; + GameObject prefabRoot = null; + try + { + prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab); + BuildRig(prefabRoot); + var cam = _cam; + var revive = _revive; + + var rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32); + cam.targetTexture = rt; + + int idx1 = cfg.dungeons[0].stageIndex, idx3 = cfg.dungeons[2].stageIndex; + + // ⓒ HUD — 던전 1 + WLDungeonSceneSetup.Current = cfg.dungeons[0]; + WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + WLStageUiProbeHooks.RaiseEnemyCount(idx1, 3, 3); + WLStageUiProbeHooks.RaiseTime(idx1, 42f, 60f); + RunHud.ApplyLayout(); RunResultUI.Hide(); revive.HideNow(); + sb.AppendLine("ⓒ HUD 던전1 \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\""); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "c_hud_dungeon1.png"))); + + // ⓐ 던전 1 클리어 + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); + RunResultUI.ApplyLayout(); + sb.AppendLine("ⓐ 던전1 \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows + + " 골드=" + RunResultUI.DungeonGold); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "a_dungeon1_clear.png"))); + + // ⓑ 던전 3 클리어 + RunResultUI.Hide(); + WLDungeonSceneSetup.Current = cfg.dungeons[2]; + WLStageUiProbeHooks.RaiseEntered(idx3, StageIdOf(idx3), 3, 60f, Vector3.zero, 13f, 1.5f, 0); + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(idx3, StageIdOf(idx3), 41.5f, 60f, 3, 3, 0, false, false); + RunResultUI.ApplyLayout(); + sb.AppendLine("ⓑ 던전3 \"" + RunResultUI.TitleText + "\" 골드=" + RunResultUI.DungeonGold); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "b_dungeon3_clear.png"))); + + // ⓓ 스테이지 모드 대조 — 새 경로(Current=null) + RunResultUI.Hide(); + WLDungeonSceneSetup.Current = null; + WLStageUiProbeHooks.RaiseEntered(2, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0); + WLStageUiProbeHooks.RaiseEnemyCount(2, 5, 12); + WLStageUiProbeHooks.RaiseTime(2, 8f, 60f); + RunHud.ApplyLayout(); + sb.AppendLine("ⓓ HUD 스테이지 \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\""); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d1_hud_stage_new.png"))); + + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseCleared(2, 3, 34.2f, 60f, 12, 12, 0, false, false); + RunResultUI.ApplyLayout(); + sb.AppendLine("ⓓ 스테이지 클리어(새 경로) \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d1_stage_clear_new.png"))); + + // ⓓ 대조군 — 816m 이전과 같은 4인자 진입점 + RunResultUI.Hide(); + RunHud.WriteStageLabel(3); + RunHud.ApplyLayout(); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d2_hud_stage_legacy.png"))); + var r = Cleared(2, 12, false); + RunResultUI.SetStageMode(true); + RunResultUI.ShowStage(in r, 0, 0, 0); + RunResultUI.ApplyLayout(); + sb.AppendLine("ⓓ 스테이지 클리어(구 4인자) \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d2_stage_clear_legacy.png"))); + + // ⓔ 실패 팝업 (ReviveDialog.cs 0줄 변경) + RunResultUI.Hide(); + StageUiBridge.ProbeSetInStage(true); + WLStageUiProbeHooks.RaiseFailed(2, 3, WL.Combat.Stage.StageFailReason.TimeLimit, 4, 12, 60f, 0); + revive.ApplyLayout(); + sb.AppendLine("ⓔ 실패 \"" + revive.LastMessage.Replace("\n", " / ") + "\" 버튼=[" + + revive.PrimaryButtonText + "] [" + revive.SecondaryButtonText + "]"); + sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "e_fail.png"))); + + cam.targetTexture = null; + UnityEngine.Object.DestroyImmediate(rt); + + sb.AppendLine(); + sb.AppendLine("픽셀 대조 (MD5) — 새 경로 vs 816m 이전 경로"); + sb.AppendLine(" HUD " + SameFile(Path.Combine(dir, "d1_hud_stage_new.png"), Path.Combine(dir, "d2_hud_stage_legacy.png"))); + sb.AppendLine(" 결과창 " + SameFile(Path.Combine(dir, "d1_stage_clear_new.png"), Path.Combine(dir, "d2_stage_clear_legacy.png"))); + } + catch (Exception e) { sb.AppendLine("예외 " + e.GetType().Name + " : " + e.Message + "\n" + e.StackTrace); } + finally + { + try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { } + DestroyRig(); + WLDungeonSceneSetup.Current = savedCurrent; + if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot); + } + string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL816m_CAPTURE.txt"); + File.WriteAllText(path, sb.ToString(), new UTF8Encoding(true)); + return sb.ToString(); + } + + static string SameFile(string a, string b) + { + if (!File.Exists(a) || !File.Exists(b)) return "파일 없음"; + string ha = Md5(a), hb = Md5(b); + return (ha == hb ? "동일 " : "다름 ") + ha.Substring(0, 12) + " / " + hb.Substring(0, 12); + } + + static string Md5(string path) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + using (var fs = File.OpenRead(path)) + return BitConverter.ToString(md5.ComputeHash(fs)).Replace("-", "").ToLowerInvariant(); + } + + static RectTransform NewRect(RectTransform parent, string name) + { + var go = new GameObject(name, typeof(RectTransform)); + go.layer = 5; + var rt = (RectTransform)go.transform; + rt.SetParent(parent, false); + rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; + rt.localScale = Vector3.one; + return rt; + } + + static string Shot(Camera cam, RenderTexture rt, string path) + { + Canvas.ForceUpdateCanvases(); + cam.Render(); + var prev = RenderTexture.active; + RenderTexture.active = rt; + var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGBA32, false); + tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); + tex.Apply(); + RenderTexture.active = prev; + var png = tex.EncodeToPNG(); + UnityEngine.Object.DestroyImmediate(tex); + File.WriteAllBytes(path, png); + return " → " + path + " (" + png.Length + " B · " + rt.width + "×" + rt.height + ")"; + } +} diff --git a/Assets/WL/UI/Scripts/RunHud.cs b/Assets/WL/UI/Scripts/RunHud.cs index d9ecca528..36fa99147 100644 --- a/Assets/WL/UI/Scripts/RunHud.cs +++ b/Assets/WL/UI/Scripts/RunHud.cs @@ -76,6 +76,7 @@ namespace WL.UI private static TextMeshProUGUI s_enemy; // "남은 적 M / T" private static TextMeshProUGUI s_notice; // 카운트다운("준비") 등 한 줄 알림 private static int s_shownStageNo = int.MinValue; + private static string s_shownDungeonLabel; // WL-816m — 던전 라벨 캐시(같은 문구면 쓰기 0 = GC 0) private static int s_shownEnemyRemain = int.MinValue, s_shownEnemyTotal = int.MinValue; private static bool s_shownEnemyHi; private static string s_shownNotice = null; @@ -360,6 +361,7 @@ namespace WL.UI s_shownSec = int.MinValue; s_timerColorDirty = true; s_shownPhaseKind = -1; s_shownPhaseZone = -1; s_shownStageNo = int.MinValue; + s_shownDungeonLabel = null; // WL-816m s_shownEnemyRemain = s_shownEnemyTotal = int.MinValue; s_shownNotice = null; if (!on) WriteNotice(null); @@ -375,6 +377,7 @@ namespace WL.UI if (s_phase == null || g == null) return; if (stageNo == s_shownStageNo) return; s_shownStageNo = stageNo; + s_shownDungeonLabel = null; // WL-816m — 던전 캐시 무효화(던전 → 스테이지 복귀) s_shownPhaseKind = 2; s_shownPhaseZone = stageNo; // 813q WritePhase 의 캐시와 충돌하지 않게 s_phase.SetText(g.stageLabelFormat, stageNo); // TMP 무할당 오버로드({0} = float) var s = St; @@ -382,6 +385,35 @@ namespace WL.UI StageLabelWrites++; } + /// + /// WL-816m — 던전 중 단계 라벨(「던전 1」). 이름은 가 + /// 던전 표(`WLDungeonDef.displayName`)에서 읽어 넘긴다 — 이 파일은 그리기만 한다. + /// 같은 문구면 쓰지 않는다(GC 0). + /// + public static void WriteDungeonLabel(string dungeonName) + { + var g = WLStageUiSettings.Instance; + if (s_phase == null || g == null || string.IsNullOrEmpty(dungeonName)) return; + string text = FormatName(g.dungeonLabelFormat, dungeonName); + if (string.Equals(text, s_shownDungeonLabel)) return; + s_shownDungeonLabel = text; + s_shownStageNo = int.MinValue; // 숫자 캐시 무효화(스테이지 복귀 시 다시 쓰게) + s_shownPhaseKind = 3; s_shownPhaseZone = -1; // 813q WritePhase 의 캐시와 충돌하지 않게 + s_phase.SetText(text); + var s = St; + if (s != null) s_phase.color = s.phaseColor; + StageLabelWrites++; + } + + /// "{0}" 한 자리를 문자열로 치환(입장 1회 경로 · 틱 부담 0). + private static string FormatName(string format, string a0) + { + if (string.IsNullOrEmpty(format)) return a0; + int i = format.IndexOf("{0}", System.StringComparison.Ordinal); + if (i < 0) return format; + return format.Substring(0, i) + a0 + format.Substring(i + 3); + } + /// 「남은 적 M / T」(값이 그대로면 쓰기 0 = GC 0). public static void WriteEnemies(int remaining, int total) { @@ -677,6 +709,7 @@ namespace WL.UI s_shownSec = int.MinValue; s_shownWarn = false; s_timerColorDirty = false; s_shownPhaseKind = -1; s_shownPhaseZone = -1; s_shownStageNo = int.MinValue; + s_shownDungeonLabel = null; // WL-816m s_shownEnemyRemain = s_shownEnemyTotal = int.MinValue; s_shownEnemyHi = false; s_shownNotice = null; if (s_notice != null && s_notice.gameObject.activeSelf) s_notice.gameObject.SetActive(false); diff --git a/Assets/WL/UI/Scripts/RunResultUI.cs b/Assets/WL/UI/Scripts/RunResultUI.cs index 8f2e0f311..0400986bd 100644 --- a/Assets/WL/UI/Scripts/RunResultUI.cs +++ b/Assets/WL/UI/Scripts/RunResultUI.cs @@ -113,6 +113,13 @@ namespace WL.UI private static TextMeshProUGUI s_badge; // 무피격 클리어 배지 private static bool s_suppressNextRunEnded; // 실패 경로 — 병행 RunEnded 로 결과창이 겹쳐 뜨는 것을 막는다 + // ── WL-816m 던전 모드 (#816) ───────────────────────────────────────── + // 🔴 이 파일은 던전인지 **스스로 판정하지 않는다** — 유일한 이음매 `StageUiBridge` 가 + // `WLDungeonSceneSetup.Current` 를 보고 이름·골드를 넘겨준다(UI ↔ 게임 로직 분리). + // s_dungeonName == null 이면 #815 스테이지 모드 그대로다(한 글자도 안 바뀐다). + private static string s_dungeonName; + private static int s_dungeonGold = -1; // -1 = 모름(보상 축이 꺼져 있거나 던전이 아님) + /// 815d 가 대입한다 — [다음 스테이지] / [처음부터]. public static System.Action StagePrimary; /// 815d 가 대입한다 — [로비로]. @@ -528,10 +535,23 @@ namespace WL.UI public static string SetStageMode(bool on) { s_stageMode = on; - if (!on) { s_hasStage = false; if (s_badge != null && s_badge.gameObject.activeSelf) s_badge.gameObject.SetActive(false); } + if (!on) + { + s_hasStage = false; + s_dungeonName = null; s_dungeonGold = -1; // WL-816m — 던전 문맥도 함께 비운다 + if (s_badge != null && s_badge.gameObject.activeSelf) s_badge.gameObject.SetActive(false); + } return "결과창 스테이지 모드=" + on; } + // ── WL-816m 던전 문맥 (실측 덤프용) ────────────────────────────────── + /// 이번 결과가 던전인가(= StageUiBridge 가 이름을 넘겼는가). + public static bool IsDungeonResult { get { return !string.IsNullOrEmpty(s_dungeonName); } } + /// 이번 결과의 던전 이름(던전이 아니면 빈 문자열). + public static string DungeonName { get { return s_dungeonName ?? ""; } } + /// 이번 결과의 획득 골드(모르면 -1). + public static int DungeonGold { get { return s_dungeonGold; } } + /// 다음 `RunEnded` 1건을 무시한다(스테이지 실패 경로 · 결과창 중복 표시 0). public static string SuppressNextRunEnded() { @@ -545,6 +565,16 @@ namespace WL.UI /// hits/potions/maxChain 은 가 스테이지 구간 동안 실측한 값이다. /// public static string ShowStage(in WL.Combat.Stage.StageClearResult r, int hits, int potions, int maxChain) + { + return ShowStage(in r, hits, potions, maxChain, null, -1); + } + + /// + /// WL-816m — 던전 문맥을 함께 받는 경로. 이 비어 있으면 + /// 위 4인자 경로와 **완전히 같다**(#815 스테이지 모드 무변경). + /// + public static string ShowStage(in WL.Combat.Stage.StageClearResult r, int hits, int potions, int maxChain, + string dungeonName, int dungeonGold) { var s = St; var g = WLStageUiSettings.Instance; @@ -552,6 +582,9 @@ namespace WL.UI if (g == null || !WLStageUiSettings.Enabled || !g.stageResultEnabled) return "815d off — 스테이지 결과 생략"; if (!WLRunUiSettings.Enabled || !s.resultEnabled) return "813q off — 결과 화면 생략"; + s_dungeonName = g.dungeonModeEnabled && !string.IsNullOrEmpty(dungeonName) ? dungeonName : null; + s_dungeonGold = s_dungeonName != null ? dungeonGold : -1; + s_stage = r; s_hasStage = true; s_stageHits = hits; s_stagePotions = potions; s_stageMaxChain = maxChain; s_stageMode = true; @@ -564,13 +597,29 @@ namespace WL.UI return TryShow(Time.unscaledTime); } - /// 스테이지 결과 제목(마지막 스테이지면 다른 문구). + /// 스테이지 결과 제목(마지막 스테이지면 다른 문구). WL-816m — 던전이면 「던전 N 클리어!」. private static string StageTitle(WLStageUiSettings g) { + // 🔴 WL-816m — 던전은 스테이지 표 id(던전 1 = 11)가 아니라 던전 이름을 쓴다. + // isLastStage(전 스테이지 클리어) 문구도 던전에는 해당하지 않는다(던전은 표의 한 줄이다). + if (s_dungeonName != null && g.dungeonModeEnabled) + return FormatName(g.dungeonClearTitleFormat, s_dungeonName); + string f = s_stage.isLastStage ? g.stageAllClearTitleFormat : g.stageClearTitleFormat; return FormatOne(f, s_stage.stageId > 0 ? s_stage.stageId : s_stage.index + 1); } + /// "{0}" 한 자리를 **문자열**로 치환(string.Format 없이 · 클리어 1회 경로라 틱 부담 0). + private static string FormatName(string format, string a0) + { + if (string.IsNullOrEmpty(format)) return a0 ?? ""; + int i = format.IndexOf("{0}", System.StringComparison.Ordinal); + if (i < 0) return format; + s_sb.Length = 0; + s_sb.Append(format, 0, i).Append(a0).Append(format, i + 3, format.Length - i - 3); + return s_sb.ToString(); + } + /// "{0}" 한 자리 치환(정수 전용 · string.Format 없이 · GC 최소). private static string FormatOne(string format, long a0) { @@ -583,7 +632,11 @@ namespace WL.UI /// 스테이지 본문(기준서 §F-1). 813q 의 행 노드를 그대로 재사용한다. private static string BuildStageBody(WLRunUiSettings s, WLStageUiSettings g) { - EnsureStageRows(s, g); + // 🔴 WL-816m — 던전이면 「획득 골드」 행이 한 줄 늘어난다. + // stageResultRows 에 Gold 를 이미 넣어 뒀다면 그 자리에 쓰고 줄을 더하지 않는다(중복 0). + bool goldRow = s_dungeonName != null && g.dungeonModeEnabled && g.dungeonGoldRowEnabled && + !g.HasRow(WLStageResultRow.Gold); + EnsureStageRows(s, g, goldRow ? 1 : 0); var body = new StringBuilder(256); string title = StageTitle(g); @@ -599,15 +652,14 @@ namespace WL.UI string label = StageLabelOf(g, kind); s_sb.Length = 0; StageValueOf(g, kind, s_sb); - string value = s_sb.ToString(); - - if (slot < s_row.Length) - { - if (s_rowLabel[slot] != null) s_rowLabel[slot].SetText(label); - if (s_rowValue[slot] != null) s_rowValue[slot].SetText(value); - if (s_row[slot] != null && !s_row[slot].gameObject.activeSelf) s_row[slot].gameObject.SetActive(true); - } - body.Append(label).Append(" : ").Append(value).Append('\n'); + WriteStageRow(slot, label, s_sb.ToString(), body); + slot++; + } + if (goldRow) + { + s_sb.Length = 0; + StageValueOf(g, WLStageResultRow.Gold, s_sb); + WriteStageRow(slot, StageLabelOf(g, WLStageResultRow.Gold), s_sb.ToString(), body); slot++; } for (int i = slot; i < s_row.Length; i++) @@ -632,9 +684,21 @@ namespace WL.UI return "스테이지 행 " + slot + "개 · 배지=" + badge; } - private static void EnsureStageRows(WLRunUiSettings s, WLStageUiSettings g) + /// 행 하나를 노드에 쓰고 본문 문자열에도 남긴다(노드가 없어도 본문은 끝까지 만든다). + private static void WriteStageRow(int slot, string label, string value, StringBuilder body) { - int want = g.stageResultRows != null ? g.stageResultRows.Length : 0; + if (slot < s_row.Length) + { + if (s_rowLabel[slot] != null) s_rowLabel[slot].SetText(label); + if (s_rowValue[slot] != null) s_rowValue[slot].SetText(value); + if (s_row[slot] != null && !s_row[slot].gameObject.activeSelf) s_row[slot].gameObject.SetActive(true); + } + body.Append(label).Append(" : ").Append(value).Append('\n'); + } + + private static void EnsureStageRows(WLRunUiSettings s, WLStageUiSettings g, int extra) + { + int want = (g.stageResultRows != null ? g.stageResultRows.Length : 0) + extra; if (s_rowRoot == null || s_row.Length >= want) return; int old = s_row.Length; System.Array.Resize(ref s_row, want); @@ -661,7 +725,8 @@ namespace WL.UI case WLStageResultRow.Retries: return g.rowRetriesLabel; case WLStageResultRow.BossClear: return g.rowBossLabel; case WLStageResultRow.Level: return g.rowLevelLabel; - case WLStageResultRow.Gold: return g.rowGoldLabel; + // 🔴 WL-816m — 던전이면 「획득 골드」, 스테이지 모드면 지금까지와 같은 「골드」. + case WLStageResultRow.Gold: return s_dungeonName != null && g.dungeonModeEnabled ? g.rowDungeonGoldLabel : g.rowGoldLabel; default: return g.rowLootLabel; } } @@ -692,8 +757,15 @@ namespace WL.UI case WLStageResultRow.BossClear: sb.Append(s_stage.win ? g.rowBossClearedText : g.rowUnknownText); break; + case WLStageResultRow.Gold: + // 🔴 WL-816m — 던전이면 섬 축이 실제로 지급할 값(StageUiBridge 가 WLIslandBridge.ComputeGold 로 받아 넘긴다). + // 던전이 아니거나 보상 축이 꺼져 있으면 지금까지처럼 「미확인」. + if (s_dungeonName != null && g.dungeonModeEnabled && s_dungeonGold >= 0) + AppendFormat2(sb, g.rowDungeonGoldFormat, s_dungeonGold, 0); + else sb.Append(g.rowUnknownText); + break; default: - // 🔴 레벨 · 골드 · 등급별 획득 = 815b 페이로드에 없다(실측) → 「미확인」 표기 + // 🔴 레벨 · 등급별 획득 = 815b 페이로드에 없다(실측) → 「미확인」 표기 sb.Append(g.rowUnknownText); break; } @@ -1196,6 +1268,7 @@ namespace WL.UI s_title = null; s_rowRoot = null; s_nextButton = null; s_closeButton = null; s_nextLabel = null; s_closeLabel = null; s_nextBg = null; s_closeBg = null; s_badge = null; s_stageMode = false; // WL-815d + s_dungeonName = null; s_dungeonGold = -1; // WL-816m s_row = new RectTransform[0]; s_rowLabel = new TextMeshProUGUI[0]; s_rowValue = new TextMeshProUGUI[0]; s_uiRoot = null; ResetState(); @@ -1221,6 +1294,7 @@ namespace WL.UI " " + s_stage.elapsedSec.ToString("F1") + "s " + s_stage.killed + "/" + s_stage.total + " 재도전=" + s_stage.retries + " 마지막=" + s_stage.isLastStage + " 보스=" + s_stage.isBoss + " · 피격=" + s_stageHits + " 물약=" + s_stagePotions + " 최대연쇄=" + s_stageMaxChain); + sb.AppendLine(" [816m] 던전=" + IsDungeonResult + " \"" + DungeonName + "\" 골드=" + s_dungeonGold); sb.AppendLine(" [813q2] 부활우선=" + ReviveBusy + "(" + ReviveBusyReason + ") · DeathFlow=" + DeathFlow.State + " 런=" + RunDirector.Phase + "(running=" + RunDirector.IsRunning + ") · died=" + DiedSeen + " request=" + ReviveRequestSeen + " revived=" + RevivedSeen + " 팝업생략부활=" + SkipPopupReviveCount + diff --git a/Assets/WL/UI/Scripts/StageUiBridge.cs b/Assets/WL/UI/Scripts/StageUiBridge.cs index ea81c0488..329a8caf8 100644 --- a/Assets/WL/UI/Scripts/StageUiBridge.cs +++ b/Assets/WL/UI/Scripts/StageUiBridge.cs @@ -71,6 +71,51 @@ namespace WL.UI private static WLStageUiSettings St { get { return WLStageUiSettings.Instance; } } + // ═════════════════════════════════════════════════════════════════════ + // WL-816m — 던전 문맥 (#816) + // PD 원문(2026-09-15) 「클리어 던전은 무조건 11로 나오고 획득한 재화 정보를 알 수 없어」. + // 🔴 판정 기준은 **`WLDungeonSceneSetup.Current` 하나**다 — null 이면 #815 스테이지 모드 + // 그대로이고 아래 값은 한 번도 읽히지 않는다. + // 🔴 골드는 여기서 계산하지 않는다 — 섬 축의 `WLIslandBridge.ComputeGold` 를 그대로 다시 부른다 + // (계산식 1곳 · 지급은 섬 축이 한다 · `Assets/WL/Island/**` 는 읽기만 · 0줄 변경). + // ═════════════════════════════════════════════════════════════════════ + + /// 지금 도는 것이 던전이면 그 표 행, 아니면 null(던전 모드가 꺼져 있어도 null). + public static WL.Island.WLDungeonDef CurrentDungeon + { + get + { + var g = St; + if (g == null || !WLStageUiSettings.Enabled || !g.dungeonModeEnabled) return null; + return WL.Island.WLDungeonSceneSetup.Current; + } + } + + /// 던전 이름(표의 displayName · 비었으면 id 로 만든다). 던전이 아니면 null. + public static string DungeonNameOf(WL.Island.WLDungeonDef d) + { + var g = St; + if (d == null || g == null) return null; + if (!string.IsNullOrEmpty(d.displayName)) return d.displayName; + return FormatOne(g.dungeonNameFallbackFormat, d.id); + } + + /// 이번 클리어로 섬에서 받을 코인(보상 축이 꺼져 있으면 -1 = 「모름」). + public static int DungeonGoldOf(in WL.Combat.Stage.StageClearResult r) + { + var cfg = WL.Island.WLIslandSettings.Instance; + if (cfg == null || cfg.enabled_ == 0 || cfg.rewardEnabled == 0) return -1; + return WL.Island.WLIslandBridge.ComputeGold(cfg, in r); + } + + /// 상단 단계 라벨을 쓴다 — 던전이면 「던전 N」, 아니면 지금까지대로 「스테이지 N」. + private static void WritePhaseLabel(int stageNo) + { + string name = DungeonNameOf(CurrentDungeon); + if (name != null) RunHud.WriteDungeonLabel(name); + else RunHud.WriteStageLabel(stageNo); + } + // ── 부팅 ────────────────────────────────────────────────────────────── [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Boot() @@ -161,7 +206,7 @@ namespace WL.UI { RunHud.SetStageMode(true); RunHud.SetVisible(true); - RunHud.WriteStageLabel(s_stageNo); + WritePhaseLabel(s_stageNo); // WL-816m — 던전이면 「던전 N」 RunHud.WriteEnemies(e.totalEnemies, e.totalEnemies); RunHud.WriteStageTime(e.timeLimit); } @@ -191,7 +236,7 @@ namespace WL.UI s_hits = 0; s_maxChain = 0; if (!g.stageHudEnabled) return; RunHud.WriteNotice(null); // 카운트다운 줄을 접는다 - RunHud.WriteStageLabel(e.stageId > 0 ? e.stageId : e.index + 1); + WritePhaseLabel(e.stageId > 0 ? e.stageId : e.index + 1); // WL-816m — 던전이면 「던전 N」 RunHud.WriteStageTime(e.timeLimit); } @@ -218,8 +263,13 @@ namespace WL.UI if (g == null || !WLStageUiSettings.Enabled) return; if (g.stageHudEnabled) RunHud.WriteNotice(null); if (!g.stageResultEnabled) return; - LastLog = RunResultUI.ShowStage(in r, s_hits, PotionsUsed, s_maxChain); - Log(g, "클리어 스테이지 " + (r.stageId > 0 ? r.stageId : r.index + 1) + " — " + LastLog); + // WL-816m — 던전이면 제목을 「던전 N 클리어!」로 바꾸고 획득 골드 행을 한 줄 더한다. + var d = CurrentDungeon; + string dName = DungeonNameOf(d); + int dGold = dName != null ? DungeonGoldOf(in r) : -1; + LastLog = RunResultUI.ShowStage(in r, s_hits, PotionsUsed, s_maxChain, dName, dGold); + Log(g, "클리어 " + (dName ?? ("스테이지 " + (r.stageId > 0 ? r.stageId : r.index + 1))) + + (dName != null ? " · 획득 골드 " + dGold : "") + " — " + LastLog); } private static void OnFailed(in WL.Combat.Stage.StageFailedEvent e) @@ -533,6 +583,10 @@ namespace WL.UI " · 입장 " + EnteredSeen + " 카운트 " + CountdownSeen + " 시작 " + StartedSeen + " 적수 " + EnemySeen + " 시간 " + TimeSeen + " 클리어 " + ClearedSeen + " 실패 " + FailedSeen + " 이탈 " + ExitedSeen); + var dd = CurrentDungeon; + sb.AppendLine(" [816m] 던전=" + (dd != null) + " \"" + (DungeonNameOf(dd) ?? "-") + "\" (Current=" + + (WL.Island.WLDungeonSceneSetup.Current != null ? WL.Island.WLDungeonSceneSetup.Current.displayName : "null") + + " · 던전모드=" + (g != null && g.dungeonModeEnabled) + ")"); sb.AppendLine(" 실측 — 피격 " + s_hits + "회 · 물약 " + PotionsUsed + "개(시작 스냅 " + s_potionsAtStart + " / 현재 " + PotionUse.UsedCount + ") · 최대 연쇄 " + s_maxChain + "(진행중 " + KillChain.ChainKills + ")"); diff --git a/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset b/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset index e81edf25c..3e08325e4 100644 --- a/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset +++ b/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset @@ -27,7 +27,7 @@ MonoBehaviour: timerWarnColor: {r: 1, g: 0.29, b: 0.278, a: 1} countdownEnabled: 1 countdownText: "\uC900\uBE44" - countdownNumberFormat: "{0}" + countdownNumberFormat: '{0}' countdownFontPx: 72 countdownColor: {r: 1, g: 0.86, b: 0.42, a: 1} countdownTopGapPx: 24 @@ -58,13 +58,7 @@ MonoBehaviour: stageResultEnabled: 1 stageClearTitleFormat: "\uC2A4\uD14C\uC774\uC9C0 {0} \uD074\uB9AC\uC5B4!" stageAllClearTitleFormat: "\uC804 \uC2A4\uD14C\uC774\uC9C0 \uD074\uB9AC\uC5B4!" - stageResultRows: - - 0 - - 1 - - 2 - - 3 - - 4 - - 5 + stageResultRows: 000000000100000002000000030000000400000005000000 rowClearTimeLabel: "\uD074\uB9AC\uC5B4 \uC2DC\uAC04" rowKillsLabel: "\uCC98\uCE58" rowEliteLabel: "\uC5D8\uB9AC\uD2B8 \uCC98\uCE58" @@ -75,7 +69,7 @@ MonoBehaviour: rowLevelLabel: "\uB808\uBCA8" rowGoldLabel: "\uACE8\uB4DC" rowLootLabel: "\uD68D\uB4DD" - rowKillsFormat: "{0} / {1}" + rowKillsFormat: '{0} / {1}' rowHitsPotionsFormat: "{0}\uD68C / {1}\uAC1C" rowRetriesFormat: "{0}\uD68C" rowBossClearedText: "\uCC98\uCE58" @@ -87,13 +81,20 @@ MonoBehaviour: nextStageText: "\uB2E4\uC74C \uC2A4\uD14C\uC774\uC9C0" restartFromFirstText: "\uCC98\uC74C\uBD80\uD130" showRestartOnLastStage: 1 - toLobbyText: "섬으로" + toLobbyText: "\uC12C\uC73C\uB85C" + dungeonModeEnabled: 1 + dungeonClearTitleFormat: "{0} \uD074\uB9AC\uC5B4!" + dungeonLabelFormat: '{0}' + dungeonNameFallbackFormat: "\uB358\uC804 {0}" + dungeonGoldRowEnabled: 1 + rowDungeonGoldLabel: "\uD68D\uB4DD \uACE8\uB4DC" + rowDungeonGoldFormat: +{0} stageFailEnabled: 1 failDeathMessage: "\uC4F0\uB7EC\uC84C\uC2B5\uB2C8\uB2E4.\n\uB2E4\uC2DC \uB3C4\uC804\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?" failTimeoutMessage: "\uC2DC\uAC04 \uCD08\uACFC!\n\uB0A8\uC740 \uC801 {0}\uB9C8\uB9AC" failAbortedMessage: "\uC2A4\uD14C\uC774\uC9C0\uAC00 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4." failRetryText: "\uB2E4\uC2DC \uB3C4\uC804" - failLobbyText: "섬으로" + failLobbyText: "\uC12C\uC73C\uB85C" suppressResultOnFail: 1 lobbyMode: 0 titleSceneName: Title diff --git a/Assets/WL/UI/Settings/WLStageUiSettings.cs b/Assets/WL/UI/Settings/WLStageUiSettings.cs index 8c31fd2bd..7b24ead64 100644 --- a/Assets/WL/UI/Settings/WLStageUiSettings.cs +++ b/Assets/WL/UI/Settings/WLStageUiSettings.cs @@ -284,6 +284,33 @@ namespace WL.UI [Tooltip("오른쪽 버튼 문구 — 누르면 「로비로」(lobbyMode).")] public string toLobbyText = "로비로"; + // ── 던전 모드 (WL-816m · #816) ─────────────────────────────────────── + // PD 원문(2026-09-15) 「클리어 던전은 무조건 11로 나오고 획득한 재화 정보를 알 수 없어」. + // 던전에서는 스테이지 표 id(던전 1 = 11)가 아니라 **던전 이름**을 쓰고 획득 골드를 한 줄 더한다. + // 🔴 판정 기준은 `WL.Island.WLDungeonSceneSetup.Current` 하나뿐이다 — + // 그것이 null 인 #815 스테이지 모드는 이 절의 어떤 값도 읽지 않는다(한 글자도 안 바뀐다). + [Header("던전 모드 (WL-816m · 던전에서만 · 스테이지 모드는 불변)")] + [Tooltip("끄면 던전에서도 지금처럼 「스테이지 N 클리어!」 로 나온다(C8 롤백 1줄).")] + public bool dungeonModeEnabled = true; + + [Tooltip("던전 클리어 제목 — {0} = 던전 이름(WLIslandSettings 던전 표의 displayName · 예 「던전 1」).")] + public string dungeonClearTitleFormat = "{0} 클리어!"; + + [Tooltip("던전 중 상단 단계 라벨 — {0} = 던전 이름(스테이지 모드의 stageLabelFormat 을 대신한다).")] + public string dungeonLabelFormat = "{0}"; + + [Tooltip("던전 표의 displayName 이 비었을 때 쓰는 이름 — {0} = 그 행의 id.")] + public string dungeonNameFallbackFormat = "던전 {0}"; + + [Tooltip("던전 클리어 화면에 획득 골드 행을 더한다(stageResultRows 에 Gold 가 이미 있으면 그 자리에 쓴다).")] + public bool dungeonGoldRowEnabled = true; + + [Tooltip("획득 골드 행 라벨.")] + public string rowDungeonGoldLabel = "획득 골드"; + + [Tooltip("획득 골드 행 값 문구 — {0} = 코인 수.")] + public string rowDungeonGoldFormat = "+{0}"; + // ── 실패 팝업 (기준서 §E) ──────────────────────────────────────────── [Header("실패 팝업 (기준서 §E · 813z ReviveDialog 2버튼)")] [Tooltip("끄면 스테이지 실패에 813z 부활 팝업이 **원래대로**(1버튼 「부활」) 뜬다.")]