Project_WL/Assets/WL/Combat/SlashCrescentMask.cs

202 lines
10 KiB
C#
Raw Permalink Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// SlashCrescentMask.cs — 풀링된 슬래시 이펙트의 크레센트 이미터 렌더러 on/off
//
// PD 지시 #747 (2026-09-05) 부속 컴포넌트.
//
// 왜 필요한가:
// 무기 궤적은 BladeTrail 이 리본으로 그린다. 그런데 원본 슬래시 프리팹에도 호(크레센트)
// 이미터가 들어 있어 그대로 두면 리본과 원본 호가 겹쳐 보이고, 원본 호는 여전히 궤적과
// 다른 방향을 향한다(#747 의 원증상). 그래서 크레센트만 골라 끈다.
//
// 왜 컴포넌트인가 (그냥 psr.enabled = false 로 하지 않는 이유):
// InGameInfo 는 이펙트 인스턴스를 **재사용**한다(dic_str_Effect 풀 + TurnOff_GO).
// 스폰 때마다 렌더러를 끄기만 하면 그 인스턴스는 영구히 꺼진 채로 돌아다니고,
// 설정에서 규칙을 지워도 원복되지 않는다. 그래서 최초 1회 **원본 enabled 상태**를
// 캐시해 두고 매 스폰마다 그 기준에서 다시 계산한다.
//
// 특히 원본에 이미 꺼져 있는 렌더러를 실수로 켜지 않는 것이 중요하다 — 실측상
// Blue_Slash_1 루트 · Orange_Slash_1 의 eff · Slash_fire_once 루트 등이
// m_Enabled = 0 인 비렌더 이미터다. 단순히 `enabled = !crescent` 로 쓰면 이들이 켜진다.
//
// 런타임에서만 AddComponent 로 붙인다 — 프리팹 에셋을 건드리지 않으므로
// 설정에서 규칙을 비우면 즉시 원복된다(C8).
//
// 1단계 산출물(스테이징). 2단계에 PM 지시로 Assets/WL/Combat/ 로 이동한다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat
{
/// <summary>
/// 풀링된 슬래시 이펙트 인스턴스에 붙어, 크레센트 이미터의 렌더러 on/off 를 관리한다.
/// 최초 1회 원래 enabled 상태를 기억해 두고 매 스폰마다 그 기준으로 다시 계산하므로,
/// 인스턴스가 재사용돼도 상태가 누적되지 않고 규칙을 비우면 원래대로 돌아온다.
/// </summary>
[DisallowMultipleComponent]
public sealed class SlashCrescentMask : MonoBehaviour
{
private ParticleSystemRenderer[] _renderers;
private string[] _names;
private bool[] _originalEnabled;
// PD #761 (2026-09-06) — 풀 재사용 함정 차단용.
// Apply 가 불린 프레임 번호와 최초 1회 캡처한 루트 로컬 스케일.
private int _appliedFrame = -1;
private Vector3 _originalScale = Vector3.one;
private bool _scaleCaptured;
/// <summary>캐시한 이미터 수. 검증용.</summary>
public int EmitterCount { get { return _renderers != null ? _renderers.Length : 0; } }
/// <summary>i 번째 이미터 이름. 검증용 — 2단계에 크레센트 이름을 실측할 때 쓴다.</summary>
public string GetEmitterName(int i) { return (_names != null && i >= 0 && i < _names.Length) ? _names[i] : null; }
/// <summary>i 번째 이미터가 원래 렌더링되고 있었는가. 검증용.</summary>
public bool WasEnabled(int i) { return _originalEnabled != null && i >= 0 && i < _originalEnabled.Length && _originalEnabled[i]; }
/// <summary>
/// 규칙에 맞춰 렌더러를 다시 계산한다.
/// <paramref name="rule"/> 가 null 이면 전부 원래 상태로 되돌린다.
/// </summary>
public void Apply(SlashPrefabEmitterRule rule)
{
Apply(rule, SuppressMode.CrescentOnly, null);
}
/// <summary>
/// 억제 모드까지 지정해 렌더러를 다시 계산한다 (PD 지시 #760 2-A · TRAIL §2-4).
///
/// · <see cref="SuppressMode.CrescentOnly"/> — 기존 동작. <paramref name="rule"/> 에 적힌 이미터만 끈다.
/// · <see cref="SuppressMode.All"/> — 전부 끈다(트레일 리본만 남는다).
/// · <see cref="SuppressMode.AllExceptListed"/> — <paramref name="keepNames"/> 에 든 것만 남기고 전부 끈다.
///
/// 어느 경우에도 <c>_originalEnabled</c> 를 AND 로 걸어 **원래 꺼져 있던 이미터는 켜지 않는다**.
/// 모드를 CrescentOnly 로 되돌리면 규칙이 비어 있는 한 원본 상태로 100% 복귀한다(C8 롤백 경로).
/// </summary>
public void Apply(SlashPrefabEmitterRule rule, SuppressMode mode, string[] keepNames)
{
if (_renderers == null) Capture();
_appliedFrame = Time.frameCount;
for (int i = 0; i < _renderers.Length; i++)
{
var psr = _renderers[i];
if (psr == null) continue;
bool suppress;
switch (mode)
{
case SuppressMode.All:
suppress = true;
break;
case SuppressMode.AllExceptListed:
suppress = !Contains(keepNames, _names[i]);
break;
default:
suppress = IsCrescent(_names[i], rule);
break;
}
// 원래 꺼져 있던 것은 계속 꺼 둔다. 켜져 있던 것만 규칙에 따라 끈다.
psr.enabled = _originalEnabled[i] && !suppress;
}
}
/// <summary>
/// 🔴 풀 재사용 함정 차단 (PD #761 · PM 실측 판정 ⓕ-B).
///
/// <see cref="Apply"/> 는 불릴 때만 이미터 상태를 갱신한다. 그래서 억제된 채 풀에 남은
/// 인스턴스가 **다른 경로**(일반 Show_Effect·스킬·몬스터 이펙트)로 재생되면 억제 상태가
/// 그대로 따라간다 — 설정을 되돌려도 그 인스턴스만 계속 꺼진 채 돌아다닌다.
///
/// InGameInfo.Show_EffectEx 는 <c>actSpawned</c>(= Apply)를 <c>SetActive(true)</c> **직전**에
/// 부르므로 두 호출은 같은 프레임이다. 따라서 "이번 활성화 프레임에 Apply 가 없었다"면
/// 다른 경로로 켜진 것이고, 이때는 반드시 원본 상태(렌더러 enabled + 루트 스케일)로 되돌린다.
/// </summary>
private void OnEnable()
{
if (_appliedFrame == Time.frameCount) return; // 이번 활성화는 Apply 가 세팅했다
RestoreOriginal();
}
/// <summary>렌더러 enabled 와 루트 로컬 스케일을 최초 캡처 상태로 되돌린다.</summary>
public void RestoreOriginal()
{
if (_renderers != null)
{
for (int i = 0; i < _renderers.Length; i++)
if (_renderers[i] != null) _renderers[i].enabled = _originalEnabled[i];
}
// PrefabArc 는 루트 스케일을 검날 길이에 맞춰 바꾼다. 풀에 그 스케일이 남으면
// 모드를 되돌린 뒤에도 그 인스턴스만 커진 채로 나온다 — 여기서 함께 되돌린다.
if (_scaleCaptured) transform.localScale = _originalScale;
}
/// <summary>PrefabArc 가 스케일을 바꾸기 직전에 부른다(최초 1회만 원본을 기억한다).</summary>
public void CaptureScale()
{
if (_scaleCaptured) return;
_originalScale = transform.localScale;
_scaleCaptured = true;
}
/// <summary>이번 프레임에 배치가 적용됐음을 표시한다(PrefabArc 는 억제를 쓰지 않는다).</summary>
public void MarkApplied() { _appliedFrame = Time.frameCount; }
/// <summary>런타임 검증용 — 현재 렌더러 on/off 상태를 "이름=0/1" 로 덤프한다(#760 판정 ⓓ).</summary>
public string DumpState()
{
if (_renderers == null) Capture();
var sb = new System.Text.StringBuilder();
for (int i = 0; i < _renderers.Length; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(_names[i]).Append('=')
.Append(_renderers[i] != null && _renderers[i].enabled ? '1' : '0')
.Append(_originalEnabled[i] ? "" : "(orig off)");
}
return sb.ToString();
}
private static bool Contains(string[] names, string emitterName)
{
if (names == null) return false;
for (int i = 0; i < names.Length; i++)
if (names[i] == emitterName) return true;
return false;
}
/// <summary>최초 1회 — 이미터 목록과 원본 enabled 상태를 캐시한다.</summary>
private void Capture()
{
var systems = GetComponentsInChildren<ParticleSystem>(true);
var list = new List<ParticleSystemRenderer>(systems.Length);
var names = new List<string>(systems.Length);
for (int i = 0; i < systems.Length; i++)
{
var psr = systems[i].GetComponent<ParticleSystemRenderer>();
if (psr == null) continue;
list.Add(psr);
names.Add(systems[i].gameObject.name);
}
_renderers = list.ToArray();
_names = names.ToArray();
_originalEnabled = new bool[_renderers.Length];
for (int i = 0; i < _renderers.Length; i++)
_originalEnabled[i] = _renderers[i] != null && _renderers[i].enabled;
}
private static bool IsCrescent(string emitterName, SlashPrefabEmitterRule rule)
{
if (rule == null || rule.crescentEmitterNames == null) return false;
for (int i = 0; i < rule.crescentEmitterNames.Length; i++)
if (rule.crescentEmitterNames[i] == emitterName) return true;
return false;
}
}
}