diff --git a/Assets/WL/Combat/Auto/ActiveSkillLock.cs b/Assets/WL/Combat/Auto/ActiveSkillLock.cs new file mode 100644 index 000000000..f9aebd03a --- /dev/null +++ b/Assets/WL/Combat/Auto/ActiveSkillLock.cs @@ -0,0 +1,170 @@ +// ───────────────────────────────────────────────────────────────────────────── +// ActiveSkillLock.cs — 액티브 스킬 사용 임시 OFF (WL-816u ③ · #816) +// +// PD 지시 원문(2026-09-15) 「우선 엑티브 스킬은 사용 Off 해줘.」 +// +// ■ 무엇을 끄나 / 무엇을 남기나 +// 끈다 : 자동전투 AI 의 스킬 시전 · 전투 패드 스킬 버튼 4칸 +// 남긴다: 기본 공격 · 이동 · 물약 · 회피 · 자동전투 토글 자체 · 몹/보스 스킬 +// +// ■ 왜 이 지점인가 (코드 실측 · 원본 0줄) +// `PCActor.Update()`(PCActor.cs:79~101) 의 자동 스킬 루프는 매 프레임 +// `if (DSUtil.CheckNull(arr_magicData[i]) || arr_magicData[i].e_SkillType == eSkillType.Passive) continue;` +// 로 시작한다. 즉 **`arr_magicData[i]` 가 null 이면 그 슬롯은 쿨다운도 안 돌고 `Use_Skill` 도 안 불린다.** +// `arr_magicData` 는 `ReSet()`(PCActor.cs:189)에서 +// `table_skilllist.Ins.Get_Data_orNull(m_ServerData.Get_EquipSkillID(i))` +// 로 다시 채워지므로, **매 프레임 비워 두면** 언제 ReSet 이 돌아도 다음 프레임에 다시 잠긴다. +// · `arr_magicData` 는 PCActor 의 private 필드다 → 원본을 고치지 않으려면 리플렉션뿐이다 +// (FieldInfo 는 1회만 찾아 캐시 · 프레임당 비용 = 배열 6칸 쓰기). +// · `ServerData.Equip` 은 **건드리지 않는다** — 장착 데이터를 지우면 스킬 UI·미션·서버 프리셋까지 +// 영향이 가고, 되돌릴 때 원래 ID 를 복원해야 한다. 여기서는 **이번 PC 인스턴스의 런타임 캐시만** 비운다. +// +// ■ 되돌리기(C8) — `WLCombatCoreSettings.activeSkillsEnabled = true` **값 하나**. +// 그러면 이 러너가 아예 뜨지 않고(오브젝트 0), 다음 `ReSet()` 이 원래 스킬을 그대로 다시 읽는다. +// +// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 가 플레이어 팝업을 띄운다). +// 🔴 `Assets/Script/**` · `Assets/FarmingIsland/**` 는 이 파일 때문에 한 줄도 바뀌지 않는다. +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(PCActor 가 Assembly-CSharp 에 있다). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Reflection; +using UnityEngine; +using WL.Combat.Core; + +namespace WL.Combat.Auto +{ + /// 액티브 스킬 사용 잠금. 정적 · 새 Manager/Singleton 0(런너는 숨김 GameObject 1개). + public static class ActiveSkillLock + { + static WLCombatCoreSettings Cfg { get { return WLCombatCoreSettings.Instance; } } + + /// 잠겨 있는가(= 액티브 스킬 OFF). 에셋이 없으면 false = 원본 동작 100 %. + public static bool Locked + { + get { var c = Cfg; return c != null && !c.activeSkillsEnabled && !RuntimeDisabled; } + } + + /// 진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false. + public static bool RuntimeDisabled; + + // ── 진단 카운터 (프로브·보고가 읽는다 · 실측만) + public static int Ticks, Cleared, PcSwaps; + public static string LastLog = ""; + /// 마지막으로 비우기 직전에 본 스킬 ID 들(= 잠그지 않았다면 시전됐을 스킬). + public static int[] LastSeenSkillIds = new int[0]; + + static FieldInfo s_magicField; + static Object s_lastPc; + static float s_nextPoll; + static bool s_faulted; + + /// PCActor 의 private `arr_magicData` (SkillListTableData[6]). 1회만 찾는다. + static FieldInfo MagicField + { + get + { + if (s_magicField == null) + s_magicField = typeof(PCActor).GetField("arr_magicData", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + return s_magicField; + } + } + + // ───────────────────────────────────────────────────────────────────── + + /// 런너가 매 프레임 부른다(프로브도 직접 부를 수 있다). 비운 슬롯 수를 돌려준다. + public static int Tick() + { + if (s_faulted || !Locked) return 0; + var cfg = Cfg; + float poll = cfg != null ? cfg.activeSkillLockPollSeconds : 0f; + if (poll > 0f) + { + float now = Time.unscaledTime; + if (now < s_nextPoll) return 0; + s_nextPoll = now + poll; + } + Ticks++; + + var pc = MyValue.MyPC; + if (DSUtil.CheckNull(pc)) { s_lastPc = null; return 0; } + if (!ReferenceEquals(pc, s_lastPc)) { s_lastPc = pc; PcSwaps++; } + + // 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 플레이어 팝업으로 띄운다. + try { return ClearOn(pc); } + catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); return 0; } + } + + /// 한 PC 의 자동 스킬 캐시를 비운다(이미 비어 있으면 아무 것도 쓰지 않는다 = GC 0). + public static int ClearOn(PCActor pc) + { + var f = MagicField; + if (f == null || DSUtil.CheckNull(pc)) return 0; + var arr = f.GetValue(pc) as SkillListTableData[]; + if (arr == null) return 0; + + int n = 0; + for (int i = 0; i < arr.Length; i++) + { + if (arr[i] == null) continue; + if (LastSeenSkillIds.Length != arr.Length) LastSeenSkillIds = new int[arr.Length]; + LastSeenSkillIds[i] = arr[i].n_SkillID; + arr[i] = null; + n++; + } + if (n > 0) + { + Cleared += n; + Log("액티브 스킬 잠금 — 슬롯 " + n + "칸 비움(누적 " + Cleared + ")"); + } + return n; + } + + static void Log(string msg) + { + LastLog = msg; + var c = Cfg; + if (c != null && c.verboseLog) Debug.Log("[ActiveSkillLock] " + msg); + } + + /// 프로브용 — 카운터 초기화. + public static void ResetDiagnostics() + { + Ticks = Cleared = PcSwaps = 0; LastLog = ""; + s_lastPc = null; s_nextPoll = 0f; s_faulted = false; + } + + public static string Dump() + { + var c = Cfg; + return "[ActiveSkillLock] 잠금=" + Locked + + " (설정 activeSkillsEnabled=" + (c != null ? c.activeSkillsEnabled.ToString() : "에셋없음") + + " · RuntimeDisabled=" + RuntimeDisabled + ")" + + " · 틱 " + Ticks + " · 비운 슬롯 " + Cleared + " · PC 교체 " + PcSwaps + + " · 필드 " + (MagicField != null) + " · " + LastLog; + } + + // ── 런너 (숨김 GameObject 1개 · 플레이 모드 전용) ──────────────────── + static ActiveSkillLockRunner s_runner; + + internal static void EnsureRunner() + { + if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return; + var go = new GameObject("[WL816u] ActiveSkillLockRunner"); + go.hideFlags = HideFlags.HideAndDontSave; + s_runner = go.AddComponent(); + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Boot() + { + if (Locked) EnsureRunner(); // 꺼져 있으면(= 스킬 사용 ON) 오브젝트 0 (C8) + } + } + + /// ActiveSkillLock 의 시간 축. 숨김 GameObject 1개 · 코루틴 0. + internal sealed class ActiveSkillLockRunner : MonoBehaviour + { + void Update() { ActiveSkillLock.Tick(); } + } +} diff --git a/Assets/WL/Combat/Auto/ActiveSkillLock.cs.meta b/Assets/WL/Combat/Auto/ActiveSkillLock.cs.meta new file mode 100644 index 000000000..5c739819e --- /dev/null +++ b/Assets/WL/Combat/Auto/ActiveSkillLock.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b05df5564c97a154e9763358004191e6 \ No newline at end of file diff --git a/Assets/WL/Combat/Core/WLCombatCoreSettings.cs b/Assets/WL/Combat/Core/WLCombatCoreSettings.cs index c23964cd0..bcfebba96 100644 --- a/Assets/WL/Combat/Core/WLCombatCoreSettings.cs +++ b/Assets/WL/Combat/Core/WLCombatCoreSettings.cs @@ -146,5 +146,16 @@ namespace WL.Combat.Core [Tooltip("적용 뒤 전투 패드 자동 버튼 아이콘(BattleUI.images[0])을 이 시간 동안 재시도로 맞춘다(초). UI 가 늦게 뜨는 경우 대비 · 0 이면 아이콘을 건드리지 않는다.")] public float autoCombatIconRetrySeconds = 8f; + + // ── WL-816u ③ (2026-09-15 PD) 「우선 엑티브 스킬은 사용 Off 해줘.」 + [Header("액티브 스킬 사용 (WL-816u ③ · PD 임시 지시)")] + [Tooltip("🔴 **되돌리기 = 이 값 하나**. false 면 액티브 스킬 사용을 전부 끈다 — " + + "① 자동전투 AI 의 스킬 시전(PCActor.Update 의 자동 스킬 루프가 읽는 arr_magicData 를 비운다) " + + "② 전투 패드의 스킬 버튼 4칸(WLBattlePadLayout 이 활성 슬롯을 0 으로 본다). " + + "기본 공격·이동·물약·회피·자동전투 자체는 그대로다. true = 816t 까지의 동작 100 %.")] + public bool activeSkillsEnabled = false; + + [Tooltip("위가 false 일 때 스킬 배열을 비우는 주기(초). 0 이하면 매 프레임.")] + public float activeSkillLockPollSeconds = 0f; } } diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLCombatCoreSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLCombatCoreSettings.asset index e7a3f10ea..f73ebd229 100644 --- a/Assets/WL/Combat/Settings/Resources/WL/WLCombatCoreSettings.asset +++ b/Assets/WL/Combat/Settings/Resources/WL/WLCombatCoreSettings.asset @@ -38,3 +38,5 @@ MonoBehaviour: autoCombatInLobby: 0 autoCombatPollSeconds: 0.25 autoCombatIconRetrySeconds: 8 + activeSkillsEnabled: 0 + activeSkillLockPollSeconds: 0 diff --git a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset index 79d63d340..be3ba3f9a 100644 --- a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset +++ b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset @@ -21,18 +21,20 @@ MonoBehaviour: fixIslandDuplicates: 1 islandCameraOwner: 1 hidePcWatchSeconds: 6 - islandSaveKey: + islandSaveKey: swapIslandPlayer: 1 pcPrefabPath: Assets/Res_Addr/PC/LH_M05.prefab islandPcScale: 0.7 mirrorFiAnimator: 1 fiPlayerModelName: Stickman interactRadius: 3.5 - promptFormat: "{0} 들어가기" - enterButtonText: "들어가기" + promptFormat: "{0} \uB4E4\uC5B4\uAC00\uAE30" + enterButtonText: "\uB4E4\uC5B4\uAC00\uAE30" enterKey: 101 gateHeight: 3.2 gateWidth: 2.4 + fontAssetPath: Assets/ThirdParty/TextMesh Pro/Addressables/Fonts & Materials/Font + SDF.asset rewardEnabled: 1 goldPerKill: 12 goldClearBonus: 60 @@ -50,16 +52,12 @@ MonoBehaviour: farmReHideDelaySeconds: 3 matchNewTileMaterials: 1 dungeonUnlockPrefsKey: WL_Island_DungeonsUnlocked - dungeonUnlockPrices: - - 10 - - 30 - - 60 - - 100 + dungeonUnlockPrices: 0a0000001e0000003c00000064000000 maxDungeons: 4 platformUseFarmPosition: 1 platformPosition: {x: 0, y: 0, z: 0} platformOffset: {x: 0, y: 0, z: 0} - platformPrefabPath: + platformPrefabPath: platformShowPad: 0 platformCenterOnPlate: 1 platformPlateLift: 0.03 @@ -67,12 +65,15 @@ MonoBehaviour: platformRadius: 1.7 platformColor: {r: 0.92, g: 0.72, b: 0.24, a: 1} platformUseToon: 1 - platformLabelFormat: "던전 열기" + platformLabelFormat: "\uB358\uC804 \uC5F4\uAE30" platformLabelHeight: 2.2 platformTriggerSize: 3.4 platformTriggerHeight: 3 platformRespawnDelaySeconds: 1.3 platformSnapToGround: 1 + platformScale: 0.5 + platformCenterOnZone: 1 + platformCenterVisibleOnly: 1 platformRequireStepOff: 1 platformStepOffTimeoutSeconds: 120 applyStartCoin: 1 @@ -100,7 +101,7 @@ MonoBehaviour: dungeons: - enabled_: 1 id: 1 - displayName: "던전 1" + displayName: "\uB358\uC804 1" sceneName: WL_Dungeon01 mapId: 901 stageIndex: 10 @@ -108,12 +109,12 @@ MonoBehaviour: gateYaw: 180 gateScale: 1 gateColor: {r: 0.45, g: 0.32, b: 0.62, a: 1} - gatePrefabPath: + gatePrefabPath: platformUseFarm: 1 platformPosition: {x: 0, y: 0, z: 0} - enabled_: 1 id: 2 - displayName: "던전 2" + displayName: "\uB358\uC804 2" sceneName: WL_Dungeon02 mapId: 902 stageIndex: 11 @@ -121,12 +122,12 @@ MonoBehaviour: gateYaw: 0 gateScale: 1 gateColor: {r: 0.28, g: 0.5, b: 0.66, a: 1} - gatePrefabPath: + gatePrefabPath: platformUseFarm: 0 platformPosition: {x: -2, y: 0, z: -2} - enabled_: 1 id: 3 - displayName: "던전 3" + displayName: "\uB358\uC804 3" sceneName: WL_Dungeon01 mapId: 903 stageIndex: 12 @@ -134,12 +135,12 @@ MonoBehaviour: gateYaw: 180 gateScale: 1 gateColor: {r: 0.62, g: 0.42, b: 0.24, a: 1} - gatePrefabPath: + gatePrefabPath: platformUseFarm: 0 platformPosition: {x: -2.5, y: 0, z: 8.5} - enabled_: 1 id: 4 - displayName: "던전 4" + displayName: "\uB358\uC804 4" sceneName: WL_Dungeon02 mapId: 904 stageIndex: 13 @@ -147,6 +148,6 @@ MonoBehaviour: gateYaw: 0 gateScale: 1 gateColor: {r: 0.55, g: 0.24, b: 0.3, a: 1} - gatePrefabPath: + gatePrefabPath: platformUseFarm: 0 platformPosition: {x: 6, y: 0, z: -2} diff --git a/Assets/WL/Island/WLIslandGateFlow.cs b/Assets/WL/Island/WLIslandGateFlow.cs index b238950e8..e116c660a 100644 --- a/Assets/WL/Island/WLIslandGateFlow.cs +++ b/Assets/WL/Island/WLIslandGateFlow.cs @@ -251,6 +251,11 @@ namespace WL.Island var farms = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); bool haveCenter = false; + // 🔴 WL-816u — 「열림 구역」 = 이 발판이 여는 자리 **전부**(보이는 밭 전체)다. + // 816g 는 **처음 만난 밭 하나**의 중심을 썼다 → 밭이 여러 칸이면 구역 정중앙이 아니다(PD 지적). + Bounds zone = new Bounds(); bool zoneSet = false; + ZoneBoundsValid = false; + for (int i = 0; i < farms.Length; i++) { var f = farms[i]; @@ -270,6 +275,8 @@ namespace WL.Island { if (!boundsSet) { b = rends[r].bounds; boundsSet = true; } else b.Encapsulate(rends[r].bounds); + if (!zoneSet) { zone = rends[r].bounds; zoneSet = true; } // WL-816u — 구역 전체 + else zone.Encapsulate(rends[r].bounds); } rends[r].enabled = false; s_hiddenRends.Add(rends[r]); RenderersHidden++; } @@ -289,9 +296,22 @@ namespace WL.Island haveCenter = true; } } + + // 🔴 WL-816u — 구역 전체의 XZ 정중앙으로 덮어쓴다(값 1개로 816g 동작 복귀). + if (zoneSet) + { + ZoneBounds = zone; + ZoneBoundsValid = true; + if (cfg.platformCenterOnZone != 0 && haveCenter) + center = new Vector3(zone.center.x, zone.min.y, zone.center.z); + } return haveCenter; } + /// 🔴 WL-816u 진단 — 「열림 구역」(숨긴 밭 전부)의 월드 바운즈. 보고·프로브가 오차를 잰다. + public static Bounds ZoneBounds; + public static bool ZoneBoundsValid; + /// 섬이 나중에 열리면 ① 그 섬의 밭을 다시 숨기고 ② 타일 재질을 기존 타일과 맞춘다(§1). static void HookIslandActivation(WLIslandSettings cfg, Scene scene) { @@ -541,37 +561,43 @@ namespace WL.Island p.transform.SetParent(transform, true); Purchaser = p; + // 🔴 WL-816u — PD 「프레임과 글자 아이콘 크기 모두 50%로 줄여줘」. + // **인스턴스 스케일만** 건드린다(프리팹 원본 0줄). 프레임·가격 글자·코인 아이콘이 한 덩어리로 줄어든다. + // 스케일은 **바운즈 재기 전에** 적용해야 중심 보정이 줄어든 크기 기준으로 맞는다. + float ps = cfg.platformScale > 0.001f ? cfg.platformScale : 1f; + AppliedScale = ps; + if (ps != 1f) p.transform.localScale = p.transform.localScale * ps; + // 🔴 FI 가격판은 프리팹 안에서 **루트보다 앞·아래**에 놓여 있다(실측 2026-09-13: // Frame/Price/Coin 이 전부 y = -0.49 · z = 루트+(-1/0/+1) — 잠긴 섬 자리의 **물 위**에 눕는 판이다). // 섬 위(y≈0)에 그대로 놓으면 **지면 아래로 들어가 안 보인다** → 프리팹 전체의 렌더 범위를 재서 // 「밭 한가운데 · 바닥 바로 위」로 맞춘다. 프리팹은 한 글자도 고치지 않는다. if (cfg.platformCenterOnPlate != 0) { - var rends = p.GetComponentsInChildren(true); - Bounds b = new Bounds(); bool set = false; - for (int i = 0; i < rends.Length; i++) - { - if (rends[i] == null) continue; - if (!set) { b = rends[i].bounds; set = true; } else b.Encapsulate(rends[i].bounds); - } - if (set) + Bounds b; + if (VisibleBounds(p.transform, cfg.platformCenterVisibleOnly != 0, out b)) { var t = transform.position; p.transform.position += new Vector3(t.x - b.center.x, t.y + cfg.platformPlateLift - b.min.y, t.z - b.center.z); + VisibleBounds(p.transform, cfg.platformCenterVisibleOnly != 0, out PlateBounds); + PlateBoundsValid = true; } } // 트리거를 **발판(가격판) 크기**로 줄인다(원본 프리팹은 섬 1칸 8 m 를 덮는 十자 두 개다). + // 🔴 WL-816u — 판을 50 % 로 줄여도 **밟는 자리는 그대로**여야 한다(PD 는 크기만 말했다). + // size 는 부모(p) 로컬 단위라 스케일만큼 역보정하면 월드 미터가 유지된다. var boxes = p.GetComponentsInChildren(true); float s = cfg.platformTriggerSize <= 0f ? 3.2f : cfg.platformTriggerSize; + float inv = 1f / ps; var localCenter = p.transform.InverseTransformPoint(transform.position); for (int i = 0; i < boxes.Length; i++) { if (boxes[i] == null) continue; - boxes[i].size = new Vector3(s, cfg.platformTriggerHeight, s); - boxes[i].center = new Vector3(localCenter.x, cfg.platformTriggerHeight * 0.5f, localCenter.z); + boxes[i].size = new Vector3(s * inv, cfg.platformTriggerHeight * inv, s * inv); + boxes[i].center = new Vector3(localCenter.x, cfg.platformTriggerHeight * inv * 0.5f, localCenter.z); } // 🔴 `Init` 은 `Purchaser.Start()` 보다 먼저여야 한다(Start 가 가격판을 그린다) — 같은 프레임이라 안전. @@ -581,6 +607,34 @@ namespace WL.Island " · 트리거 " + s.ToString("F1") + " m · 더미 Island 연결"); } + // ── WL-816u 진단(프로브·보고가 읽는다 · 실측만) + public static float AppliedScale = 1f; + public static Bounds PlateBounds; + public static bool PlateBoundsValid; + + /// + /// 🔴 WL-816u — 「**보이는** 프레임」의 월드 바운즈. + /// visibleOnly 면 파티클·트레일·꺼진 렌더러·꺼진 오브젝트를 제외한다(FI Purchaser 는 물보라 파티클을 품고 있다). + /// + public static bool VisibleBounds(Transform root, bool visibleOnly, out Bounds b) + { + b = new Bounds(); + bool set = false; + var rends = root.GetComponentsInChildren(true); + for (int i = 0; i < rends.Length; i++) + { + var r = rends[i]; + if (r == null) continue; + if (visibleOnly) + { + if (r is ParticleSystemRenderer || r is TrailRenderer || r is LineRenderer) continue; + if (!r.enabled || !r.gameObject.activeInHierarchy) continue; + } + if (!set) { b = r.bounds; set = true; } else b.Encapsulate(r.bounds); + } + return set; + } + static System.Reflection.FieldInfo s_purchaserField; /// FI 가 섬 구입에 쓰는 **바로 그 프리팹**(가격판·코인 아이콘·물보라·SFX 가 전부 들어 있다). diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index 6a83995cf..91a842cd5 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -295,6 +295,23 @@ namespace WL.Island [Tooltip("1 이면 발판 자리를 지면 높이에 맞춘다(위에서 아래로 레이).")] public int platformSnapToGround = 1; + // ── WL-816u (2026-09-15 PD) 「열림 발판의 크기는 열림 구역으로 설정 된 위치의 정중앙에 배치하고, + // 프레임과 글자 아이콘 크기 모두 50%로 줄여줘 (현재는 너무 커)」 + [Tooltip("🔴 WL-816u — 발판(FI 가격판 프레임 + 가격 글자 + 코인 아이콘)의 인스턴스 스케일 배수. " + + "PD 「50 %」 = 0.5. **되돌리기 = 1**(= 816t 까지의 크기). 프리팹 원본은 한 글자도 고치지 않는다. " + + "트리거 한 변(platformTriggerSize)은 이 값과 무관하게 **월드 미터 그대로** 유지된다(스케일 역보정).")] + public float platformScale = 0.5f; + + [Tooltip("🔴 WL-816u — 1 이면 발판을 「열림 구역」 **전체**의 XZ 정중앙에 놓는다. " + + "열림 구역 = 이 발판이 여는 자리(던전 1 = 숨겨 둔 밭 구역 전부 · 그 외 = 표의 platformPosition). " + + "0 이면 816g 동작(= 처음 만난 밭 **하나**의 중심).")] + public int platformCenterOnZone = 1; + + [Tooltip("🔴 WL-816u — 1 이면 「보이는 프레임의 중심」을 맞춘다: 파티클·트레일·꺼진 렌더러를 " + + "중심 계산에서 제외한다(FI Purchaser 프리팹에는 물보라 파티클이 들어 있어 전부 세면 중심이 밀린다). " + + "0 이면 816g 동작(= 모든 렌더러 바운즈).")] + public int platformCenterVisibleOnly = 1; + [Tooltip("1 이면 새 발판은 플레이어가 그 자리에서 **내려온 뒤에** 생긴다 — " + "골드가 많을 때 가만히 서서 던전이 연달아 팔리는 것을 막는다(816g 실측).")] public int platformRequireStepOff = 1; diff --git a/Assets/WL/UI/Scripts/RunResultUI.cs b/Assets/WL/UI/Scripts/RunResultUI.cs index 0400986bd..cd597bc1b 100644 --- a/Assets/WL/UI/Scripts/RunResultUI.cs +++ b/Assets/WL/UI/Scripts/RunResultUI.cs @@ -1173,6 +1173,11 @@ namespace WL.UI leftText = last ? g815.restartFromFirstText : g815.nextStageText; rightText = g815.toLobbyText; leftOn = !last || g815.showRestartOnLastStage; + // 🔴 WL-816u — 던전은 왼쪽 버튼이 「같은 던전 재입장」이라 문구도 그에 맞춘다. + // s_dungeonName == null(= #815 스테이지 모드)이면 이 두 줄은 한 번도 돌지 않는다. + if (s_dungeonName != null && g815.dungeonModeEnabled && g815.dungeonPrimaryRetry && + !string.IsNullOrEmpty(g815.dungeonPrimaryText)) + { leftText = g815.dungeonPrimaryText; leftOn = true; } } LayoutButton(s_nextButton, s_nextBg, s_nextLabel, s, u, leftOn ? -1 : 0, leftText, s.resultNextBgColor, s.resultNextLabelColor); LayoutButton(s_closeButton, s_closeBg, s_closeLabel, s, u, leftOn ? 1 : 0, rightText, s.resultCloseBgColor, s.resultCloseLabelColor); diff --git a/Assets/WL/UI/Scripts/StageUiBridge.cs b/Assets/WL/UI/Scripts/StageUiBridge.cs index 329a8caf8..e00f51aea 100644 --- a/Assets/WL/UI/Scripts/StageUiBridge.cs +++ b/Assets/WL/UI/Scripts/StageUiBridge.cs @@ -59,6 +59,8 @@ namespace WL.UI public static int MenuDeferred; public static int MenuReassertCalls, MenuReassertHits; // 원본이 다시 켠 것을 도로 끈 횟수 public static int RetryCalls, NextCalls, LobbyCalls, RestartCalls; + /// WL-816u — 던전 결과창 왼쪽 버튼이 「같은 던전 재입장」으로 돈 횟수(실측 진단). + public static int DungeonRetryCalls; public static string LastLog = ""; public static string LastMissingPath = ""; public static bool InStage { get { return s_inStage; } } @@ -382,6 +384,20 @@ namespace WL.UI /// [다음 스테이지] — 마지막 스테이지면 [처음부터](= Enter(0)). public static string OnNextStage() { + // 🔴 WL-816u — PD 2026-09-15 「던전 진입 후 다음 던전으로 가기를 선택할 경우 몬스터가 등장하지 않고 + // 아무것도 할 수 없어. 임시로 스테이지 전환 없이 스테이지 1에 등장한 몬스터가 재등장하게 해줘.」 + // → 던전 모드에서는 왼쪽 버튼이 **같은 인덱스 재입장**(실패 팝업의 [다시 도전]과 **같은 경로**)이다. + // 스테이지 모드(#815 · CurrentDungeon == null)는 한 줄도 지나가지 않는다. + var gDg = St; + if (gDg != null && gDg.dungeonPrimaryRetry && CurrentDungeon != null) + { + DungeonRetryCalls++; + ClearFailPopup(); + bool okd = WL.Combat.Stage.StageDirector.Retry(); + LastLog = "던전 재입장 — StageDirector.Retry() = " + okd; + return LastLog; + } + var last = WL.Combat.Stage.StageDirector.LastResult; if (last.isLastStage) { @@ -557,7 +573,7 @@ namespace WL.UI EnteredSeen = CountdownSeen = StartedSeen = EnemySeen = TimeSeen = ClearedSeen = FailedSeen = ExitedSeen = 0; MenuHideCalls = MenuRestoreCalls = MenuMissing = 0; MenuReassertCalls = MenuReassertHits = 0; s_nextReassert = 0f; - RetryCalls = NextCalls = LobbyCalls = RestartCalls = 0; + RetryCalls = NextCalls = LobbyCalls = RestartCalls = DungeonRetryCalls = 0; s_hits = s_maxChain = 0; s_potionsAtStart = PotionUse.UsedCount; LastLog = ""; LastMissingPath = ""; return "상태 초기화"; diff --git a/Assets/WL/UI/Scripts/WLBattlePadLayout.cs b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs index e33a663be..1f3708365 100644 --- a/Assets/WL/UI/Scripts/WLBattlePadLayout.cs +++ b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs @@ -105,7 +105,10 @@ namespace WL.UI var cards = target.skillCards; int total = cards != null ? cards.Length : 0; - int active = Mathf.Clamp(s.activeSkillSlots, 0, total); + // 🔴 WL-816u ③ — PD 「우선 엑티브 스킬은 사용 Off 해줘」. 잠금이 걸려 있으면 활성 슬롯 0 + // = 스킬 버튼 4칸이 전부 비활성(아래 SetActive(isSkill)). 되돌리기는 설정 값 하나. + int want = WL.Combat.Auto.ActiveSkillLock.Locked ? 0 : s.activeSkillSlots; + int active = Mathf.Clamp(want, 0, total); LastActiveSlots = active; LastReserveSlots = Mathf.Max(0, total - active); diff --git a/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset b/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset index 3e08325e4..06cd705cc 100644 --- a/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset +++ b/Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset @@ -89,6 +89,8 @@ MonoBehaviour: dungeonGoldRowEnabled: 1 rowDungeonGoldLabel: "\uD68D\uB4DD \uACE8\uB4DC" rowDungeonGoldFormat: +{0} + dungeonPrimaryRetry: 1 + dungeonPrimaryText: "\uB2E4\uC2DC \uB3C4\uC804" 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" diff --git a/Assets/WL/UI/Settings/WLStageUiSettings.cs b/Assets/WL/UI/Settings/WLStageUiSettings.cs index 7b24ead64..ad59df4e6 100644 --- a/Assets/WL/UI/Settings/WLStageUiSettings.cs +++ b/Assets/WL/UI/Settings/WLStageUiSettings.cs @@ -311,6 +311,16 @@ namespace WL.UI [Tooltip("획득 골드 행 값 문구 — {0} = 코인 수.")] public string rowDungeonGoldFormat = "+{0}"; + // ── WL-816u (2026-09-15 PD 임시 지시) 「던전 진입 후 다음 던전으로 가기를 선택할 경우 몬스터가 + // 등장하지 않고 아무것도 할 수 없어. 임시로 스테이지 전환 없이 스테이지 1에 등장한 몬스터가 재등장하게 해줘.」 + [Tooltip("🔴 WL-816u — 켜면 **던전에서만** 결과창 왼쪽 버튼이 「같은 던전 재입장」(StageDirector.Retry · " + + "실패 팝업 [다시 도전]과 같은 경로)이 된다. 끄면 816m 까지의 동작(= StageDirector.Next · 표의 다음 줄). " + + "스테이지 모드(#815)는 이 값과 무관하다. **되돌리기 = 이 값 0**.")] + public bool dungeonPrimaryRetry = true; + + [Tooltip("위가 켜졌을 때 던전 결과창 왼쪽 버튼 문구. 비우면 nextStageText(「다음 스테이지」) 그대로.")] + public string dungeonPrimaryText = "다시 도전"; + // ── 실패 팝업 (기준서 §E) ──────────────────────────────────────────── [Header("실패 팝업 (기준서 §E · 813z ReviveDialog 2버튼)")] [Tooltip("끄면 스테이지 실패에 813z 부활 팝업이 **원래대로**(1버튼 「부활」) 뜬다.")]