223 lines
13 KiB
C#
223 lines
13 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLEffectScaleSettings.cs — 원본 풀 스폰 이펙트(상태이상·히트) 크기 값의 단일 출처(SOT · C45)
|
||
//
|
||
// PD 지시 #813 추가(2026-09-10) 「상태이상 효과(스턴) 크기 및 플레이어 스킬이 몬스터·플레이어 크기에 비해
|
||
// 비정상적으로 큼 → 캐릭터/몬스터 크기를 고려해 1차로 조절」 · 발주서 WL-811s §1-3
|
||
//
|
||
// ■ 왜 필요한가 (전부 실측 · `AgentScripts/staging/WL811s/MEASURE.txt`)
|
||
// 액터 실높이 = PC(Ai01 × ClassConfig.f_Scale 0.7) **1.191 m** · 존 잡몹 **0.54~0.73 m** ·
|
||
// 엘리트(× 1.3) 1.22~2.07 m · 보스 Anubis(× 1.5) **3.13 m**
|
||
// 이펙트 원 지름(프리팹 인스턴스 → ParticleSystem.Simulate t 스윕 → 파티클 실좌표 90 백분위) =
|
||
// Effect_Stun **5.58 m**(PC 높이의 4.7배 · 잡몹 높이의 10배) · Effect_Hit_10501 8.84 m ·
|
||
// Effect_Barrier 22.0 m … → PD 지적 그대로 「대상보다 이펙트가 크다」가 수치로 확인된다.
|
||
//
|
||
// ■ 규칙(Lead 제안 · 이 에셋은 그 규칙을 **상한**으로 적용한다 = 하향만 · 이미 작은 이펙트는 그대로)
|
||
// 목표 최대 지름 = 기준 높이 × 계수 (상태이상 0.6 · 착탄 0.8) → 배율 = 목표 지름 ÷ 프리팹 원 지름
|
||
// 보스 **패턴**(Effect_*Charging · Effect_Anubis_Skill1End 등)은 규칙 없음 = 원본 100 %(발주서 §1-1).
|
||
//
|
||
// ■ 어떻게 (813m3 NullMaterialGuard 의 「원본 이펙트 풀 루트 스윕」 방식 그대로 · 원본 훅 0)
|
||
// 상태이상·히트 이펙트는 전부 원본 `InGameInfo.Show_Effect` → `dic_str_Effect` 풀이 스폰한다(WL 코드가 아니다).
|
||
// 그래서 스폰 지점 훅 대신 **풀 루트(`InGameInfo.tf_Effects`)의 직속 자식**을 LateUpdate 에서 훑어
|
||
// 활성화된 인스턴스에 `localScale = 원 스케일 × 배율` 을 **대입**(누적 곱이 아니라 대입 = 멱등)하고,
|
||
// 원본 `TurnOff_GO.Off_Imm` 이 `SetActive(false)` 로 반납하면 원 스케일로 되돌린다.
|
||
//
|
||
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
|
||
// C8 롤백: 에셋이 없거나 enabled = false → 러너도 스윕도 없고 적용분은 전부 원복 = 원본 100 %.
|
||
//
|
||
// 🔴 원본 프리팹·머티리얼은 고치지 않는다(발주서 §2 금지). 런타임 인스턴스의 localScale 만 만진다.
|
||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Reaction
|
||
{
|
||
/// <summary>인스턴스 이름(끝의 "(Clone)" 제외)과 규칙 문자열을 어떻게 맞추나.</summary>
|
||
public enum EffectScaleMatch
|
||
{
|
||
/// <summary>이름 전체가 같다(기본).</summary>
|
||
Exact = 0,
|
||
/// <summary>이름이 이 문자열로 시작한다(계열 일괄 규칙).</summary>
|
||
Prefix = 1,
|
||
/// <summary>이름 안에 이 문자열이 있다.</summary>
|
||
Contains = 2
|
||
}
|
||
|
||
[Serializable]
|
||
public struct EffectScaleRule
|
||
{
|
||
[Tooltip("프리팹 이름(Assets/Res_Addr/Effect/<이름>.prefab). 인스턴스는 '<이름>(Clone)' 이다.")]
|
||
public string prefab;
|
||
|
||
[Tooltip("매칭 방식. 규칙은 배열 순서대로 검사해 처음 맞는 것을 쓴다(Exact 를 앞에, Prefix 를 뒤에).")]
|
||
public EffectScaleMatch match;
|
||
|
||
[Tooltip("고정 배율(프리팹 원 localScale × 이 값). 1 = 원본 그대로 · 0 이하 = 이 규칙 무시.")]
|
||
public float scale;
|
||
|
||
[Tooltip("scaleByTargetHeight 가 켜졌고 대상 액터를 찾았을 때 targetRatio·baseDiameter 로 배율을 다시 계산한다.")]
|
||
public bool useTargetHeight;
|
||
|
||
[Tooltip("대상 높이 × 이 계수 = 목표 최대 지름(m). Lead 규칙: 상태이상 0.6 · 착탄 0.8.")]
|
||
public float targetRatio;
|
||
|
||
[Tooltip("프리팹 원 지름 실측(m · 파티클 실좌표 90 백분위 · MEASURE.txt [B]). 0 이면 높이 기반 계산을 못 한다.")]
|
||
public float baseDiameter;
|
||
|
||
[Tooltip("표시용 메모(데이터 · 코드가 읽지 않는다).")]
|
||
public string note;
|
||
}
|
||
|
||
[CreateAssetMenu(fileName = "WLEffectScaleSettings", menuName = "WL/Effect Scale Guard Settings", order = 327)]
|
||
public sealed class WLEffectScaleSettings : ScriptableObject
|
||
{
|
||
public const string ResourcesPath = "WL/WLEffectScaleSettings";
|
||
|
||
static WLEffectScaleSettings s_cached;
|
||
static bool s_lookupDone;
|
||
|
||
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
|
||
public static WLEffectScaleSettings Instance
|
||
{
|
||
get
|
||
{
|
||
if (!s_lookupDone) { s_cached = Resources.Load<WLEffectScaleSettings>(ResourcesPath); s_lookupDone = true; }
|
||
return s_cached;
|
||
}
|
||
}
|
||
|
||
/// <summary>QA·A/B 전용 런타임 스위치. 0 = 에셋의 enabled · 1 = 강제 on · -1 = 강제 off.</summary>
|
||
public static int RuntimeOverride;
|
||
|
||
/// <summary>가드가 살아 있는가. false 면 스윕도 러너도 없다(C8).</summary>
|
||
public static bool Enabled
|
||
{
|
||
get
|
||
{
|
||
var i = Instance;
|
||
if (i == null || RuntimeOverride < 0) return false;
|
||
return RuntimeOverride > 0 || i.enabled;
|
||
}
|
||
}
|
||
|
||
/// <summary>테스트용: 캐시를 비운다.</summary>
|
||
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||
|
||
[Header("전체 스위치")]
|
||
[Tooltip("끄면 러너도 스윕도 없고 이미 적용된 배율은 전부 원복된다 = 원본 100 %. C8 롤백 1순위.")]
|
||
public bool enabled = true;
|
||
|
||
[Tooltip("적용·원복을 Console 에 남긴다(인스턴스당 1줄 · GC 발생 · 기본 off).")]
|
||
public bool verboseLog = false;
|
||
|
||
[Header("스윕 (813m3 NullMaterialGuard 와 같은 풀 루트 스윕)")]
|
||
[Tooltip("원본 이펙트 풀 `InGameInfo.tf_Effects` 를 훑는다 — 상태이상·히트가 전부 여기다. 끄면 이 발주는 무효.")]
|
||
public bool sweepIngameEffects = true;
|
||
|
||
[Tooltip("원본 오브젝트 풀 `InGameInfo.tf_Objs` 도 훑는다(드랍·813f 등급 빔 · 기본 off).")]
|
||
public bool sweepIngameObjs = false;
|
||
|
||
[Tooltip("811i SkillSpectacle 의 Live/Pool 루트도 훑는다. 🔴 811i 는 이미 행마다 scale 을 곱하므로 " +
|
||
"켜면 이중 축소가 된다 — 기본 off 를 유지할 것.")]
|
||
public bool sweepSkillSpectacle = false;
|
||
|
||
[Tooltip("메인 PC 하위를 훑는다(813k 레벨업 폭발이 PC 에 붙는다 · 813w4 가 이미 0.5 로 낮췄으므로 기본 off).")]
|
||
public bool sweepMainPc = false;
|
||
|
||
[Tooltip("스윕 주기(초 · unscaled). 0 = 매 프레임(권장) — 활성화 프레임에 바로 적용해야 1프레임 원본 크기 깜빡임이 없다.")]
|
||
public float sweepIntervalSeconds = 0f;
|
||
|
||
[Tooltip("한 스윕에서 **처음 보는** 인스턴스를 몇 개까지 등록하나(이름 읽기 = 유일한 할당 지점). 남으면 다음 스윕.")]
|
||
public int maxNewPerSweep = 24;
|
||
|
||
[Tooltip("추적 슬롯 수(사전 할당). 넘으면 죽은 인스턴스를 정리하고, 그래도 모자라면 새 인스턴스를 건너뛴다.")]
|
||
public int trackCapacity = 512;
|
||
|
||
[Tooltip("원본 `TurnOff_GO` 가 반납(SetActive(false))할 때 원 스케일로 되돌린다. 끄면 적용된 채로 풀에 남는다(멱등이라 안전하지만 C8 이 더 깔끔해진다).")]
|
||
public bool restoreOnReturn = true;
|
||
|
||
[Header("대상 높이 기반 (옵션 · 기본 off = 아래 rules[].scale 고정 배율)")]
|
||
[Tooltip("켜면 이펙트가 붙은 대상 액터의 실높이(발밑→머리)로 배율을 다시 계산한다. " +
|
||
"대상을 못 찾으면(위치 지정 스폰 등) 고정 배율로 떨어진다.")]
|
||
public bool scaleByTargetHeight = false;
|
||
|
||
[Tooltip("고정 배율을 역산할 때 쓴 기준 높이(m). 실측 PC(Ai01 × f_Scale 0.7) = 1.191 m.")]
|
||
public float referenceHeight = 1.191f;
|
||
|
||
[Tooltip("대상 높이 기반 계산의 배율 하한(너무 작아져 안 보이는 것 방지).")]
|
||
public float minScale = 0.05f;
|
||
|
||
[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];
|
||
|
||
/// <summary>인스턴스 이름으로 규칙을 찾는다(선형 · alloc 0). 없으면 −1.</summary>
|
||
public int FindRule(string instanceName, int coreLength)
|
||
{
|
||
if (rules == null || string.IsNullOrEmpty(instanceName)) return -1;
|
||
for (int i = 0; i < rules.Length; i++)
|
||
{
|
||
if (rules[i].scale <= 0f && !rules[i].useTargetHeight) continue;
|
||
if (NameMatches(instanceName, coreLength, rules[i].prefab, rules[i].match)) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>"<이름>(Clone)" 에서 "(Clone)" 을 뺀 유효 길이(부분 문자열을 만들지 않는다 = alloc 0).</summary>
|
||
public static int CoreLength(string n)
|
||
{
|
||
if (string.IsNullOrEmpty(n)) return 0;
|
||
int len = n.Length;
|
||
if (len > 7 && string.CompareOrdinal(n, len - 7, "(Clone)", 0, 7) == 0) len -= 7;
|
||
return len;
|
||
}
|
||
|
||
/// <summary>부분 문자열 생성 없이 이름을 맞춘다(alloc 0).</summary>
|
||
public static bool NameMatches(string n, int coreLength, string pattern, EffectScaleMatch match)
|
||
{
|
||
if (string.IsNullOrEmpty(pattern) || coreLength <= 0) return false;
|
||
int pl = pattern.Length;
|
||
if (pl > coreLength) return false;
|
||
switch (match)
|
||
{
|
||
case EffectScaleMatch.Exact:
|
||
return pl == coreLength && string.CompareOrdinal(n, 0, pattern, 0, pl) == 0;
|
||
case EffectScaleMatch.Prefix:
|
||
return string.CompareOrdinal(n, 0, pattern, 0, pl) == 0;
|
||
default:
|
||
for (int i = 0; i + pl <= coreLength; i++)
|
||
if (string.CompareOrdinal(n, i, pattern, 0, pl) == 0) return true;
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|