Project_WL/AgentScripts/WL834_ThrustProbe.cs

161 lines
12 KiB
C#

// WL834_ThrustProbe.cs — 「찌르기 모션이 있는가」 객관 실측 + 무기 프리팹이 소켓에서 어느 축으로 뻗는지 (PD 2026-10-11 「창 모션이 없다면 찌르기 형태의 모션이 없는지 체크」)
// unity command run_script --file AgentScripts/WL834_ThrustProbe.cs --entry WL834_ThrustProbe.Run
// 방법: 치비(LH_M05 · 런타임 스케일)에 무기 프리팹을 소켓에 끼우고 후보 클립을 30 fps 로 샘플 → 칼끝(가장 긴 렌더러 장축 끝) 경로로
// path(총 이동) · chord(출발점에서 최대 거리) · straight(chord/path · 1 = 직선) · fwd(캐릭터 전방 성분 최대) · bladeAng(칼날 방향 누적 회전)
// 찌르기 = straight 높고(≥0.6) bladeAng 작고(<120°) fwd 큼. 휘두르기 = bladeAng 큼. 각 클립 한 줄.
// 무기 축: 소켓 로컬에서 자루→칼끝 방향(단위) — 검(Elven_Sword_01)과 창(PP_Theme_01_Spear_001)·활(PP_Theme_05_Bow_003)을 나란히 찍어 방향 규약 차이를 본다.
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public static class WL834_ThrustProbe
{
const string R = "Assets/ExplosiveLLC/";
const string S = "Assets/Shinabro/Platform_Animation/Animation/07_Spear/Stander@";
const string T = R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@";
const string K = R + "Knight Warrior Mecanim Animation Pack/Animations/Knight@";
class Cand { public string note, fbx, weapon; public int socket; public Cand(string n, string f, string w, int s) { note = n; fbx = f; weapon = w; socket = s; } }
static readonly Cand[] Cands =
{
new Cand("창 Shinabro Attack1", S + "Spear_Attack1.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro Attack2", S + "Spear_Attack2.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro Attack3", S + "Spear_Attack3.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro DodgeAttack", S + "Spear_DodgeAttack.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro CrouchAttack", S + "Spear_CrouchAttack.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro JumpAttack1", S + "Spear_JumpAttack1.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro Skill4_Push", S + "Spear_Skill4_Push.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("창 Shinabro Skill6_Move", S + "Spear_Skill6_Move.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed Attack1_S", T + "Attack1_S.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed Attack2_S", T + "Attack2_S.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed Attack3_S", T + "Attack3_S.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed SpecialAttack1", T + "SpecialAttack1.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed RangeAttack1", T + "RangeAttack1.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("양손 2Handed MoveAttack1_S", T + "MoveAttack1_S.FBX", "PP_Theme_01_Spear_001", 2),
new Cand("검 Knight Attack1_S(휘두르기 기준)", K + "Attack1_S.FBX", "Elven_Sword_01", 2),
new Cand("검 Knight Attack3_S(기준)", K + "Attack3_S.FBX", "Elven_Sword_01", 2),
};
public static object Run()
{
var sb = new StringBuilder();
string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01");
var model = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/PC/" + pcName + ".prefab");
if (model == null) return "PC 프리팹 없음";
float rootScale = 1f;
try
{
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText("Assets/ResWork/Table/Export/ClassConfig.json"));
foreach (var row in arr) if ((string)row["n_ClassID"] == "10101") { float f; float.TryParse((string)row["f_Scale"], out f); rootScale = Mathf.Max(f, 0.01f) * Mathf.Max(WL.Character.WLCharacterSwapSettings.HeightCompensation, 0.01f); break; }
}
catch (System.Exception) { }
var scene = EditorSceneManager.NewPreviewScene();
bool animMode = false;
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene);
go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; go.transform.localScale = Vector3.one * rootScale;
var pc = go.GetComponentInChildren<PCActor>(true);
if (pc == null || pc.tfs_weapon == null) return "PCActor/tfs_weapon 없음";
var anim = go.GetComponentInChildren<Animator>();
var animGo = anim.gameObject; anim.applyRootMotion = false;
var root = go.transform;
// ── 무기 축(소켓 로컬) ─────────────────────────────────────────
sb.AppendLine("■ 무기 축(소켓 로컬 · 자루→칼끝 단위벡터 · 길이 m) [소켓 index: 0 shield 1 righthand 2 onehand 3/4 bladedancer 5 orb 6 archer_l]");
foreach (var w in new[] { new[] { "Elven_Sword_01", "2" }, new[] { "PP_Theme_01_Spear_001", "2" }, new[] { "PP_Theme_01_Hammer_001", "1" }, new[] { "PP_Theme_05_Bow_003", "6" }, new[] { "Dagger1", "3" } })
{
int si = int.Parse(w[1]);
var socket = pc.tfs_weapon[si];
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + w[0] + ".prefab");
if (socket == null || prefab == null) { sb.AppendLine(" " + w[0] + ": 소켓/프리팹 없음"); continue; }
var inst = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
inst.transform.SetParent(socket, false); inst.transform.localPosition = Vector3.zero; inst.transform.localRotation = Quaternion.identity; inst.transform.localScale = Vector3.one;
Vector3 hiltL, tipL; float len;
if (Blade(socket, out hiltL, out tipL, out len))
sb.AppendLine(string.Format(" {0,-24} @{1}({2}) 축 {3} · 길이 {4:F2} m · 자루 {5} 칼끝 {6}", w[0], socket.name, si, V((tipL - hiltL).normalized), len, V(hiltL), V(tipL)));
Object.DestroyImmediate(inst);
}
// ── 클립 실측 ─────────────────────────────────────────────────
sb.AppendLine("■ 클립 실측 (path 총이동 · chord 최대거리 · straight=chord/path · fwd 전방 최대 · bladeAng 칼날 누적회전 · 판정)");
AnimationMode.StartAnimationMode(); animMode = true;
foreach (var c in Cands)
{
var clip = AssetDatabase.LoadAllAssetsAtPath(c.fbx).OfType<AnimationClip>().FirstOrDefault(x => !x.name.StartsWith("__preview"));
if (clip == null) { sb.AppendLine(" [" + c.note + "] 클립 없음 " + c.fbx); continue; }
var socket = pc.tfs_weapon[c.socket];
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + c.weapon + ".prefab");
var inst = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
inst.transform.SetParent(socket, false); inst.transform.localPosition = Vector3.zero; inst.transform.localRotation = Quaternion.identity; inst.transform.localScale = Vector3.one;
Vector3 hiltL, tipL; float len;
if (!Blade(socket, out hiltL, out tipL, out len)) { sb.AppendLine(" [" + c.note + "] 칼날 실측 실패"); Object.DestroyImmediate(inst); continue; }
float fr = clip.frameRate > 0 ? clip.frameRate : 30f; int n = Mathf.Max(2, Mathf.RoundToInt(clip.length * fr));
var tips = new Vector3[n + 1]; var hilts = new Vector3[n + 1];
for (int i = 0; i <= n; i++)
{
AnimationMode.BeginSampling(); AnimationMode.SampleAnimationClip(animGo, clip, Mathf.Min(i / fr, clip.length)); AnimationMode.EndSampling();
tips[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(tipL) - root.position);
hilts[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(hiltL) - root.position);
}
float path = 0f, chord = 0f, fwd = -99f, bladeAng = 0f; int kMax = 0;
Vector3 pd = (tips[0] - hilts[0]).normalized;
for (int i = 1; i <= n; i++)
{
path += Vector3.Distance(tips[i], tips[i - 1]);
float d2 = Vector3.Distance(tips[i], tips[0]); if (d2 > chord) { chord = d2; kMax = i; }
var bd = (tips[i] - hilts[i]).normalized; bladeAng += Vector3.Angle(pd, bd); pd = bd;
}
for (int i = 0; i <= n; i++) if (tips[i].z > fwd) fwd = tips[i].z;
float pathToMax = 0f; for (int i = 1; i <= kMax; i++) pathToMax += Vector3.Distance(tips[i], tips[i - 1]);
float straight = pathToMax > 1e-4f ? chord / pathToMax : 0f;
// 찌르기 창: 칼끝이 가장 빨리 움직인 구간(최대 보폭의 30% 이상 연속)의 직진도
string verdict = (straight >= 0.6f && bladeAng < 150f && chord >= 0.4f) ? "찌르기" : (bladeAng >= 150f ? "휘두르기" : "애매");
sb.AppendLine(string.Format(" {0,-34} len={1:F2}s path={2:F2} chord={3:F2} straight={4:F2} fwd={5:F2} bladeAng={6:F0}° → {7}", c.note, clip.length, path, chord, straight, fwd, bladeAng, verdict));
Object.DestroyImmediate(inst);
}
AnimationMode.StopAnimationMode(); animMode = false;
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
static bool Blade(Transform socket, out Vector3 hiltLocal, out Vector3 tipLocal, out float len)
{
hiltLocal = tipLocal = Vector3.zero; len = 0f;
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.sharedMesh : null);
if (mesh == null) continue;
var ws = Vector3.Scale(mesh.bounds.size, Abs(r.transform.lossyScale));
float longest = Mathf.Max(ws.x, Mathf.Max(ws.y, ws.z));
if (longest > bestExtent) { bestExtent = longest; best = r; bestMesh = mesh; }
}
if (best == null) return false;
var b = bestMesh.bounds; var size = Vector3.Scale(b.size, Abs(best.transform.lossyScale));
int axis = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2);
Vector3 axisDir = Vector3.zero; axisDir[axis] = 1f;
Vector3 eA = best.transform.TransformPoint(b.center - axisDir * b.extents[axis]);
Vector3 eB = best.transform.TransformPoint(b.center + axisDir * b.extents[axis]);
bool aIsHilt = (eA - socket.position).sqrMagnitude <= (eB - socket.position).sqrMagnitude;
hiltLocal = socket.InverseTransformPoint(aIsHilt ? eA : eB);
tipLocal = socket.InverseTransformPoint(aIsHilt ? eB : eA);
len = Vector3.Distance(eA, eB);
return true;
}
static string V(Vector3 v) { return string.Format("({0:F2},{1:F2},{2:F2})", v.x, v.y, v.z); }
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
}