diff --git a/Assets/WL/Combat/DashDriver.cs b/Assets/WL/Combat/DashDriver.cs
index e4ecd9ab1..ef39efb7d 100644
--- a/Assets/WL/Combat/DashDriver.cs
+++ b/Assets/WL/Combat/DashDriver.cs
@@ -94,6 +94,11 @@ namespace WL.Combat
var cfg = WLCombatMotionSettings.Instance;
if (cfg == null || !cfg.dashEnabled) return false;
+ // ── PD #816x 최종 게이트 — 사거리 안에 적이 있으면 대쉬 0, 그 적을 제자리에서 친다.
+ // 대쉬의 유일한 진입점이 여기(MyActor.cs:242)라 어떤 경로로 와도 반드시 통과한다.
+ if (cfg.dashBlockIfEnemyInReach && cfg.dashBlockFinalGate
+ && BlockByEnemyInReach(pc, target, reach, cfg)) return true; // true = 호출부가 Play_Run 을 부르지 않는다
+
// 거리 창 밖이면 기존 동작. (min 미만은 애초에 여기까지 오지 않지만 방어적으로 본다)
float minDist = Mathf.Max(cfg.dashMinDistance, reach + Mathf.Max(cfg.dashMinBeyondReach, 0f)); // PD #790: 사정거리 + 여유 밖에서만 대쉬
if (distance < minDist || distance > cfg.dashMaxDistance) return false;
@@ -111,6 +116,93 @@ namespace WL.Combat
return driver.BeginInternal(pc, agent, cfg, target, distance, reach);
}
+ // ─────────────────────────────────────────────────────────────────────────
+ // PD #816x — 사거리 안 적 우선 · 대쉬 억제
+ //
+ // PD 원문: "플레이어의 공격 사정거리에 적이 있을 경우에는 대시(몬스터에게 접근)하지 않고
+ // 현재 위치에서 공격 사거리에 닿는 적을 공격하도록 해줘."
+ //
+ // 왜 여기인가 — 기존 #790 차단은 MyActor(원본) 안에서 `Find_Target_Facing(RetargetSearchRange, true)`
+ // 로만 돌았고, 그 '사거리'는 `Get_AttackReach`(정지거리 + 상대 반경 보정 + 크기 보정 + 여유)라
+ // PD 가 화면에서 보는 **사거리 원**(WeaponTrailDriver.ResolveAttackReach = 기본 정지거리 + AttackRange_Up + 여유)
+ // 과 같은 값이 아니다. 두 해석이 갈리면 "원 안에 있는데 대쉬" 가 그대로 난다.
+ // 그래서 대쉬의 유일한 진입점에서 **두 해석의 큰 쪽 + 여유**로 다시 한 번 본다(값은 전부 SO).
+ //
+ // 🔴 원본 0줄 — 타겟 교체는 공개 API `Actor.Set_Target(Actor)` · 공격은 `PCActor.WL_PlayAttackNow()`.
+ // ─────────────────────────────────────────────────────────────────────────
+
+ /// 게이트가 대쉬를 막은 누적 횟수 · 그중 타겟을 바꾼 횟수(검증용 읽기 전용).
+ public static int BlockedByReachCount, BlockedRetargetCount;
+ /// 마지막 차단 요약(검증용 문자열).
+ public static string LastBlockInfo = "(none)";
+
+ /// 게이트가 쓰는 사거리(m) — 전투 reach 와 화면 사거리 원 중 큰 쪽 + 여유.
+ public static float GateReach(float reach, WLCombatMotionSettings cfg)
+ {
+ float r = reach;
+ if (cfg.dashBlockUseRingReach)
+ {
+ float ring = WeaponTrailDriver.LastAttackReach; // 화면에 그려지는 그 값(816p 사거리 원과 동일 출처)
+ if (ring > r) r = ring;
+ }
+ return r + Mathf.Max(cfg.dashBlockReachBonus, 0f);
+ }
+
+ /// 사거리 안에 적이 있으면 그 적을 제자리에서 치고 true(= 대쉬 안 함).
+ private static bool BlockByEnemyInReach(PCActor pc, Actor target, float reach, WLCombatMotionSettings cfg)
+ {
+ if (ActorInfo.Ins == null) return false;
+
+ float gate = GateReach(reach, cfg);
+ if (gate <= 0.01f) return false;
+
+ Vector3 my = pc.Get_position();
+ var actors = ActorInfo.Ins.Get_EnemyActors(pc.IsEnemy(), my, gate);
+ Actor best = null;
+ float bestDist = float.MaxValue;
+ for (int i = 0; i < actors.Count; i++)
+ {
+ var a = actors[i];
+ if (a == null || a.IsDead()) continue;
+ Vector3 d = a.Get_position() - my; d.y = 0f;
+ float dist = d.magnitude;
+ if (dist > gate) continue; // Get_EnemyActors 는 수직 성분도 보므로 수평으로 다시 자른다
+ if (dist < bestDist) { bestDist = dist; best = a; }
+ }
+ ActorInfo.Ins.Release_Actors(actors);
+
+ if (best == null) return false;
+
+ BlockedByReachCount++;
+ bool retargeted = false;
+ if (cfg.dashBlockRetarget && best != target)
+ {
+ if (pc.IsMainPC())
+ {
+ if (target != null) target.Set_Outline(false);
+ best.Set_Outline(true);
+ }
+ pc.Set_Target(best); // 공개 API — 원본 수정 0줄
+ BlockedRetargetCount++;
+ retargeted = true;
+ }
+
+ if (cfg.dashBlockLookAtTarget)
+ {
+ Vector3 dir = best.Get_position() - my; dir.y = 0f;
+ if (dir.sqrMagnitude > 0.0001f) pc.transform.rotation = Quaternion.LookRotation(dir);
+ }
+
+ pc.WL_PlayAttackNow(); // 제자리 공격 (호출부는 CurAnim < Attack 구간이라 콤보를 끊지 않는다)
+
+ LastBlockInfo = string.Format("gate={0:F2}m d={1:F2}m target={2} retarget={3}",
+ gate, bestDist, best.name, retargeted);
+#if UNITY_EDITOR
+ if (cfg.verboseLog) Debug.Log("[WL #816x] 대쉬 억제 " + LastBlockInfo, pc);
+#endif
+ return true;
+ }
+
private bool BeginInternal(PCActor pc, NavMeshAgent agent, WLCombatMotionSettings cfg,
Actor target, float distance, float reach)
{
diff --git a/Assets/WL/Combat/Reaction/EffectScaleGuard.cs b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs
index cad0a2483..46dfbc9dd 100644
--- a/Assets/WL/Combat/Reaction/EffectScaleGuard.cs
+++ b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs
@@ -59,6 +59,7 @@ namespace WL.Combat.Reaction
static bool[] s_applied;
static TurnOff_GO[] s_off;
static bool[] s_offResolved;
+ static bool[] s_isHit; // #816x: 피격 이펙트인가(이름 접두사 판정 · 등록 때 1회)
static int s_count;
static int s_capacity;
static readonly Dictionary s_map = new Dictionary(512);
@@ -162,6 +163,10 @@ namespace WL.Combat.Reaction
s_factor[slot] = r >= 0 ? Mathf.Max(0f, st.rules[r].scale) : 0f;
if (r >= 0) RuledCount++;
+ // #816x — 피격 이펙트 판정(이름 접두사 · 등록 때 1회). 규칙이 없어도 위치·배율은 적용한다.
+ s_isHit[slot] = !string.IsNullOrEmpty(st.hitNamePrefix)
+ && nm.StartsWith(st.hitNamePrefix, StringComparison.Ordinal);
+
s_map[id] = slot;
TrackedCount = s_count;
return slot;
@@ -172,15 +177,18 @@ namespace WL.Combat.Reaction
{
var t = s_tf[slot];
if (t == null) return;
- if (s_rule[slot] < 0) return; // 규칙 없음 = 원본 100 %
+ bool hit = s_isHit[slot] && st.hitTopEnabled; // #816x
+ if (s_rule[slot] < 0 && !hit) return; // 규칙 없음 + 피격 이펙트도 아님 = 원본 100 %
bool active = t.gameObject.activeSelf;
if (active)
{
if (s_applied[slot]) return;
- float f = ResolveFactor(slot, st);
+ float f = s_rule[slot] >= 0 ? ResolveFactor(slot, st) : 1f;
+ if (hit) f *= Mathf.Max(0.01f, st.hitScaleMultiplier); // #816x 크기 ×1.5
if (f <= 0f) return;
t.localScale = s_orig[slot] * f; // 대입(멱등) — 누적 곱이 아니다
+ if (hit) LiftToEnemyTop(t, st); // #816x 적 상단으로 이동
s_applied[slot] = true;
AppliedCount++; ActiveScaled++;
LastName = t.name; LastFactor = f;
@@ -211,6 +219,55 @@ namespace WL.Combat.Reaction
return Mathf.Clamp(byHeight, Mathf.Max(0.0001f, st.minScale), Mathf.Max(st.minScale, st.maxScale));
}
+ static void EnsureReflection()
+ {
+ if (s_reflectDone) return;
+ s_reflectDone = true;
+ s_fiTarget2 = typeof(TurnOff_GO).GetField("m_target2", BindingFlags.Instance | BindingFlags.NonPublic);
+ s_fiTfTop = typeof(Actor).GetField("tf_Top", BindingFlags.Instance | BindingFlags.NonPublic);
+ }
+
+ // ───────────────────────────────────────── #816x 피격 이펙트 = 적 상단
+
+ /// 적 상단으로 올린 횟수 · 주인 적을 못 찾아 원본 위치로 둔 횟수 · 마지막 높이(검증용).
+ public static int HitTopApplied, HitTopMissed;
+ public static float LastHitTopY;
+
+ ///
+ /// 피격 이펙트를 그 적의 **상단**(Actor.tf_Top = 게임이 eEffectLocation.Top 에 쓰는 머리 앵커)으로 올린다.
+ /// 원본은 몸 중심(`target.Get_Center_position()`)에 스폰하므로 y 만 갈아끼운다(x·z = 타격 지점 유지).
+ /// 주인 적을 못 찾으면 **아무것도 하지 않는다**(원본 위치 그대로 · HitTopMissed 로 센다).
+ ///
+ static void LiftToEnemyTop(Transform t, WLEffectScaleSettings st)
+ {
+ if (ActorInfo.Ins == null) { HitTopMissed++; return; }
+ EnsureReflection();
+ if (s_fiTfTop == null) { HitTopMissed++; return; }
+
+ Vector3 p = t.position;
+ float range = Mathf.Max(st.hitOwnerSearchRange, 0.1f);
+ var pc = MyValue.MyPC;
+ var actors = ActorInfo.Ins.Get_EnemyActors(pc != null && pc.IsEnemy(), p, range);
+ Actor best = null; float bestSq = float.MaxValue;
+ for (int i = 0; i < actors.Count; i++)
+ {
+ var a = actors[i];
+ if (a == null) continue;
+ if (!st.hitApplyToBoss && a.IsSubRole(eSubRol.Boss)) continue;
+ float sq = (a.Get_position() - p).sqrMagnitude;
+ if (sq < bestSq) { bestSq = sq; best = a; }
+ }
+ ActorInfo.Ins.Release_Actors(actors);
+ if (best == null) { HitTopMissed++; return; }
+
+ var top = s_fiTfTop.GetValue(best) as Transform; // 참조형 필드 → 박싱 없음
+ if (top == null) { HitTopMissed++; return; }
+
+ p.y = top.position.y + st.hitTopExtraY;
+ t.position = p;
+ HitTopApplied++; LastHitTopY = p.y;
+ }
+
/// 이 이펙트가 따라다니는 대상 액터의 실높이(발밑→머리). 원본 private 필드라 리플렉션(캐시 1회).
static bool TryTargetHeight(int slot, out float height)
{
@@ -224,12 +281,7 @@ namespace WL.Combat.Reaction
var off = s_off[slot];
if (off == null) return false;
- if (!s_reflectDone)
- {
- s_reflectDone = true;
- s_fiTarget2 = typeof(TurnOff_GO).GetField("m_target2", BindingFlags.Instance | BindingFlags.NonPublic);
- s_fiTfTop = typeof(Actor).GetField("tf_Top", BindingFlags.Instance | BindingFlags.NonPublic);
- }
+ EnsureReflection();
if (s_fiTarget2 == null || s_fiTfTop == null) return false;
var actor = s_fiTarget2.GetValue(off) as Actor; // 참조형 필드 → 박싱 없음
@@ -286,6 +338,7 @@ namespace WL.Combat.Reaction
Array.Resize(ref s_applied, want);
Array.Resize(ref s_off, want);
Array.Resize(ref s_offResolved, want);
+ Array.Resize(ref s_isHit, want);
s_capacity = want;
}
@@ -300,7 +353,7 @@ namespace WL.Combat.Reaction
{
s_id[w] = s_id[i]; s_tf[w] = s_tf[i]; s_orig[w] = s_orig[i];
s_factor[w] = s_factor[i]; s_rule[w] = s_rule[i]; s_applied[w] = s_applied[i];
- s_off[w] = s_off[i]; s_offResolved[w] = s_offResolved[i];
+ s_off[w] = s_off[i]; s_offResolved[w] = s_offResolved[i]; s_isHit[w] = s_isHit[i];
}
w++;
}
@@ -330,6 +383,7 @@ namespace WL.Combat.Reaction
SweepCount = TrackedCount = RuledCount = 0;
AppliedCount = RestoredCount = SkippedCount = ActiveScaled = 0;
HeightResolved = HeightMissed = 0;
+ HitTopApplied = HitTopMissed = 0; LastHitTopY = 0f;
LastName = ""; LastFactor = 0f;
}
}
diff --git a/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs
index af5a51736..eed534908 100644
--- a/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs
+++ b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs
@@ -151,6 +151,31 @@ namespace WL.Combat.Reaction
[Tooltip("대상 높이 기반 계산의 배율 상한(보스처럼 큰 대상에서 원본보다 커지는 것 방지).")]
public float maxScale = 1.5f;
+ // ── 피격 이펙트 위치·크기 (#816x) ────────────────────────────────────────
+ // PD 지시 #816 (2026-09-15): "적 타격시 발생하는 피격 이펙트가 적 상단에 발생하게 하고, 크기를 50% 키워줘."
+ // 원본은 `ProjectileBase.Show_HitEffect → Get_HitEffectPos`(= target.Get_Center_position() 또는 투사체 위치)
+ // 로 **몸 중심**에 스폰한다. 원본 1줄도 못 고치므로 EffectScaleGuard 의 풀 스윕이
+ // '활성화된 그 프레임에'(LateUpdate · 렌더 앞) 위치를 적 상단으로 올리고 배율을 곱한다.
+ // '적 상단' = 게임이 eEffectLocation.Top 에 쓰는 그 앵커(Actor.tf_Top) = 머리 높이.
+ [Header("피격 이펙트 위치·크기 (PD #816x)")]
+ [Tooltip("켜면 이름이 hitNamePrefix 로 시작하는 이펙트를 적 상단(Actor.tf_Top)으로 올린다. 끄면 원본 위치(몸 중심) 그대로")]
+ public bool hitTopEnabled = true;
+
+ [Tooltip("대상 이름 접두사. 원본 히트 이펙트는 전부 Effect_Hit_<클래스ID> · Effect_Hit_<보스스킬> 이다")]
+ public string hitNamePrefix = "Effect_Hit_";
+
+ [Tooltip("피격 이펙트 크기 배율(위 rules 로 정해진 배율에 추가로 곱한다). 1.5 = PD 지시 '50% 크게'")]
+ public float hitScaleMultiplier = 1.5f;
+
+ [Tooltip("적 상단(머리 끝)에서 더 올릴 높이(m). 0 이면 정확히 머리 끝")]
+ public float hitTopExtraY = 0.1f;
+
+ [Tooltip("이펙트가 어느 적의 것인지 찾는 반경(m). 원본이 그 적의 몸 중심에 스폰하므로 짧아도 된다")]
+ public float hitOwnerSearchRange = 2.5f;
+
+ [Tooltip("켜면 보스에게도 적용한다. 끄면 보스 피격 이펙트는 원본 위치·크기 그대로")]
+ public bool hitApplyToBoss = true;
+
[Header("규칙 (배열 순서대로 검사 · 처음 맞는 것 · 안 맞으면 손대지 않는다 = 원본 100 %)")]
public EffectScaleRule[] rules = new EffectScaleRule[0];
diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
index 42726d62b..23786cc73 100644
--- a/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
+++ b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
@@ -26,6 +26,12 @@ MonoBehaviour:
referenceHeight: 1.191
minScale: 0.05
maxScale: 1.5
+ hitTopEnabled: 1
+ hitNamePrefix: Effect_Hit_
+ hitScaleMultiplier: 1.5
+ hitTopExtraY: 0.1
+ hitOwnerSearchRange: 2.5
+ hitApplyToBoss: 1
rules:
- prefab: Effect_Stun
match: 0
diff --git a/Assets/WL/Combat/WLCombatMotionSettings.cs b/Assets/WL/Combat/WLCombatMotionSettings.cs
index 1e6aed60e..09765b271 100644
--- a/Assets/WL/Combat/WLCombatMotionSettings.cs
+++ b/Assets/WL/Combat/WLCombatMotionSettings.cs
@@ -200,6 +200,25 @@ namespace WL.Combat
public bool dashBlockIfEnemyInReach = true;
public static bool DashBlockIfEnemyInReach { get { var i = Instance; return i != null && i.dashBlockIfEnemyInReach; } }
+ // ── PD #816x (2026-09-15) "사정거리에 적이 있으면 대쉬하지 말고 제자리에서 그 적을 공격" ──
+ // 기존 #790 차단은 MyActor 안(원본)에서만 돌아 놓치는 경우가 있었다(락 타겟·사거리 해석 차이).
+ // 아래 값들은 **대쉬의 유일한 진입점인 DashDriver.TryBegin** 에서 마지막으로 한 번 더 본다.
+ [Tooltip("대쉬 직전 최종 게이트를 켠다. 사거리 안에 적이 하나라도 있으면 대쉬를 시작하지 않는다")]
+ public bool dashBlockFinalGate = true;
+
+ [Tooltip("게이트가 보는 사거리에 화면 사거리 원(WeaponTrailDriver.LastAttackReach)을 함께 쓴다. " +
+ "PD 화면에서 원 안에 보이는 적이 게이트에도 '안'으로 잡히게 하는 스위치")]
+ public bool dashBlockUseRingReach = true;
+
+ [Tooltip("게이트 사거리에 더하는 여유(m). 원 테두리에 걸친 적까지 '안'으로 본다")]
+ public float dashBlockReachBonus = 0.35f;
+
+ [Tooltip("게이트가 사거리 안 적을 찾으면 타겟을 그 적(가장 가까운)으로 바꾼다. 끄면 기존 타겟을 그대로 두고 대쉬만 막는다")]
+ public bool dashBlockRetarget = true;
+
+ [Tooltip("제자리 공격 직전에 그 적 쪽으로 몸을 돌린다(수평 회전만)")]
+ public bool dashBlockLookAtTarget = true;
+
[Header("■ 검증 (에디터 전용)")]
[Tooltip("켜면 전진 시작·종료·이동량을 콘솔에 남긴다")]
public bool verboseLog = false;
diff --git a/Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset b/Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset
index 4b00b3152..bb43f5946 100644
--- a/Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset
+++ b/Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset
@@ -29,6 +29,11 @@ MonoBehaviour:
dashFailCooldownSeconds: 2
dashMinBeyondReach: 0.8
dashBlockIfEnemyInReach: 1
+ dashBlockFinalGate: 1
+ dashBlockUseRingReach: 1
+ dashBlockReachBonus: 0.35
+ dashBlockRetarget: 1
+ dashBlockLookAtTarget: 1
dashStateName: dash
dashClipName: Chase_Start
dashCrossFadeSeconds: 0.06
diff --git a/Assets/WL/Settings/Resources/WL/WLGameplaySettings.asset b/Assets/WL/Settings/Resources/WL/WLGameplaySettings.asset
index ab8498662..47258009d 100644
--- a/Assets/WL/Settings/Resources/WL/WLGameplaySettings.asset
+++ b/Assets/WL/Settings/Resources/WL/WLGameplaySettings.asset
@@ -29,6 +29,13 @@ MonoBehaviour:
eliteProjectileScale: 0.3
bossProjectileScale: 0.3
projectileScaleVisibleOnly: 1
+ mobBodyScaleEnabled: 1
+ mobBodyScale: 1.5
+ eliteBodyScale: 1
+ bossBodyScale: 1
+ mobBodyScaleKeepUnderBossThreshold: 0
+ bodyScaleBossThreshold: 2
+ bodyScaleThresholdEpsilon: 0.02
hideBattlePad: 0
battlePadPaths:
- IngameUIs/BattleUI
diff --git a/Assets/WL/Settings/WLCollisionTuning.cs b/Assets/WL/Settings/WLCollisionTuning.cs
index b8dbee3b8..237d493e3 100644
--- a/Assets/WL/Settings/WLCollisionTuning.cs
+++ b/Assets/WL/Settings/WLCollisionTuning.cs
@@ -61,7 +61,12 @@ namespace WL.Settings
public static void ApplyMob(NavMeshAgent agent, eSubRol subRole)
{
var s = WLGameplaySettings.Instance;
- if (agent == null || s == null || !s.agentTuningEnabled) return;
+ if (agent == null || s == null) return;
+
+ // #816x — 몸 크기(PD "기본적인 몬스터의 크기도 50% 키워줘"). 에이전트 튜닝 스위치와 독립이다.
+ ApplyMobBodyScale(agent.transform, subRole, s);
+
+ if (!s.agentTuningEnabled) return;
bool isBoss = subRole == eSubRol.Boss;
bool isElite = subRole == eSubRol.Elite;
@@ -81,6 +86,48 @@ namespace WL.Settings
agent.obstacleAvoidanceType = (ObstacleAvoidanceType)(int)s.mobAvoidance;
}
+ // ── #816x 몬스터 몸 크기 ────────────────────────────────────────────────
+ /// 몸 크기를 적용한 누적 횟수 · 보스 경계에서 잘린 횟수 · 마지막 값(검증용 읽기 전용).
+ public static int BodyScaleApplied, BodyScaleClamped;
+ public static float LastBodyScaleBase, LastBodyScaleApplied;
+
+ ///
+ /// 몬스터 루트 스케일에 타입별 배율을 곱한다(#816x · PD "기본적인 몬스터의 크기도 50% 키워줘").
+ ///
+ /// 호출 시점 = MobActor.Set 안, `transform.localScale = f_DefaultScale * Vector3.one`(:87) **직후**(:118).
+ /// 따라서 base 는 언제나 테이블 기본값이고, 풀에서 재사용돼도 다시 기본값에서 곱한다 = **누적 없음**.
+ /// 루트 스케일이므로 자식(콜라이더·tf_Top·HP 바)은 함께 커지고,
+ /// NavMeshAgent.radius(월드 단위 · 바로 아래에서 별도 계산) 와 데이터 값(이동 속도 · f_BaseATKRange)은 **그대로다**.
+ ///
+ public static void ApplyMobBodyScale(Transform root, eSubRol subRole, WLGameplaySettings s)
+ {
+ if (root == null || s == null || !s.mobBodyScaleEnabled) return;
+
+ float m = subRole == eSubRol.Boss ? s.bossBodyScale
+ : subRole == eSubRol.Elite ? s.eliteBodyScale
+ : s.mobBodyScale;
+ if (m <= 0f || Mathf.Abs(m - 1f) < 0.0001f) return;
+
+ float bas = root.localScale.x;
+ if (bas <= 0.0001f) return;
+
+ float want = bas * m;
+
+ // 원본이 localScale.x > 2 를 '보스급' 판정에 쓴다(ProjectileBase:305/308 · Actor:950).
+ // 스위치가 켜져 있을 때만 그 경계를 넘지 않게 자른다(기본은 꺼짐 = PD 지시 그대로 ×배율).
+ float th = s.bodyScaleBossThreshold;
+ if (s.mobBodyScaleKeepUnderBossThreshold && th > 0f && bas <= th && want > th)
+ {
+ want = th - Mathf.Max(s.bodyScaleThresholdEpsilon, 0f);
+ BodyScaleClamped++;
+ }
+
+ root.localScale = Vector3.one * want;
+ BodyScaleApplied++;
+ LastBodyScaleBase = bas;
+ LastBodyScaleApplied = want;
+ }
+
///
/// 이 투사체에 곱할 크기 배율. 1 이면 변경하지 않는다.
/// 플레이어·펫 투사체는 항상 1(무변경) — 몬스터(eRole.Mob)가 쏜 것만 대상.
diff --git a/Assets/WL/Settings/WLGameplaySettings.cs b/Assets/WL/Settings/WLGameplaySettings.cs
index c686dcc5f..414c978d4 100644
--- a/Assets/WL/Settings/WLGameplaySettings.cs
+++ b/Assets/WL/Settings/WLGameplaySettings.cs
@@ -158,6 +158,36 @@ namespace WL.Settings
"몹의 공격 사거리만 줄어 빗나간다. 끄면 판정 박스까지 전부 축소한다(= PD 지시 문자 그대로).")]
public bool projectileScaleVisibleOnly = true;
+ // ── 몬스터 몸 크기 (#816x) ───────────────────────────────────────────────
+ // PD 지시 #816 (2026-09-15): "기본적인 몬스터의 크기도 50% 키워줘" → 일반 몹 배율 1.5
+ // 적용 지점 = MobActor.Set 이 이미 부르는 WLCollisionTuning.ApplyMob(원본 훅 재사용 · 새 훅 0줄).
+ // MobActor.cs:87 이 transform.localScale = f_DefaultScale 을 대입한 **직후**(:118)라
+ // 매 스폰·풀 재사용마다 테이블 기본값에서 다시 곱한다 = 누적 없음.
+ // 데이터 표(MonsterList.json f_DefaultScale)는 손대지 않는다 — 런타임 루트 스케일만.
+ [Header("■ 몬스터 몸 크기 (PD 지시 #816x)")]
+ [Tooltip("켜면 몬스터 루트 스케일에 아래 배율을 곱한다. 끄면 테이블 f_DefaultScale 그대로(C8 롤백).")]
+ public bool mobBodyScaleEnabled = true;
+
+ [Tooltip("일반 몹(None)의 몸 크기 배율. 1.5 = PD 지시 '50% 크게'. 1 이면 무변경.")]
+ public float mobBodyScale = 1.5f;
+
+ [Tooltip("정예 몹(Elite)의 몸 크기 배율. PD 지시는 '기본적인 몬스터'라 기본 1(무변경).")]
+ public float eliteBodyScale = 1f;
+
+ [Tooltip("보스(Boss)의 몸 크기 배율. PD 지시 범위 밖이라 기본 1(무변경).")]
+ public float bossBodyScale = 1f;
+
+ [Tooltip("켜면 배율 결과가 보스 판정 경계(bodyScaleBossThreshold)를 넘지 않도록 자른다.\n" +
+ "원본 ProjectileBase.Check_TargetBoss(:305/308) 와 Actor.Get_ScaleSizeDist(:950) 가 " +
+ "localScale.x > 2 를 '보스급' 판정에 쓰기 때문이다. 끄면 PD 지시대로 곧이곧대로 ×배율(기본 = 끔).")]
+ public bool mobBodyScaleKeepUnderBossThreshold = false;
+
+ [Tooltip("위 자르기의 경계값. 원본 코드가 쓰는 2 와 같아야 한다.")]
+ public float bodyScaleBossThreshold = 2f;
+
+ [Tooltip("경계에서 얼마나 아래로 자를지(여유). 0.02 = 1.98 로 자른다.")]
+ public float bodyScaleThresholdEpsilon = 0.02f;
+
// ── 인게임 HUD 오버라이드 (#775) ─────────────────────────────────────────
// PD 지시 #775 (2026-09-06): "우측 하단 공격 버튼 및 스킬 아이콘 버튼은 추후 변경해서 사용할
// 예정이므로 지금은 보이지 않게 숨기고, 포트레이트 뷰에 맞게 WL 프로젝트 UI를 배치해줘."