Project_WL/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs

223 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}
}