// WL923_Probe.cs — PD 지시 #923 시험(Play 중 · 섬) // Arrows(seconds, dodgeOnCharge, dodgeEvery) : 활 기본 화살(Projectile_1040x)이 켜질 때마다(첫 프레임) 겉모습 · 원래 화살 메시 · 3타 겉모습 · PC 동작 · // PC 정면-화살 방향 각 · PC 정면-대상 방향 각을 기록 · dodgeOnCharge 1 = 3타 모으기 시작 순간 대상 반대쪽으로 구르기(3타 끊기) · // dodgeEvery > 0 = 그 간격(초)마다 대상 반대쪽으로 구르기 · 끝에 풀 인스턴스 수 · 「#923 전 방식이면 안 붙었을 새 인스턴스」 수 // Log() : 기록 using System.Text; using UnityEngine; public static class WL923_Probe { const string Key = "wl923log"; static StringBuilder Buf() { var sb = System.AppDomain.CurrentDomain.GetData(Key) as StringBuilder; if (sb == null) { sb = new StringBuilder(); System.AppDomain.CurrentDomain.SetData(Key, sb); } return sb; } public static object Log() { return Buf().ToString(); } static float FlatAngle(Vector3 a, Vector3 b) { a.y = 0f; b.y = 0f; if (a.sqrMagnitude < 1e-6f || b.sqrMagnitude < 1e-6f) return -1f; return Vector3.Angle(a, b); } static System.Collections.Generic.List Pool(string name) { var f = typeof(ProjectileInfo).GetField("dic_projectile", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var dic = f != null && ProjectileInfo.Ins != null ? f.GetValue(ProjectileInfo.Ins) as System.Collections.Generic.Dictionary> : null; System.Collections.Generic.List list = null; if (dic != null) dic.TryGetValue(name, out list); return list; } static bool MeshShown(ProjectileBase pb) { foreach (var mr in pb.GetComponentsInChildren(false)) if (mr.enabled && (mr.transform == pb.transform || mr.transform.parent == pb.transform)) return true; return false; } /// PC 상태(공격이 안 나갈 때) — IsStop · CC · 피격 시간 · 대상 · 거리 · 조이스틱 · Auto. public static object State() { var pc = MyValue.MyPC as PCActor; if (pc == null) return "PC 없음"; var bf = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public; System.Func F = n => { for (var t = pc.GetType(); t != null; t = t.BaseType) { var f = t.GetField(n, bf); if (f != null) return f.GetValue(pc); var p = t.GetProperty(n, bf); if (p != null) return p.GetValue(pc); } return "?"; }; var tg = pc.Get_Target(); return "동작 " + pc.Get_CurAnim() + " · IsStop " + F("IsStop") + " · CC " + pc.IsCC() + " · m_HitTime " + F("m_HitTime") + " · m_Auto " + F("m_Auto") + " · 조이 " + pc.JoystickStatus + " · 대상 " + (DSUtil.CheckNull(tg) ? "-" : tg.name + " 거리 " + Vector3.Distance(pc.Get_position(), tg.Get_position()).ToString("F2")) + " · isFollowOwner " + F("isFollowOwner") + " · 대쉬 " + WL.Combat.DashDriver.IsDashingNow(pc) + " · timeScale " + Time.timeScale + " · enabled " + pc.enabled; } /// 대검 각성(칼날 폭풍 · WLBladeStorm) — slot 2 발동 · moveAt 초부터 moveSec 초 이동(PC 오른쪽) · dodgeAt > 0 이면 그때 구르기 · 0.25 초마다 상태 기록 · 끝 요약. public static object Storm(float seconds, float moveAt, float moveSec, float dodgeAt) { var pc = MyValue.MyPC as PCActor; if (pc == null) return "PC 없음"; var sb = Buf(); sb.Length = 0; float t0 = Time.time, tl = -1f; bool moving = false, moved = false, dodged = false; var p0 = pc.Get_position(); Vector3 pm = Vector3.zero; int k0 = WL.Combat.Weapon.WLBladeStorm.Ticks, h0 = WL.Combat.Weapon.WLBladeStorm.Hits, f0 = WL.Combat.Weapon.WLBladeStorm.FxSpawned, e0 = WL.Combat.Weapon.WLBladeStorm.Ends, a0 = WL.Combat.Weapon.WLBladeStorm.Aborts; var right = pc.transform.right; right.y = 0f; right.Normalize(); float lastYaw = pc.transform.eulerAngles.y, yawSum = 0f; var d = WL.IslandMob.WLTrailTest.Dummy; string res = WL.Combat.Weapon.WLSkillHotkey.Fire(2); UnityEngine.Events.UnityAction act = null; act = () => { float t = Time.time - t0; var an = pc.m_animation; var si = an.GetCurrentAnimatorStateInfo(0); float yaw = pc.transform.eulerAngles.y; yawSum += Mathf.Abs(Mathf.DeltaAngle(lastYaw, yaw)); lastYaw = yaw; if (!moved && moveSec > 0f && t >= moveAt) { if (!moving) { moving = true; pm = pc.Get_position(); sb.AppendFormat("{0:F2} ▶ 이동 시작 · 조이 {1}", t, pc.JoystickStatus).Append('\n'); } pc.Run_byJoystick(right); if (t >= moveAt + moveSec) { moving = false; moved = true; var mv = pc.Get_position() - pm; mv.y = 0f; sb.AppendFormat("{0:F2} ■ 이동 끝 · {1:F2} m · 동작 {2}", t, mv.magnitude, pc.Get_CurAnim()).Append('\n'); } } if (!dodged && dodgeAt > 0f && t >= dodgeAt) { dodged = true; sb.AppendFormat("{0:F2} 구르기 {1}", t, DodgeAway(3f)).Append('\n'); } if (t - tl >= 0.25f || t > seconds) { tl = t; sb.AppendFormat("{0:F2} {1} · 상태 {2} n={3:F2} 속도 {4:F2} · 조이 {5} · 회전 누적 {6:F0}° · 판정 {7} 명중 {8} · 이펙트 {9}{10}", t, pc.Get_CurAnim(), si.IsName("Attack2") ? "Attack2" : si.IsName("idle") ? "idle" : si.IsName("dodge") ? "dodge" : si.shortNameHash.ToString(), si.normalizedTime, an.speed, pc.JoystickStatus, yawSum, WL.Combat.Weapon.WLBladeStorm.Ticks - k0, WL.Combat.Weapon.WLBladeStorm.Hits - h0, WL.Combat.Weapon.WLBladeStorm.FxSpawned - f0, d != null ? " · 더미 거리 " + Vector3.Distance(d.Get_position(), pc.Get_position()).ToString("F2") : "").Append('\n'); } if (t > seconds) { Application.onBeforeRender -= act; if (moving) { } sb.AppendFormat("끝 · 판정 {0} · 명중 {1} · 이펙트 {2} · 정상 끝 +{3} · 끊김 +{4} · 마지막 회전 {5:F0}° / {6:F2} 초 · {7} · {8}", WL.Combat.Weapon.WLBladeStorm.Ticks - k0, WL.Combat.Weapon.WLBladeStorm.Hits - h0, WL.Combat.Weapon.WLBladeStorm.FxSpawned - f0, WL.Combat.Weapon.WLBladeStorm.Ends - e0, WL.Combat.Weapon.WLBladeStorm.Aborts - a0, WL.Combat.Weapon.WLBladeStorm.LastSpinTotal, WL.Combat.Weapon.WLBladeStorm.LastSeconds, WL.Combat.Weapon.WLBladeStorm.LastLog, res).Append('\n'); } }; Application.onBeforeRender += act; return "칼날 폭풍 관찰 " + seconds + " 초 · " + res; } /// 이펙트 프리팹을 y 1000 에 띄워 시각 t 마다 Simulate — 입자 있는 렌더러별 경계(루트 기준 중심 · 크기) · 전체 경계 · 「ground」 이름 렌더러 경계. public static object FxBounds(string path, string timesCsv) { var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (prefab == null) return "없음 " + path; var root = new Vector3(0f, 1000f, 0f); var go = Object.Instantiate(prefab, root, prefab.transform.rotation); var sb = new StringBuilder(); foreach (var ts in timesCsv.Split(',')) { float t = float.Parse(ts, System.Globalization.CultureInfo.InvariantCulture); foreach (var ps in go.GetComponentsInChildren(true)) if (ps.transform.parent == null || ps.transform.parent.GetComponentInParent() == null) ps.Simulate(t, true, true, false); Bounds all = new Bounds(); bool has = false; Bounds gr = new Bounds(); bool hg = false; sb.Append("■ t ").Append(ts).Append('\n'); foreach (var r in go.GetComponentsInChildren(true)) { var ps = r.GetComponent(); if (ps == null || ps.particleCount == 0 || !r.enabled) continue; var b = r.bounds; b.center -= root; if (!has) { all = b; has = true; } else all.Encapsulate(b); if (r.name.StartsWith("ground") || r.name.StartsWith("floor")) { if (!hg) { gr = b; hg = true; } else gr.Encapsulate(b); } sb.Append(" ").Append(r.transform.parent != null ? r.transform.parent.name + "/" : "").Append(r.name).Append(" n ").Append(ps.particleCount).Append(" 중심 ").Append(b.center.ToString("F2")).Append(" 크기 ").Append(b.size.ToString("F2")).Append('\n'); } sb.Append(" 전체 중심 ").Append(has ? all.center.ToString("F2") : "-").Append(" 크기 ").Append(has ? all.size.ToString("F2") : "-").Append(" · 바닥 중심 ").Append(hg ? gr.center.ToString("F2") : "-").Append(" 크기 ").Append(hg ? gr.size.ToString("F2") : "-").Append('\n'); } Object.Destroy(go); return sb.ToString(); } /// 마지막 궤적(호) 배치 — 이벤트 · 종류 · 목표 법선/이등분 · 배치 법선 · 카메라 정면과의 |내적|(1 = 정면으로 보임 · 0 = 옆면 선) · PC 정면. public static object Arc() { var p = WL.Combat.WeaponTrailDriver.LastPlacement; var cam = Camera.main; var pc = MyValue.MyPC as PCActor; if (cam == null || pc == null) return "카메라/PC 없음"; var cf = cam.transform.forward; return p.source + " → " + p.effect + " · " + p.kind + " · 목표 법선 " + p.targetNormal.ToString("F2") + " 이등분 " + p.targetBisector.ToString("F2") + " · 배치 법선 " + p.arcNormal.ToString("F2") + " · |법선·카메라| " + Mathf.Abs(Vector3.Dot(p.arcNormal.normalized, cf)).ToString("F2") + " · 카메라 정면 " + cf.ToString("F2") + " · PC 정면 " + pc.transform.forward.ToString("F2") + " · 호 기울임 " + WL.Combat.WeaponTrailDriver.ArcTilted; } /// 시험용 — WLAttackFacing 를 이번 Play 동안만 끄고 켠다(비교 측정 · JSON 은 그대로). public static object FacingOn(int on) { var f = typeof(WL.Combat.WLAttackFacing).GetField("s_cfg", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); var cfg = f != null ? f.GetValue(null) as WL.Combat.WLAttackFacingJson : null; if (cfg == null) return "설정 없음"; cfg.enabled = on; return "WLAttackFacing enabled = " + on; } /// 시험용 — 대기 중인 스킬(m_Magic)을 비운다(NoAuto 로 쿨이 999 가 되면 걸린 스킬이 못 나가 기본 공격까지 막힌다). public static object ClearMagic() { var pc = MyValue.MyPC as PCActor; if (pc == null) return "PC 없음"; var bf = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public; for (var t = pc.GetType(); t != null; t = t.BaseType) { var f = t.GetField("m_Magic", bf); if (f != null) { var was = f.GetValue(pc); f.SetValue(pc, null); return "m_Magic " + (was == null ? "null" : "있음") + " → null"; } } return "필드 없음"; } /// 시험 더미를 PC 정면 dist m 에 세운다(멈춤) · PC 상태 · 대상. public static object Near(float dist) { var pc = MyValue.MyPC as PCActor; var d = WL.IslandMob.WLTrailTest.Dummy; if (pc == null) return "PC 없음"; if (d == null) return "더미 없음 · PC " + pc.Get_CurAnim() + " · 대상 " + (DSUtil.CheckNull(pc.Get_Target()) ? "-" : pc.Get_Target().name); var f = pc.transform.forward; f.y = 0f; if (f.sqrMagnitude < 1e-4f) f = Vector3.forward; f.Normalize(); var want = pc.Get_position() + f * dist; UnityEngine.AI.NavMeshHit hit; if (UnityEngine.AI.NavMesh.SamplePosition(want, out hit, 1.5f, UnityEngine.AI.NavMesh.AllAreas)) want = hit.position; d.Stop(true); d.Set_Warp(want); return "더미 " + d.name + " → 거리 " + Vector3.Distance(pc.Get_position(), d.Get_position()).ToString("F2") + " · PC " + pc.Get_CurAnim() + " · 대상 " + (DSUtil.CheckNull(pc.Get_Target()) ? "-" : pc.Get_Target().name); } /// 대상 반대쪽으로 구르기(원본 WLDoubleTapDodge.TryDodge — 두 번 탭과 같은 경로). public static string DodgeAway(float dist) { var pc = MyValue.MyPC as PCActor; var cam = Camera.main; if (pc == null || cam == null) return "PC/카메라 없음"; var t = pc.Get_Target(); Vector3 away = -pc.transform.forward; if (!DSUtil.CheckNull(t)) { away = pc.Get_position() - t.Get_position(); away.y = 0f; } if (away.sqrMagnitude < 1e-4f) away = -pc.transform.forward; var sp = cam.WorldToScreenPoint(pc.Get_position() + away.normalized * dist); var m = typeof(WL.Combat.WLDoubleTapDodge).GetMethod("TryDodge", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); if (m == null) return "TryDodge 없음"; m.Invoke(null, new object[] { new Vector2(sp.x, sp.y) }); return WL.Combat.WLDoubleTapDodge.LastLog; } public static object Arrows(float seconds, int dodgeOnCharge, float dodgeEvery) { var pc = MyValue.MyPC as PCActor; if (pc == null) return "PC 없음"; var sb = Buf(); sb.Length = 0; float t0 = Time.time, nextDodge = dodgeEvery > 0f ? dodgeEvery : -1f; var wasActive = new System.Collections.Generic.Dictionary(); var everSeen = new System.Collections.Generic.HashSet(); int ch0 = WL.Combat.Weapon.WLBowChargeReceiver.Charges, chBase = ch0, cons0 = WL.Combat.Weapon.WLBowChargeReceiver.Consumed, drop0 = WL.Combat.Weapon.WLBowChargeReceiver.Dropped; int snap0 = WL.Combat.WLAttackFacing.Snaps; int n = 0, noVis = 0, mesh = 0, chargedOther = 0, wouldMiss = 0, faceBad = 0, dodges = 0, chargedAll = 0; float faceMax = 0f; UnityEngine.Events.UnityAction act = null; act = () => { float t = Time.time - t0; if (dodgeOnCharge != 0 && WL.Combat.Weapon.WLBowChargeReceiver.Charges > ch0) { ch0 = WL.Combat.Weapon.WLBowChargeReceiver.Charges; dodges++; sb.AppendFormat("{0:F2} 3타 모으기 → 구르기 {1}", t, DodgeAway(3f)).Append('\n'); } var dm = WL.IslandMob.WLTrailTest.Dummy; if (dm != null && pc.Get_CurAnim() != eAnim.Dodge && Vector3.Distance(dm.Get_position(), pc.Get_position()) > 6f) Near(4f); if (nextDodge > 0f && t >= nextDodge) { nextDodge += dodgeEvery; dodges++; sb.AppendFormat("{0:F2} 구르기 {1}", t, DodgeAway(3f)).Append('\n'); } foreach (var name in new[] { "Projectile_10402", "Projectile_10401" }) { var list = Pool(name); if (list == null) continue; foreach (var pb in list) { if (pb == null) continue; int id = pb.GetInstanceID(); bool on = pb.gameObject.activeInHierarchy; bool was; wasActive.TryGetValue(id, out was); wasActive[id] = on; if (!on || was) continue; var pd = pb.Get_ProjectTileData(); if (pd == null || !ReferenceEquals(pd.shooter, pc)) continue; bool first = everSeen.Add(id); if (first) { // #923 전 방식(HitboxSpawned 때 풀에서 찾기)은 이 순간 풀에 이미 붙은 인스턴스가 하나라도 있으면 새 인스턴스를 건너뛰었다 int attachedOthers = 0; foreach (var o in list) if (o != null && o != pb && o.transform.Find("~WL_Visual") != null) attachedOthers++; if (attachedOthers > 0) wouldMiss++; } var vis = pb.transform.Find("~WL_Visual"); var chg = pb.transform.Find("~WL_Charged"); bool v = vis != null && vis.gameObject.activeSelf, c = chg != null && chg.gameObject.activeSelf, m = MeshShown(pb); var cur = pc.Get_CurAnim(); var tg = pc.Get_Target(); float aArrow = FlatAngle(pc.transform.forward, pb.transform.forward); float aTarget = DSUtil.CheckNull(tg) ? -1f : FlatAngle(pc.transform.forward, tg.Get_position() - pc.Get_position()); n++; if (!v && !c) noVis++; if (m) mesh++; if (c) chargedAll++; var ci = pc.m_animation.GetCurrentAnimatorClipInfo(0); string clip = ci.Length > 0 && ci[0].clip != null ? ci[0].clip.name : "-"; if (c && clip.IndexOf("Bow3") < 0) chargedOther++; if (aArrow > faceMax) faceMax = aArrow; if (aArrow > 45f) faceBad++; sb.AppendFormat("{0:F2} 화살 #{1} {2}{3} · 겉모습 {4} · 3타 {5} · 원래 메시 {6} · 동작 {7} {10} · 정면-화살 {8:F0}° · 정면-대상 {9:F0}°", t, n, name.Substring(11), first ? " 새 인스턴스" : "", v ? "O" : "X", c ? "O" : "-", m ? "보임" : "-", cur, aArrow, aTarget, clip).Append('\n'); } } if (t > seconds) { Application.onBeforeRender -= act; var p2 = Pool("Projectile_10402"); var p1 = Pool("Projectile_10401"); sb.AppendFormat("끝 · 화살 {0} · 겉모습 없음 {1} · 원래 메시 보임 {2} · 3타 겉모습 {3}(3타 클립 아닐 때 {4}) · 정면-화살 45° 넘음 {5}(최대 {6:F0}°) · 구르기 {7} · 풀 10402 {8} / 10401 {9} · 옛 방식이면 안 붙었을 새 인스턴스 {10} · 강화 창 모으기 +{11} 사용 +{12} 닫음 +{13} · 방향 맞춤 +{14} 최근 {15:F0}°", n, noVis, mesh, chargedAll, chargedOther, faceBad, faceMax, dodges, p2 != null ? p2.Count : 0, p1 != null ? p1.Count : 0, wouldMiss, WL.Combat.Weapon.WLBowChargeReceiver.Charges - chBase, WL.Combat.Weapon.WLBowChargeReceiver.Consumed - cons0, WL.Combat.Weapon.WLBowChargeReceiver.Dropped - drop0, WL.Combat.WLAttackFacing.Snaps - snap0, WL.Combat.WLAttackFacing.LastSnapDeg).Append('\n'); } }; Application.onBeforeRender += act; return "화살 관찰 " + seconds + " 초"; } }