// WL785_Probe.cs — PD 지시 #785/#786/#787 런타임 검증 (Play 전용 · 서버 쓰기 없음 · Assets 무수정)
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.Combo --args '[10501, "combo10501", 1.2]'
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.Dir --args '[10501, "dir10501"]'
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.MoveAtk--args '["move10501"]'
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.Dash --args '["dash30", 3.0]'
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.Report --args '["combo10501"]'
// unity command run_script --file AgentScripts/WL785_Probe.cs --entry WL785_Probe.Regress
//
// 측정 항목
// ⓐ 콤보 : 클립 전이 시각 · 몹 HP 감소(=타격) 횟수/시각 · 콤보 총 소요
// ⓑ 리본 : 매 프레임 (드라이버가 읽은 실제 칼끝/손잡이) vs (리본이 마지막에 기록한 점) 오차 —
// 리본이 칼날 위에 생기는지의 직접 증거. 메시 마지막 리브의 정점 2개도 함께 잰다.
// ⓒ 예산 : UnityStats(setPass/batches/drawCalls/triangles) 를 전투 중 최대값으로 기록
// ⓓ 대쉬 : DashDriver 의 정적 카운터(시작 시각·이동 거리·공격 전환 여부)
//
// BladeTrail 은 HideFlags.DontSave 라 FindObjectsByType 에 잡히지 않는다 → 드라이버의 GetTrail(i) 로 읽는다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.AI;
public static class WL785_Probe
{
const string OutDir = "Screenshots_WL/combat5";
const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
// ── 진입점 ──────────────────────────────────────────────────────────────
public static object Combo(int classId, string tag, float dist)
{
return Start(h => h.CoCombo(classId, tag, dist), tag);
}
public static object Dir(int classId, string tag)
{
return Start(h => h.CoDir(classId, tag), tag);
}
public static object MoveAtk(string tag)
{
return Start(h => h.CoMoveAtk(tag), tag);
}
public static object Dash(string tag, float dist)
{
return Start(h => h.CoDash(tag, dist), tag);
}
/// 스윙 한 번을 from~to 초 구간에서 step 간격으로 연속 캡처(리본 룩 육안 확인용).
public static object Burst(string tag, float from, float to, float step)
{
return Start(h => h.CoBurst(tag, from, to, step), tag);
}
static object Start(Func make, string tag)
{
if (!Application.isPlaying) return "not playing";
if (GameObject.Find("__wl785probe") != null) return "probe already running";
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
if (me == null) return "MyActor none";
var host = new GameObject("__wl785probe").AddComponent();
host.StartCoroutine(make(host));
return "started " + tag;
}
public static object Report(string tag)
{
var p = System.IO.Path.Combine(OutDir, "p_" + tag + ".txt");
return System.IO.File.Exists(p) ? System.IO.File.ReadAllText(p) : "(no report yet) " + p;
}
/// 회귀 점검 — 펫·무적·어그로·자동교전 설정과 현재 씬 상태를 한 번에 읽는다.
public static object Regress()
{
if (!Application.isPlaying) return "not playing";
var sb = new StringBuilder();
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
var pets = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
sb.AppendLine("scene=" + string.Join("+", Enumerable.Range(0, UnityEngine.SceneManagement.SceneManager.sceneCount)
.Select(i => UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).name)));
sb.AppendLine("PC=" + (me != null ? me.name + " id=" + me.Get_ID() : "(none)") + " 활성 펫=" + pets.Length);
sb.AppendLine("spawnPlayerPet=" + WL.Settings.WLGameplaySettings.SpawnPlayerPet +
" playerInvincible=" + WL.Settings.WLGameplaySettings.PlayerInvincible);
sb.AppendLine("pcAggroOnHit=" + WL.Settings.WLTargetingSettings.PcAggroOnHit +
" autoEngage=" + WL.Settings.WLTargetingSettings.AutoEngage +
" retargetOnKill=" + WL.Settings.WLTargetingSettings.RetargetOnKill +
" attackStartRetarget=" + WL.Settings.WLTargetingSettings.AttackStartRetarget);
var st = WL.Combat.SlashTrailSettings.Instance;
sb.AppendLine("trail: enable=" + (st != null && st.enableWeaponTrail) + " drawRibbon=" + (st != null && st.drawRibbon) +
" burstMode=" + (st != null ? st.burstMode.ToString() : "?") +
" mat=" + (st != null && st.materialAdditive != null ? st.materialAdditive.name + "/" + st.materialAdditive.shader.name : "(null)"));
var mc = WL.Combat.WLCombatMotionSettings.Instance;
sb.AppendLine("motion: attackStep=" + (mc != null && mc.enableAttackStep) +
" dash=" + (mc != null && mc.dashEnabled) +
" window=[" + (mc != null ? mc.dashMinDistance + "," + mc.dashMaxDistance : "?") + "]" +
" dashStop=" + (mc != null ? mc.dashStopDistance.ToString() : "?"));
sb.AppendLine("DashDriver: count=" + WL.Combat.DashDriver.DashCount + " last=" + WL.Combat.DashDriver.LastInfo);
sb.AppendLine("AttackStep: count=" + WL.Combat.AttackStepDriver.StepCount + " last=" + (WL.Combat.AttackStepDriver.LastInfo ?? "(none)"));
sb.AppendLine(Budget("now"));
return sb.ToString();
}
public static string Budget(string label)
{
return "budget[" + label + "] setPass=" + UnityEditor.UnityStats.setPassCalls +
" batches=" + UnityEditor.UnityStats.batches +
" draw=" + UnityEditor.UnityStats.drawCalls +
" tris=" + UnityEditor.UnityStats.triangles +
" verts=" + UnityEditor.UnityStats.vertices;
}
// ── 코루틴 호스트 ────────────────────────────────────────────────────────
class Host : MonoBehaviour
{
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 Actor Target(Actor a)
{
var f = typeof(Actor).GetField("m_Target", BF);
return f != null ? f.GetValue(a) as Actor : null;
}
static void SuppressPets(bool s)
{
foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (p.gameObject.activeSelf == s) p.gameObject.SetActive(!s);
}
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 SetTarget(Actor me, Actor t)
{
var m = typeof(Actor).GetMethod("Change_Target", BF);
if (m != null) { try { m.Invoke(me, new object[] { false, t, false }); } catch { } }
}
static MobActor Nearest(Actor me)
{
return UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)
.Where(m => HP(m) > 0)
.OrderBy(m => Vector3.Distance(m.transform.position, me.transform.position))
.FirstOrDefault();
}
static void WarpTo(Actor me, Vector3 pos)
{
var na = me.GetComponent();
NavMeshHit hit;
if (NavMesh.SamplePosition(pos, out hit, 4f, NavMesh.AllAreas)) pos = hit.position;
if (na != null && na.enabled) na.Warp(pos); else me.transform.position = pos;
}
static void Done(string tag, StringBuilder sb)
{
System.IO.Directory.CreateDirectory(OutDir);
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "p_" + tag + ".txt"), sb.ToString());
var go = GameObject.Find("__wl785probe");
if (go != null) UnityEngine.Object.Destroy(go);
}
IEnumerator Capture(string tag, string suffix, List log, float el)
{
yield return new WaitForEndOfFrame();
Texture2D tex = null;
try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch (Exception e) { log.Add("cap ex " + e.GetType().Name); }
if (tex != null)
{
System.IO.Directory.CreateDirectory(OutDir);
var path = System.IO.Path.Combine(OutDir, tag + "_" + suffix + ".png");
System.IO.File.WriteAllBytes(path, tex.EncodeToPNG());
UnityEngine.Object.Destroy(tex);
log.Add(el.ToString("F2") + "s CAP " + path);
}
}
// ── 리본 정합 측정 ───────────────────────────────────────────────────
class RibbonStat
{
public float maxTipErr, maxHiltErr, sumTipErr, sumHiltErr;
public int n, maxSamples, maxTris, maxVerts;
public string matInfo = "(none)", lastRib = "(none)";
public int interpolated;
public override string ToString()
{
return string.Format(
"리본정합 n={0} tip오차 max={1:F4}m avg={2:F4}m hilt오차 max={3:F4}m avg={4:F4}m " +
"샘플max={5} 삼각형max={6} 정점max={7} 보간샘플={8}\n mat={9}\n 마지막리브={10}",
n, maxTipErr, n > 0 ? sumTipErr / n : 0f, maxHiltErr, n > 0 ? sumHiltErr / n : 0f,
maxSamples, maxTris, maxVerts, interpolated, matInfo, lastRib);
}
}
// ★ 반드시 WaitForEndOfFrame 뒤에서 부른다. Update 타이밍에 재면 드라이버의 LateUpdate 가
// 아직 이번 프레임 샘플을 넣기 전이라, "칼날 현재 위치 vs 리본 마지막 점" 이 한 프레임 어긋난다.
static void SampleRibbon(RibbonStat rs)
{
var drivers = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (var d in drivers)
{
if (!d.IsSwinging) continue;
for (int i = 0; i < d.ActiveSlotCount; i++)
{
var tr = d.GetTrail(i);
if (tr == null || tr.SampleCount < 1) continue;
int last = tr.SampleCount - 1;
float te = Vector3.Distance(d.GetTipWorld(i), tr.GetTip(last));
float he = Vector3.Distance(d.GetHiltWorld(i), tr.GetHilt(last));
rs.maxTipErr = Mathf.Max(rs.maxTipErr, te);
rs.maxHiltErr = Mathf.Max(rs.maxHiltErr, he);
rs.sumTipErr += te; rs.sumHiltErr += he; rs.n++;
rs.maxSamples = Mathf.Max(rs.maxSamples, tr.SampleCount);
rs.maxTris = Mathf.Max(rs.maxTris, tr.TriangleCount);
rs.interpolated = Mathf.Max(rs.interpolated, tr.InterpolatedSamples);
var mf = tr.GetComponent(); var mr = tr.GetComponent();
if (mf != null && mf.sharedMesh != null)
{
rs.maxVerts = Mathf.Max(rs.maxVerts, mf.sharedMesh.vertexCount);
int vc = mf.sharedMesh.vertexCount;
if (vc >= 2)
{
var vs = mf.sharedMesh.vertices;
rs.lastRib = string.Format("inner={0} outer={1} 폭={2:F3}m | 실칼날 hilt={3} tip={4} 길이={5:F3}m",
vs[vc - 2].ToString("F3"), vs[vc - 1].ToString("F3"),
Vector3.Distance(vs[vc - 2], vs[vc - 1]),
d.GetHiltWorld(i).ToString("F3"), d.GetTipWorld(i).ToString("F3"),
Vector3.Distance(d.GetHiltWorld(i), d.GetTipWorld(i)));
}
}
if (mr != null && mr.sharedMaterial != null)
rs.matInfo = mr.sharedMaterial.name + " / " + mr.sharedMaterial.shader.name +
" materials=" + mr.sharedMaterials.Length + " enabled=" + mr.enabled;
}
}
}
// ── ⓐ 콤보 ───────────────────────────────────────────────────────────
public IEnumerator CoCombo(int classId, string tag, float dist)
{
var sb = new StringBuilder();
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First();
SuppressPets(true);
var pc = me as PCActor;
if (classId > 0 && pc != null)
{
var data = table_classconfig.Ins.Get_Data_orNull(classId);
if (data == null) { sb.AppendLine("class data null " + classId); Done(tag, sb); yield break; }
pc.Change_Class(data, true);
yield return new WaitForSeconds(1.5f);
}
var anim = me.GetComponentInChildren();
sb.AppendLine("== #786 콤보 검증 class=" + classId + " ctrl=" +
(anim != null && anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "(null)") + " ==");
var t = Nearest(me);
if (t == null) { sb.AppendLine("no live mob"); Done(tag, sb); yield break; }
var dir = t.transform.position - me.transform.position; dir.y = 0f;
WarpTo(me, t.transform.position - dir.normalized * dist);
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
SetTarget(me, t);
yield return null;
var watch = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)
.Where(m => HP(m) > 0 && Vector3.Distance(m.transform.position, me.transform.position) < 7f).ToList();
var lastHp = watch.ToDictionary(m => m, m => HP(m));
sb.AppendLine("target=" + t.name + " d=" + Vector3.Distance(me.transform.position, t.transform.position).ToString("F2") +
" watched=" + watch.Count);
me.Play_Attack(0, (float)ASpd(me));
float t0 = Time.time;
string lastClip = ""; int caps = 0;
var clipLog = new List(); var hitLog = new List(); var capLog = new List();
var rs = new RibbonStat();
int maxSetPass = 0, maxBatch = 0, maxDraw = 0, maxTri = 0;
float nextCap = -1f; string capName = "";
float firstHit = -1f, lastHit = -1f;
while (Time.time - t0 < 4.0f)
{
SuppressPets(true);
float el = Time.time - t0;
string clip = "(none)"; float nt = 0f;
if (anim != null && anim.layerCount > 0)
{
var ci = anim.GetCurrentAnimatorClipInfo(0);
if (ci.Length > 0) clip = ci[0].clip.name;
nt = anim.GetCurrentAnimatorStateInfo(0).normalizedTime;
}
if (clip != lastClip)
{
clipLog.Add(el.ToString("F3") + "s " + clip + (anim.IsInTransition(0) ? " (전이중)" : ""));
lastClip = clip;
if (clip.IndexOf("Attack", StringComparison.OrdinalIgnoreCase) >= 0) { nextCap = el + 0.12f; capName = clip; }
}
foreach (var m in watch)
{
if (m == null) continue;
double hp = HP(m), prev = lastHp[m];
if (Math.Abs(hp - prev) > 0.01)
{
hitLog.Add(el.ToString("F3") + "s " + m.name + " " + prev.ToString("F0") + " -> " + hp.ToString("F0") +
" (clip=" + clip + " n=" + nt.ToString("F2") + ")");
lastHp[m] = hp;
if (firstHit < 0f) firstHit = el;
lastHit = el;
}
}
yield return new WaitForEndOfFrame();
SampleRibbon(rs);
maxSetPass = Mathf.Max(maxSetPass, UnityEditor.UnityStats.setPassCalls);
maxBatch = Mathf.Max(maxBatch, UnityEditor.UnityStats.batches);
maxDraw = Mathf.Max(maxDraw, UnityEditor.UnityStats.drawCalls);
maxTri = Mathf.Max(maxTri, UnityEditor.UnityStats.triangles);
if (nextCap > 0f && el >= nextCap)
{
nextCap = -1f;
yield return Capture(tag, (++caps) + "_" + capName.Replace("&", "n"), capLog, el);
continue;
}
yield return null;
}
sb.AppendLine("-- 클립 전이 --"); foreach (var s in clipLog) sb.AppendLine(" " + s);
sb.AppendLine("-- 타격(몹 HP 감소) 총 " + hitLog.Count + "회 --"); foreach (var s in hitLog) sb.AppendLine(" " + s);
sb.AppendLine("콤보 소요(첫 타격→마지막 타격) = " + (firstHit >= 0f ? (lastHit - firstHit).ToString("F3") + "s" : "?") +
" 첫 타격 " + firstHit.ToString("F3") + "s");
sb.AppendLine("-- " + rs + " --");
sb.AppendLine("budget(전투 중 최대) setPass=" + maxSetPass + " batches=" + maxBatch + " draw=" + maxDraw + " tris=" + maxTri);
sb.AppendLine("-- 캡처 --"); foreach (var s in capLog) sb.AppendLine(" " + s);
SuppressPets(false);
Done(tag, sb);
}
// ── ⓑ 4방향 ─────────────────────────────────────────────────────────
public IEnumerator CoDir(int classId, string tag)
{
var sb = new StringBuilder("== #785 4방향 공격 검증 class=" + classId + " ==\n");
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First();
SuppressPets(true);
var pc = me as PCActor;
if (classId > 0 && pc != null)
{
var data = table_classconfig.Ins.Get_Data_orNull(classId);
if (data != null) { pc.Change_Class(data, true); yield return new WaitForSeconds(1.5f); }
}
var anim = me.GetComponentInChildren();
var t = Nearest(me);
if (t == null) { sb.AppendLine("no live mob"); Done(tag, sb); yield break; }
string[] names = { "N", "E", "S", "W" };
Vector3[] offs = { Vector3.forward, Vector3.right, Vector3.back, Vector3.left };
var capLog = new List();
for (int k = 0; k < 4; k++)
{
// 몹의 북/동/남/서 1.2 m 지점에 서서 몹을 향해 때린다 → 스윙 방향이 4가지로 갈린다.
WarpTo(me, t.transform.position + offs[k] * 1.2f);
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
SetTarget(me, t);
yield return null;
yield return null;
var rs = new RibbonStat();
double hp0 = HP(t);
me.Play_Attack(0, (float)ASpd(me));
float t0 = Time.time; bool captured = false;
while (Time.time - t0 < 0.60f)
{
yield return new WaitForEndOfFrame();
SampleRibbon(rs);
if (!captured && Time.time - t0 > 0.38f)
{
captured = true;
yield return Capture(tag, names[k], capLog, Time.time - t0);
continue;
}
yield return null;
}
sb.AppendLine("[" + names[k] + "] pc=" + me.transform.position.ToString("F2") +
" fwd=" + me.transform.forward.ToString("F2") +
" dmg=" + (hp0 - HP(t)).ToString("F0"));
sb.AppendLine(" " + rs);
// 다음 방향 전에 콤보를 끊는다
me.Play_Idle();
yield return new WaitForSeconds(1.2f);
}
sb.AppendLine("-- 캡처 --"); foreach (var s in capLog) sb.AppendLine(" " + s);
SuppressPets(false);
Done(tag, sb);
}
// ── ⓒ 이동 중 공격 ───────────────────────────────────────────────────
public IEnumerator CoMoveAtk(string tag)
{
var sb = new StringBuilder("== #785 이동 중 공격(리본이 무기와 분리되지 않는가) ==\n");
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First();
SuppressPets(true);
var t = Nearest(me);
if (t == null) { sb.AppendLine("no live mob"); Done(tag, sb); yield break; }
var dir = t.transform.position - me.transform.position; dir.y = 0f;
WarpTo(me, t.transform.position - dir.normalized * 1.2f);
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
SetTarget(me, t);
yield return null;
var agent = me.GetComponent();
var rs = new RibbonStat();
var capLog = new List();
Vector3 side = Vector3.Cross(Vector3.up, me.transform.forward).normalized;
me.Play_Attack(0, (float)ASpd(me));
float t0 = Time.time; bool cap = false;
Vector3 p0 = me.transform.position;
while (Time.time - t0 < 1.2f)
{
// 공격 도중 강제로 옆으로 민다 — 궤적이 캐릭터를 따라오는지 본다.
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.Move(side * (2.5f * Time.deltaTime));
yield return new WaitForEndOfFrame();
SampleRibbon(rs);
if (!cap && Time.time - t0 > 0.32f) { cap = true; yield return Capture(tag, "moving", capLog, Time.time - t0); continue; }
yield return null;
}
sb.AppendLine("강제 측면 이동 = " + Vector3.Distance(p0, me.transform.position).ToString("F2") + "m");
sb.AppendLine(rs.ToString());
foreach (var s in capLog) sb.AppendLine(" " + s);
SuppressPets(false);
Done(tag, sb);
}
// ── 리본 룩 연속 캡처 ────────────────────────────────────────────────
public IEnumerator CoBurst(string tag, float from, float to, float step)
{
var sb = new StringBuilder("== 리본 연속 캡처 " + from + "~" + to + "s / " + step + "s ==\n");
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First();
SuppressPets(true);
var t = Nearest(me);
if (t == null) { sb.AppendLine("no live mob"); Done(tag, sb); yield break; }
var dir = t.transform.position - me.transform.position; dir.y = 0f;
WarpTo(me, t.transform.position - dir.normalized * 1.3f);
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
SetTarget(me, t);
me.Play_Idle();
yield return null; yield return null;
var rs = new RibbonStat();
var capLog = new List();
me.Play_Attack(0, (float)ASpd(me));
float t0 = Time.time;
float next = from;
int idx = 0;
while (Time.time - t0 < to + 0.1f)
{
yield return new WaitForEndOfFrame();
SampleRibbon(rs);
float el = Time.time - t0;
if (el >= next && next <= to)
{
idx++;
yield return Capture(tag, idx.ToString("00") + "_" + Mathf.RoundToInt(el * 1000f) + "ms", capLog, el);
next += step;
continue;
}
yield return null;
}
sb.AppendLine(rs.ToString());
foreach (var s in capLog) sb.AppendLine(" " + s);
SuppressPets(false);
Done(tag, sb);
}
// ── ⓓ 대쉬 ──────────────────────────────────────────────────────────
public IEnumerator CoDash(string tag, float dist)
{
var sb = new StringBuilder("== #787 대쉬 검증 목표거리=" + dist.ToString("F1") + "m ==\n");
var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First();
SuppressPets(true);
var t = Nearest(me);
if (t == null) { sb.AppendLine("no live mob"); Done(tag, sb); yield break; }
int dash0 = WL.Combat.DashDriver.DashCount;
var dir = t.transform.position - me.transform.position; dir.y = 0f;
WarpTo(me, t.transform.position - dir.normalized * dist);
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
me.Play_Idle();
SetTarget(me, t);
yield return null;
float d0 = Vector3.Distance(me.transform.position, t.transform.position);
double hp0 = HP(t);
sb.AppendLine("시작 실제거리=" + d0.ToString("F2") + "m pc=" + me.transform.position.ToString("F2") +
" target=" + t.name);
var anim = me.GetComponentInChildren();
float t0 = Time.time;
string lastClip = ""; var log = new List(); var capLog = new List();
float dashStart = -1f, dashEnd = -1f, attackStart = -1f;
bool capped = false;
while (Time.time - t0 < 3.5f)
{
SuppressPets(true);
float el = Time.time - t0;
string clip = "(none)";
if (anim != null && anim.layerCount > 0)
{
var ci = anim.GetCurrentAnimatorClipInfo(0);
if (ci.Length > 0) clip = ci[0].clip.name;
}
if (clip != lastClip)
{
float dNow = Vector3.Distance(me.transform.position, t.transform.position);
log.Add(el.ToString("F3") + "s " + clip + " d=" + dNow.ToString("F2") + "m");
if (clip.IndexOf("Chase_Start", StringComparison.OrdinalIgnoreCase) >= 0 && dashStart < 0f) dashStart = el;
if (dashStart >= 0f && dashEnd < 0f && clip.IndexOf("Chase_Start", StringComparison.OrdinalIgnoreCase) < 0) dashEnd = el;
if (attackStart < 0f && clip.IndexOf("Attack", StringComparison.OrdinalIgnoreCase) >= 0) attackStart = el;
lastClip = clip;
}
if (!capped && dashStart >= 0f && el >= dashStart + 0.12f) { capped = true; yield return Capture(tag, "dash", capLog, el); continue; }
if (HP(t) < hp0 - 0.01) break; // 첫 타격이 들어가면 종료
yield return null;
}
float dEnd = Vector3.Distance(me.transform.position, t.transform.position);
sb.AppendLine("-- 클립 --"); foreach (var s in log) sb.AppendLine(" " + s);
sb.AppendLine("대쉬 진입=" + (dashStart >= 0f ? dashStart.ToString("F3") + "s" : "(없음)") +
" 대쉬 종료=" + (dashEnd >= 0f ? dashEnd.ToString("F3") + "s" : "-") +
" 공격 시작=" + (attackStart >= 0f ? attackStart.ToString("F3") + "s" : "(없음)"));
sb.AppendLine("이동 거리(거리 감소분)=" + (d0 - dEnd).ToString("F2") + "m 종료 거리=" + dEnd.ToString("F2") + "m" +
" 피해=" + (hp0 - HP(t)).ToString("F0"));
sb.AppendLine("DashDriver: count(+)=" + (WL.Combat.DashDriver.DashCount - dash0) +
" last=" + WL.Combat.DashDriver.LastInfo +
" startDist=" + WL.Combat.DashDriver.LastStartDistance.ToString("F2") +
" moved=" + WL.Combat.DashDriver.LastMovedDistance.ToString("F2") +
" endedInAttack=" + WL.Combat.DashDriver.LastEndedInAttack);
foreach (var s in capLog) sb.AppendLine(" " + s);
SuppressPets(false);
Done(tag, sb);
}
}
}