953 lines
58 KiB
C#
953 lines
58 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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
|
||
{
|
||
/// <summary>클래스별로 궤적 트레일을 켤지 정하는 방식.</summary>
|
||
public enum TrailClassMode
|
||
{
|
||
/// <summary>실측값으로 자동 판정한다(공격 사거리 + 무기 메시 형상).</summary>
|
||
Auto = 0,
|
||
/// <summary>무조건 켠다.</summary>
|
||
ForceOn = 1,
|
||
/// <summary>무조건 끈다(원거리 클래스 등).</summary>
|
||
ForceOff = 2
|
||
}
|
||
|
||
/// <summary>스윙이 끝난 검끝 자리에 터뜨릴 버스트 처리 방식.</summary>
|
||
public enum BurstMode
|
||
{
|
||
/// <summary>버스트를 쓰지 않는다(트레일만).</summary>
|
||
None = 0,
|
||
/// <summary>
|
||
/// 기존 <c>InGameInfo.Show_Effect(prefab, pos, euler)</c> 를 **검끝 위치**로 호출한다.
|
||
/// 힘민지 코드를 한 줄도 고치지 않고 동작하는 경로 — 다만 프리팹의 크레센트 이미터가
|
||
/// 함께 나오므로, 크레센트를 끄려면 2단계에서 프리팹 일괄 편집이 필요하다.
|
||
/// </summary>
|
||
ReuseShowEffect = 1,
|
||
|
||
/// <summary>
|
||
/// PD 지시 #761 (2026-09-06) — 구 WL <c>SlashVfxPlayer.PlayArcFromSamples</c> 방식.
|
||
///
|
||
/// **힘민지 원본 슬래시 프리팹을 억제 없이 그대로** 쓰되, 스윙 창에서 모은 검끝 궤적으로
|
||
/// 평면(법선 n)·중심·칼끝 방향(outward)을 구해 그 평면 위에 눕혀 배치한다.
|
||
/// 크기는 검날 길이에 맞춘다.
|
||
///
|
||
/// 🔴 이 모드에서는 <see cref="SlashCrescentMask"/> 억제를 적용하지 않는다 —
|
||
/// #760 2-A 가 만든 "이미터 전부 억제 → 원본 색이 사라짐" 증상(PD #761 원문)의
|
||
/// 직접 원인이 억제였기 때문이다. 프리팹은 원본 색·원본 이미터 그대로 재생된다.
|
||
/// </summary>
|
||
PrefabArc = 2
|
||
}
|
||
|
||
/// <summary>
|
||
/// <see cref="BurstMode.PrefabArc"/> 에서 프리팹을 궤적의 어느 지점에 놓을지.
|
||
/// (크레센트 이미터의 **시각 중심**이 그 지점에 오도록 루트를 옮긴다)
|
||
/// </summary>
|
||
public enum ArcAnchor
|
||
{
|
||
/// <summary>스윙 중간 프레임의 자루(손) 위치. 구 WL 기본값.</summary>
|
||
HiltMid = 0,
|
||
/// <summary>자루와 칼끝의 중간 = 쓸고 간 면의 한가운데.</summary>
|
||
Center = 1,
|
||
/// <summary>스윙 중간 프레임의 칼끝 위치.</summary>
|
||
Tip = 2
|
||
}
|
||
|
||
/// <summary>버스트를 스윙의 어느 시점에 터뜨릴지.</summary>
|
||
public enum BurstTiming
|
||
{
|
||
/// <summary>
|
||
/// 스윙이 끝난 검끝 자리. 구 WL <c>SlashVfxPlayer.EndSwing</c> 가 실제로 구현한 방식이고
|
||
/// 타격 위치에 스파크가 남아 임팩트감이 좋다. 다만 ShowEffect 이벤트가 한 클립에
|
||
/// 2번 뜨는 쌍검 클립(10201·10202 DualBlades_Attack1)에서는 버스트가 1회로 줄어든다.
|
||
/// </summary>
|
||
OnSwingEnd = 0,
|
||
/// <summary>
|
||
/// ShowEffect 이벤트가 뜬 그 프레임의 검끝 자리. 이벤트 1개당 버스트 1개라
|
||
/// **기존 동작의 스폰 횟수가 그대로 보존된다**(쌍검 2연타 = 2회).
|
||
/// </summary>
|
||
OnEvent = 1
|
||
}
|
||
|
||
/// <summary>
|
||
/// 원본 슬래시 프리팹의 파티클 이미터를 어디까지 억제할지 (PD 지시 #760 2-A · TRAIL §2-4).
|
||
///
|
||
/// 구 WL 룩은 "텍스처 리본 + 검끝 스파크" 두 가지뿐이다. 힘민지 원본 프리팹은 그 위에
|
||
/// 글로우·그림자·먼지·연기를 얹고 있어 리본이 묻힌다. 크레센트만 끄는 기존 방식
|
||
/// (<see cref="CrescentOnly"/>)으로는 나머지가 남는다 — 그래서 "남길 것을 지정하고
|
||
/// 나머지를 끄는" 반전 모드를 둔다.
|
||
///
|
||
/// 어느 모드든 <see cref="SlashCrescentMask"/> 가 캐시한 **원본 enabled 상태**를 기준으로
|
||
/// 계산하므로, 원래 꺼져 있던 이미터를 켜는 일은 없고 모드를 되돌리면 100% 원복된다(C8).
|
||
/// </summary>
|
||
public enum SuppressMode
|
||
{
|
||
/// <summary>기존 동작 — <c>crescentRules</c> 에 지정한 이미터만 끈다. 롤백 경로.</summary>
|
||
CrescentOnly = 0,
|
||
/// <summary>루트 이하 모든 파티클 렌더러를 끈다(트레일 리본만 남는다).</summary>
|
||
All = 1,
|
||
/// <summary>
|
||
/// 전면 억제하되 <c>keepEmitterNames</c>(전역) 또는 프리팹 규칙의
|
||
/// <c>keepEmitterNames</c> 에 든 이름만 남긴다. 구 WL 룩의 기본값 — 스파크만 남긴다.
|
||
/// </summary>
|
||
AllExceptListed = 2
|
||
}
|
||
|
||
/// <summary>
|
||
/// 슬래시 프리팹 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 팩 원본에서 유래한 기존 결함이다.
|
||
/// </summary>
|
||
[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).
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>공격 클립이 휘두르기인지 찌르기인지.</summary>
|
||
public enum SwingKind
|
||
{
|
||
/// <summary>칼끝 궤적으로 자동 분류한다(기본).</summary>
|
||
Auto = 0,
|
||
/// <summary>휘두르기 — 호 이펙트를 스윙 평면에 눕힌다.</summary>
|
||
Swing = 1,
|
||
/// <summary>찌르기 — 직선 이펙트를 찌르기 축에 맞춘다.</summary>
|
||
Stab = 2
|
||
}
|
||
|
||
/// <summary>호의 반지름을 무엇에 맞출지. 균등 스케일이라 하나만 정확히 맞출 수 있다.</summary>
|
||
public enum ArcRadiusFit
|
||
{
|
||
/// <summary>바깥 가장자리 = 칼끝 반경. 눈이 먼저 읽는 쪽이라 기본값.</summary>
|
||
Outer = 0,
|
||
/// <summary>안쪽 가장자리 = 손잡이 반경.</summary>
|
||
Inner = 1,
|
||
/// <summary>두 반경의 최소자승 절충.</summary>
|
||
Both = 2
|
||
}
|
||
|
||
/// <summary>찌르기 이펙트를 칼끝 경로의 어디에 놓을지.</summary>
|
||
public enum StabAnchor
|
||
{
|
||
/// <summary>이펙트 기준점을 칼끝 출발점에 맞춘다(기본 — 이펙트가 경로를 그대로 덮는다).</summary>
|
||
Start = 0,
|
||
/// <summary>이펙트 중앙을 경로 중앙에 맞춘다.</summary>
|
||
Center = 1,
|
||
/// <summary>이펙트 앞끝을 최대 전진점에 맞춘다.</summary>
|
||
End = 2
|
||
}
|
||
|
||
/// <summary>
|
||
/// 원본 ShowEffect 프리팹 이름 → 새 이펙트 프리팹 이름 치환 1행 (PD #792 매핑 테이블).
|
||
///
|
||
/// 🔴 원본 애니메이션 이벤트와 원본 프리팹은 **한 글자도 건드리지 않는다**.
|
||
/// 이벤트가 넘겨 준 이름(Effect_Slash_{CID}_{N})을 런타임에 여기서 갈아 끼운다.
|
||
/// 이 목록을 비우면 전역 두 개(swingArcEffect / stabEffect)만 쓰고,
|
||
/// useMappedEffects 를 끄면 그 즉시 원본 이펙트로 돌아간다(C8 롤백 경로).
|
||
/// </summary>
|
||
[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 = "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 이펙트 프리팹 1종의 기하 실측 결과(에디터 베이크 산출물 · <see cref="SlashArcMeasure"/>).
|
||
///
|
||
/// 왜 굽는가 — 임포트된 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 과 일치한다(교차 검증).
|
||
/// </summary>
|
||
[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 = "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// (클래스, 클립)별 스윙 프레임 캘리브레이션 1행 — **캐릭터 로컬 좌표**.
|
||
///
|
||
/// 왜 필요한가 (PD #792 타이밍) — 이펙트는 원본처럼 ShowEffect 이벤트 시점에 떠야 하는데,
|
||
/// 그 시점에는 아직 스윙이 진행 중이라 평면·반지름·피벗을 알 수 없다. 그래서 조합마다
|
||
/// 스윙이 끝난 뒤 프레임을 재서 캐릭터 로컬로 저장해 두고, 다음부터는 이벤트가 뜬 그 프레임에
|
||
/// 즉시 배치한다(스폰 지연 0 프레임). 에디터 베이크가 채우면 **첫 스윙부터** 정확하다.
|
||
///
|
||
/// 캐릭터 로컬로 저장하는 이유 — #789 로 공격 중 액터가 루트모션만큼(0.1~0.3 m) 움직이고
|
||
/// 방향도 바뀐다. 월드 좌표로 저장하면 다음 공격에서 전혀 다른 자리에 뜬다.
|
||
/// 회전+이동만 쓰고 스케일은 쓰지 않는다 — 반지름·길이는 미터 단위로 따로 담는다.
|
||
/// </summary>
|
||
[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 = "";
|
||
}
|
||
|
||
/// <summary>PrefabArc 배치에 쓰는 값 묶음. 프리팹 규칙 → 전역 순으로 해석된다.</summary>
|
||
public struct ArcPlacement
|
||
{
|
||
public Vector3 eulerOffset;
|
||
public bool fitToBlade;
|
||
public float scale;
|
||
public ArcAnchor anchor;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 클래스(직업)별 오버라이드 1행. 클래스 ID·색·폭 등을 개별 지정한다.
|
||
/// 비워 두면(<see cref="TrailClassMode.Auto"/>) 실측 자동 판정을 쓴다.
|
||
/// </summary>
|
||
[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];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 무기 궤적 트레일의 단일 출처(SOT). 코드 상수 금지(C45) — 모든 값을 여기서 읽는다.
|
||
/// 에셋 경로(2단계 생성 예정): <c>Assets/Resources/WL/SlashTrailSettings.asset</c>
|
||
///
|
||
/// 아래 기본값은 **구 WL 프로토타입에서 PM 이 실측해 확정한 값**을 옮긴 것이다
|
||
/// (출처는 각 Tooltip 에 표기). 힘민지 무기·이펙트 기준 재실측은 2단계 과제다.
|
||
/// </summary>
|
||
[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;
|
||
|
||
/// <summary>
|
||
/// 설정 에셋을 가져온다(1회 로드 후 캐시). 에셋이 없으면 null 을 돌려주고,
|
||
/// 호출부는 조용히 기존(캐릭터 기준) 동작으로 되돌아간다 — 즉 에셋이 없어도
|
||
/// 게임은 지금과 똑같이 굴러간다(C8 프로덕션 보호 · 롤백 경로).
|
||
/// </summary>
|
||
public static SlashTrailSettings Instance
|
||
{
|
||
get
|
||
{
|
||
if (s_lookupDone) return s_cached;
|
||
s_lookupDone = true;
|
||
s_cached = Resources.Load<SlashTrailSettings>(ResourcesPath);
|
||
return s_cached;
|
||
}
|
||
}
|
||
|
||
/// <summary>캐시를 비운다(에디터에서 에셋을 새로 만들었을 때 · 테스트용).</summary>
|
||
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<SlashTrailClassOverride> classOverrides = new List<SlashTrailClassOverride>();
|
||
|
||
[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<SlashPrefabEmitterRule> crescentRules = new List<SlashPrefabEmitterRule>();
|
||
|
||
[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<SlashEffectRemap> effectRemaps = new List<SlashEffectRemap>();
|
||
|
||
[Tooltip("이펙트 프리팹의 기하 실측 결과(에디터 베이크 산출물). " +
|
||
"비어 있으면 런타임이 바운딩 박스 폴백으로 배치한다 — 정확도가 크게 떨어진다")]
|
||
public List<SlashEffectGeometry> effectGeometry = new List<SlashEffectGeometry>();
|
||
|
||
[Tooltip("(클래스, 클립)별 스윙 프레임 캐시. 비어 있으면 첫 스윙은 스윙이 끝난 뒤 배치되고, " +
|
||
"그때 잰 값이 런타임 캐시에 들어가 두 번째 스윙부터 이벤트 시점에 즉시 배치된다. " +
|
||
"에디터 베이크로 채우면 첫 스윙부터 정확하다")]
|
||
public List<SwingCalibration> swingCalibrations = new List<SwingCalibration>();
|
||
|
||
[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;
|
||
|
||
/// <summary>이 프리팹의 실측 기하. 없으면 null.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>(클래스, 클립) 캘리브레이션. 없으면 null.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 원본 이벤트 프리팹 이름 + 클래스 + 분류 결과 → 실제로 띄울 이펙트 이름.
|
||
/// 규칙이 하나도 없으면 전역 두 개(swingArcEffect / stabEffect)를 쓴다.
|
||
/// 클래스가 지정된 행이 클래스 무관 행보다 우선한다(더 좁은 규칙이 이긴다).
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>이 프리팹의 PrefabArc 배치 값. 규칙에 overrideArc 가 없으면 전역값을 쓴다.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>이 프리팹에서 남길 이미터 이름. 프리팹 규칙이 비어 있으면 전역 목록을 쓴다.</summary>
|
||
public string[] ResolveKeepNames(SlashPrefabEmitterRule rule)
|
||
{
|
||
if (rule != null && rule.keepEmitterNames != null && rule.keepEmitterNames.Length > 0)
|
||
return rule.keepEmitterNames;
|
||
return keepEmitterNames;
|
||
}
|
||
|
||
/// <summary>프리팹 이름에 해당하는 크레센트 억제 규칙. 없으면 null.</summary>
|
||
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;
|
||
|
||
/// <summary>클래스 ID 에 해당하는 오버라이드 행. 없으면 null.</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|