// WL761_Seq.cs — PD 지시 #761 + #762 런타임 검증 시퀀스 (Play 중 · Assets 무수정 · 서버 쓰기 없음) // unity command run_script --file AgentScripts/WL761_Seq.cs --entry WL761_Seq.RunAt --args '[10101, "10101", 1.5, 1]' // unity command run_script --file AgentScripts/WL761_Seq.cs --entry WL761_Seq.Report --args '["10101"]' // unity command run_script --file AgentScripts/WL761_Seq.cs --entry WL761_Seq.SetArc --args '[0, 0, 0, 1.0, 0]' (euler xyz, scale, anchor) // unity command run_script --file AgentScripts/WL761_Seq.cs --entry WL761_Seq.Cfg // WL760_Seq 확장판: 클립/HP/마스크/트레일 + **PC 위치 시계열(#762)** + **PrefabArc 배치 정보(#761)** 를 기록하고 // 호가 놓이는 순간(LastArcInfo 변화)에 캡처한다. 결과 = Screenshots_WL/combat3/seq_.txt (Assets 밖). 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 WL761_Seq { const string OutDir = "Screenshots_WL/combat3"; const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; public static object RunAt(int classId, string tag, float dist, int noPet) { if (!Application.isPlaying) return "not playing"; var me = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault(); if (me == null) return "MyActor none"; if (GameObject.Find("__wl761seq") != null) return "sequence already running"; var host = new GameObject("__wl761seq").AddComponent(); host.StartCoroutine(host.Co(me, classId, tag, dist, noPet != 0)); return "started " + tag + " dist=" + dist + " noPet=" + noPet; } public static object Report(string tag) { var p = System.IO.Path.Combine(OutDir, "seq_" + tag + ".txt"); return System.IO.File.Exists(p) ? System.IO.File.ReadAllText(p) : "(no report yet) " + p; } /// 런타임 SlashTrailSettings 의 PrefabArc 값을 바꾼다(메모리만 · 디스크 저장 없음 · 캡처 반복용). public static object SetArc(float ex, float ey, float ez, float scale, int anchor) { var st = WL.Combat.SlashTrailSettings.Instance; if (st == null) return "settings null"; st.arcEulerOffset = new Vector3(ex, ey, ez); st.arcScale = scale; st.arcAnchor = (WL.Combat.ArcAnchor)anchor; WL.Combat.WeaponTrailDriver.ClearArcCache(); return Cfg(); } /// 런타임 burstMode / suppressMode / drawRibbon / fitToBlade 를 바꾼다(메모리만 · 판정 ⓔ 롤백 확인용). public static object SetBurst(int burstMode, int suppressMode, int drawRibbon, int fitToBlade) { var st = WL.Combat.SlashTrailSettings.Instance; if (st == null) return "settings null"; st.burstMode = (WL.Combat.BurstMode)burstMode; st.suppressMode = (WL.Combat.SuppressMode)suppressMode; st.drawRibbon = drawRibbon != 0; st.arcFitToBlade = fitToBlade != 0; WL.Combat.WeaponTrailDriver.ClearArcCache(); return Cfg(); } /// 런타임 설정 요약. public static object Cfg() { var st = WL.Combat.SlashTrailSettings.Instance; var mo = WL.Combat.WLCombatMotionSettings.Instance; var sb = new StringBuilder(); sb.AppendLine(st == null ? "SlashTrailSettings=null" : string.Format("Slash: enable={0} burstMode={1} burstTiming={2} suppress={3} keep=[{4}] drawRibbon={5} autoOrient={6} euler={7} fit={8} scale={9} anchor={10} minSamples={11}", st.enableWeaponTrail, st.burstMode, st.burstTiming, st.suppressMode, st.keepEmitterNames != null ? string.Join(",", st.keepEmitterNames) : "", st.drawRibbon, st.arcAutoOrient, st.arcEulerOffset.ToString("F0"), st.arcFitToBlade, st.arcScale, st.arcAnchor, st.arcMinSamples)); sb.AppendLine(mo == null ? "WLCombatMotionSettings=null" : string.Format("Motion: enable={0} sec={1} scale={2} max={3} easeOut={4} stopDist={5}", mo.enableAttackStep, mo.attackStepSeconds, mo.attackStepScale, mo.attackStepMaxDistance, mo.attackStepEaseOut, mo.attackStepStopDistance)); sb.AppendLine("lastArc=" + (WL.Combat.WeaponTrailDriver.LastArcInfo ?? "(none)")); sb.AppendLine("step: count=" + WL.Combat.AttackStepDriver.StepCount + " planned=" + WL.Combat.AttackStepDriver.LastPlannedDistance.ToString("F2") + " actual=" + WL.Combat.AttackStepDriver.LastActualDistance.ToString("F2") + " " + (WL.Combat.AttackStepDriver.LastInfo ?? "")); return sb.ToString(); } /// 런타임 모션 설정을 바꾼다(메모리만). public static object SetMotion(int enable, float seconds, float scale) { var mo = WL.Combat.WLCombatMotionSettings.Instance; if (mo == null) return "motion settings null"; mo.enableAttackStep = enable != 0; if (seconds > 0f) mo.attackStepSeconds = seconds; if (scale >= 0f) mo.attackStepScale = scale; return Cfg(); } class SeqHost : 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 void SuppressPets(bool suppress) { foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (p.gameObject.activeSelf == suppress) p.gameObject.SetActive(!suppress); } public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet) { var sb = new StringBuilder(); System.IO.Directory.CreateDirectory(OutDir); if (noPet) 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("class=" + classId + " ctrl=" + (anim != null && anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "(null)") + " scene=" + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); sb.AppendLine(Cfg().ToString()); var mobs = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None) .Where(m => HP(m) > 0).OrderBy(m => Vector3.Distance(m.transform.position, me.transform.position)).ToArray(); if (mobs.Length == 0) { sb.AppendLine("no live MobActor"); Done(tag, sb); yield break; } var t = mobs[0]; var dir = t.transform.position - me.transform.position; dir.y = 0f; if (Mathf.Abs(dir.magnitude - dist) > 0.1f) { var pos = t.transform.position - dir.normalized * dist; var na = me.GetComponent(); if (na != null && na.enabled) na.Warp(pos); else me.transform.position = pos; } me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z)); var setT = typeof(Actor).GetMethod("Change_Target", BF); if (setT != null) { try { setT.Invoke(me, new object[] { false, (Actor)t, false }); } catch { } } 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) < 8f).ToList(); var lastHpAll = watch.ToDictionary(m => m, m => HP(m)); double hp0 = HP(t); Vector3 pos0 = me.transform.position; sb.AppendLine("target=" + t.name + " hp0=" + hp0.ToString("F0") + " d=" + Vector3.Distance(pos0, t.transform.position).ToString("F2") + " pcPos0=" + pos0.ToString("F2") + " watched=" + watch.Count); double aspd = 1.0; 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) aspd = Convert.ToDouble(gm.Invoke(stat, new object[] { eStat.FinalAttackSpeed })); } catch { } me.Play_Attack(0, (float)aspd); sb.AppendLine("Play_Attack(0," + aspd.ToString("F2") + ")"); float t0 = Time.time; string lastClip = ""; int caps = 0; var seenMasks = new HashSet(); var clipLog = new List(); var hpLog = new List(); var maskLog = new List(); var arcLog = new List(); var posLog = new List(); var stepLog = new List(); string lastArc = WL.Combat.WeaponTrailDriver.LastArcInfo; int lastStep = WL.Combat.AttackStepDriver.StepCount; float nextCapAt = -1f; string capClip = ""; float lastPosLog = -1f; float maxOffMesh = 0f; Vector3 prevPos = pos0; while (Time.time - t0 < 4.2f) { if (noPet) 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("F2") + "s " + clip); lastClip = clip; capClip = clip; } // ── #762 PC 위치 시계열 (0.05 s 간격 + NavMesh 이탈 검사) if (el - lastPosLog >= 0.05f) { lastPosLog = el; Vector3 p = me.transform.position; NavMeshHit nh; float off = 0f; if (NavMesh.SamplePosition(p, out nh, 2f, NavMesh.AllAreas)) off = Vector3.Distance(p, nh.position); else off = 999f; if (off > maxOffMesh) maxOffMesh = off; posLog.Add(string.Format("{0:F2}s pos={1} Δ0={2:F3} Δf={3:F3} off={4:F3} clip={5} n={6:F2}", el, p.ToString("F2"), Vector3.Distance(pos0, p), Vector3.Distance(prevPos, p), off, clip, nt)); prevPos = p; } if (WL.Combat.AttackStepDriver.StepCount != lastStep) { lastStep = WL.Combat.AttackStepDriver.StepCount; stepLog.Add(string.Format("{0:F2}s STEP#{1} planned={2:F2} clip={3}", el, lastStep, WL.Combat.AttackStepDriver.LastPlannedDistance, clip)); } foreach (var m in watch) { if (m == null) continue; double hp = HP(m); double prev = lastHpAll[m]; if (Math.Abs(hp - prev) > 0.01) { hpLog.Add(el.ToString("F2") + "s " + m.name + " " + prev.ToString("F0") + " -> " + hp.ToString("F0") + " (" + clip + ")"); lastHpAll[m] = hp; } } foreach (var m in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { int id = m.GetInstanceID(); if (seenMasks.Add(id)) maskLog.Add(el.ToString("F2") + "s " + m.gameObject.name + " :: " + m.DumpState()); } // ── #761 호가 놓이는 순간에 캡처 string arcNow = WL.Combat.WeaponTrailDriver.LastArcInfo; if (arcNow != lastArc) { lastArc = arcNow; arcLog.Add(el.ToString("F2") + "s " + arcNow); nextCapAt = el + 0.03f; } if (nextCapAt > 0f && el >= nextCapAt) { nextCapAt = -1f; yield return new WaitForEndOfFrame(); Texture2D tex = null; try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch { } if (tex != null) { string safe = capClip.Replace("&", "n").Replace("/", "_"); var path = System.IO.Path.Combine(OutDir, "seq_" + tag + "_" + (++caps) + "_" + safe + ".png"); System.IO.File.WriteAllBytes(path, tex.EncodeToPNG()); UnityEngine.Object.Destroy(tex); arcLog.Add(el.ToString("F2") + "s CAP " + path); } continue; } yield return null; } sb.AppendLine("== clips =="); foreach (var s in clipLog) sb.AppendLine(" " + s); sb.AppendLine("== arc placements (" + arcLog.Count + ") =="); foreach (var s in arcLog) sb.AppendLine(" " + s); sb.AppendLine("== slash masks (" + maskLog.Count + ") =="); foreach (var s in maskLog) sb.AppendLine(" " + s); sb.AppendLine("== attack steps (" + stepLog.Count + ") =="); foreach (var s in stepLog) sb.AppendLine(" " + s); sb.AppendLine("== pc position (" + posLog.Count + ") =="); foreach (var s in posLog) sb.AppendLine(" " + s); sb.AppendLine("== hp changes (" + hpLog.Count + ") =="); foreach (var s in hpLog) sb.AppendLine(" " + s); Vector3 pend = me.transform.position; sb.AppendLine("final pcPos=" + pend.ToString("F2") + " totalΔ=" + Vector3.Distance(pos0, pend).ToString("F3") + " m" + " maxNavMeshOff=" + maxOffMesh.ToString("F3") + " m" + " dmgTotal=" + (hp0 - HP(t)).ToString("F0") + " hits=" + hpLog.Count); if (noPet) SuppressPets(false); Done(tag, sb); } void Done(string tag, StringBuilder sb) { System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "seq_" + tag + ".txt"), sb.ToString()); Destroy(gameObject); } } }