// ───────────────────────────────────────────────────────────────────────────── // WLLootSettings.cs — 전리품 연출 값의 단일 출처(SOT · C45) // // PD 지시 #813 · 발주서 WL-813f §1 (2026-09-09) · 기준서 v1 §B 요소 4 // // 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLLootSettings.asset // → Resources.Load("WL/WLLootSettings") 1회 로드 후 캐시(WLGrowthSettings 관례 그대로). // 에셋이 없거나 enabled = false 면 Enabled == false → DropItem.cs 의 훅 2줄이 즉시 반환하고 // 산포·빔·사운드·자동 줍기·폭발 드랍이 전부 꺼진다 = 원본 드랍 100%(C8 롤백 1순위 스위치). // 아래 필드 기본값은 "에셋 부재 시의 폴백" 이 아니라 에셋 생성 시 초기값이다. // // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(DropItem/MobActor 가 Assembly-CSharp 에 있다). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Loot { [CreateAssetMenu(fileName = "WLLootSettings", menuName = "WL/Loot Settings", order = 322)] public sealed class WLLootSettings : ScriptableObject { public const string ResourcesPath = "WL/WLLootSettings"; /// 등급 배열 길이 = eItemGrade 8단계 + 여유 1(기준서 §B 요소 4 등급 색 9행). public const int GradeSlots = 9; static WLLootSettings s_cached; static bool s_lookupDone; /// 설정 에셋(1회 로드 후 캐시). 없으면 null. public static WLLootSettings Instance { get { if (!s_lookupDone) { s_cached = Resources.Load(ResourcesPath); s_lookupDone = true; } return s_cached; } } /// 진단·A/B 전용 런타임 스위치(WLCombatCoreSettings.RuntimeDisabled 선례). Play 종료 시 도메인 리로드로 자동 false. public static bool RuntimeDisabled; /// 전리품 연출이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off. public static bool Enabled { get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; } } /// 테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다(에셋 제거/복구 실험). public static void ClearCache() { s_cached = null; s_lookupDone = false; } // ─────────────────────────────────────────── 전체 스위치 [Header("전체 스위치")] [Tooltip("끄면 산포·빔·사운드·자동 줍기·폭발 드랍이 전부 비활성 — 원본 DropItem 동작 100%. 롤백 1순위.")] public bool enabled = true; [Tooltip("드랍 처리 결과를 Console 에 남긴다(검증용 · 기본 off · GC 가 발생하므로 서비스 빌드에서 켜지 말 것).")] public bool verboseLog = false; // ─────────────────────────────────────────── ① 드랍 산포 [Header("① 드랍 산포 (기준서 §B: 산포 반경 0.6~1.2 m)")] [Tooltip("끄면 원본이 계산한 위치를 그대로 쓴다(골드는 원본대로 2~3 m).")] public bool scatterEnabled = true; [Tooltip("드랍 지점(몹 위치)에서 떨어지는 최소 거리(m).")] public float scatterRadiusMin = 0.6f; [Tooltip("드랍 지점에서 떨어지는 최대 거리(m).")] public float scatterRadiusMax = 1.2f; [Tooltip("NavMesh 스냅 반경(m). 표집점이 NavMesh 밖이면 이 반경 안에서 가장 가까운 점으로 당긴다. 0 이면 스냅하지 않는다.")] public float scatterNavSnapRadius = 0.8f; [Tooltip("포물선 시작 높이(m · 등급 1 기준). Co_Move_Split 의 시작점을 올려 튀는 높이를 만든다.")] public float bounceHeightBase = 0.35f; [Tooltip("등급 1 을 넘는 등급 1단계마다 더해지는 높이(m).")] public float bounceHeightPerGrade = 0.08f; [Tooltip("튀는 높이 상한(m).")] public float bounceHeightMax = 1.0f; [Tooltip("한 번의 처치에서 여러 개가 떨어질 때 서로 다른 방향으로 부채꼴로 흩는다(같은 자리 겹침 방지).")] public bool fanOutSameKill = true; // ─────────────────────────────────────────── ② 등급 빔 + 사운드 [Header("② 등급 빔 + 사운드 (기준서 §B: 등급 4 이상만)")] [Tooltip("빔·사운드를 낼 최소 아이템 등급(table_itemlist.n_ItemGrade). 기준서 = 4.")] public int beamMinGrade = 4; [Tooltip("등급별 빔 프리팹(인덱스 0 = 등급 1 … 인덱스 8 = 등급 9). 비워 두면 그 등급은 빔 없음. 보유 FX_LootDrop_* 만 쓴다(새 아트 0).")] public GameObject[] beamByGrade = new GameObject[GradeSlots]; [Tooltip("빔을 드랍 오브젝트의 자식으로 붙인다(자석으로 끌려갈 때 같이 따라온다). 끄면 드랍 지점에 고정.")] public bool beamParentToDrop = true; [Tooltip("빔 스폰 오프셋(m · 드랍 오브젝트 기준).")] public Vector3 beamOffset = new Vector3(0f, 0f, 0f); [Tooltip("빔 프리팹 스케일 배율(1 = 원본).")] public float beamScale = 0.6f; [Tooltip("빔 최대 수명(초). 드랍 소멸(원본 20 s)보다 길게 잡아야 줍기 전에 꺼지지 않는다. 드랍이 사라지면 이 시간과 무관하게 즉시 파괴한다.")] public float beamMaxLifetimeSeconds = 21f; [Tooltip("EffectBudget.Heavy 슬롯을 잡고 빔을 스폰한다(상한 초과 = 빔만 스킵 · 드랍 자체는 그대로). 코어가 꺼져 있으면 제한 없음.")] public bool beamUseEffectBudget = true; [Tooltip("등급 빔과 함께 낼 사운드(원본 eSound · 보유 클립만). 기준서 = s015_GoldPowerUp.")] public eSound beamSound = eSound.s015_GoldPowerUp; [Tooltip("빔 사운드 최대 볼륨(거리 감쇠 전). 0 이면 사운드 없음.")] [Range(0f, 1f)] public float beamSoundVolume = 0.5f; [Tooltip("빔 사운드를 다시 낼 최소 간격(초 · unscaled). 폭발 드랍이 겹칠 때 소리가 뭉치는 것을 막는다.")] public float beamSoundMinIntervalSeconds = 0.12f; // ─────────────────────────────────────────── ③ 자동 줍기 반경 [Header("③ 자동 줍기 반경 (기준서 §B: 1.5 m · 원본 자석 상태기 재사용)")] [Tooltip("끄면 원본 그대로 접촉해야만 줍는다.")] public bool autoPickupEnabled = true; [Tooltip("PC 중심에서 이 거리 안에 들어온 드랍을 자석 상태로 바꾼다(m).")] public float autoPickupRadius = 1.5f; [Tooltip("자석 판정 주기(초). 0 이면 매 프레임. 드랍이 많을 때 비용을 줄인다.")] public float autoPickupScanIntervalSeconds = 0.1f; [Tooltip("자석 1단계에서 위로 띄우는 높이(m · 원본 주석 코드 값 3).")] public float magnetRiseHeight = 3f; [Tooltip("자석 1단계 속도(원본 주석 코드 값 5). 2단계는 원본이 15 로 올린다.")] public float magnetRiseSpeed = 5f; // ─────────────────────────────────────────── ④ 보스/엘리트 폭발 드랍 [Header("④ 폭발 드랍 (기준서 §B: 엘리트 1~2건 · 보스 3~5건)")] [Tooltip("끄면 원본대로 처치당 1건만 추첨한다.")] public bool burstEnabled = true; [Tooltip("엘리트 처치 시 추가 드랍 최소 건수.")] public int eliteBurstMin = 1; [Tooltip("엘리트 처치 시 추가 드랍 최대 건수(포함).")] public int eliteBurstMax = 2; [Tooltip("보스 처치 시 추가 드랍 최소 건수.")] public int bossBurstMin = 3; [Tooltip("보스 처치 시 추가 드랍 최대 건수(포함).")] public int bossBurstMax = 5; [Tooltip("일반 잡몹 추가 드랍 건수(기준서 = 0 · 잡몹은 골드 100% + 백 15~25% 로 충분).")] public int normalBurstCount = 0; [Tooltip("폭발 드랍 1건 사이의 간격(초). 0 이면 같은 프레임에 한꺼번에.")] public float burstIntervalSeconds = 0.07f; [Tooltip("추가 드랍 1건마다 몹의 RandomBag 을 다시 추첨한다. 추첨이 빈손이면 아래 골드로 대체.")] public bool burstRollsBag = true; [Tooltip("백 추첨이 빈손일 때 대신 떨어뜨리는 골드 = 몹 n_DropGold × 이 비율(최소 1). 0 이면 빈손일 때 아무것도 떨어지지 않는다. " + "⚠ 이 값이 0 보다 크면 보스/엘리트 골드 수입이 건수만큼 늘어난다(경제 영향 · PD 튜닝 지점).")] [Range(0f, 1f)] public float burstGoldShare = 0.25f; [Tooltip("EffectBudget.ClusterBurst 슬롯을 잡고 폭발 드랍을 시작한다. 811e 군집 폭발과 슬롯(상한 1)을 공유하므로 기본 off.")] public bool burstUseClusterBudget = false; // ─────────────────────────────────────────── ⑤ 동시 드랍 오브젝트 상한 [Header("⑤ 동시 드랍 오브젝트 상한 (모바일 과밀 방지)")] [Tooltip("살아 있는 드랍 오브젝트가 이 수를 넘으면 폭발 드랍의 추가 건을 더 내지 않는다(원본 기본 드랍은 절대 막지 않는다). " + "0 이면 제한 없음. 참고: 원본 풀 자체가 코인 30 · 아이템 2 로 이미 상한이다(DropItemInfo.Start).")] public int maxConcurrentDrops = 24; [Tooltip("살아 있는 드랍이 이 수를 넘으면 등급 빔도 내지 않는다(EffectBudget 과 별개의 2차 방어). 0 이면 제한 없음.")] public int beamSuppressAboveDrops = 12; } }