// 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); } } }