2026-09-06 23:24:25 +00:00
|
|
|
|
// WL792_Verify.cs — #792 무기 정합 배치 검증 (Play 전용)
|
|
|
|
|
|
//
|
|
|
|
|
|
// unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Dirs --args '[10501]'
|
|
|
|
|
|
// unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Dirs --args '[10101]'
|
|
|
|
|
|
// unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Combo --args '[10501]'
|
|
|
|
|
|
// unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Moving --args '[10501]'
|
|
|
|
|
|
// unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Budget
|
|
|
|
|
|
//
|
|
|
|
|
|
// 재는 것 (발주 §4)
|
|
|
|
|
|
// 법선 오차(도) · 호 방향(볼록) 오차(도) · 반지름 비(호/칼끝) · 중심 오프셋(m) · 스폰 지연(ms)
|
|
|
|
|
|
// 찌르기: 축 방향 오차(도) · 길이 비
|
|
|
|
|
|
// 값은 추정이 아니라 WeaponTrailDriver.LastPlacement 에 담긴 **목표값과 배치 결과값**을 그대로 쓴다(C39).
|
|
|
|
|
|
//
|
|
|
|
|
|
// 캡처: Screenshots_WL/combat6/ (클립 피크 프레임 · 방향별 · 이동 중 · 콤보 연속)
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using WL.Combat;
|
|
|
|
|
|
|
|
|
|
|
|
public static class WL792_Verify
|
|
|
|
|
|
{
|
|
|
|
|
|
const string OutDir = "Screenshots_WL/combat6";
|
|
|
|
|
|
const BindingFlags BF = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
|
|
|
|
|
|
|
|
|
|
public static void Dirs(int classId) { Launch(h => h.Co_Dirs(classId), "dirs" + classId); }
|
|
|
|
|
|
public static void Combo(int classId) { Launch(h => h.Co_Combo(classId), "combo" + classId); }
|
2026-09-07 05:01:36 +00:00
|
|
|
|
public static void ComboSpeed(int classId, float speed) { Launch(h => h.Co_ComboSpeed(classId, speed), "comboS" + classId); } // #804 클립 원속 재실측
|
2026-09-06 23:24:25 +00:00
|
|
|
|
public static void Moving(int classId) { Launch(h => h.Co_Moving(classId), "moving" + classId); }
|
|
|
|
|
|
public static void Budget() { Launch(h => h.Co_Budget(), "budget"); }
|
|
|
|
|
|
public static void Regress() { Launch(h => h.Co_Regress(), "regress"); }
|
|
|
|
|
|
public static void Trace(int classId) { Launch(h => h.Co_Trace(classId), "trace" + classId); }
|
2026-09-07 05:01:36 +00:00
|
|
|
|
public static void Trace3(int classId) { Launch(h => h.Co_Trace3(classId), "trace3_" + classId); } // #804 Attack3 한 클립 프레임 추적
|
2026-09-06 23:24:25 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측).</summary>
|
|
|
|
|
|
public static void Reset()
|
|
|
|
|
|
{
|
|
|
|
|
|
WeaponTrailDriver.ClearMappedCache();
|
|
|
|
|
|
SlashTrailSettings.ClearCache();
|
|
|
|
|
|
Debug.Log("[WL #792] 런타임 캐시 초기화 — 다음 스윙부터 다시 실측한다");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>런타임 캘리브레이션 캐시를 그대로 덤프한다(베이크 전 확인용).</summary>
|
|
|
|
|
|
public static void DumpCal()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# 런타임 캘리브레이션 " + WeaponTrailDriver.RuntimeCalibrations.Count + "행 " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
foreach (var kv in WeaponTrailDriver.RuntimeCalibrations)
|
|
|
|
|
|
{
|
|
|
|
|
|
var c = kv.Value;
|
|
|
|
|
|
sb.AppendLine(string.Format("{0,-30} {1,-5} sweep={2,6:F1}° tipR={3:F3} hiltR={4:F3} stabLen={5:F3} | {6}",
|
|
|
|
|
|
kv.Key, c.kind, c.sweepDeg, c.tipRadius, c.hiltRadius, c.stabLength, c.source));
|
|
|
|
|
|
}
|
|
|
|
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
|
|
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "v_cal.txt"), sb.ToString(), new System.Text.UTF8Encoding(false));
|
|
|
|
|
|
Debug.Log(sb.ToString());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void Launch(Func<Host, IEnumerator> make, string tag)
|
|
|
|
|
|
{
|
|
|
|
|
|
var old = GameObject.Find("__wl792verify");
|
|
|
|
|
|
if (old != null) UnityEngine.Object.DestroyImmediate(old);
|
|
|
|
|
|
var go = new GameObject("__wl792verify");
|
|
|
|
|
|
var h = go.AddComponent<Host>();
|
|
|
|
|
|
h.tag_ = tag;
|
|
|
|
|
|
h.StartCoroutine(make(h));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
public class Host : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public string tag_ = "x";
|
|
|
|
|
|
|
|
|
|
|
|
// ── 유틸 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
static double HP(Actor a)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mi = typeof(Actor).GetMethod("Get_HP", BF);
|
|
|
|
|
|
if (mi == null || a == null) return double.NaN;
|
|
|
|
|
|
try { return Convert.ToDouble(mi.Invoke(a, null)); } catch { return double.NaN; }
|
|
|
|
|
|
}
|
|
|
|
|
|
static double ASpd(Actor me)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var fi = typeof(Actor).GetField("m_Stat", BF);
|
|
|
|
|
|
var stat = fi != null ? fi.GetValue(me) : null;
|
|
|
|
|
|
var gm = stat != null ? stat.GetType().GetMethod("Get_Stat", new[] { typeof(eStat) }) : null;
|
|
|
|
|
|
if (gm != null) return Convert.ToDouble(gm.Invoke(stat, new object[] { eStat.FinalAttackSpeed }));
|
|
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
return 1.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
static void SuppressPets()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
|
|
|
|
if (p.gameObject.activeSelf) p.gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
static MyActor Me()
|
|
|
|
|
|
{
|
|
|
|
|
|
return UnityEngine.Object.FindObjectsByType<MyActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
static string ClipName(Animator a)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (a == null || a.layerCount == 0) return "(none)";
|
|
|
|
|
|
var ci = a.GetCurrentAnimatorClipInfo(0);
|
|
|
|
|
|
return ci.Length > 0 && ci[0].clip != null ? ci[0].clip.name : "(none)";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator SetClass(StringBuilder sb, int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
var pc = me as PCActor;
|
|
|
|
|
|
if (classId > 0 && pc != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var data = table_classconfig.Ins != null ? table_classconfig.Ins.Get_Data_orNull(classId) : null;
|
|
|
|
|
|
if (data == null) { sb.AppendLine("!! class data null " + classId); yield break; }
|
|
|
|
|
|
pc.Change_Class(data, true);
|
|
|
|
|
|
yield return new WaitForSeconds(1.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 상시 규칙 — 펫 금지 · 무적 유지
|
|
|
|
|
|
SuppressPets();
|
|
|
|
|
|
if (me != null) me.Immortal(1);
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator Cap(string name)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
|
|
Texture2D tex = null;
|
|
|
|
|
|
try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch { }
|
|
|
|
|
|
if (tex != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
|
|
|
|
System.IO.File.WriteAllBytes(System.IO.Path.Combine(OutDir, name + ".png"), tex.EncodeToPNG());
|
|
|
|
|
|
UnityEngine.Object.Destroy(tex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 배치 1회를 재고 한 줄로 ─────────────────────────────────────────
|
|
|
|
|
|
static string Row(string label, WeaponTrailDriver.MappedPlacement p)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (p.kind == SwingKind.Stab)
|
|
|
|
|
|
{
|
|
|
|
|
|
float axErr = Vector3.Angle(p.stabAxisTarget, p.stabAxisPlaced);
|
|
|
|
|
|
float lenRatio = p.stabLenTarget > 1e-4f ? p.stabLenPlaced / p.stabLenTarget : 0f;
|
|
|
|
|
|
float startOff = Vector3.Distance(p.stabStartTarget, p.stabStartPlaced);
|
|
|
|
|
|
return string.Format(
|
|
|
|
|
|
"{0,-14} | 찌르기 {1,-20} 축오차 {2,5:F1}° 길이비 {3,5:F2} 시작오프 {4,5:F3} m 스케일 {5:F3} 지연 {6,5:F1} ms {7}",
|
|
|
|
|
|
label, p.effect, axErr, lenRatio, startOff, p.scale, p.delayMs, p.fromCache ? "[캐시]" : "[실측]");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
float nErr = Vector3.Angle(p.targetNormal, p.arcNormal);
|
|
|
|
|
|
float bErr = Vector3.Angle(p.targetBisector, p.arcBisector);
|
|
|
|
|
|
float rRatio = p.tipRadius > 1e-4f ? p.arcRadius / p.tipRadius : 0f;
|
|
|
|
|
|
float cOff = Vector3.Distance(p.targetPivot, p.arcCenter);
|
|
|
|
|
|
return string.Format(
|
|
|
|
|
|
"{0,-14} | 휘두르기 {1,-18} 법선 {2,5:F1}° 볼록 {3,5:F1}° 반지름비 {4,5:F2} 중심 {5,5:F3} m " +
|
|
|
|
|
|
"스윕(스윙/메시) {6,5:F0}/{7,5:F0}° 스케일 {8:F3} 지연 {9,5:F1} ms {10}",
|
|
|
|
|
|
label, p.effect, nErr, bErr, rRatio, cOff, p.sweepSwing, p.sweepMesh, p.scale,
|
|
|
|
|
|
p.delayMs, p.fromCache ? "[캐시]" : "[실측]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>공격 1회를 돌리고, 배치가 일어나면 그 시점의 지표와 캡처를 남긴다.</summary>
|
|
|
|
|
|
IEnumerator OneAttack(StringBuilder sb, string label, bool capture)
|
|
|
|
|
|
{
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) yield break;
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
|
|
|
|
|
int before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
bool shot = false;
|
|
|
|
|
|
while (Time.time - t0 < 1.1f)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
if (WeaponTrailDriver.MappedSpawnCount > before)
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = WeaponTrailDriver.LastPlacement;
|
|
|
|
|
|
sb.AppendLine(" " + Row(label + " " + ClipName(anim), p));
|
|
|
|
|
|
sb.AppendLine(" " + WeaponTrailDriver.LastMappedInfo);
|
|
|
|
|
|
before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
if (capture && !shot)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 배치 직후 이펙트가 가장 잘 보이는 프레임 근처에서 한 장
|
|
|
|
|
|
yield return new WaitForSeconds(0.06f);
|
|
|
|
|
|
yield return Cap(tag_ + "_" + label.Replace(" ", "").Replace("/", "-"));
|
|
|
|
|
|
shot = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ① 방향 × 클립 ───────────────────────────────────────────────────
|
|
|
|
|
|
public IEnumerator Co_Dirs(int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 방향별 정합 검증 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
|
|
|
|
|
|
string[] dn = { "N", "E", "S", "W" };
|
|
|
|
|
|
float[] dy = { 0f, 90f, 180f, 270f };
|
|
|
|
|
|
|
|
|
|
|
|
for (int d = 0; d < 4; d++)
|
|
|
|
|
|
{
|
|
|
|
|
|
sb.AppendLine("── 방향 " + dn[d] + " (yaw " + dy[d] + "°)");
|
|
|
|
|
|
for (int combo = 1; combo <= 3; combo++)
|
|
|
|
|
|
{
|
|
|
|
|
|
me.transform.rotation = Quaternion.Euler(0f, dy[d], 0f);
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
yield return OneAttack(sb, dn[d] + "/A" + combo, true);
|
|
|
|
|
|
yield return new WaitForSeconds(0.35f);
|
|
|
|
|
|
}
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
|
sb.AppendLine("판정 목표 — 법선/볼록 ≤ 10° · 반지름비 0.9~1.1 · 중심 ≤ 0.10 m · 지연 ≤ 1 프레임(≈16.7 ms)");
|
|
|
|
|
|
sb.AppendLine("찌르기 목표 — 축 오차 ≤ 10° · 길이비 0.9~1.1");
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ①-b 클립 전체 칼끝 궤적 추적 (스윙 창이 맞는지 보는 진단) ──────
|
|
|
|
|
|
// WeaponTrailDriver 의 궤적 샘플은 ShowEffect 이벤트부터 swingWindowNormalized 동안만 모인다.
|
|
|
|
|
|
// 그 창이 실제 스윙 구간을 못 잡으면(예: 9 샘플 · 칼끝 이동 9 cm) 캘리브레이션이 무의미해진다.
|
|
|
|
|
|
// 여기서는 창과 무관하게 **클립 전체**를 매 프레임 훑어 각속도 프로파일을 낸다.
|
|
|
|
|
|
public IEnumerator Co_Trace(int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 클립 전체 칼끝 궤적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
|
|
|
|
|
for (int round = 0; round < 9; round++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var drv = me.GetComponent<WeaponTrailDriver>();
|
|
|
|
|
|
if (drv == null || drv.ActiveSlotCount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 드라이버는 첫 ShowEffect 때 붙는다 — 한 번 때려서 붙인다
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
yield return new WaitForSeconds(1.2f);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int spawn0 = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
string prevClip = ClipName(anim);
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
|
|
|
|
|
|
// 🔴 클립이 **처음부터** 도는 순간을 잡아야 한다. Play_Attack 직후 한두 프레임은
|
|
|
|
|
|
// 아직 이전 상태이거나 전이 블렌드 중이라, 그때의 칼끝 이동은 블렌드가 만든 가짜 점프다
|
|
|
|
|
|
// (1차 추적에서 이 때문에 Attack3 에 1.0 m 짜리 허위 스텝이 잡혔다).
|
|
|
|
|
|
float tw = Time.time;
|
|
|
|
|
|
string clip0 = null;
|
|
|
|
|
|
while (Time.time - tw < 1.0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
var s2 = anim.GetCurrentAnimatorStateInfo(0);
|
|
|
|
|
|
string c2 = ClipName(anim);
|
|
|
|
|
|
if (!anim.IsInTransition(0) && c2 != "(none)" && (s2.normalizedTime % 1f) < 0.12f)
|
|
|
|
|
|
{ clip0 = c2; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (clip0 == null) { yield return new WaitForSeconds(0.5f); continue; }
|
|
|
|
|
|
|
|
|
|
|
|
var rows = new List<string>();
|
|
|
|
|
|
Vector3 prevTip = drv.GetTipWorld(0), prevDir = (drv.GetTipWorld(0) - drv.GetHiltWorld(0)).normalized;
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
float pathLen = 0f, maxStep = 0f, totalAng = 0f;
|
|
|
|
|
|
int eventFrame = -1, fi = 0;
|
|
|
|
|
|
float ntPeak = 0f, stepPeak = 0f;
|
|
|
|
|
|
|
|
|
|
|
|
while (Time.time - t0 < 2.0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
fi++;
|
|
|
|
|
|
var st = anim.GetCurrentAnimatorStateInfo(0);
|
|
|
|
|
|
float nt = st.normalizedTime % 1f;
|
|
|
|
|
|
if (ClipName(anim) != clip0) break;
|
|
|
|
|
|
if (nt >= 0.80f && st.normalizedTime >= 0.80f) break; // 콤보 창(0.70~0.84) 안에서 끊어야 다음 타로 이어진다
|
|
|
|
|
|
|
|
|
|
|
|
Vector3 tip = drv.GetTipWorld(0);
|
|
|
|
|
|
Vector3 hilt = drv.GetHiltWorld(0);
|
|
|
|
|
|
if (tip == Vector3.zero) continue;
|
|
|
|
|
|
Vector3 dir = (tip - hilt).normalized;
|
|
|
|
|
|
|
|
|
|
|
|
float step = Vector3.Distance(tip, prevTip);
|
|
|
|
|
|
float ang = Vector3.Angle(prevDir, dir);
|
|
|
|
|
|
pathLen += step; totalAng += ang;
|
|
|
|
|
|
if (step > maxStep) { maxStep = step; }
|
|
|
|
|
|
if (step > stepPeak) { stepPeak = step; ntPeak = nt; }
|
|
|
|
|
|
if (WeaponTrailDriver.MappedSpawnCount > spawn0 && eventFrame < 0) eventFrame = fi;
|
|
|
|
|
|
|
|
|
|
|
|
rows.Add(string.Format(" f{0,-3} nt={1,5:F3} 칼끝이동={2,6:F3} m 칼날각속={3,6:F2}°/f swing={4}",
|
|
|
|
|
|
fi, nt, step, ang, drv.IsSwinging ? 1 : 0));
|
|
|
|
|
|
prevTip = tip; prevDir = dir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sb.AppendLine(string.Format("── {0} 프레임 {1} · 칼끝 경로 {2:F3} m · 최대 스텝 {3:F3} m (nt={4:F3}) · 칼날 총 회전 {5:F1}°",
|
|
|
|
|
|
clip0, fi, pathLen, maxStep, ntPeak, totalAng));
|
|
|
|
|
|
foreach (var r in rows) sb.AppendLine(r);
|
|
|
|
|
|
yield return new WaitForSeconds(0.02f); // 콤보를 이어 붙여 Attack2/3 까지 훑는다
|
|
|
|
|
|
}
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ② 콤보 연속 ─────────────────────────────────────────────────────
|
|
|
|
|
|
public IEnumerator Co_Combo(int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 콤보 연속 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
|
|
|
|
|
int before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
int shots = 0;
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
|
|
|
|
|
|
while (Time.time - t0 < 6f && shots < 6)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
if (WeaponTrailDriver.MappedSpawnCount > before)
|
|
|
|
|
|
{
|
|
|
|
|
|
before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
var p = WeaponTrailDriver.LastPlacement;
|
|
|
|
|
|
sb.AppendLine(" " + Row("combo" + shots + " " + ClipName(anim), p));
|
|
|
|
|
|
yield return new WaitForSeconds(0.05f);
|
|
|
|
|
|
yield return Cap(tag_ + "_seq" + shots);
|
|
|
|
|
|
shots++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Time.time - t0 > 1.2f && shots < 6)
|
|
|
|
|
|
{
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
t0 = Time.time;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.AppendLine("캡처 " + shots + "장");
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 05:01:36 +00:00
|
|
|
|
// ── ②-S 콤보 연속 · 지정 공격 속도 (#804 · 2026-09-07) ──────────────────
|
|
|
|
|
|
// 런타임 공격 속도(FinalAttackSpeed 약 3.5)에서는 Attack3 스윙 창 안 샘플이 6~9 개뿐이라
|
|
|
|
|
|
// 캘리브레이션 품질이 낮다(실측 path 0.25 m · Stab 오판). 클립 원속(1.0)으로 돌려 샘플을 채운다.
|
|
|
|
|
|
// 베이크된 캘리브레이션은 이벤트 시점에 캐릭터 로컬로 펼치므로 공격 속도와 무관하게 재사용된다.
|
|
|
|
|
|
public IEnumerator Co_ComboSpeed(int classId, float speed)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #804 콤보 연속(속도 " + speed.ToString("F2") + ") class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
|
|
|
|
|
int before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
int shots = 0;
|
|
|
|
|
|
float gap = 1.3f / Mathf.Max(speed, 0.1f);
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
me.Play_Attack(0, speed);
|
|
|
|
|
|
float tEnd = Time.time + 6f * gap + 3f;
|
|
|
|
|
|
while (Time.time < tEnd && shots < 6)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
if (WeaponTrailDriver.MappedSpawnCount > before)
|
|
|
|
|
|
{
|
|
|
|
|
|
before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
var p = WeaponTrailDriver.LastPlacement;
|
|
|
|
|
|
sb.AppendLine(" " + Row("comboS" + shots + " " + ClipName(anim), p));
|
|
|
|
|
|
yield return new WaitForSeconds(0.05f);
|
|
|
|
|
|
yield return Cap(tag_ + "_seq" + shots);
|
|
|
|
|
|
shots++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Time.time - t0 > gap && shots < 6)
|
|
|
|
|
|
{
|
|
|
|
|
|
me.Play_Attack(0, speed);
|
|
|
|
|
|
t0 = Time.time;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.AppendLine("캡처 " + shots + "장");
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── #804 Attack3 한 클립을 프레임마다 추적 — nt · 칼끝 이동 · 스윙 창 on/off · 배치 스폰 ─────
|
|
|
|
|
|
public IEnumerator Co_Trace3(int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #804 Attack3 프레임 추적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
float aspd = (float)ASpd(me);
|
|
|
|
|
|
sb.AppendLine("FinalAttackSpeed=" + aspd.ToString("F3") + " animator.speed=" + anim.speed.ToString("F3"));
|
|
|
|
|
|
|
|
|
|
|
|
float t0 = Time.time; float tEnd = Time.time + 12f;
|
|
|
|
|
|
me.Play_Attack(0, aspd);
|
|
|
|
|
|
bool recorded = false;
|
|
|
|
|
|
var rows = new List<string>();
|
|
|
|
|
|
while (Time.time < tEnd && !recorded)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
if (Time.time - t0 > 1.2f) { me.Play_Attack(0, aspd); t0 = Time.time; }
|
|
|
|
|
|
string c = ClipName(anim);
|
|
|
|
|
|
if (!c.Contains("Attack3")) continue;
|
|
|
|
|
|
// Attack3 진입 — 클립이 끝날 때까지 매 프레임 기록
|
|
|
|
|
|
var drv = me.GetComponent<WeaponTrailDriver>();
|
|
|
|
|
|
Vector3 prevTip = drv != null ? drv.GetTipWorld(0) : Vector3.zero;
|
|
|
|
|
|
int spawn0 = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
int fi = 0; float path = 0f;
|
|
|
|
|
|
while (ClipName(anim).Contains("Attack3") && fi < 200)
|
|
|
|
|
|
{
|
|
|
|
|
|
var st = anim.GetCurrentAnimatorStateInfo(0);
|
|
|
|
|
|
float nt = st.normalizedTime % 1f;
|
|
|
|
|
|
Vector3 tip = drv != null ? drv.GetTipWorld(0) : Vector3.zero;
|
|
|
|
|
|
float step = Vector3.Distance(tip, prevTip); if (fi > 0) path += step;
|
|
|
|
|
|
rows.Add(string.Format(" f{0,-3} nt={1,5:F3} inTrans={2} speed={3:F2} tipStep={4:F3} path={5:F2} swing={6} spawned={7} localTip={8}",
|
|
|
|
|
|
fi, nt, anim.IsInTransition(0) ? 1 : 0, anim.speed, step, path, drv != null && drv.IsSwinging ? 1 : 0,
|
|
|
|
|
|
WeaponTrailDriver.MappedSpawnCount - spawn0, me.transform.InverseTransformPoint(tip).ToString("F2")));
|
|
|
|
|
|
prevTip = tip; fi++;
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
recorded = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.AppendLine(recorded ? "Attack3 frames=" + rows.Count : "!! Attack3 not reached in 12 s");
|
|
|
|
|
|
foreach (var r in rows) sb.AppendLine(r);
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 23:24:25 +00:00
|
|
|
|
// ── ③ 이동 중 공격 ──────────────────────────────────────────────────
|
|
|
|
|
|
public IEnumerator Co_Moving(int classId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 이동 중 공격 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
yield return SetClass(sb, classId);
|
|
|
|
|
|
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
|
|
|
|
|
|
Vector3 p0 = me.transform.position;
|
|
|
|
|
|
me.transform.rotation = Quaternion.Euler(0f, 45f, 0f);
|
|
|
|
|
|
yield return OneAttack(sb, "move", true);
|
|
|
|
|
|
sb.AppendLine(" 루트모션 이동 " + Vector3.Distance(p0, me.transform.position).ToString("F3") + " m");
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ⑤ 회귀 — 3콤보 3히트 · 자동 교전 · 대쉬 ────────────────────────
|
|
|
|
|
|
public IEnumerator Co_Regress()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 회귀 " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
SuppressPets();
|
|
|
|
|
|
me.Immortal(1);
|
|
|
|
|
|
var anim = me.GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
|
|
|
|
|
// 자동 교전 그대로 두고 관찰만 한다(어그로 조작 없음)
|
|
|
|
|
|
var clips = new List<string>();
|
|
|
|
|
|
var hitTargets = new Dictionary<string, double>();
|
|
|
|
|
|
int placements0 = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
Vector3 p0 = me.transform.position;
|
|
|
|
|
|
float maxMove = 0f;
|
|
|
|
|
|
string last = "";
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
|
|
|
|
|
|
while (Time.time - t0 < 14f)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
string c = ClipName(anim);
|
|
|
|
|
|
if (c != last && c.Contains("Attack")) { clips.Add(c); last = c; }
|
|
|
|
|
|
else if (c != last) last = c;
|
|
|
|
|
|
maxMove = Mathf.Max(maxMove, Vector3.Distance(p0, me.transform.position));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var m in UnityEngine.Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
|
|
|
|
|
|
{
|
|
|
|
|
|
string k = m.GetInstanceID().ToString();
|
|
|
|
|
|
double hp = HP(m);
|
|
|
|
|
|
if (double.IsNaN(hp)) continue;
|
|
|
|
|
|
if (!hitTargets.ContainsKey(k)) hitTargets[k] = hp;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int damaged = 0;
|
|
|
|
|
|
foreach (var kv in hitTargets)
|
|
|
|
|
|
foreach (var m in UnityEngine.Object.FindObjectsByType<MobActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
|
|
|
|
if (m.GetInstanceID().ToString() == kv.Key && HP(m) < kv.Value) { damaged++; break; }
|
|
|
|
|
|
|
|
|
|
|
|
sb.AppendLine("자동 교전 14 s — 공격 클립 전이 " + clips.Count + "회");
|
|
|
|
|
|
var uniq = new HashSet<string>(clips);
|
|
|
|
|
|
sb.AppendLine(" 등장 클립: " + string.Join(", ", uniq));
|
|
|
|
|
|
sb.AppendLine(" 3콤보 성립(Attack1/2/3 모두 등장) = " + (uniq.Count >= 3));
|
|
|
|
|
|
sb.AppendLine(" #792 이펙트 배치 " + (WeaponTrailDriver.MappedSpawnCount - placements0) + "회");
|
|
|
|
|
|
sb.AppendLine(" HP 가 줄어든 몬스터 " + damaged + "체 (히트 판정 정상)");
|
|
|
|
|
|
sb.AppendLine(" 이동(대쉬 포함) 최대 변위 " + maxMove.ToString("F2") + " m");
|
|
|
|
|
|
sb.AppendLine(" " + Stats());
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ④ 예산 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
public IEnumerator Co_Budget()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.AppendLine("# WL #792 예산 " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
var me = Me();
|
|
|
|
|
|
if (me == null) { Done(sb); yield break; }
|
|
|
|
|
|
SuppressPets();
|
|
|
|
|
|
me.Immortal(1);
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
|
sb.AppendLine("기준(공격 없음) " + Stats());
|
|
|
|
|
|
|
|
|
|
|
|
int before = WeaponTrailDriver.MappedSpawnCount;
|
|
|
|
|
|
me.Play_Attack(0, (float)ASpd(me));
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
|
int maxSet = 0, maxBatch = 0, maxDraw = 0, maxTri = 0;
|
|
|
|
|
|
while (Time.time - t0 < 1.2f)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
|
|
maxSet = Mathf.Max(maxSet, UnityEditor.UnityStats.setPassCalls);
|
|
|
|
|
|
maxBatch = Mathf.Max(maxBatch, UnityEditor.UnityStats.batches);
|
|
|
|
|
|
maxDraw = Mathf.Max(maxDraw, UnityEditor.UnityStats.drawCalls);
|
|
|
|
|
|
maxTri = Mathf.Max(maxTri, UnityEditor.UnityStats.triangles);
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.AppendLine(string.Format("스윙 중 최대 setPass={0} batches={1} draw={2} tris={3}", maxSet, maxBatch, maxDraw, maxTri));
|
|
|
|
|
|
sb.AppendLine("예산 — SetPass ≤150 · 배치 ≤300 · 드로우 ≤400 · 삼각형 ≤30만");
|
|
|
|
|
|
sb.AppendLine("배치 횟수 " + (WeaponTrailDriver.MappedSpawnCount - before));
|
|
|
|
|
|
|
|
|
|
|
|
// 살아 있는 이펙트 인스턴스의 파티클 수
|
|
|
|
|
|
int parts = 0;
|
|
|
|
|
|
foreach (var name in new[] { "Effect_WLSwingArc", "Effect_WLStab" })
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var ps in UnityEngine.Object.FindObjectsByType<ParticleSystem>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
|
|
|
|
|
|
{
|
|
|
|
|
|
var root = ps.transform.root;
|
|
|
|
|
|
if (root != null && root.name.StartsWith(name)) parts += ps.particleCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.AppendLine("살아 있는 매핑 이펙트 파티클 합 " + parts + " (목표 ≤ 30)");
|
|
|
|
|
|
Done(sb);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string Stats()
|
|
|
|
|
|
{
|
|
|
|
|
|
return "setPass=" + UnityEditor.UnityStats.setPassCalls +
|
|
|
|
|
|
" batches=" + UnityEditor.UnityStats.batches +
|
|
|
|
|
|
" draw=" + UnityEditor.UnityStats.drawCalls +
|
|
|
|
|
|
" tris=" + UnityEditor.UnityStats.triangles;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Done(StringBuilder sb)
|
|
|
|
|
|
{
|
|
|
|
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
|
|
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "v_" + tag_ + ".txt"),
|
|
|
|
|
|
sb.ToString(), new System.Text.UTF8Encoding(false));
|
|
|
|
|
|
Debug.Log(sb.ToString());
|
|
|
|
|
|
var go = GameObject.Find("__wl792verify");
|
|
|
|
|
|
if (go != null) UnityEngine.Object.Destroy(go);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|