// ───────────────────────────────────────────────────────────────────────────── // SlashTrailSettings.cs — 무기 궤적(검날 트레일) 설정 단일 출처(SOT) // // PD 지시 #747 (2026-09-05): // "공격 이펙트가 플레이어 무기 궤적에 맞게 보이도록 수정해줘. // (WL 프로젝트에 구현한 방식 참고) 현재 힘민지에서는 이펙트가 공격 중인 무기에 // 맞춰진게 아니라 캐릭터 기준으로 보이도록 되어있어." // // 목적: // 조직 규칙 C45(하드 코딩 절대 금지)에 따라 궤적 이펙트가 쓰는 **모든 수치·이름·색**을 // 코드 상수가 아닌 이 ScriptableObject 에 둔다. 코드에는 어떤 게임 데이터 값도 넣지 않는다. // // 근거: // 구 WL 프로토타입 `Backup/WL_Prototype_2026-09-04/Assets_WL/Scripts/Combat/CombatSettings.cs` // 가 같은 목적의 SO 였고, 그 필드 중 **궤적에 실제로 쓰인 것만** 추려 이식했다. // (전투 수치·몬스터 AI·데미지 팝업 등 힘민지에 이미 테이블로 존재하는 항목은 제외 — C45) // // 🔴 어셈블리 주의: 힘민지 `Assets/Script/` 에는 .asmdef 가 없다(전부 Assembly-CSharp). // 이 파일이 놓일 `Assets/WL/Combat/` 에도 **.asmdef 를 만들지 말 것** — 만들면 // Actor/PCActor/InGameInfo(Assembly-CSharp)를 참조하지 못해 컴파일이 깨진다. // // 1단계 산출물(스테이징). 2단계에 PM 지시로 `Assets/WL/Combat/` 로 이동한다. // ───────────────────────────────────────────────────────────────────────────── using System; using System.Collections.Generic; using UnityEngine; namespace WL.Combat { /// 클래스별로 궤적 트레일을 켤지 정하는 방식. public enum TrailClassMode { /// 실측값으로 자동 판정한다(공격 사거리 + 무기 메시 형상). Auto = 0, /// 무조건 켠다. ForceOn = 1, /// 무조건 끈다(원거리 클래스 등). ForceOff = 2 } /// 스윙이 끝난 검끝 자리에 터뜨릴 버스트 처리 방식. public enum BurstMode { /// 버스트를 쓰지 않는다(트레일만). None = 0, /// /// 기존 InGameInfo.Show_Effect(prefab, pos, euler) 를 **검끝 위치**로 호출한다. /// 힘민지 코드를 한 줄도 고치지 않고 동작하는 경로 — 다만 프리팹의 크레센트 이미터가 /// 함께 나오므로, 크레센트를 끄려면 2단계에서 프리팹 일괄 편집이 필요하다. /// ReuseShowEffect = 1, /// /// PD 지시 #761 (2026-09-06) — 구 WL SlashVfxPlayer.PlayArcFromSamples 방식. /// /// **힘민지 원본 슬래시 프리팹을 억제 없이 그대로** 쓰되, 스윙 창에서 모은 검끝 궤적으로 /// 평면(법선 n)·중심·칼끝 방향(outward)을 구해 그 평면 위에 눕혀 배치한다. /// 크기는 검날 길이에 맞춘다. /// /// 🔴 이 모드에서는 억제를 적용하지 않는다 — /// #760 2-A 가 만든 "이미터 전부 억제 → 원본 색이 사라짐" 증상(PD #761 원문)의 /// 직접 원인이 억제였기 때문이다. 프리팹은 원본 색·원본 이미터 그대로 재생된다. /// PrefabArc = 2 } /// /// 에서 프리팹을 궤적의 어느 지점에 놓을지. /// (크레센트 이미터의 **시각 중심**이 그 지점에 오도록 루트를 옮긴다) /// public enum ArcAnchor { /// 스윙 중간 프레임의 자루(손) 위치. 구 WL 기본값. HiltMid = 0, /// 자루와 칼끝의 중간 = 쓸고 간 면의 한가운데. Center = 1, /// 스윙 중간 프레임의 칼끝 위치. Tip = 2 } /// 버스트를 스윙의 어느 시점에 터뜨릴지. public enum BurstTiming { /// /// 스윙이 끝난 검끝 자리. 구 WL SlashVfxPlayer.EndSwing 가 실제로 구현한 방식이고 /// 타격 위치에 스파크가 남아 임팩트감이 좋다. 다만 ShowEffect 이벤트가 한 클립에 /// 2번 뜨는 쌍검 클립(10201·10202 DualBlades_Attack1)에서는 버스트가 1회로 줄어든다. /// OnSwingEnd = 0, /// /// ShowEffect 이벤트가 뜬 그 프레임의 검끝 자리. 이벤트 1개당 버스트 1개라 /// **기존 동작의 스폰 횟수가 그대로 보존된다**(쌍검 2연타 = 2회). /// OnEvent = 1 } /// /// 원본 슬래시 프리팹의 파티클 이미터를 어디까지 억제할지 (PD 지시 #760 2-A · TRAIL §2-4). /// /// 구 WL 룩은 "텍스처 리본 + 검끝 스파크" 두 가지뿐이다. 힘민지 원본 프리팹은 그 위에 /// 글로우·그림자·먼지·연기를 얹고 있어 리본이 묻힌다. 크레센트만 끄는 기존 방식 /// ()으로는 나머지가 남는다 — 그래서 "남길 것을 지정하고 /// 나머지를 끄는" 반전 모드를 둔다. /// /// 어느 모드든 가 캐시한 **원본 enabled 상태**를 기준으로 /// 계산하므로, 원래 꺼져 있던 이미터를 켜는 일은 없고 모드를 되돌리면 100% 원복된다(C8). /// public enum SuppressMode { /// 기존 동작 — crescentRules 에 지정한 이미터만 끈다. 롤백 경로. CrescentOnly = 0, /// 루트 이하 모든 파티클 렌더러를 끈다(트레일 리본만 남는다). All = 1, /// /// 전면 억제하되 keepEmitterNames(전역) 또는 프리팹 규칙의 /// keepEmitterNames 에 든 이름만 남긴다. 구 WL 룩의 기본값 — 스파크만 남긴다. /// AllExceptListed = 2 } /// /// 슬래시 프리팹 1종의 크레센트(호 본체) 이미터 억제 규칙. /// /// 실측(2026-09-05) — Effect_Slash_* 20종은 **원본 프리팹 5종을 공유**한다: /// Blue_Slash_1 (10101_1·2) 크레센트 = slash_L01 ×2 · slash01_blue · flare02_blue /// Blue_Sword (10101_3·10102_3·4·10501_3) 크레센트 = Center_flash · "Center_flash (1)" /// ※ 이 계열엔 이름에 slash 가 든 이미터가 하나도 없다 /// Orange_Slash_1 (10102_1·2) 크레센트 = "slash_a (1)" · "slash_a (2)" · "slash_b (1)" /// ※ 전부 Billboard — "Mesh = 크레센트" 휴리스틱이 통하지 않는다 /// BlueSlash (10201_1·2·3) 크레센트 = BlueSlash /// 인라인 10202_1~4 크레센트 = OrangeSlash (·"OrangeSlash (1)") /// Lana 10501_1·2 / 10502_1·2 크레센트 후보 = glow · shadow (텍스처 없음 — 아래 주의) /// HCFX_Dust_04 (10502_3) 크레센트 **0개** (순수 먼지 버스트) /// /// 🔴 미확인 / PM·PD 결정 필요: /// Lana 계열 4종이 쓰는 1AB_mat·1Add_mat 은 **텍스처가 한 장도 물려 있지 않다**(전 슬롯 fileID 0). /// 호 형상을 렌더할 수 없는 상태이며 이는 Lana 팩 원본에서 유래한 기존 결함이다. /// [Serializable] public sealed class SlashPrefabEmitterRule { [Tooltip("대상 슬래시 프리팹 이름 (예: Effect_Slash_10502_1). ShowEffect 이벤트의 stringParameter 와 같다")] public string prefabName; [Tooltip("이 프리팹에서 렌더러를 끌 이미터 GameObject 이름. 정확히 일치해야 한다(공백·괄호 포함)")] public string[] crescentEmitterNames = new string[0]; [Tooltip("SuppressMode.AllExceptListed 일 때 이 프리팹에서만 남길 이미터 이름. " + "비우면 전역 keepEmitterNames 를 쓴다. 프리팹마다 스파크 이미터 이름이 다르면 여기에 적는다(C45)")] public string[] keepEmitterNames = new string[0]; // ── PD #761 PrefabArc 전용 (프리팹별 배치 보정) ───────────────────────── [Tooltip("체크하면 아래 arc* 값을 이 프리팹에만 적용한다. 끄면 설정 에셋의 전역 arc* 값을 쓴다")] public bool overrideArc; [Tooltip("궤적 평면 프레임(Z=평면 법선 · Y=칼끝 방향) 기준의 추가 회전(도). " + "프리팹마다 크레센트가 그려지는 로컬 축 규약이 달라 자동 정렬이 뒤집히거나 " + "90도 돌아가는 경우를 캡처 실측으로 보정한다. 0 = 자동 정렬 그대로")] public Vector3 arcEulerOffset = Vector3.zero; [Tooltip("체크하면 크레센트의 시각 지름이 검날 길이 × 2 가 되도록 스케일을 맞춘다(구 WL slashFitToBlade). " + "끄면 arcScale 을 그대로 로컬 스케일로 쓴다")] public bool arcFitToBlade = true; [Tooltip("최종 스케일 배수. arcFitToBlade 가 켜져 있으면 맞춘 크기에 이 값을 곱한다")] public float arcScale = 1f; [Tooltip("크레센트의 시각 중심을 궤적의 어느 지점에 놓을지")] public ArcAnchor arcAnchor = ArcAnchor.HiltMid; } // ═════════════════════════════════════════════════════════════════════════ // PD 지시 #792 (2026-09-07) — 무기 스윙에 "딱 맞게" 배치 // // ① "무기 궤적을 그대로 따라가야 한다"가 아니라 **무기를 휘두르는 방향에 맞게 // 이펙트의 각도·위치·크기를 무기에 딱 맞게** 배치해 자연스럽게 보이게 할 것. // ② 휘두르기 = NamuFX `Slash_B_recolor_1` · 찌르기 = EricWang `FX_Blue Stab`. // // 아래 타입들은 그 배치에 필요한 **데이터**를 담는다 — 코드에는 값을 넣지 않는다(C45). // ═════════════════════════════════════════════════════════════════════════ /// 공격 클립이 휘두르기인지 찌르기인지. public enum SwingKind { /// 칼끝 궤적으로 자동 분류한다(기본). Auto = 0, /// 휘두르기 — 호 이펙트를 스윙 평면에 눕힌다. Swing = 1, /// 찌르기 — 직선 이펙트를 찌르기 축에 맞춘다. Stab = 2 } /// 호의 반지름을 무엇에 맞출지. 균등 스케일이라 하나만 정확히 맞출 수 있다. public enum ArcRadiusFit { /// 바깥 가장자리 = 칼끝 반경. 눈이 먼저 읽는 쪽이라 기본값. Outer = 0, /// 안쪽 가장자리 = 손잡이 반경. Inner = 1, /// 두 반경의 최소자승 절충. Both = 2 } /// 찌르기 이펙트를 칼끝 경로의 어디에 놓을지. public enum StabAnchor { /// 이펙트 기준점을 칼끝 출발점에 맞춘다(기본 — 이펙트가 경로를 그대로 덮는다). Start = 0, /// 이펙트 중앙을 경로 중앙에 맞춘다. Center = 1, /// 이펙트 앞끝을 최대 전진점에 맞춘다. End = 2 } /// /// 원본 ShowEffect 프리팹 이름 → 새 이펙트 프리팹 이름 치환 1행 (PD #792 매핑 테이블). /// /// 🔴 원본 애니메이션 이벤트와 원본 프리팹은 **한 글자도 건드리지 않는다**. /// 이벤트가 넘겨 준 이름(Effect_Slash_{CID}_{N})을 런타임에 여기서 갈아 끼운다. /// 이 목록을 비우면 전역 두 개(swingArcEffect / stabEffect)만 쓰고, /// useMappedEffects 를 끄면 그 즉시 원본 이펙트로 돌아간다(C8 롤백 경로). /// [Serializable] public sealed class SlashEffectRemap { [Tooltip("원본 이벤트의 stringParameter. 예: Effect_Slash_10501_1. 비우면 이 행은 무시된다")] public string sourcePrefab; [Tooltip("이 행을 적용할 클래스 ID. 0 이면 클래스와 무관하게 적용")] public int classId; [Tooltip("휘두르기/찌르기 강제 지정. Auto 면 칼끝 궤적으로 자동 분류한다")] public SwingKind kind = SwingKind.Auto; [Tooltip("휘두르기로 판정됐을 때 쓸 이펙트 프리팹 이름(Assets/Res_Addr/Effect/ 기준). " + "비우면 전역 swingArcEffect 를 쓴다. 클래스별 색 변형은 여기에 지정한다")] public string swingEffect = ""; [Tooltip("찌르기로 판정됐을 때 쓸 이펙트 프리팹 이름. 비우면 전역 stabEffect 를 쓴다")] public string stabEffect = ""; } /// /// 이펙트 프리팹 1종의 기하 실측 결과(에디터 베이크 산출물 · ). /// /// 왜 굽는가 — 임포트된 FBX 는 Read/Write 가 꺼져 있는 것이 기본이라 /// **빌드된 플레이어에서 Mesh.vertices 가 실패**한다(에디터에서만 우연히 통한다). /// 그래서 정점 기반 정밀 실측은 에디터에서 한 번만 하고 결과를 여기에 데이터로 남긴다 /// (C45 — 값의 SOT 는 SO 하나). 런타임은 읽기만 하고, 없으면 바운딩 박스 폴백으로 떨어진다. /// /// 실측값(2026-09-07 · Effect_WLSwingArc ← NamuFX Slash_B_recolor_1): /// normal (0,1,0) · center (0,0,0) · bisector (0,0,-1) · r_out 1.2200 · r_in 0.5733 · sweep 226.7° /// 안/바깥 비 0.470 은 구 WL 이 독립적으로 실측한 0.47/1.0 과 일치한다(교차 검증). /// [Serializable] public sealed class SlashEffectGeometry { [Tooltip("대상 이펙트 프리팹 이름(Assets/Res_Addr/Effect/ 기준)")] public string prefabName; [Tooltip("이 프리팹을 호(휘두르기)로 쓸지 직선(찌르기)으로 쓸지")] public SwingKind kind = SwingKind.Swing; [Tooltip("호 실측이 유효한가(베이크 완료 여부)")] public bool arcValid; [Tooltip("실측에 쓴 이미터 이름 / 메모(추적성)")] public string emitter = ""; [Tooltip("호가 놓인 평면의 법선(루트 로컬 · 시작→끝이 이 축에서 CCW)")] public Vector3 arcNormal = Vector3.forward; [Tooltip("호가 그리는 원의 중심(루트 로컬 · 스케일 1)")] public Vector3 arcCenter = Vector3.zero; [Tooltip("원 중심 → 호 중앙(볼록 방향 · 단위)")] public Vector3 arcBisector = Vector3.up; [Tooltip("원 중심 → 호 시작(단위 · 검증용)")] public Vector3 arcStartDir = Vector3.right; [Tooltip("호 바깥 반지름(m · 루트 스케일 1)")] public float arcRadiusOuter = 1f; [Tooltip("호 안쪽 반지름(m · 루트 스케일 1)")] public float arcRadiusInner = 0.47f; [Tooltip("호가 덮는 각도(도)")] public float arcSweepDeg = 180f; [Tooltip("베이크 결과 호의 진행 방향이 뒤집혀 보이면 켠다(캡처로 확정). 재베이크 시 반영된다")] public bool flipSweep; [Tooltip("찌르기 실측이 유효한가")] public bool stabValid; [Tooltip("찌르기 진행 축(루트 로컬 · 꼬리 → 칼끝)")] public Vector3 stabAxis = Vector3.forward; [Tooltip("부착 기준점 = 프리팹 루트를 축에 투영한 지점(루트 로컬 · 스케일 1)")] public Vector3 stabStart = Vector3.zero; [Tooltip("기준점에서 앞끝까지의 도달 거리(m · 루트 스케일 1)")] public float stabLength = 1f; [Tooltip("이펙트 면의 법선(루트 로컬). 카메라를 향하게 롤을 맞출 때 쓴다")] public Vector3 stabPlaneNormal = Vector3.up; [Tooltip("베이크 결과 축이 반대로 잡히면 켠다(캡처로 확정)")] public bool flipAxis; [Tooltip("베이크 시각(추적성)")] public string bakedAt = ""; } /// /// (클래스, 클립)별 스윙 프레임 캘리브레이션 1행 — **캐릭터 로컬 좌표**. /// /// 왜 필요한가 (PD #792 타이밍) — 이펙트는 원본처럼 ShowEffect 이벤트 시점에 떠야 하는데, /// 그 시점에는 아직 스윙이 진행 중이라 평면·반지름·피벗을 알 수 없다. 그래서 조합마다 /// 스윙이 끝난 뒤 프레임을 재서 캐릭터 로컬로 저장해 두고, 다음부터는 이벤트가 뜬 그 프레임에 /// 즉시 배치한다(스폰 지연 0 프레임). 에디터 베이크가 채우면 **첫 스윙부터** 정확하다. /// /// 캐릭터 로컬로 저장하는 이유 — #789 로 공격 중 액터가 루트모션만큼(0.1~0.3 m) 움직이고 /// 방향도 바뀐다. 월드 좌표로 저장하면 다음 공격에서 전혀 다른 자리에 뜬다. /// 회전+이동만 쓰고 스케일은 쓰지 않는다 — 반지름·길이는 미터 단위로 따로 담는다. /// [Serializable] public sealed class SwingCalibration { [Tooltip("table_classconfig 의 n_ClassID")] public int classId; [Tooltip("ShowEffect 이벤트의 stringParameter. 예: Effect_Slash_10501_1.\n" + "실측(2026-09-07) — 서브클립 Attack{N}_S_{CID} 는 Effect_Slash_{CID}_{N} 을 1:1 로 갖는다. " + "즉 이 문자열 하나가 (클래스, 클립) 조합을 유일하게 식별하고, 이벤트가 뜬 그 순간에 " + "바로 손에 들어온다 — 애니메이터 상태를 뒤져 클립 이름을 알아낼 필요가 없다.\n" + "🔴 한 클립에 ShowEffect 가 2번 있는 쌍검 클립(10201·10202)은 두 이벤트가 같은 키를 쓴다.")] public string clipName; [Tooltip("이 클립의 분류 결과")] public SwingKind kind = SwingKind.Swing; [Tooltip("스윙 평면 법선(캐릭터 로컬 · 시작→끝이 이 축에서 CCW)")] public Vector3 planeNormal = Vector3.right; [Tooltip("스윙 피벗 = 칼끝 궤적의 원 중심(캐릭터 로컬)")] public Vector3 pivot = Vector3.zero; [Tooltip("피벗 → 스윙 중앙(볼록 방향 · 캐릭터 로컬 · 단위)")] public Vector3 bisector = Vector3.forward; [Tooltip("피벗 → 스윙 시작(캐릭터 로컬 · 단위 · 검증용)")] public Vector3 startDir = Vector3.forward; [Tooltip("칼끝 반경(m)")] public float tipRadius = 1f; [Tooltip("손잡이 반경(m)")] public float hiltRadius = 0.3f; [Tooltip("스윙이 덮은 각도(도)")] public float sweepDeg; [Tooltip("찌르기 축(캐릭터 로컬 · 단위)")] public Vector3 stabAxis = Vector3.forward; [Tooltip("칼끝 출발점(캐릭터 로컬)")] public Vector3 stabStart = Vector3.zero; [Tooltip("찌르기 거리(m)")] public float stabLength = 0.5f; [Tooltip("실측 품질 = 이 캘리브레이션을 만든 스윙에서 칼끝이 지나간 경로 길이(m).\n" + "🔴 왜 필요한가(실측 2026-09-07) — 콤보가 중간에 끊기거나 다음 타로 넘어가면 스윙 창이 " + "일찍 닫혀 **잘린 스윙**이 잡힌다(칼끝 이동 0.09 m · 9 샘플). 그 값이 캐시에 한 번 들어가면 " + "영원히 그 잘린 프레임으로 배치된다 — 실제로 Attack2/Attack3 의 휘두르기/찌르기 분류가 " + "서로 뒤바뀌었다. 그래서 매 스윙마다 다시 재고 **더 긴 궤적이 나오면 갈아 끼운다**.")] public float quality; [Tooltip("이 행을 만든 근거(런타임 자동 실측 / 에디터 베이크 · 추적성)")] public string source = ""; } /// PrefabArc 배치에 쓰는 값 묶음. 프리팹 규칙 → 전역 순으로 해석된다. public struct ArcPlacement { public Vector3 eulerOffset; public bool fitToBlade; public float scale; public ArcAnchor anchor; } /// /// 클래스(직업)별 오버라이드 1행. 클래스 ID·색·폭 등을 개별 지정한다. /// 비워 두면() 실측 자동 판정을 쓴다. /// [Serializable] public sealed class SlashTrailClassOverride { [Tooltip("table_classconfig 의 n_ClassID (예: 10101 · 10502). 0 이면 이 행은 무시된다")] public int classId; [Tooltip("이 클래스에서 궤적 트레일을 켤지")] public TrailClassMode mode = TrailClassMode.Auto; [Tooltip("체크하면 아래 색/폭 값으로 전역 기본값을 덮어쓴다")] public bool overrideLook; [Tooltip("가산 겹 색(HDR) — overrideLook 일 때만 쓴다")] [ColorUsage(true, true)] public Color colorAdditive = Color.white; [Tooltip("알파 겹 색(HDR) — overrideLook 일 때만 쓴다")] [ColorUsage(true, true)] public Color colorAlpha = Color.white; [Tooltip("리본 폭 비율 — overrideLook 일 때만 쓴다")] [Range(0.05f, 1f)] public float widthScale = 0.53f; [Tooltip("체크하면 아래 무기 슬롯만 궤적을 그린다(양손 단검 = 양쪽 · 메이스+방패 = 오른손만)")] public bool overrideWeaponSlots; [Tooltip("궤적을 그릴 PCActor.tfs_weapon 인덱스 목록. overrideWeaponSlots 일 때만 쓴다")] public int[] weaponSlotIndices = new int[0]; } /// /// 무기 궤적 트레일의 단일 출처(SOT). 코드 상수 금지(C45) — 모든 값을 여기서 읽는다. /// 에셋 경로(2단계 생성 예정): Assets/Resources/WL/SlashTrailSettings.asset /// /// 아래 기본값은 **구 WL 프로토타입에서 PM 이 실측해 확정한 값**을 옮긴 것이다 /// (출처는 각 Tooltip 에 표기). 힘민지 무기·이펙트 기준 재실측은 2단계 과제다. /// [CreateAssetMenu(fileName = "SlashTrailSettings", menuName = "WL/Slash Trail Settings")] public sealed class SlashTrailSettings : ScriptableObject { // ── 로드 경로 ──────────────────────────────────────────────────────────── // C45 주의: 아래는 "게임 데이터 값"이 아니라 **에셋 로드 경로**다. // 힘민지도 같은 방식으로 경로를 코드에 둔다(PCActor.Change_Weapon 의 // "Assets/Res_Addr/Weapon/{0}.prefab", InGameInfo.Load_Effect 의 // "Assets/Res_Addr/Effect/{0}.prefab"). 동일 규약을 따른다. public const string ResourcesPath = "WL/SlashTrailSettings"; private static SlashTrailSettings s_cached; private static bool s_lookupDone; /// /// 설정 에셋을 가져온다(1회 로드 후 캐시). 에셋이 없으면 null 을 돌려주고, /// 호출부는 조용히 기존(캐릭터 기준) 동작으로 되돌아간다 — 즉 에셋이 없어도 /// 게임은 지금과 똑같이 굴러간다(C8 프로덕션 보호 · 롤백 경로). /// public static SlashTrailSettings Instance { get { if (s_lookupDone) return s_cached; s_lookupDone = true; s_cached = Resources.Load(ResourcesPath); return s_cached; } } /// 캐시를 비운다(에디터에서 에셋을 새로 만들었을 때 · 테스트용). public static void ClearCache() { s_cached = null; s_lookupDone = false; } // ── 마스터 스위치 ──────────────────────────────────────────────────────── [Header("■ 마스터 스위치")] [Tooltip("끄면 무기 궤적 트레일을 전부 비활성하고 기존(캐릭터 기준) 이펙트 경로를 그대로 쓴다. " + "라이브 롤백 스위치 — 코드 되돌리기 없이 원복할 수 있다(C8)")] public bool enableWeaponTrail = true; // ── 스윙 창 ────────────────────────────────────────────────────────────── [Header("■ 스윙 창 (언제부터 언제까지 궤적을 모으는가)")] [Tooltip("ShowEffect 애니메이션 이벤트가 뜬 시점부터 클립 정규화 시간으로 이만큼 동안 궤적을 모은다. " + "정규화 시간이라 공격 속도(m_animation.speed)·클립 길이와 무관하게 같은 구간을 잡는다 · 임시 0.35")] [Range(0.02f, 1f)] public float swingWindowNormalized = 0.35f; [Tooltip("안전 상한(초). 애니메이터 상태를 못 읽거나 클립이 멈춰도 이 시간이 지나면 창을 닫는다 · 임시 0.60")] public float swingMaxSeconds = 0.60f; [Tooltip("스윙 창이 열린 직후 이 시간(초) 안에는 상태 전환으로 창을 닫지 않는다. " + "콤보 전환 프레임에 창이 즉시 닫히는 것을 막는 유예 · 임시 0.03")] public float swingStateGraceSeconds = 0.03f; [Tooltip("체크하면 애니메이터 상태가 바뀌는 순간 스윙 창을 닫는다(콤보 다음 타로 넘어갈 때 꼬리를 끊는다)")] public bool endSwingOnStateChange = true; // ── 리본 형상 ──────────────────────────────────────────────────────────── [Header("■ 리본 형상")] [Tooltip("트레일이 기억하는 최대 샘플(프레임) 수 · 구 WL 실측 기본 48")] [Range(4, 256)] public int maxSamples = 48; [Tooltip("직전 샘플과 검끝이 이 거리(m) 미만이면 샘플을 버린다. 정지 중 정점이 겹쳐 " + "면이 뒤집히는 것을 막는다 · 임시 0.004")] public float minSampleDistance = 0.004f; [Tooltip("리본 폭 비율. 1 = 손잡이~칼끝 전체. " + "구 WL 실측: 원본 SlashMesh_00 안쪽/바깥 반지름 = 0.47/1.0 이므로 0.53 이 원본과 같은 두께")] [Range(0.05f, 1f)] public float widthScale = 0.53f; [Tooltip("꼬리(오래된 쪽) 폭 비율. 1 = 균일, 작을수록 꼬리가 얇아진다(원본 크레센트의 테이퍼) · 구 WL 실측 0.25")] [Range(0f, 1f)] public float tailWidthRatio = 0.25f; [Tooltip("검끝 바깥으로 리본을 더 뻗는 비율(검날 길이 대비). 0 = 칼끝에서 끝난다 · 임시 0")] [Range(0f, 0.5f)] public float tipExtendRatio = 0f; // ── #785 샘플 보간 ─────────────────────────────────────────────────────── [Header("■ #785 샘플 보간 (한 프레임에 검이 크게 움직일 때)")] [Tooltip("직전 샘플과 검끝 거리가 이 값(m)을 넘으면 사이에 중간 샘플을 끼워 각진 리본을 막는다. " + "0 = 보간하지 않는다. 손잡이는 선형, 칼날 벡터는 Slerp 라 실제 호가 복원된다")] public float maxSegmentDistance = 0.22f; [Tooltip("한 프레임에 끼울 수 있는 중간 샘플의 최대 개수. 모바일 예산상 3 이면 충분하다")] [Range(0, 4)] public int maxSubSteps = 3; // ── 페이드 ─────────────────────────────────────────────────────────────── [Header("■ 페이드")] [Tooltip("스윙이 끝난 뒤 트레일이 사라지는 시간(초) · 구 WL 실측 기본 0.18")] public float fadeSeconds = 0.18f; [Tooltip("페이드 중 알파를 그대로 유지하는 구간 비율(0~1). " + "구 WL 실측: 원본 Custom2 그라디언트가 0.51 까지 알파 1 유지 후 0 까지 감소")] [Range(0f, 0.95f)] public float fadeHoldRatio = 0.51f; // ── 머티리얼·색 ────────────────────────────────────────────────────────── [Header("■ 머티리얼 (URP · 2단계에 생성해 연결)")] [Tooltip("가산 겹 머티리얼. URP 셰이더 'Universal Render Pipeline/Particles/Unlit' + Blending Mode = Additive 를 전제한다. " + "비우면 그 겹을 그리지 않는다")] public Material materialAdditive; [Tooltip("알파 겹 머티리얼. 같은 셰이더 + Blending Mode = Alpha. 비우면 1겹(가산)만 그린다")] public Material materialAlpha; [Header("■ 색 (정점색으로 넣는다 · URP Particles/Unlit 는 정점색을 곱한다)")] [Tooltip("가산 겹 색(HDR). 구 WL NamuFX 원본 실측 피크 = (0.086, 1.553, 2.024)")] [ColorUsage(true, true)] public Color colorAdditive = new Color(0.086f, 1.553f, 2.024f, 1f); [Tooltip("알파 겹 색(HDR). 구 WL NamuFX 원본 Custom Data 2 그라디언트 시작값 실측 = (0, 0.762, 1.582)")] [ColorUsage(true, true)] public Color colorAlpha = new Color(0f, 0.762f, 1.582f, 1f); [Tooltip("정점색이 먹지 않는 셰이더를 쓸 때 켠다. MaterialPropertyBlock 으로 _BaseColor 에도 같은 색을 넣는다. " + "2단계 Play 확인 후 필요하면 켠다")] public bool alsoApplyColorViaPropertyBlock; // ── #785 그라디언트 (참고 이미지의 색 배치) ───────────────────────────── [Header("■ #785 그라디언트 (안쪽 주황/노랑 → 바깥·꼬리 파랑/보라)")] [Tooltip("켜면 아래 두 Gradient 로 정점색을 굽는다. 끄면 예전 단색(colorAdditive/colorAlpha) 경로로 돌아간다 — C8 롤백 스위치")] public bool useGradients = true; [Tooltip("잔상축 색. t=1 이 검의 현재 위치(머리), t=0 이 소멸 직전 꼬리. " + "PD 참고 이미지 기준 기본값 = 머리 주황/노랑 → 꼬리 파랑/보라")] public Gradient gradientAlongSwing = new Gradient(); [Tooltip("칼날축 색. t=0 이 손잡이(안쪽), t=1 이 칼끝(바깥). " + "알파 채널은 안/바깥 세기 조절에 쓴다(가장자리 페이드 자체는 셰이더가 한다)")] public Gradient gradientAlongBlade = new Gradient(); [Tooltip("두 그라디언트를 섞는 비율. 0 = 잔상축 색만, 1 = 칼날축 색만, 0.5 = 반반. " + "곱이 아니라 Lerp 라 채도가 죽지 않는다")] [Range(0f, 1f)] public float bladeGradientWeight = 0.5f; [Tooltip("그라디언트 색에 곱하는 밝기 배수. 가산 블렌드라 1 이상이면 글로우가 강해진다")] [Range(0f, 8f)] public float colorIntensity = 1f; // ── UV ─────────────────────────────────────────────────────────────────── [Header("■ UV")] [Tooltip("가산 겹 u 범위. 텍스처가 시트(3x3 등)면 한 칸만 쓰도록 좁힌다. " + "구 WL 실측: T_SlashSheet01 프레임 2 = 0.667~1.0. 힘민지 텍스처는 2단계 재실측 대상")] [Range(0f, 1f)] public float uvUMin = 0f; [Range(0f, 1f)] public float uvUMax = 1f; [Tooltip("가산 겹 v 범위(0 = 손잡이 쪽, 1 = 칼끝 쪽)")] [Range(0f, 1f)] public float uvVMin = 0f; [Range(0f, 1f)] public float uvVMax = 1f; [Tooltip("켜면 알파 겹은 UV 0~1 전체를 쓴다(마스크가 시트가 아니라 한 장일 때)")] public bool alphaLayerUvFull = true; // ── NamuFX 호환 채널 (힘민지에서는 기본 OFF) ──────────────────────────── [Header("■ NamuFX 호환 채널 (힘민지에는 NamuFX 가 없다 · 기본 OFF)")] [Tooltip("구 WL 의 NamuFX 셰이더(SH_Master_improved)는 색을 TEXCOORD2, 디졸브를 TEXCOORD1 에서 읽었다. " + "그런 셰이더를 쓸 때만 켠다. URP Particles/Unlit 은 정점색을 쓰므로 힘민지에서는 꺼 둔다")] public bool writeCustomDataChannels = false; [Tooltip("알파 겹 디졸브 진행량 상한(구 WL NamuFX Custom Data 1 실측 = 수명 동안 0 → 0.98). " + "writeCustomDataChannels 일 때만 의미가 있다")] [Range(0f, 1f)] public float dissolveMax = 0.98f; // ── 무기·소켓 실측 ─────────────────────────────────────────────────────── [Header("■ 무기 소켓 · 검날 실측")] [Tooltip("PCActor.tfs_weapon 에서 무기를 못 찾을 때 계층에서 이름으로 찾을 후보(위에서부터). " + "비워 두면 이름 탐색을 하지 않는다")] public string[] socketNameCandidates = new string[0]; [Tooltip("무기 메시 최장축이 이 길이(m) 미만이면 '검날 아님'으로 보고 궤적을 그리지 않는다. " + "오브(구체)·소형 장식 배제용 · 임시 0.25")] public float minBladeLength = 0.25f; [Tooltip("무기 메시의 (최장축 / 두 번째 축) 비율이 이 값 미만이면 '검날 아님'으로 본다. " + "방패(납작·넓음 ≈ 1.2)를 배제하고 검·단검·메이스(≈ 4~9)만 통과시킨다 · 임시 2.2. " + "🔴 이 기본값은 구 WL 무기 형상에서 추정한 값이다 — 힘민지 무기 실측은 2단계 과제")] public float minBladeAspect = 2.2f; [Tooltip("검날 길이를 못 재면 쓰는 대체 길이(m) · 임시 0.8")] public float bladeLengthFallback = 0.8f; [Tooltip("한 소켓에 렌더러가 여럿이면(검+장식) 메시 최장축이 가장 긴 것을 검날로 본다. " + "끄면 첫 번째 렌더러를 쓴다")] public bool pickLongestRenderer = true; // ── 근접 클래스 판정 ───────────────────────────────────────────────────── [Header("■ 근접 클래스 판정 (C45 — 코드에 클래스 ID 를 넣지 않는다)")] [Tooltip("근접으로 볼 table_classconfig 의 e_BattleType 목록. 1순위 판정 기준이다. " + "비우면 아래 f_AttackRange 임계값으로만 판정한다.\n" + "기본값 근거 — ClassConfig.json 실측(전 12행): " + "OneHand=10101·10102 / DualWeapon=10201·10202 / Shield=10501·10502 (모두 슬래시 프리팹 보유), " + "Staff=10301~10303 · Bow=10401·10402 (슬래시 프리팹 없음 → 제외), " + "TwoHand=10601 (무기 프리팹 자체가 빈칸이라 어차피 궤적을 못 그린다)")] public eBattleType[] meleeBattleTypes = new eBattleType[] { eBattleType.OneHand, eBattleType.DualWeapon, eBattleType.Shield, eBattleType.TwoHand }; [Tooltip("meleeBattleTypes 가 비었을 때만 쓰는 2순위 기준. " + "table_classconfig 의 f_AttackRange 가 이 값(m) 이하이면 근접으로 본다.\n" + "기본값 근거 — ClassConfig.json 실측: 근접 = 1.0(쌍검) / 1.5(한손·방패) · " + "원거리 = 5.0(스태프) / 6.0(활) · 양손 = 3.0. 2.0 이면 근접만 통과한다")] public float meleeMaxAttackRange = 2.0f; [Tooltip("클래스별 개별 지정. 여기 있는 클래스는 위 자동 판정보다 우선한다. " + "자동 판정이 틀리는 클래스만 넣으면 된다")] public List classOverrides = new List(); [Tooltip("체크하면 table_classconfig 의 n_WeaponLHIndex / n_WeaponRHIndex 가 가리키는 소켓만 본다. " + "끄면 PCActor.tfs_weapon 에서 자식이 있는 소켓을 전부 훑는다.\n" + "근거 — 실측상 tfs_weapon = [0]w_shield [1]w_righthand [2]w_onehand " + "[3]w_bladedancer_l [4]w_bladedancer_r [5]w_orb [6]w_archer_l 이고, " + "클래스 전환 시 이전 무기가 남아 있을 수 있어 테이블 인덱스를 쓰는 편이 정확하다")] public bool useTableWeaponIndices = true; // ── 검끝 버스트 ────────────────────────────────────────────────────────── [Header("■ 검끝 버스트 (스윙 끝 지점에 원본 이펙트를 1회)")] [Tooltip("None = 트레일만 그린다 / ReuseShowEffect = 기존 슬래시 프리팹을 **검끝 위치**로 1회 재생한다. " + "ReuseShowEffect 는 힘민지 코드를 고치지 않고 쓸 수 있는 경로다")] public BurstMode burstMode = BurstMode.ReuseShowEffect; [Tooltip("버스트를 스윙 끝에서 터뜨릴지(구 WL 구현), 이벤트마다 터뜨릴지(기존 스폰 횟수 보존).\n" + "🔴 PM/PD 결정 대기 항목 — 발주서 본문은 '스윙 시작 시 1회'라 했고 구 WL 코드는 " + "EndSwing 에서 터뜨린다. 서로 다르므로 옵션으로 두고 2단계 캡처로 고른다.\n" + "실측 근거 — 10201·10202 의 DualBlades_Attack1 클립은 ShowEffect 이벤트가 " + "2개(0.0435 / 0.1292)라, OnSwingEnd 면 버스트가 2회 → 1회로 줄어든다")] public BurstTiming burstTiming = BurstTiming.OnSwingEnd; [Tooltip("버스트를 검끝에서 스윙 진행 방향으로 이만큼(m) 밀어 낸다 · 임시 0")] public float burstForwardOffset = 0f; [Tooltip("프리팹별 크레센트(호 본체) 이미터 억제 규칙. 비워 두면 아무것도 끄지 않는다(기본 · 가장 안전).\n" + "🔴 전역 이름 목록을 쓰지 않는 이유(실측): 20개 슬래시 프리팹은 원본 5종을 공유하는데 " + "이미터 이름 규칙이 원본마다 다르고 **충돌한다**. 예컨대 'glow' 는 Blue_Slash_1·Blue_Sword " + "계열에서는 버스트(글로우)지만 Lana 계열(10501_1/2·10502_1/2)에서는 크레센트 후보다. " + "전역 목록으로 끄면 멀쩡한 버스트까지 사라진다 — 그래서 프리팹 단위로 지정한다.\n" + "이 규칙은 런타임에만 적용되며 프리팹 에셋을 건드리지 않는다(즉시 원복 가능 · C8).")] public List crescentRules = new List(); [Tooltip("원본 슬래시 프리팹의 파티클을 어디까지 끌지. " + "CrescentOnly = 기존(규칙에 적은 것만) · All = 전부 · AllExceptListed = keepEmitterNames 만 남김.\n" + "구 WL 룩(리본 + 스파크)은 AllExceptListed + keep=['sparks'] 다. CrescentOnly 로 되돌리면 즉시 원복(C8)")] public SuppressMode suppressMode = SuppressMode.CrescentOnly; [Tooltip("SuppressMode.AllExceptListed 에서 남길 이미터 GameObject 이름(전역 기본). " + "프리팹별로 다르면 crescentRules 의 keepEmitterNames 로 덮어쓴다. 정확히 일치해야 한다")] public string[] keepEmitterNames = new string[] { "sparks" }; // ── PD #761 PrefabArc (전역 기본값) ───────────────────────────────────── [Header("■ PrefabArc — 원본 프리팹을 스윙 평면에 눕혀 배치 (PD #761)")] [Tooltip("리본(BladeTrail 메시)을 그릴지. PrefabArc 로 원본 크레센트를 되살리면 리본은 " + "겹쳐 보이므로 기본 OFF. 끄더라도 궤적 샘플링은 계속하므로(평면 계산에 필요) " + "enableWeaponTrail 은 켜 둔 채로 둔다. 켜면 즉시 리본이 돌아온다(C8 롤백 경로)")] public bool drawRibbon = false; [Tooltip("켜면 프리팹의 크레센트 이미터를 실측해 그 로컬 축 규약을 자동으로 궤적 평면에 맞춘다. " + "끄면 프리팹 루트를 그대로 (Z=평면 법선 · Y=칼끝) 프레임에 놓는다(구 WL 원안).\n" + "자동 판정 = 카메라 정렬(View) 이 아닌 렌더러 중 시각 지름이 가장 큰 것을 크레센트로 본다")] public bool arcAutoOrient = true; [Tooltip("전역 추가 회전(도) — 프리팹 규칙에 overrideArc 가 없을 때 쓴다")] public Vector3 arcEulerOffset = Vector3.zero; [Tooltip("전역 — 크레센트 지름을 검날 길이 × 2 에 맞춘다(구 WL slashFitToBlade)")] public bool arcFitToBlade = true; [Tooltip("전역 — 최종 스케일 배수")] public float arcScale = 1f; [Tooltip("전역 — 크레센트 시각 중심을 놓을 지점")] public ArcAnchor arcAnchor = ArcAnchor.HiltMid; [Tooltip("PrefabArc 로 호를 잡는 데 필요한 최소 궤적 샘플 수. 이보다 적으면 " + "평면을 못 구하므로 검끝 위치 버스트로 떨어진다(구 WL 도 3)")] [Range(3, 32)] public int arcMinSamples = 3; // ── PD #777 — 이펙트가 플레이어를 따라간다 ─────────────────────────────── [Header("■ 이펙트 추종 (PD #777)")] [Tooltip("켜면 스폰된 슬래시 이펙트가 스폰 시점의 PC 상대 오프셋을 유지하며 따라온다.\n" + "끄면 지금처럼 월드에 고정된다(기존 동작 · C8 롤백 경로).\n" + "필요한 이유(실측) — PrefabArc 는 스윙이 끝난 자리에 이펙트를 월드 고정으로 두는데, " + "#762 전진(공격당 0.30/0.70/0.65 m · 0.25 s)이 캐릭터만 앞으로 보내므로 " + "이펙트 수명 동안 무기와 이펙트가 벌어진다. 부모를 바꾸지 않고 좌표만 따라가므로 " + "InGameInfo 풀 구조는 그대로다")] public bool arcFollowPlayer = true; [Tooltip("PC 의 회전까지 따라갈지. 켜면 이펙트가 PC 로컬 좌표계에 고정돼 캐릭터가 돌면 같이 돈다. " + "끄면 위치만 따라가고 스윙 평면(방향)은 그대로 유지된다 — 스윙은 이미 끝난 동작이라 " + "방향이 유지되는 편이 자연스럽다고 보고 기본은 끔")] public bool arcFollowRotation = false; [Tooltip("추종을 유지할 시간(초). 0 = 이펙트 수명 전체(풀에 반환될 때까지). " + "값을 주면 그 시간 뒤 이펙트가 그 자리에 남는다")] public float arcFollowSeconds = 0f; // ── PD #792 — NamuFX 호 + 찌르기 스프라이트를 무기에 딱 맞게 ────────── [Header("■ #792 무기 정합 배치 (NamuFX Slash_B / FX_Blue Stab)")] [Tooltip("끄면 #792 경로를 통째로 비활성하고 이전(#761 PrefabArc / #785 리본) 동작으로 돌아간다. " + "라이브 롤백 스위치 — 코드를 되돌리지 않고 원복한다(C8)")] public bool useMappedEffects = true; [Tooltip("휘두르기 클립에 쓸 이펙트 프리팹 이름(Assets/Res_Addr/Effect/ 기준). " + "PD #792 지정 = NamuFX Slash_B_recolor_1 을 감싼 Effect_WLSwingArc")] public string swingArcEffect = "Effect_WLSwingArc"; [Tooltip("찌르기 클립에 쓸 이펙트 프리팹 이름. PD #792 지정 = EricWang FX_Blue Stab 을 감싼 Effect_WLStab")] public string stabEffect = "Effect_WLStab"; [Tooltip("원본 프리팹 이름/클래스별 치환 규칙. 비우면 위 전역 두 개만 쓴다. " + "클래스별 색 변형을 쓰고 싶을 때 여기에 클래스별 프리팹을 지정한다")] public List effectRemaps = new List(); [Tooltip("이펙트 프리팹의 기하 실측 결과(에디터 베이크 산출물). " + "비어 있으면 런타임이 바운딩 박스 폴백으로 배치한다 — 정확도가 크게 떨어진다")] public List effectGeometry = new List(); [Tooltip("(클래스, 클립)별 스윙 프레임 캐시. 비어 있으면 첫 스윙은 스윙이 끝난 뒤 배치되고, " + "그때 잰 값이 런타임 캐시에 들어가 두 번째 스윙부터 이벤트 시점에 즉시 배치된다. " + "에디터 베이크로 채우면 첫 스윙부터 정확하다")] public List swingCalibrations = new List(); [Header("□ #792 휘두르기/찌르기 자동 분류 (C45 — 코드에 숫자를 넣지 않는다)")] [Tooltip("스윙 창 동안 **칼날 벡터(칼끝−손잡이)** 가 누적으로 이 각도(도) 이상 돌면 휘두르기로 본다.\n" + "🔴 칼끝 궤적의 스윕 각이나 캐릭터 전방 내적으로 가르지 않는다 — 둘 다 흔들린다(실측 2026-09-07):\n" + " · 스윕 각은 창이 잘리면 같은 클립에서 118°~485° 로 요동친다.\n" + " · 전방 내적은 스윙 도중 캐릭터가 돌면 부호가 뒤집혀 같은 클립의 분류가 뒤바뀐다.\n" + " 칼날 회전량은 캐릭터의 위치·방향과 무관한 순수 무기 회전이라 안정적이다.\n" + "기본값 근거 — 10501 실측: Attack1 434° · Attack2 424° · Attack3 67°. 150 은 그 간격 한가운데다")] public float swingMinSweepDegrees = 150f; [Tooltip("칼끝 경로 / 손잡이 경로 비. 이 값 이상이면 손은 거의 제자리인데 칼끝만 크게 돈 것이므로 " + "칼날 회전량이 작아도 휘두르기로 본다(손목 스냅 스윙 보호).\n" + "반대로 이 값 미만이면 손과 칼끝이 함께 이동한 것 — 찌르기(런지)의 특징이다")] public float swingMinTipHiltRatio = 3f; [Tooltip("칼끝 궤적의 직진도(끝점 거리 / 경로 길이)가 이 값 이상이어야 찌르기 후보다. 1 = 완전 직선")] [Range(0.5f, 1f)] public float stabMinStraightness = 0.86f; [Tooltip("칼끝 진행 방향과 캐릭터 전방의 내적이 이 값 이상이어야 찌르기로 본다")] [Range(0f, 1f)] public float stabMinForwardDot = 0.55f; [Tooltip("칼끝 전진 거리가 이 값(m) 미만이면 찌르기로 보지 않는다(제자리 동작 배제)")] public float stabMinDistance = 0.18f; [Tooltip("스윙 창 안에서 **실제로 휘두른 구간만** 남기는 속도 임계 비율. " + "한 프레임 칼끝 이동이 그 스윙 최대 이동의 이 배수 미만이면 앞뒤 꼬리로 보고 잘라 낸다.\n" + "근거(실측 2026-09-07) — Attack3_S_10501 은 타격이 2 프레임(칼끝 1.0 m)만에 끝나고 그 뒤 " + "18 프레임을 거의 정지한 채 버틴다. 창 전체를 그대로 피팅하면 정지 구간이 원피팅을 지배해 " + "반지름이 0.04 m 로 붕괴한다. 0 이면 자르지 않는다")] [Range(0f, 0.6f)] public float swingTrimSpeedRatio = 0.2f; [Header("□ #792 호(휘두르기) 정합")] [Tooltip("호의 반지름을 무엇에 맞출지. 균등 스케일이라 하나만 정확히 맞출 수 있다")] public ArcRadiusFit arcRadiusFit = ArcRadiusFit.Outer; [Tooltip("정합한 크기에 곱하는 배수. 1 = 칼끝 반경과 정확히 일치(기본 · C3-5)")] [Range(0.4f, 2.5f)] public float arcSizeScale = 1f; [Tooltip("호를 스윙 평면에서 법선 방향으로 이만큼(m) 띄운다. 0 = 평면 위. " + "무기 메시와 Z-파이팅이 보이면 아주 작은 값을 준다")] public float arcPlaneOffset = 0f; [Tooltip("호를 볼록 방향으로 이만큼(m) 밀어 낸다. 0 = 원 중심이 스윙 피벗")] public float arcRadialOffset = 0f; [Tooltip("이펙트 수명(초). 0 이면 프리팹의 파티클 duration 을 그대로 쓴다")] public float arcLifetimeSeconds = 0f; [Header("□ #792 찌르기 정합")] [Tooltip("이펙트를 칼끝 경로의 어디에 놓을지")] public StabAnchor stabAnchor = StabAnchor.Start; [Tooltip("길이 정합 배수. 1 = 찌르기 거리와 정확히 일치")] [Range(0.4f, 3f)] public float stabSizeScale = 1f; [Tooltip("켜면 이펙트의 납작한 면이 메인 카메라를 향하도록 축 둘레 롤을 맞춘다. " + "FX_Blue Stab 계열은 이미터가 전부 Local 정렬이라 스스로 카메라를 보지 않는다 — " + "끄면 캐릭터의 위쪽(transform.up)을 기준으로 롤을 잡는다")] public bool stabFaceCamera = true; [Tooltip("이펙트 수명(초). 0 이면 프리팹의 파티클 duration 을 쓴다. " + "FX_Blue Stab 루트는 duration 5 초 · looping 이라 반드시 값을 준다")] public float stabLifetimeSeconds = 0.9f; [Tooltip("찌르기 이펙트에서 남길 이미터 이름. 비우면 전부 남긴다.\n" + "실측(2026-09-07) — FX_Blue Stab 은 렌더 이미터 10개 · 고유 머티리얼 9개라 " + "모바일 예산(스윙당 추가 드로우 ≤ 3)을 크게 넘는다. 여기서 줄인다.\n" + "남길 이미터가 곧 시각 길이를 정하므로 이 목록을 바꾸면 **기하를 다시 베이크**해야 한다")] public string[] stabKeepEmitterNames = new string[0]; [Header("□ #792 추종")] [Tooltip("#792 경로에서 이펙트가 캐릭터 회전까지 따라갈지. " + "이벤트 시점(스윙 도중)에 배치하므로 캐릭터가 아직 돌고 있다 — 기본 ON. " + "레거시 경로의 arcFollowRotation 과 분리해 두어 서로 영향이 없다(C8)")] public bool mappedFollowRotation = true; /// 이 프리팹의 실측 기하. 없으면 null. public SlashEffectGeometry FindGeometry(string prefabName) { if (effectGeometry == null || string.IsNullOrEmpty(prefabName)) return null; for (int i = 0; i < effectGeometry.Count; i++) { var g = effectGeometry[i]; if (g != null && g.prefabName == prefabName) return g; } return null; } /// (클래스, 클립) 캘리브레이션. 없으면 null. public SwingCalibration FindCalibration(int classId, string clipName) { if (swingCalibrations == null || string.IsNullOrEmpty(clipName)) return null; for (int i = 0; i < swingCalibrations.Count; i++) { var c = swingCalibrations[i]; if (c != null && c.classId == classId && c.clipName == clipName) return c; } return null; } /// /// 원본 이벤트 프리팹 이름 + 클래스 + 분류 결과 → 실제로 띄울 이펙트 이름. /// 규칙이 하나도 없으면 전역 두 개(swingArcEffect / stabEffect)를 쓴다. /// 클래스가 지정된 행이 클래스 무관 행보다 우선한다(더 좁은 규칙이 이긴다). /// public string ResolveEffectName(string sourcePrefab, int classId, SwingKind kind, out SwingKind forcedKind) { forcedKind = SwingKind.Auto; string swing = swingArcEffect; string stab = stabEffect; if (effectRemaps != null) { bool hit = false; for (int pass = 0; pass < 2 && !hit; pass++) { for (int i = 0; i < effectRemaps.Count; i++) { var r = effectRemaps[i]; if (r == null || string.IsNullOrEmpty(r.sourcePrefab)) continue; if (r.sourcePrefab != sourcePrefab) continue; if (pass == 0 && r.classId != classId) continue; if (pass == 1 && r.classId != 0) continue; if (r.kind != SwingKind.Auto) forcedKind = r.kind; if (!string.IsNullOrEmpty(r.swingEffect)) swing = r.swingEffect; if (!string.IsNullOrEmpty(r.stabEffect)) stab = r.stabEffect; hit = true; break; } } } var k = forcedKind != SwingKind.Auto ? forcedKind : kind; return k == SwingKind.Stab ? stab : swing; } /// 이 프리팹의 PrefabArc 배치 값. 규칙에 overrideArc 가 없으면 전역값을 쓴다. public ArcPlacement ResolveArc(SlashPrefabEmitterRule rule) { var p = new ArcPlacement(); if (rule != null && rule.overrideArc) { p.eulerOffset = rule.arcEulerOffset; p.fitToBlade = rule.arcFitToBlade; p.scale = rule.arcScale; p.anchor = rule.arcAnchor; } else { p.eulerOffset = arcEulerOffset; p.fitToBlade = arcFitToBlade; p.scale = arcScale; p.anchor = arcAnchor; } if (p.scale <= 0.0001f) p.scale = 1f; return p; } /// 이 프리팹에서 남길 이미터 이름. 프리팹 규칙이 비어 있으면 전역 목록을 쓴다. public string[] ResolveKeepNames(SlashPrefabEmitterRule rule) { if (rule != null && rule.keepEmitterNames != null && rule.keepEmitterNames.Length > 0) return rule.keepEmitterNames; return keepEmitterNames; } /// 프리팹 이름에 해당하는 크레센트 억제 규칙. 없으면 null. public SlashPrefabEmitterRule FindCrescentRule(string prefabName) { if (crescentRules == null || string.IsNullOrEmpty(prefabName)) return null; for (int i = 0; i < crescentRules.Count; i++) { var r = crescentRules[i]; if (r != null && r.prefabName == prefabName) return r; } return null; } // ── 검증 ───────────────────────────────────────────────────────────────── [Header("■ 검증 (에디터 전용)")] [Tooltip("켜면 스윙 창 개폐·소켓 해석 결과를 콘솔에 남긴다. 에디터에서만 동작한다")] public bool verboseLog = false; [Tooltip("켜면 검끝 궤적 샘플을 씬 뷰 기즈모로 그린다. 에디터에서만 동작한다")] public bool drawGizmos = false; /// 클래스 ID 에 해당하는 오버라이드 행. 없으면 null. public SlashTrailClassOverride FindOverride(int classId) { if (classOverrides == null) return null; for (int i = 0; i < classOverrides.Count; i++) { var o = classOverrides[i]; if (o != null && o.classId != 0 && o.classId == classId) return o; } return null; } } }