Project_WL/Assets/WL/Combat/CombatAnimMapping.cs

148 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
[Tooltip("#839 서브클립 프레임 범위 덮기(임포터 firstFrame/lastFrame). lastFrame ≤ 0 이면 원본 클립 범위 그대로. 이벤트 정규화 시각은 이 범위 기준.")]
public int firstFrame = -1;
public int lastFrame = -1;
[Tooltip("#834 상태 재생 속도 배율(AnimatorState.speed). 0 = 건드리지 않음 · 1 = 원속 · 1.3 = 30 % 빠르게. 이벤트는 정규화 시각이라 함께 당겨진다.")]
public float speed;
[Tooltip("#948 서브클립에 씌울 아바타 마스크 에셋 경로(Assets/… .mask). 비우면 원본 클립 설정 그대로. 상체 마스크(루트·다리 제외) = 점프·이동 없이 제자리에서 상체만 움직인다(지팡이 3타)")]
public string maskPath = "";
[Tooltip("#948 임포터 「Root Transform Position (Y) → Bake Into Pose」: -1 = 원본 클립 설정 그대로 · 0 = 끔(위아래 움직임이 포즈에서 빠져 루트모션으로 → PC 는 Y 루트모션을 버리므로 점프가 사라진다) · 1 = 켬")]
public int bakeRootY = -1;
[Tooltip("#948-6 애니메이터 상태 거울(AnimatorState.mirror · humanoid 왼손↔오른손): 0 = 그대로 · 1 = 거울. 오른손 동작 클립을 「지팡이를 들지 않은 손」 동작으로 쓸 때(지팡이 3타 = Mage@RangeAttack2 거울)")]
public int mirror;
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;
}
}
}