345 lines
20 KiB
C#
345 lines
20 KiB
C#
// WL778_Seq.cs — PD 지시 #777 + #778 런타임 실측 (Play 중 · Assets 무수정 · 서버 쓰기 없음)
|
|
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.RunAt --args '[10501, "before_10501", 1.5, 1, 4.5]'
|
|
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.Report --args '["before_10501"]'
|
|
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.SetFollow --args '[1, 0, 0]'
|
|
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.Cfg
|
|
//
|
|
// #777 : 슬래시 이펙트 인스턴스의 월드 위치 vs PC 위치 vs 검(자루·검끝) 위치를 매 프레임 기록해
|
|
// 「스폰 후 PC 이동량」과 「이펙트가 무기에서 벌어진 거리」를 수치로 낸다.
|
|
// #778 : Animator 상태 진입/전이 시각 · 타격(몹 HP 감소) 시각 · 스윙(ShowEffect) 시각을 기록해
|
|
// 타격 간 간격과 콤보 사이클 길이를 낸다.
|
|
// 결과 = Screenshots_WL/combat4/seq_<tag>.txt + 캡처 PNG (모두 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 WL778_Seq
|
|
{
|
|
const string OutDir = "Screenshots_WL/combat4";
|
|
const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
|
|
public static object RunAt(int classId, string tag, float dist, int noPet, float seconds)
|
|
{
|
|
if (!Application.isPlaying) return "not playing";
|
|
var me = UnityEngine.Object.FindObjectsByType<MyActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
|
|
if (me == null) return "MyActor none";
|
|
if (GameObject.Find("__wl778seq") != null) return "sequence already running";
|
|
var host = new GameObject("__wl778seq").AddComponent<SeqHost>();
|
|
host.StartCoroutine(host.Co(me, classId, tag, dist, noPet != 0, seconds <= 0f ? 4.5f : seconds));
|
|
return "started " + tag + " class=" + classId + " dist=" + dist + " noPet=" + noPet + " sec=" + seconds;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>#777 추종 설정을 런타임에 바꾼다(메모리만 · 디스크 저장 없음 · a/b 비교용).</summary>
|
|
public static object SetFollow(int follow, int followRotation, float seconds)
|
|
{
|
|
var st = WL.Combat.SlashTrailSettings.Instance;
|
|
if (st == null) return "settings null";
|
|
st.arcFollowPlayer = follow != 0;
|
|
st.arcFollowRotation = followRotation != 0;
|
|
st.arcFollowSeconds = seconds;
|
|
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} timing={2} suppress={3} drawRibbon={4} autoOrient={5} fit={6} scale={7} anchor={8} | FOLLOW player={9} rot={10} sec={11}",
|
|
st.enableWeaponTrail, st.burstMode, st.burstTiming, st.suppressMode, st.drawRibbon,
|
|
st.arcAutoOrient, st.arcFitToBlade, st.arcScale, st.arcAnchor,
|
|
st.arcFollowPlayer, st.arcFollowRotation, st.arcFollowSeconds));
|
|
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("follower: attaches=" + WL.Combat.SlashArcFollower.AttachCount + " last=" + (WL.Combat.SlashArcFollower.LastInfo ?? "(none)"));
|
|
sb.AppendLine("lastArc=" + (WL.Combat.WeaponTrailDriver.LastArcInfo ?? "(none)"));
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>스폰된 슬래시 인스턴스의 파티클 시뮬레이션 공간을 덤프한다(#777 잔상 위험 실측).</summary>
|
|
public static object SimSpace()
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var m in UnityEngine.Object.FindObjectsByType<WL.Combat.SlashCrescentMask>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
{
|
|
sb.Append(m.gameObject.name).Append(" active=").Append(m.gameObject.activeInHierarchy).Append(" :: ");
|
|
foreach (var ps in m.GetComponentsInChildren<ParticleSystem>(true))
|
|
sb.Append(ps.gameObject.name).Append('=').Append(ps.main.simulationSpace).Append(' ');
|
|
sb.AppendLine();
|
|
}
|
|
return sb.Length == 0 ? "(스폰된 슬래시 인스턴스 없음 — 공격 후 다시 호출)" : sb.ToString();
|
|
}
|
|
|
|
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<PetActor>(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, float seconds)
|
|
{
|
|
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<Animator>();
|
|
sb.AppendLine("class=" + classId + " ctrl=" + (anim != null && anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "(null)")
|
|
+ " scene=" + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
|
|
+ " fixedDt=" + Time.fixedDeltaTime.ToString("F4"));
|
|
sb.Append(Cfg());
|
|
|
|
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(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 (dir.sqrMagnitude > 1e-4f && Mathf.Abs(dir.magnitude - dist) > 0.1f)
|
|
{
|
|
var pos = t.transform.position - dir.normalized * dist;
|
|
var na = me.GetComponent<NavMeshAgent>();
|
|
if (na != null && na.enabled && na.isOnNavMesh) 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<MobActor>(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));
|
|
Vector3 pos0 = me.transform.position;
|
|
sb.AppendLine("target=" + t.name + " hp0=" + HP(t).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 { }
|
|
|
|
var drv = me.GetComponent<WL.Combat.WeaponTrailDriver>();
|
|
me.Play_Attack(0, (float)aspd);
|
|
sb.AppendLine("Play_Attack(0," + aspd.ToString("F2") + ") animSpeed=" + (anim != null ? anim.speed : 0f).ToString("F2"));
|
|
|
|
float t0 = Time.time;
|
|
int lastStateHash = 0; bool lastInTr = false;
|
|
int lastSwing = drv != null ? drv.SwingCount : 0;
|
|
int lastStep = WL.Combat.AttackStepDriver.StepCount;
|
|
int lastAttach = WL.Combat.SlashArcFollower.AttachCount;
|
|
string lastArc = WL.Combat.WeaponTrailDriver.LastArcInfo;
|
|
|
|
var stateLog = new List<string>(); var hitLog = new List<string>();
|
|
var swingLog = new List<string>(); var stepLog = new List<string>();
|
|
var fxLog = new List<string>(); var capLog = new List<string>();
|
|
|
|
// #777 — 추적 중인 이펙트 인스턴스
|
|
Transform fx = null; Vector3 fxSpawnPos = Vector3.zero, pcAtSpawn = Vector3.zero;
|
|
float fxSpawnT = -1f, fxMaxPcMove = 0f, fxMaxDrift = 0f, fxMaxHiltDist = 0f, fxMinHiltDist = 999f;
|
|
int fxSamples = 0; string fxName = "";
|
|
var fxRows = new List<string>();
|
|
var fxSummaries = new List<string>();
|
|
|
|
float capNext = -1f; int caps = 0; float capInterval = 0.1f; int capMax = 8;
|
|
var hitTimes = new List<float>();
|
|
|
|
while (Time.time - t0 < seconds)
|
|
{
|
|
if (noPet) SuppressPets(true);
|
|
float el = Time.time - t0;
|
|
|
|
// ── #778 상태·전이
|
|
if (anim != null && anim.layerCount > 0)
|
|
{
|
|
var cur = anim.GetCurrentAnimatorStateInfo(0);
|
|
bool inTr = anim.IsInTransition(0);
|
|
if (cur.fullPathHash != lastStateHash)
|
|
{
|
|
lastStateHash = cur.fullPathHash;
|
|
string cn = "(?)";
|
|
var ci = anim.GetCurrentAnimatorClipInfo(0);
|
|
if (ci.Length > 0 && ci[0].clip != null) cn = ci[0].clip.name + " len=" + ci[0].clip.length.ToString("F3");
|
|
stateLog.Add(string.Format("{0:F3}s STATE {1} n={2:F3}", el, cn, cur.normalizedTime));
|
|
}
|
|
if (inTr != lastInTr)
|
|
{
|
|
lastInTr = inTr;
|
|
if (inTr)
|
|
{
|
|
var ti = anim.GetAnimatorTransitionInfo(0);
|
|
var nx = anim.GetNextAnimatorStateInfo(0);
|
|
string nn = "(?)";
|
|
var nci = anim.GetNextAnimatorClipInfo(0);
|
|
if (nci.Length > 0 && nci[0].clip != null) nn = nci[0].clip.name;
|
|
stateLog.Add(string.Format("{0:F3}s TRANS-BEGIN dur={1:F3}s -> {2} (src n={3:F3})",
|
|
el, ti.duration, nn, cur.normalizedTime));
|
|
}
|
|
else stateLog.Add(string.Format("{0:F3}s TRANS-END (now n={1:F3})", el, cur.normalizedTime));
|
|
}
|
|
}
|
|
|
|
// ── 스윙(ShowEffect) · 전진(Move) · 추종 부착
|
|
if (drv != null && drv.SwingCount != lastSwing)
|
|
{
|
|
lastSwing = drv.SwingCount;
|
|
swingLog.Add(string.Format("{0:F3}s SWING#{1}", el, lastSwing));
|
|
}
|
|
if (WL.Combat.AttackStepDriver.StepCount != lastStep)
|
|
{
|
|
lastStep = WL.Combat.AttackStepDriver.StepCount;
|
|
stepLog.Add(string.Format("{0:F3}s STEP#{1} planned={2:F2}m", el, lastStep, WL.Combat.AttackStepDriver.LastPlannedDistance));
|
|
}
|
|
|
|
// ── #777 이펙트 스폰 감지 → 추적 시작
|
|
string arcNow = WL.Combat.WeaponTrailDriver.LastArcInfo;
|
|
bool spawned = (arcNow != lastArc) || (WL.Combat.SlashArcFollower.AttachCount != lastAttach);
|
|
if (spawned)
|
|
{
|
|
lastArc = arcNow; lastAttach = WL.Combat.SlashArcFollower.AttachCount;
|
|
if (fx != null) fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
|
|
var mask = WL.Combat.WeaponTrailDriver.LastMask;
|
|
fx = mask != null ? mask.transform : null;
|
|
fxName = mask != null ? mask.gameObject.name : "(none)";
|
|
fxSpawnT = el; fxSpawnPos = fx != null ? fx.position : Vector3.zero; pcAtSpawn = me.transform.position;
|
|
fxMaxPcMove = 0f; fxMaxDrift = 0f; fxMaxHiltDist = 0f; fxMinHiltDist = 999f; fxSamples = 0;
|
|
fxLog.Add(string.Format("{0:F3}s SPAWN {1} fxPos={2} pcPos={3} follower={4} | {5}",
|
|
el, fxName, fxSpawnPos.ToString("F2"), pcAtSpawn.ToString("F2"),
|
|
fx != null && fx.GetComponent<WL.Combat.SlashArcFollower>() != null
|
|
&& fx.GetComponent<WL.Combat.SlashArcFollower>().IsFollowing, arcNow));
|
|
if (capNext < 0f) capNext = el; // 첫 스폰부터 캡처 시퀀스 시작
|
|
}
|
|
|
|
// ── #777 프레임 시계열
|
|
if (fx != null && fx.gameObject.activeInHierarchy)
|
|
{
|
|
Vector3 pcNow = me.transform.position;
|
|
Vector3 fxNow = fx.position;
|
|
float pcMove = Vector3.Distance(pcAtSpawn, pcNow);
|
|
// 「떨어진 느낌」의 크기 = PC 대비 상대 위치가 스폰 시점에서 얼마나 벌어졌는가
|
|
float drift = Vector3.Distance(fxNow - pcNow, fxSpawnPos - pcAtSpawn);
|
|
float hiltD = (drv != null && drv.ActiveSlotCount > 0)
|
|
? Vector3.Distance(fxNow, drv.GetHiltWorld(0)) : -1f;
|
|
if (pcMove > fxMaxPcMove) fxMaxPcMove = pcMove;
|
|
if (drift > fxMaxDrift) fxMaxDrift = drift;
|
|
if (hiltD >= 0f) { if (hiltD > fxMaxHiltDist) fxMaxHiltDist = hiltD; if (hiltD < fxMinHiltDist) fxMinHiltDist = hiltD; }
|
|
fxSamples++;
|
|
if (fxRows.Count < 220)
|
|
fxRows.Add(string.Format("{0:F3}s [{1}] +{2:F3}s fx={3} pc={4} pcMove={5:F3} drift={6:F3} hiltD={7:F3}",
|
|
el, fxName, el - fxSpawnT, fxNow.ToString("F2"), pcNow.ToString("F2"), pcMove, drift, hiltD));
|
|
}
|
|
else if (fx != null)
|
|
{
|
|
fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
|
|
fxLog.Add(string.Format("{0:F3}s OFF {1} (수명 {2:F3}s)", el, fxName, el - fxSpawnT));
|
|
fx = null;
|
|
}
|
|
|
|
// ── 타격
|
|
foreach (var m in watch)
|
|
{
|
|
if (m == null) continue;
|
|
double hp = HP(m), prev = lastHpAll[m];
|
|
if (Math.Abs(hp - prev) > 0.01)
|
|
{
|
|
hitLog.Add(string.Format("{0:F3}s HIT {1} {2:F0} -> {3:F0}", el, m.name, prev, hp));
|
|
hitTimes.Add(el);
|
|
lastHpAll[m] = hp;
|
|
}
|
|
}
|
|
|
|
// ── 캡처 시퀀스 (0.1 s 간격)
|
|
if (capNext >= 0f && caps < capMax && el >= capNext)
|
|
{
|
|
capNext = el + capInterval;
|
|
yield return new WaitForEndOfFrame();
|
|
Texture2D tex = null;
|
|
try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch { }
|
|
if (tex != null)
|
|
{
|
|
var path = System.IO.Path.Combine(OutDir, string.Format("seq_{0}_{1:00}_{2:F2}s.png", tag, ++caps, el));
|
|
System.IO.File.WriteAllBytes(path, tex.EncodeToPNG());
|
|
UnityEngine.Object.Destroy(tex);
|
|
capLog.Add(string.Format("{0:F3}s CAP {1}", el, path));
|
|
}
|
|
continue;
|
|
}
|
|
// 🔴 반드시 프레임 끝에서 샘플링한다 — SlashArcFollower 는 LateUpdate 에서
|
|
// 좌표를 갱신하므로 Update 단계(yield return null)에서 읽으면 이펙트 위치가
|
|
// 한 프레임 뒤처져 보이고, 그 차이가 그 프레임의 PC 이동량으로 잘못 잡힌다.
|
|
yield return new WaitForEndOfFrame();
|
|
}
|
|
|
|
if (fx != null) fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
|
|
|
|
sb.AppendLine("== #778 states/transitions (" + stateLog.Count + ") =="); foreach (var s in stateLog) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== swings ShowEffect (" + swingLog.Count + ") =="); foreach (var s in swingLog) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== hits (" + hitLog.Count + ") =="); foreach (var s in hitLog) sb.AppendLine(" " + s);
|
|
if (hitTimes.Count >= 2)
|
|
{
|
|
var gaps = new StringBuilder();
|
|
for (int i = 1; i < hitTimes.Count; i++) gaps.Append((hitTimes[i] - hitTimes[i - 1]).ToString("F3")).Append("s ");
|
|
sb.AppendLine(" hit gaps: " + gaps + " | first->last = " + (hitTimes[hitTimes.Count - 1] - hitTimes[0]).ToString("F3") + "s");
|
|
}
|
|
sb.AppendLine("== attack steps (" + stepLog.Count + ") =="); foreach (var s in stepLog) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== #777 fx events (" + fxLog.Count + ") =="); foreach (var s in fxLog) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== #777 fx summary (" + fxSummaries.Count + ") =="); foreach (var s in fxSummaries) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== #777 fx timeline (" + fxRows.Count + ") =="); foreach (var s in fxRows) sb.AppendLine(" " + s);
|
|
sb.AppendLine("== captures (" + capLog.Count + ") =="); foreach (var s in capLog) sb.AppendLine(" " + s);
|
|
|
|
Vector3 pend = me.transform.position;
|
|
sb.AppendLine("final pcPos=" + pend.ToString("F2") + " totalΔ=" + Vector3.Distance(pos0, pend).ToString("F3") + "m"
|
|
+ " swings=" + (drv != null ? drv.SwingCount : 0) + " steps=" + WL.Combat.AttackStepDriver.StepCount
|
|
+ " attaches=" + WL.Combat.SlashArcFollower.AttachCount);
|
|
if (noPet) SuppressPets(false);
|
|
Done(tag, sb);
|
|
}
|
|
|
|
static string FxSummary(string name, float t, int n, float pcMove, float drift, float minH, float maxH)
|
|
{
|
|
return string.Format("{0} spawn@{1:F3}s frames={2} PC최대이동={3:F3}m 이펙트상대이탈최대={4:F3}m 자루거리 {5:F3}~{6:F3}m",
|
|
name, t, n, pcMove, drift, minH > 900f ? 0f : minH, maxH);
|
|
}
|
|
|
|
void Done(string tag, StringBuilder sb)
|
|
{
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "seq_" + tag + ".txt"), sb.ToString());
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
}
|