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버튼 「부활」) 뜬다.")]