Project_WL/AgentScripts/WL833_FrameTables.cs

143 lines
11 KiB
C#
Raw Permalink 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.

// WL833_FrameTables.cs — 무기 계열별 공격 전진 「프레임 표」를 팩 원본(루트모션) 클립에서 굽는다 (PD 2026-10-10 「제자리 고정 공격 패턴 → 모션에 맞는 전진」)
// unity command run_script --file AgentScripts/WL833_FrameTables.cs --entry WL833_FrameTables.Run --args "[false, 0.6]" (dry · 표만 출력)
// unity command run_script --file AgentScripts/WL833_FrameTables.cs --entry WL833_FrameTables.Run --args "[true, 0.6]" (WLCombatMotionSettings.asset 에 기록)
//
// 구조(실측): 전진은 AttackRootMotion.FrameTable 모드 — 재생 중 클립 이름에 clipContains 가 포함된 표의 프레임별 cm 를 NavMeshAgent.Move 로 민다.
// #791 표는 검(Knight) 3클립뿐이라 망치·단검(이름이 Attack{N}_S_… 라 검 표를 잘못 씀)·창·활·지팡이(표 없음 = 제자리)가 어긋났다.
// 방법: 팩의 루트모션 원본 클립(Knight@Attack1.FBX 등 · _S 아님)의 RootT.z 커브를 30 fps 프레임마다 읽어 Δ × 치비 humanScale(LH_M05 0.4233)
// × 100 = 프레임별 전진 cm(뒤로 가는 프레임은 0). scale 인자(기본 0.6)는 #791 검 표(발 접지 실측 15/30/28 cm)가 원본 루트모션(22/58/54 cm)의
// 약 0.5~0.7 배였던 것에 맞춘 배율 — 발 미끄러짐을 줄인다. 검 3표는 PD 승인값(09-07)이라 **값 그대로** 두고 키만 Attack{N}_S_101 로 좁힌다.
// 활 2·3타(MoveAttack 7.4 m 달리기 사격)는 표를 만들지 않는다(제자리 사격).
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using WL.Combat;
public static class WL833_FrameTables
{
const string SettingsPath = "Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset";
const string PcPath = "Assets/Res_Addr/PC/LH_M05.prefab";
const string R = "Assets/ExplosiveLLC/";
const string K = R + "Knight Warrior Mecanim Animation Pack/Animations/Knight@";
const string H = R + "Hammer Warrior Mecanim Animation Pack/Animations/Hammer@";
const string N = R + "Ninja Warrior Mecanim Animation Pack/Animations/Ninja@";
const string A = R + "Archer Warrior Mecanim Animation Pack/Animations/Archer@";
const string M = R + "Mage Warrior Mecanim Animation Pack/Animations/Mage@";
const string S = "Assets/Shinabro/Platform_Animation/Animation/07_Spear/Stander@";
class Spec { public string key; public string fbx; public string note; public float mult = 1f; public bool pass; public bool stepGate; public Spec(string k, string f, string n, float m = 1f, bool p = false, bool g = false) { key = k; fbx = f; note = n; mult = m; pass = p; stepGate = g; } }
// #841 스텝 창: 팩 FBX 임포터 원본 클립의 Foot* 이벤트(발 접지) 앞 0.30 s ~ 뒤 0.05 s 만 전진을 남긴다(발이 고정된 구간 = 0). 총량은 재분배하지 않는다(미끄러짐 제거가 목적).
const float StepGateBefore = 0.30f, StepGateAfter = 0.05f;
static List<float> FootEventTimes(string fbx)
{
var l = new List<float>();
var importer = AssetImporter.GetAtPath(fbx) as ModelImporter;
if (importer == null) return l;
var clips = importer.clipAnimations; if (clips == null || clips.Length == 0) clips = importer.defaultClipAnimations;
if (clips == null || clips.Length == 0) return l;
var c = clips[0];
if (c.events != null) foreach (var e in c.events) if (e.functionName != null && e.functionName.StartsWith("Foot")) l.Add(e.time); // 임포터 이벤트 time = 정규화
return l;
}
static readonly Spec[] Specs =
{
// #839 해머 재배열(1타 = 팩 Attack2 · 2타 = Attack1 · 3타 = Attack3) · #841 PD 「발 스텝이 나갈 때만 전진 · 발 고정 구간은 이동 금지」 → stepGate(팩 Foot 이벤트 앞 0.3 s 창 밖은 0)
new Spec("Attack1_S_105", H + "Attack2.FBX", "망치 1타(팩 Attack2 · 스텝 창)", 1f, false, true), new Spec("Attack2_S_105", H + "Attack1.FBX", "망치 2타(팩 Attack1 · 스텝 창)", 1f, false, true), new Spec("Attack3_S_105", H + "Attack3.FBX", "망치 3타(팩 Attack3 회전 · 스텝 창)", 1f, false, true),
new Spec("Attack1_S_102", N + "Attack1.FBX", "단검 1타"), new Spec("Attack2_S_102", N + "Attack2.FBX", "단검 2타"),
new Spec("Attack3_S_102", N + "Attack3.FBX", "단검 3타(#834 PD 「마지막 타는 전진하며」 · 배율 ×1.6 = 원본 루트모션 거의 그대로)", 1.6f),
// #839 PD 「단검 마지막 타(4타 DodgeAttack) 공격 시점에 한발짝 전진」 — 원본 힘민지 클립 루트 이동 실측용(0 이면 합성 표로)
// #840 PD 「4타 돌진베기 경로에 적이 있으면 통과하며 공격」 — 멈춤 거리 무시(passThrough) · 배율 1.2 ≈ 116 cm(적 0.6~1 m 를 지나쳐 뒤로)
new Spec("Attack4_S_102", "Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_DodgeAttack_Attack4.FBX", "단검 4타(#840 통과 돌진 · 원본 루트 1.62 m × 1.2 배율)", 1.2f, true),
// #839 창 = Spearman 팩(Shinabro 표 3개는 더 이상 쓰는 클립이 없어 폐기)
// #840 창 재배열: 1타 = Attack2(찌르기) · 2타 = 찌르기 구간 ×3(표 없음 · 제자리 연타) · 3타 = Attack1(내려찌르기)
new Spec("Attack1_S_107", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack2.FBX", "창 1타(Spearman Attack2 찌르기)"),
new Spec("Attack3_S_107", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack1.FBX", "창 3타(Spearman Attack1 내려찌르기)"),
new Spec("Bow1_S", A + "Attack1.FBX", "활 1타(제자리 사격 · 한 걸음)"),
new Spec("Bow2_S", A + "Attack1.FBX", "활 2타(#834 Attack1 로 통일)"), new Spec("Bow3_S", A + "Attack1.FBX", "활 3타(#834 Attack1 로 통일)"),
new Spec("Cast1_S", M + "Attack2.FBX", "지팡이 1타"), new Spec("Cast2_S", M + "Attack3.FBX", "지팡이 2타"),
// #838 대검 10801 = 2Handed Warrior 팩 루트모션 클립
// #841 대검 재배열: 1타 = 팩 Attack2 · 2타 = Attack3 · 3타 = Attack1
new Spec("Attack1_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack2.FBX", "대검 1타(팩 Attack2)"),
new Spec("Attack2_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack3.FBX", "대검 2타(팩 Attack3)"),
new Spec("Attack3_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack1.FBX", "대검 3타(팩 Attack1)"),
new Spec("Attack4_S_108", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack3.FBX", "대검 4타(#840 Spearman Attack3 점프 찌르기 · 원본 1.16 m × 0.6 ≈ 69 cm)"),
};
public static object Run(bool apply, float scale)
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<WLCombatMotionSettings>(SettingsPath);
if (settings == null) return "설정 에셋 없음 " + SettingsPath;
var pc = AssetDatabase.LoadAssetAtPath<GameObject>(PcPath);
var an = pc != null ? pc.GetComponentInChildren<Animator>(true) : null;
float hs = an != null && an.avatar != null ? an.humanScale : 0f;
if (hs <= 0f) return "치비 humanScale 을 못 읽음 " + PcPath;
sb.AppendLine(string.Format("humanScale={0:F4} scale={1:F2} mode={2} frameTableScale={3:F2}", hs, scale, settings.attackMoveMode, settings.frameTableScale));
var result = new List<AttackFrameTable>();
// ① 검 3표(#791 PD 승인값) — 키만 좁힌다
foreach (var old in settings.attackFrameTables)
{
if (old == null) continue;
string k = old.clipContains;
if (k == "Attack1_S" || k == "Attack2_S" || k == "Attack3_S") k += "_101";
if (!k.StartsWith("Attack") || !k.EndsWith("_101")) continue; // 검 3표만 보존(다른 키는 아래에서 새로)
result.Add(new AttackFrameTable { clipContains = k, frames = (int[])old.frames.Clone(), cm = (float[])old.cm.Clone() });
sb.AppendLine(string.Format("[검] {0,-14} 보존 · 합 {1:F1} cm · 프레임 {2}", k, old.Total(), string.Join(",", old.frames.Select(x => x.ToString()).ToArray())));
}
// ② 새 표 — 팩 원본 루트모션
foreach (var s in Specs)
{
var clip = AssetDatabase.LoadAllAssetsAtPath(s.fbx).OfType<AnimationClip>().FirstOrDefault(c => !c.name.StartsWith("__preview"));
if (clip == null) { sb.AppendLine("[" + s.note + "] 클립 없음 " + s.fbx); continue; }
AnimationCurve z = null;
foreach (var b in AnimationUtility.GetCurveBindings(clip)) if (b.propertyName == "RootT.z") { z = AnimationUtility.GetEditorCurve(clip, b); break; }
if (z == null) { sb.AppendLine("[" + s.note + "] RootT.z 없음 " + clip.name); continue; }
float fr = clip.frameRate > 0f ? clip.frameRate : 30f;
int n = Mathf.Max(1, Mathf.RoundToInt(fr * clip.length));
var frames = new List<int>(); var cms = new List<float>();
float prev = z.Evaluate(0f), raw = 0f, fwdRaw = 0f, gatedOut = 0f;
List<float> foot = s.stepGate ? FootEventTimes(s.fbx) : null;
string gateNote = "";
if (s.stepGate)
{
var w = new List<string>();
foreach (var t in foot) w.Add(Mathf.RoundToInt((t - StepGateBefore / clip.length) * n) + "~" + Mathf.RoundToInt((t + StepGateAfter / clip.length) * n));
gateNote = " · 스텝 창 " + (foot.Count == 0 ? "없음(Foot 이벤트 0 → 전부 0)" : string.Join(",", w.ToArray()));
}
for (int f = 1; f <= n; f++)
{
float v = z.Evaluate(Mathf.Min(f / fr, clip.length));
float d = v - prev; prev = v;
raw += d;
if (d <= 0f) continue;
fwdRaw += d;
float cm = d * hs * 100f * scale * Mathf.Max(s.mult, 0f);
if (s.stepGate)
{
bool inWin = false; float tf = f / (float)n;
foreach (var t in foot) if (tf >= t - StepGateBefore / clip.length && tf <= t + StepGateAfter / clip.length) { inWin = true; break; }
if (!inWin) { gatedOut += cm; continue; }
}
if (cm < 0.1f) continue;
frames.Add(f); cms.Add(Mathf.Round(cm * 10f) / 10f);
}
if (s.stepGate) gateNote += " · 제거 " + gatedOut.ToString("0.0") + " cm";
float total = 0f; foreach (var c in cms) total += c;
result.Add(new AttackFrameTable { clipContains = s.key, frames = frames.ToArray(), cm = cms.ToArray(), passThrough = s.pass });
sb.AppendLine(string.Format("[{0}] {1,-14} {2} len={3:F2}s n={4} · 원본 Δz 순={5:F2} 전진={6:F2}(×hs {7:F2} m) → 표 합 {8:F1} cm · 프레임 {9}~{10} ({11}개){12}",
s.note, s.key, clip.name, clip.length, n, raw, fwdRaw, fwdRaw * hs, total, frames.Count > 0 ? frames[0] : 0, frames.Count > 0 ? frames[frames.Count - 1] : 0, frames.Count, gateNote));
}
if (!apply) return sb.Append("(dry)").ToString();
settings.attackFrameTables = result;
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
sb.AppendLine("APPLIED — 표 " + result.Count + "개 기록 (" + SettingsPath + ")");
return sb.ToString();
}
}