// WL890_Probe.cs — PD 지시 #890 · #891 시험(Play 중) // ShieldInfo : 방패(왼손 소켓 0) · 팔(Elbow_L → Wrist_L) · 몸 중심(Chest_M) 기하 — 팔 바깥쪽 방향 · 방패 면 방향 · 그림자 설정 // ShieldOutward : 방패 래퍼 자식을 「팔 바깥쪽」으로 dist(월드 m) 만큼 옮긴다(실행 중만 · 파일 무수정) → 래퍼 루트 기준 로컬 값을 돌려준다(JSON 에 넣을 값) // ShieldShadow : 방패 렌더러 그림자 받기 켬/끔(실행 중만) // Q / Chain : Q 키와 같은 함수 호출 · 연결 상태 using System.Text; using UnityEngine; public static class WL890_Probe { static Transform Bone(PCActor pc, string name) { foreach (var t in pc.GetComponentsInChildren(true)) if (t.name == name) return t; return null; } static Transform ShieldChild(PCActor pc, out Transform root) { root = null; var s = pc.tfs_weapon[0]; if (s == null || s.childCount == 0) return null; root = s.GetChild(0); return root.childCount > 0 ? root.GetChild(0) : root; } static Vector3 Outward(PCActor pc) { var elbow = Bone(pc, "Elbow_L"); var wrist = Bone(pc, "Wrist_L"); var chest = Bone(pc, "Chest_M"); if (elbow == null || wrist == null || chest == null) return Vector3.zero; var arm = (wrist.position - elbow.position).normalized; var o = wrist.position - chest.position; o -= Vector3.Dot(o, arm) * arm; // 팔 방향 성분 제거 = 팔에 수직인 「몸 → 팔」 방향 return o.sqrMagnitude > 1e-8f ? o.normalized : Vector3.zero; } public static object ShieldInfo() { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null) return "방패 없음"; var wrist = Bone(pc, "Wrist_L"); var chest = Bone(pc, "Chest_M"); var outw = Outward(pc); var sb = new StringBuilder(); var ci = pc.m_animation.GetCurrentAnimatorClipInfo(0); sb.Append("clip=").Append(ci.Length > 0 ? ci[0].clip.name : "?").Append(" root=").Append(root.name).Append(" child=").Append(ch.name).Append(" childLocal=").Append(ch.localPosition.ToString("F3")); sb.Append("\n 팔 바깥쪽(월드)=").Append(outw.ToString("F2")).Append(" · 방패 두께축(Z) · 바깥쪽 = ").Append(Vector3.Dot(ch.forward, outw).ToString("F2")); var rends = ch.GetComponentsInChildren(true); var b = rends.Length > 0 ? rends[0].bounds : new Bounds(ch.position, Vector3.zero); foreach (var r in rends) b.Encapsulate(r.bounds); sb.Append("\n 방패 중심 − 손목 = ").Append((b.center - wrist.position).ToString("F3")).Append(" · 바깥쪽 성분 ").Append(Vector3.Dot(b.center - wrist.position, outw).ToString("F3")) .Append(" m · 방패 크기 ").Append(b.size.ToString("F2")).Append(" · 손목 − 가슴 ").Append(Vector3.Distance(wrist.position, chest.position).ToString("F2")); foreach (var r in rends) sb.Append("\n renderer ").Append(r.name).Append(" receive=").Append(r.receiveShadows).Append(" cast=").Append(r.shadowCastingMode).Append(" mat=").Append(r.sharedMaterial != null ? r.sharedMaterial.shader.name : "-"); return sb.ToString(); } public static object ShieldOutward(float dist) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null || ch == root) return "방패 자식 없음"; var outw = Outward(pc); if (outw == Vector3.zero) return "뼈 없음"; var local = root.InverseTransformVector(outw * dist); ch.localPosition = local; return "자식 localPosition = (" + local.x.ToString("F3") + ", " + local.y.ToString("F3") + ", " + local.z.ToString("F3") + ") · 월드 " + dist + " m"; } public static object ShieldLocal(float x, float y, float z) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null || ch == root) return "방패 자식 없음"; ch.localPosition = new Vector3(x, y, z); return "자식 localPosition = " + ch.localPosition.ToString("F3"); } /// 자식 회전(로컬 오일러) + 「팔 바깥쪽」 dist(m) 위치를 한 번에(실행 중만). 돌려준 값 = JSON 에 넣을 로컬 위치 · 회전. public static object ShieldPose(float ex, float ey, float ez, float dist) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null || ch == root) return "방패 자식 없음"; ch.localRotation = Quaternion.Euler(ex, ey, ez); ch.localPosition = Vector3.zero; // 회전 뒤 방패 중심이 손목보다 바깥쪽 dist 가 되게(중심 기준) var wrist = Bone(pc, "Wrist_L"); var outw = Outward(pc); var rends = ch.GetComponentsInChildren(true); var b = rends[0].bounds; foreach (var r in rends) b.Encapsulate(r.bounds); float now = Vector3.Dot(b.center - wrist.position, outw); var local = root.InverseTransformVector(outw * (dist - now)); ch.localPosition = local; return "euler(" + ex + "," + ey + "," + ez + ") 바깥 " + dist + " m(이전 " + now.ToString("F3") + ") · 로컬 위치 (" + local.x.ToString("F3") + ", " + local.y.ToString("F3") + ", " + local.z.ToString("F3") + ") · 장식면(-Z)·바깥쪽 " + Vector3.Dot(-ch.forward, outw).ToString("F2"); } /// 방패 재질(실행 중 인스턴스)의 어두운 면 색 · 그림자 받기를 바꾼다(파일 무수정). public static object ShieldMat(float dark, int receive) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null) return "방패 없음"; var sb = new StringBuilder(); foreach (var r in ch.GetComponentsInChildren(true)) { var m = r.material; // 인스턴스 if (m.HasProperty("_GoochDarkColor")) m.SetColor("_GoochDarkColor", new Color(dark, dark, dark, 1f)); if (m.HasProperty("_ReceiveShadows")) m.SetFloat("_ReceiveShadows", receive); if (receive == 0) { m.DisableKeyword("_MK_RECEIVE_SHADOWS"); m.DisableKeyword("_RECEIVESHADOWS_ON"); } else { m.EnableKeyword("_MK_RECEIVE_SHADOWS"); m.EnableKeyword("_RECEIVESHADOWS_ON"); } r.receiveShadows = receive != 0; sb.Append(r.name).Append(" dark=").Append(dark).Append(" receive=").Append(receive).Append(' '); } return sb.ToString(); } public static object ShieldShadow(bool receive) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; Transform root; var ch = ShieldChild(pc, out root); if (ch == null) return "방패 없음"; int n = 0; foreach (var r in ch.GetComponentsInChildren(true)) { r.receiveShadows = receive; n++; } return "receiveShadows=" + receive + " · " + n + "개"; } public static object Q() { return WL.Combat.Weapon.WLSkillHotkey.Fire(); } static readonly string[] States = { "shieldattack", "dashslash", "slashflurry_b1", "slashflurry_a1", "slashflurry_b2", "idle", "run", "dodge", "attack", "Attack2", "Attack3", "hit" }; /// Q 와 같은 함수로 주력 스킬 발동 → 0.1 s(게임 시간)마다 상태 · 방패 치기/연속 베기 타격 수를 파일에. public static object QTimeline(string path, float seconds) { var pc = MyValue.MyPC; if (pc == null) return "PC 없음"; System.IO.File.WriteAllText(path, "Q → " + WL.Combat.Weapon.WLSkillHotkey.Fire() + "\n"); pc.StartCoroutine(Co(pc, path, seconds)); return "started"; } static System.Collections.IEnumerator Co(PCActor pc, string path, float seconds) { int bash = 0, flurry = 0; WL.Combat.Core.CombatHandler hh = (in WL.Combat.Core.HitConfirmedEvent e) => { var s = e.projectile != null ? e.projectile.m_SkillListTableData : null; if (s == null) return; if (s.n_SkillID == 703502) bash++; else if (s.n_SkillID == 703103) flurry++; }; WL.Combat.Core.CombatEvents.HitConfirmed.Add(hh); float t = 0f, next = 0f; while (t < seconds && pc != null) { if (t >= next) { next += 0.1f; var an = pc.m_animation; string st = "?"; var si = an.GetCurrentAnimatorStateInfo(0); foreach (var n in States) if (si.IsName(n)) { st = n; break; } if (an.IsInTransition(0)) { var ni = an.GetNextAnimatorStateInfo(0); foreach (var n in States) if (ni.IsName(n)) { st += ">" + n; break; } } System.IO.File.AppendAllText(path, "t=" + t.ToString("F2") + " anim=" + pc.Get_CurAnim() + " state=" + st + " 방패치기타격=" + bash + " 연속베기타격=" + flurry + "\n"); } yield return null; t += Time.deltaTime; } WL.Combat.Core.CombatEvents.HitConfirmed.Remove(hh); System.IO.File.AppendAllText(path, "END " + Chain() + " · dust " + WL.Combat.WLDoubleTapDodge.DustScaled + "\n"); } public static object Chain() { return "chain 예약 " + WL.Combat.Weapon.WLSkillChain.Triggered + " 발동 " + WL.Combat.Weapon.WLSkillChain.Cast + " 실패 " + WL.Combat.Weapon.WLSkillChain.Failed + " · " + WL.Combat.Weapon.WLSkillChain.LastLog + " | hotkey " + WL.Combat.Weapon.WLSkillHotkey.Fired + "/" + WL.Combat.Weapon.WLSkillHotkey.Failed + " · " + WL.Combat.Weapon.WLSkillHotkey.LastLog + " | flurry " + WL.Combat.Weapon.WLSlashFlurry.Taken + " · vfx swings " + WL.Combat.Weapon.WLSkillVfx.Swings; } }