// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// 애니메이션 이벤트 1개. 시간은 **클립 로컬 초**(정규화 아님 — Unity AnimationEvent 와 같은 단위).
[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;
/// {CID} 치환. 매핑 데이터를 클래스마다 복사하지 않기 위한 최소한의 템플릿이다.
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;
}
}
/// 한 컨트롤러 상태에 물릴 클립 1개와 그 클립에 심을 이벤트들.
[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 events = new List();
}
/// 클래스 1개의 전투 애니메이션 매핑.
[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 states = new List();
}
///
/// 전 클래스 전투 애니메이션 매핑 SOT.
/// 에디터 셋업 스크립트(WL_CombatAnimSetup)만 읽는다 — 런타임 참조 없음.
///
[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 classes = new List();
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;
}
}
}