Project_WL/AgentScripts/WL792_Verify.cs

576 lines
29 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.

// 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); }
public static void ComboSpeed(int classId, float speed) { Launch(h => h.Co_ComboSpeed(classId, speed), "comboS" + classId); } // #804 클립 원속 재실측
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); }
public static void Trace3(int classId) { Launch(h => h.Co_Trace3(classId), "trace3_" + classId); } // #804 Attack3 한 클립 프레임 추적
/// <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);
}
// ── ②-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);
}
// ── ③ 이동 중 공격 ──────────────────────────────────────────────────
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);
}
}
}