Project_WL/Assets/WL/Combat/CombatAnimMapping.cs

133 lines
6.6 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
// ─────────────────────────────────────────────────────────────────────────────
// CombatAnimMapping.cs — 클래스 ↔ 전투 애니메이션 세트 매핑 데이터 (ScriptableObject)
//
// PD 지시 #760 (2026-09-06) 1단계 산출물 · **스테이징**.
// 2단계에 PM 지시로 Assets/WL/Combat/ 로 옮기고 에셋(CombatAnimMapping.asset)을 만든다.
//
// 왜 ScriptableObject 인가 (C45 하드코딩 금지):
// "어떤 클래스가 어떤 Shinabro 세트를 쓰는가" · "이벤트를 몇 초에 심는가" 는
// 설계 판단이지 코드 상수가 아니다. 세트 교체·타이밍 보정이 코드 재컴파일 없이
// 되어야 라이브에서 감당된다. 값의 SOT 는 이 에셋 하나다.
//
// 왜 클립 GUID 가 아니라 에셋 경로인가:
// 에디터 전용 셋업 스크립트(WL_CombatAnimSetup)만 이 데이터를 읽는다.
// 런타임은 이 에셋을 참조하지 않는다(컨트롤러가 이미 클립을 물고 있다).
// 경로는 사람이 읽고 검증할 수 있고, 이동 시 즉시 에러로 드러난다.
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat
{
/// <summary>애니메이션 이벤트 1개. 시간은 **클립 로컬 초**(정규화 아님 — Unity AnimationEvent 와 같은 단위).</summary>
[Serializable]
public sealed class AnimEventSpec
{
[Tooltip("함수명. Actor 계열의 public 메서드여야 한다: Projectile / ShowEffect / LookAtTarget / Move / Hit / SkillEvent / FootL / FootR")]
public string functionName = "Projectile";
[Tooltip("클립 시작 기준 초. 0 이상 · 클립 길이 미만.")]
public float time;
[Tooltip("정규화 값으로 지정하고 싶을 때 켠다. 켜면 time 을 무시하고 normalizedTime × 클립길이 로 계산한다.")]
public bool useNormalized;
[Range(0f, 1f)] public float normalizedTime;
[Tooltip("stringParameter. {CID} 는 대상 클래스 ID 로 치환된다. 예: Projectile_{CID} · Effect_Slash_{CID}_1")]
public string stringParameter = "";
public int intParameter;
public float floatParameter;
/// <summary>{CID} 치환. 매핑 데이터를 클래스마다 복사하지 않기 위한 최소한의 템플릿이다.</summary>
public string ResolveString(int classId)
{
return string.IsNullOrEmpty(stringParameter)
? string.Empty
: stringParameter.Replace("{CID}", classId.ToString());
}
public float ResolveTime(float clipLength)
{
return useNormalized ? normalizedTime * clipLength : time;
}
}
/// <summary>한 컨트롤러 상태에 물릴 클립 1개와 그 클립에 심을 이벤트들.</summary>
[Serializable]
public sealed class StateClipSpec
{
[Tooltip("AnimatorController 의 상태 이름. 실측 규약: attack / Attack2 / Attack3 / Bow_Attack2 / Sword&Shield_Attack2 …")]
public string stateName = "attack";
[Tooltip("소스 FBX 에셋 경로 (Assets/ 부터). 예: Assets/Res_Addr/Animations/Animation/Paladin/attack.FBX")]
public string sourceFbxPath = "";
[Tooltip("그 FBX 안에서 만들/쓸 서브클립 이름. {CID} 치환 가능. " +
"예: Sword&Shield_Attack1_{CID}. 비우면 sourceClipName 을 그대로 쓴다(이벤트 편집 없음).")]
public string subClipName = "";
[Tooltip("서브클립을 뜰 원본 클립 이름(임포터의 기존 clipAnimations 중 하나). " +
"비우면 FBX 의 첫 클립을 쓴다.")]
public string sourceClipName = "";
[Tooltip("이 상태로 들어오는 전이의 exitTime 을 이 값으로 맞춘다. 0 이면 건드리지 않는다. " +
"실측: 힘민지 기존 콤보 구간 0.70~0.84 · 구 WL 규칙 0.78")]
[Range(0f, 1f)] public float incomingExitTime;
[Tooltip("새로 만들 상태인가. true 면 컨트롤러에 상태를 추가하고 직전 상태에서 전이를 잇는다.")]
public bool createIfMissing;
public List<AnimEventSpec> events = new List<AnimEventSpec>();
}
/// <summary>클래스 1개의 전투 애니메이션 매핑.</summary>
[Serializable]
public sealed class ClassCombatAnimSpec
{
[Tooltip("ClassConfig.json 의 n_ClassID")]
public int classId;
[Tooltip("사람이 읽기 위한 메모 (예: OneHand → 01_Sword&Shield)")]
public string note = "";
[Tooltip("적용 대상 컨트롤러 경로. 비우면 ClassConfig 의 s_AnimationController 를 읽는다(권장 — SOT 이중화 방지).")]
public string controllerPathOverride = "";
[Tooltip("끄면 이 클래스는 건드리지 않는다. 이미 WL 과 같은 세트인 클래스는 꺼 둔다.")]
public bool enabled = true;
public List<StateClipSpec> states = new List<StateClipSpec>();
}
/// <summary>
/// 전 클래스 전투 애니메이션 매핑 SOT.
/// 에디터 셋업 스크립트(WL_CombatAnimSetup)만 읽는다 — 런타임 참조 없음.
/// </summary>
[CreateAssetMenu(fileName = "CombatAnimMapping", menuName = "WL/Combat Anim Mapping", order = 310)]
public sealed class CombatAnimMapping : ScriptableObject
{
[Tooltip("이 매핑이 만들어진 근거 문서 (추적성 · P16)")]
[TextArea(2, 4)]
public string source = "PD 지시 #760 · AgentScripts/staging/WL_Combat2/MAPPING.md";
[Tooltip("컨트롤러 백업을 남길 폴더. 비우면 백업하지 않는다(권장하지 않음).")]
public string backupRoot = "공유/개발팀_백업/WL/Animators_{yyyyMMdd_HHmm}";
[Tooltip("모든 전이 블렌드 시간(정규화). 실측: 힘민지 전 컨트롤러 0.25 로 통일돼 있다.")]
public float transitionDuration = 0.25f;
public List<ClassCombatAnimSpec> classes = new List<ClassCombatAnimSpec>();
public ClassCombatAnimSpec Find(int classId)
{
for (int i = 0; i < classes.Count; i++)
if (classes[i] != null && classes[i].classId == classId) return classes[i];
return null;
}
}
}