Project_WL/AgentScripts/WL879_Probe.cs

423 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL879_Probe.cs — PD 지시 #879 장비 파츠 스킬 스모크 프로브(Play 중 · 런타임)
// unity command run_script --file AgentScripts/WL879_Probe.cs --entry WL879_Probe.Grant --args '["1253:5,1155:5"]' // 그룹:등급(1 일반 … 7 신화) 지급 + 장착
// unity command run_script --file AgentScripts/WL879_Probe.cs --entry WL879_Probe.Dump
// unity command run_script --file AgentScripts/WL879_Probe.cs --entry WL879_Probe.Stats
// unity command run_script --file AgentScripts/WL879_Probe.cs --entry WL879_Probe.UnequipAll
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using WL.Equip;
using WL.PartSkill;
public static class WL879_Probe
{
public static object Grant(string spec)
{
var sb = new StringBuilder();
if (!WLInventory.EnsureLoaded()) return "인벤토리 로드 실패";
foreach (var part in spec.Split(','))
{
var kv = part.Split(':');
int g = int.Parse(kv[0].Trim()), tier = kv.Length > 1 ? int.Parse(kv[1].Trim()) : 1;
var step = WLEquipGrades.BaseStepOfTier(tier);
if (step == null) { sb.Append("등급 없음 ").Append(tier).Append(" | "); continue; }
ItemJson found = null;
foreach (var j in WLEquipTables.Items)
if (j != null && j.IsEquipment && j.GroupNo == g && (int)j.ItemGrade == step.frGrade && j.GradeLv == step.gradeLv) { found = j; break; }
if (found == null) { sb.Append("아이템 없음 ").Append(g).Append('@').Append(tier).Append(" | "); continue; }
var gained = new List<Item>();
WLInventory.AddItem(found.Index, 1, 1, gained);
var it = gained.Count > 0 ? gained[gained.Count - 1] : null;
if (it == null)
{ // 이미 있는 묶음 — 같은 표 행의 첫 아이템
foreach (var x in WLInventory.SortedItems()) if (x.itemJson.Index == found.Index) { it = x; break; }
}
if (it != null) { WLInventory.Equip(it.rawItem._id); sb.Append(found.Item_name).Append('(').Append(WLEquipGrades.Label(found, false)).Append(") 장착 | "); }
}
WLInventory.Commit("WL879 프로브 지급");
sb.Append("\n").Append(WLPartSkillState.LastLog);
return sb.ToString();
}
/// <summary>시험용 — 런타임에만 그룹 → 스킬 대응을 바꾼다(파일 무수정 · 아이템 미배정 스킬 점검용).</summary>
public static object MapGroup(int group, string key)
{
var f = typeof(WLPartSkillData).GetField("s_groupToKey", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var d = f != null ? f.GetValue(null) as Dictionary<int, string> : null;
if (d == null) return "사전 없음";
d[group] = key;
WLPartSkillState.Rebuild("probe-map");
return group + " → " + key + " · " + WLPartSkillState.LastLog;
}
/// <summary>시험용 — PC 를 가장 가까운 적 옆(dist m)으로 옮긴다(NavMeshAgent.Warp) + 적 정보.</summary>
public static object Approach(float dist)
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var r = WLPartSkillRunner.Instance;
Actor e = r != null ? r.Nearest(pc.Get_position(), 60f) : null;
if (e == null && ActorInfo.Ins != null) e = ActorInfo.Ins.Get_Nearest_EnemyActor_orNull(pc.IsEnemy(), pc.Get_position(), 60f);
if (e == null) return "적 없음";
var dir = pc.Get_position() - e.Get_position(); dir.y = 0f; if (dir.sqrMagnitude < 0.01f) dir = Vector3.forward;
var to = e.Get_position() + dir.normalized * dist;
var ag = pc.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (ag != null && ag.enabled) ag.Warp(to); else pc.transform.position = to;
return "적 " + e.name + " hp " + e.Get_HP().ToString("F0") + "/" + e.Get_MaxHP().ToString("F0") + " boss=" + e.IsSubRole(eSubRol.Boss) + " immortal=" + e.Is_Immortal() + " → PC " + to.ToString("F1") + " · hitConfirmed raised=" + WL.Combat.Core.CombatEvents.HitConfirmed.Raised;
}
/// <summary>시험용 — 기본 공격 적중을 n 번 흉내 내 무기 시너지 동작 경로를 돈다(실제 HitConfirmed 경로는 전투 스테이지에서 확인).</summary>
public static object FakeBasicHit(int n)
{
var r = WLPartSkillRunner.Instance; if (r == null || r.WeaponSyn == null) return "무기 시너지 없음";
var e = r.Nearest(r.PcPos, 30f); if (e == null) return "적 없음";
var f = typeof(SkWeaponSyn).GetField("_last", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
for (int i = 0; i < n; i++) { if (f != null) f.SetValue(r.WeaponSyn, -9f); r.WeaponSyn.OnBasicHit(e); }
return r.WeaponSyn.def.key + " " + r.WeaponSyn.State + " casts=" + r.WeaponSyn.casts;
}
/// <summary>#881 해머 분신 — 풀 상태 · 남은 컴포넌트 · 재생 중인 클립 길이 · 클립 이벤트(타격 시각).</summary>
public static object CloneInfo()
{
var sb = new StringBuilder();
sb.Append("spawned=").Append(WLPartSkillClone.Spawned).Append(" built=").Append(WLPartSkillClone.Built).Append(" impacts=").Append(WLPartSkillClone.Impacts).Append(" last=").Append(WLPartSkillClone.LastLog).AppendLine();
var f = typeof(WLPartSkillClone).GetField("s_pool", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var pool = f != null ? f.GetValue(null) as System.Collections.IList : null;
if (pool == null) return sb.Append("pool 없음").ToString();
foreach (var o in pool)
{
var t = o.GetType();
var go = t.GetField("go").GetValue(o) as GameObject;
var anim = t.GetField("anim").GetValue(o) as Animator;
bool busy = (bool)t.GetField("busy").GetValue(o);
sb.Append("- ").Append(go != null ? go.name : "null").Append(" active=").Append(go != null && go.activeSelf).Append(" busy=").Append(busy);
if (go != null)
{
sb.Append(" pos=").Append(go.transform.position.ToString("F1")).Append(" scale=").Append(go.transform.localScale.ToString("F2"));
sb.Append(" mb=").Append(go.GetComponentsInChildren<MonoBehaviour>(true).Length).Append(" col=").Append(go.GetComponentsInChildren<Collider>(true).Length);
sb.Append(" ps=").Append(go.GetComponentsInChildren<ParticleSystem>(true).Length).Append(" trail=").Append(go.GetComponentsInChildren<TrailRenderer>(true).Length);
var rs = go.GetComponentsInChildren<Renderer>(true);
sb.Append(" rend=").Append(rs.Length);
var kinds = new HashSet<string>(); foreach (var r in rs) kinds.Add(r.GetType().Name);
sb.Append('(').Append(string.Join(",", new List<string>(kinds).ToArray())).Append(')');
if (rs.Length > 0 && rs[0].sharedMaterial != null) sb.Append(" mat=").Append(rs[0].sharedMaterial.name).Append('/').Append(rs[0].sharedMaterial.shader.name);
}
if (anim != null && go != null && go.activeInHierarchy && anim.runtimeAnimatorController != null)
{
var st = anim.GetCurrentAnimatorStateInfo(0);
sb.Append(" st.len=").Append(st.length.ToString("F2")).Append(" nt=").Append(st.normalizedTime.ToString("F2"));
foreach (var c in anim.GetCurrentAnimatorClipInfo(0))
{
sb.Append(" clip=").Append(c.clip.name).Append(" len=").Append(c.clip.length.ToString("F2"));
foreach (var ev in c.clip.events) sb.Append(" ev[").Append(ev.functionName).Append('@').Append(ev.time.ToString("F2")).Append(']');
}
}
sb.AppendLine();
}
return sb.ToString();
}
/// <summary>#881 FR 스킬 목록 PPT 캡처용 — 살아 있는 이펙트를 모두 거두고, keys 를 PC 앞 fwd m 에 반경 radius(m) 크기로 놓아 t 초 시점에서 멈춘다.
/// 반환 = 놓은 지점의 화면 비율 좌표(vx, vy · 캡처 자르기용).</summary>
public static object Shot(string keysCsv, float t, float radius, float fwd)
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var run = WLPartSkillRunner.Instance;
WLPartSkillFx.ReleaseAll();
var cam = Camera.main;
Vector3 f = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized : Vector3.forward;
var p = pc.transform.position + f * fwd;
var sb = new StringBuilder();
foreach (var k in keysCsv.Split(','))
{
var key = k.Trim(); if (key.Length == 0) continue;
float sc = run != null ? run.FxScale(key, radius) : 1f;
var go = WLPartSkillFx.Play(key, p + Vector3.up * 0.3f, Quaternion.identity, sc, 30f);
if (go == null) { sb.Append(key).Append(" 없음 "); continue; }
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(false)) { ps.Simulate(t, false, true, true); ps.Pause(false); }
sb.Append(key).Append(" x").Append(sc.ToString("F2")).Append(' ');
}
var vp = cam != null ? cam.WorldToViewportPoint(p) : Vector3.zero;
return sb.Append("| vp ").Append(vp.x.ToString("F3")).Append(' ').Append(vp.y.ToString("F3")).ToString();
}
/// <summary>#881 분신 자세 확인 — 켜진 분신과 PC 의 머리 뼈 높이(뿌리 기준) · 분신 회전 · 재생 시점.</summary>
public static object CloneBones()
{
var sb = new StringBuilder();
var f = typeof(WLPartSkillClone).GetField("s_pool", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var pool = f != null ? f.GetValue(null) as System.Collections.IList : null;
System.Func<Transform, string> head = root =>
{
Transform h = null, skel = null;
foreach (var t in root.GetComponentsInChildren<Transform>(true)) if (t.name == "Root_M") { skel = t; break; }
foreach (var t in (skel != null ? skel : root).GetComponentsInChildren<Transform>(true)) if (t.name.ToLower().Contains("head")) { h = t; break; }
return h == null ? "head 없음" : h.name + " dy=" + (h.position.y - root.position.y).ToString("F2");
};
var pc = WLPartSkillCombat.PC;
if (pc != null) sb.Append("PC ").Append(head(pc.transform)).Append(" rot=").Append(pc.transform.eulerAngles.ToString("F0")).Append('\n');
if (pool != null)
foreach (var o in pool)
{
var t = o.GetType();
var go = t.GetField("go").GetValue(o) as GameObject;
var anim = t.GetField("anim").GetValue(o) as Animator;
if (go == null || !go.activeInHierarchy) continue;
sb.Append("clone ").Append(head(go.transform)).Append(" rot=").Append(go.transform.eulerAngles.ToString("F0"));
if (anim != null) sb.Append(" nt=").Append(anim.GetCurrentAnimatorStateInfo(0).normalizedTime.ToString("F2"));
sb.Append('\n');
}
return sb.ToString();
}
/// <summary>#881 캡처 사이 정리 — 살아 있는 파츠 스킬 이펙트 · 분신을 모두 거둔다.</summary>
public static object FxClear() { WLPartSkillFx.ReleaseAll(); WLPartSkillClone.ReleaseAll(); return "fx live=" + WLPartSkillFx.Live; }
/// <summary>#881 Shot 과 같되 멈추지 않고 그대로 재생(메시 애니메이션 이펙트용 · 시간 배율은 호출 쪽에서 늦춘다).</summary>
public static object ShotLive(string keysCsv, float radius, float fwd)
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var run = WLPartSkillRunner.Instance;
WLPartSkillFx.ReleaseAll();
var cam = Camera.main;
Vector3 f = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized : Vector3.forward;
var p = pc.transform.position + f * fwd;
var sb = new StringBuilder();
foreach (var k in keysCsv.Split(','))
{
var key = k.Trim(); if (key.Length == 0) continue;
float sc = run != null ? run.FxScale(key, radius) : 1f;
var go = WLPartSkillFx.Play(key, p + Vector3.up * 0.3f, Quaternion.LookRotation(f, Vector3.up), sc, 30f);
sb.Append(key).Append(go != null ? " x" + sc.ToString("F2") : " 없음").Append(' ');
}
return sb.ToString();
}
/// <summary>#881 PC 계층의 렌더러 종류 · 파티클 · 트레일 수(분신 복제에서 뺄 것 확인).</summary>
public static object PcRenderers()
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var sb = new StringBuilder();
foreach (var r in pc.GetComponentsInChildren<Renderer>(true))
sb.Append(r.GetType().Name).Append(':').Append(r.name).Append(r.gameObject.activeInHierarchy ? "" : "(off)").Append(r.enabled ? "" : "(dis)").Append(" | ");
sb.Append("\nps=").Append(pc.GetComponentsInChildren<ParticleSystem>(true).Length).Append(" ctrl=").Append(pc.m_animation != null && pc.m_animation.runtimeAnimatorController != null ? pc.m_animation.runtimeAnimatorController.name : "-");
return sb.ToString();
}
// ── 장비 화면(UI) ───────────────────────────────────────────────────
public static object GuiOpen() { var g = WLEquipGui.Open(); return g != null ? "open · " + g.PartSkillUiStateForProbe : "열기 실패"; }
public static object GuiDetail(int slotIndex) { var g = WLEquipGui.Instance; if (g == null) return "GUI 없음"; g.OpenSkillDetailForProbe(slotIndex); return g.PartSkillUiStateForProbe; }
public static object GuiBook() { var g = WLEquipGui.Instance; if (g == null) return "GUI 없음"; g.OpenSynergyBookForProbe(); return g.PartSkillUiStateForProbe; }
public static object GuiItem(int part) { var g = WLEquipGui.Instance; var it = WLInventory.EquippedInSlot((eFREquipmentType)part); if (g == null || it == null) return "없음"; g.ClosePartSkillForProbe(); g.SelectForProbe(it.rawItem._id); return "item " + it.itemJson.Item_name; }
public static object GuiCloseAll() { var g = WLEquipGui.Instance; if (g == null) return "GUI 없음"; g.ClosePopupsForProbe(); return g.PartSkillUiStateForProbe; }
public static object GuiClose() { WLEquipGui.Close(); return "closed"; }
/// <summary>시험 뒤 정리 — 도감 「찾은 시너지」(PlayerPrefs) 를 비운다(PD 첫 확인 때 시험 기록이 보이지 않게).</summary>
public static object ResetSeen()
{
int n = WLPartSkillState.SeenSynergies.Count;
WLPartSkillState.SeenSynergies.Clear();
PlayerPrefs.DeleteKey("WL_PartSkill_Synergies"); PlayerPrefs.Save();
return "도감 기록 " + n + " 건 삭제";
}
public static object UnequipAll()
{
if (!WLInventory.EnsureLoaded()) return "인벤토리 로드 실패";
var ids = new List<int>(WLInventory.Data.equipItems);
foreach (var id in ids) WLInventory.Unequip(id);
WLInventory.Commit("WL879 프로브 전부 해제");
return "해제 " + ids.Count + " · " + WLPartSkillState.LastLog;
}
public static object Dump()
{
var sb = new StringBuilder();
sb.AppendLine("data: " + WLPartSkillData.LastLog);
sb.AppendLine("state v" + WLPartSkillState.Version + ": " + WLPartSkillState.LastLog);
var L = WLPartSkillState.Current;
sb.AppendLine("mods cool×" + L.coolMul.ToString("F2") + " range×" + L.rangeMul.ToString("F2") + " dur×" + L.durMul.ToString("F2") + " proj×" + L.projSpeedMul.ToString("F2") + " dmg×" + L.dmgMul.ToString("F2") + " move+" + L.moveBonus.ToString("F2") + " regen " + L.regenPct.ToString("F2") + "/" + L.regenPeriod + "s spiritKing=" + L.spiritKing);
foreach (var kv in L.statAdds) sb.Append(kv.Key).Append('+').Append(kv.Value.ToString("0.###")).Append(' ');
sb.AppendLine();
sb.AppendLine("hook1 n=" + WLPartStats.Hook1Count + " " + WLPartStats.LastHook1);
var r = WLPartSkillRunner.Instance;
sb.AppendLine("runner: " + (r != null ? WLPartSkillRunner.LastLog + "\n " + r.Dump() : "없음"));
sb.AppendLine("combat hits=" + WLPartSkillCombat.Hits + " kills=" + WLPartSkillCombat.Kills + " blocks=" + WLPartSkillCombat.Blocks + " statuses=" + WLPartSkillCombat.Statuses + " dmg=" + WLPartSkillCombat.DamageTotal.ToString("F0") + " absorbed=" + WLPartSkillHooks.Absorbed);
foreach (var kv in WLPartSkillCombat.DamageByKey) sb.Append(kv.Key).Append('=').Append(kv.Value.ToString("F0")).Append('/').Append(WLPartSkillCombat.HitsByKey.ContainsKey(kv.Key) ? WLPartSkillCombat.HitsByKey[kv.Key] : 0).Append(' ');
sb.AppendLine();
sb.AppendLine("fx spawned=" + WLPartSkillFx.Spawned + " reused=" + WLPartSkillFx.Reused + " live=" + WLPartSkillFx.Live + " missing=" + WLPartSkillFx.MissingPrefab + " nullMatHidden=" + WLPartSkillFx.NullMatHidden + " " + string.Join(",", WLPartSkillFx.MissingKeys.ToArray()));
sb.AppendLine("hud banners=" + WLPartSkillHud.Banners + " last=" + WLPartSkillHud.LastBanner + " · seen=" + string.Join(",", new List<string>(WLPartSkillState.SeenSynergies).ToArray()));
// WLWeaponSkillSlots.Dump() 는 ObscuredFloat 변환 예외(기존 코드 · 이 작업 범위 밖)라 LastLog 만 읽는다
sb.AppendLine("weaponSkills=" + WL.Combat.Weapon.WLWeaponSettings.Instance.weaponSkills + " applied=" + WL.Combat.Weapon.WLWeaponSkillSlots.Applied + " slots: " + WL.Combat.Weapon.WLWeaponSkillSlots.LastLog);
return sb.ToString();
}
/// <summary>이펙트 배율 1 일 때 반경 실측 — 파티클을 sec 초 미리 돌려 렌더러 경계를 잰다(fxRadius 보정용).</summary>
public static object Measure(float sec)
{
var sb = new StringBuilder();
var keys = new List<string>(WLPartSkillData.S.fxRadius.Keys);
if (!keys.Contains("fx_synergy")) keys.Add("fx_synergy");
var far = new Vector3(0f, -500f, 0f);
foreach (var k in keys)
{
var go = WLPartSkillFx.Get(k, far, Quaternion.identity, 1f);
if (go == null) { sb.Append(k).Append(": 없음\n"); continue; }
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Simulate(sec, false, true, false);
bool any = false; var b = new Bounds(far, Vector3.zero);
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
{
if (!r.enabled) continue;
var rb = r.bounds;
if (rb.size.sqrMagnitude < 1e-6f) continue;
if (!any) { b = rb; any = true; } else b.Encapsulate(rb);
}
float rad = any ? Mathf.Max(Mathf.Abs(b.max.x - far.x), Mathf.Abs(b.min.x - far.x), Mathf.Abs(b.max.z - far.z), Mathf.Abs(b.min.z - far.z)) : 0f;
sb.Append(k).Append(": r=").Append(rad.ToString("F2")).Append(" size=").Append(b.size.ToString("F2")).Append(" ps=").Append(go.GetComponentsInChildren<ParticleSystem>(true).Length).Append('\n');
WLPartSkillFx.Release(go);
}
return sb.ToString();
}
/// <summary>반경 추정 2 — 파티클 = (방출 모양 반경 + 시작 크기/2) × 월드 배율 · 메시 = 렌더러 경계. 큰 값 = 그 이펙트의 「배율 1 반경」.</summary>
public static object Measure2()
{
var sb = new StringBuilder();
var keys = new List<string>(WLPartSkillData.S.fxRadius.Keys);
if (!keys.Contains("fx_synergy")) keys.Add("fx_synergy");
foreach (var k in keys)
{
var go = WLPartSkillFx.Get(k, new Vector3(0f, -500f, 0f), Quaternion.identity, 1f);
if (go == null) { sb.Append(k).Append(": 없음\n"); continue; }
float best = 0f; string who = "";
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
{
var m = ps.main; var sh = ps.shape;
float s = Mathf.Abs(ps.transform.lossyScale.x);
float size = m.startSize.mode == ParticleSystemCurveMode.TwoConstants ? m.startSize.constantMax : m.startSize.constant;
if (m.startSize3D) size = Mathf.Max(m.startSizeX.constantMax, m.startSizeZ.constantMax);
float rad = sh.enabled ? Mathf.Max(sh.radius * Mathf.Max(sh.scale.x, sh.scale.z), 0f) : 0f;
float speed = m.startSpeed.mode == ParticleSystemCurveMode.TwoConstants ? m.startSpeed.constantMax : m.startSpeed.constant;
float life = m.startLifetime.mode == ParticleSystemCurveMode.TwoConstants ? m.startLifetime.constantMax : m.startLifetime.constant;
float travel = Mathf.Min(Mathf.Abs(speed) * life, 50f) * 0.5f;
float r = (rad + size * 0.5f + travel) * s;
if (r > best) { best = r; who = ps.name + " size" + size.ToString("F2") + " rad" + rad.ToString("F2") + " trv" + travel.ToString("F2") + " s" + s.ToString("F2"); }
}
foreach (var mr in go.GetComponentsInChildren<MeshRenderer>(true))
{
var e = mr.bounds.extents; float r = Mathf.Max(e.x, e.z);
if (r > best) { best = r; who = "mesh " + mr.name; }
}
sb.Append(k).Append(": r=").Append(best.ToString("F2")).Append(" ← ").Append(who).Append(" | root s=").Append(go.transform.lossyScale.x.ToString("F2")).Append('\n');
WLPartSkillFx.Release(go);
}
return sb.ToString();
}
/// <summary>크기 눈 보정 — keys 를 PC 주변 격자에 배율 scale 로 life 초 띄운다(캡처로 캐릭터 키 1.8 m 와 비교).</summary>
public static object Gallery(string keysCsv, float scale, float spacing, float life)
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var keys = keysCsv.Split(',');
var cam = Camera.main;
Vector3 fwd = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized : Vector3.forward;
Vector3 right = Vector3.Cross(Vector3.up, fwd);
var o = pc.transform.position;
var sb = new StringBuilder();
int cols = 3;
for (int i = 0; i < keys.Length; i++)
{
int cx = i % cols - 1, cz = i / cols - 1;
var p = o + right * (cx * spacing) + fwd * (-cz * spacing);
var go = WLPartSkillFx.Play(keys[i].Trim(), p + Vector3.up * 0.05f, Quaternion.identity, scale, life);
sb.Append(keys[i]).Append(go != null ? " ok" : " 없음").Append(" @(").Append(cx).Append(',').Append(cz).Append(") | ");
}
return sb.ToString();
}
/// <summary>반경 실측 3 — 새 인스턴스를 멀리 두고 t 초 시뮬레이트 뒤 살아 있는 파티클의 실제 위치 · 크기로 수평 반경(90 % 분위 · 최대)을 잰다.</summary>
public static object Measure3(float t)
{
var sb = new StringBuilder();
var keys = new List<string>(WLPartSkillData.S.fxRadius.Keys);
if (!keys.Contains("fx_synergy")) keys.Add("fx_synergy");
var origin = new Vector3(1000f, 0f, 1000f);
var buf = new ParticleSystem.Particle[4096];
foreach (var k in keys)
{
var prefab = WLPartSkillFx.Prefab(k);
if (prefab == null) { sb.Append(k).Append(": 없음\n"); continue; }
var go = Object.Instantiate(prefab, origin, Quaternion.identity);
var list = new List<float>();
float top = 0f;
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
{
var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy;
}
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Simulate(t, false, true, true);
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
{
int n = ps.GetParticles(buf);
bool local = ps.main.simulationSpace == ParticleSystemSimulationSpace.Local;
float sc = Mathf.Abs(ps.transform.lossyScale.x);
for (int i = 0; i < n; i++)
{
var p = local ? ps.transform.TransformPoint(buf[i].position) : buf[i].position;
float size = buf[i].GetCurrentSize(ps) * (local ? sc : 1f);
var d = p - origin; d.y = 0f;
list.Add(d.magnitude + size * 0.5f);
top = Mathf.Max(top, p.y - origin.y + size * 0.5f);
}
}
float mesh = 0f;
foreach (var mr in go.GetComponentsInChildren<MeshRenderer>(true)) { var e = mr.bounds.extents; mesh = Mathf.Max(mesh, Mathf.Max(e.x, e.z)); }
list.Sort();
float p90 = list.Count > 0 ? list[Mathf.Clamp(Mathf.RoundToInt(list.Count * 0.9f) - 1, 0, list.Count - 1)] : 0f;
float mx = list.Count > 0 ? list[list.Count - 1] : 0f;
sb.Append(k).Append(": p90=").Append(p90.ToString("F2")).Append(" max=").Append(mx.ToString("F2")).Append(" top=").Append(top.ToString("F2")).Append(" mesh=").Append(mesh.ToString("F2")).Append(" n=").Append(list.Count).Append('\n');
Object.Destroy(go);
}
return sb.ToString();
}
/// <summary>캡처용 — keys 를 PC 앞 격자에 놓고 t 초 시점에서 파티클을 멈춘다(순간 이펙트의 모습 · 분홍 여부 확인).</summary>
public static object Freeze(string keysCsv, float t, float scale, float spacing)
{
var pc = WLPartSkillCombat.PC; if (pc == null) return "PC 없음";
var cam = Camera.main;
Vector3 fwd = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized : Vector3.forward;
Vector3 right = Vector3.Cross(Vector3.up, fwd);
var keys = keysCsv.Split(',');
var sb = new StringBuilder();
for (int i = 0; i < keys.Length; i++)
{
var p = pc.transform.position + fwd * 1.5f + right * ((i - (keys.Length - 1) * 0.5f) * spacing);
var go = WLPartSkillFx.Play(keys[i].Trim(), p + Vector3.up * 0.5f, Quaternion.identity, scale, 8f);
if (go == null) { sb.Append(keys[i]).Append(" 없음 | "); continue; }
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(false)) { ps.Simulate(t, false, true, true); ps.Pause(false); }
sb.Append(keys[i]).Append(" @").Append(i).Append(" | ");
}
return sb.ToString();
}
public static object Stats()
{
var info = WLEquipStats.LivePcInfo();
if (info == null) return "PC 없음";
var sb = new StringBuilder();
foreach (var s in new[] { eStat.ATK, eStat.BaseDmg, eStat.MaxHP, eStat.HP, eStat.DEF, eStat.FinalDef, eStat.CRI, eStat.CRI_DMG_Multiplier, eStat.ATK_Multiplier, eStat.HP_Multiplier, eStat.DEF_Multiplier,
eStat.ATK_SPD_Multiplier, eStat.MOVE_SPD_Multiplier, eStat.FinalMoveSpeed, eStat.COOLTIME_Multiplier, eStat.HP_REGEN_Multiplier, eStat.EXP_Multiplier, eStat.GOLD_Multiplier, eStat.SKILL_Multiplier })
sb.Append(s).Append('=').Append(info.Get_Stat(s).ToString("0.####")).Append(" ");
sb.AppendLine();
sb.AppendLine(WLEquipStats.StatusLine());
sb.AppendLine("equip report: " + WLEquipStats.LastReport);
var pc = WLPartSkillCombat.PC;
if (pc != null) sb.AppendLine("moveSpeed=" + pc.Get_MoveSpeed().ToString("F2") + " navBase=" + (WLPartSkillRunner.Instance != null ? WLPartSkillRunner.Instance.NavBase.ToString("F2") : "-"));
foreach (eFREquipmentType t in new[] { eFREquipmentType.Weapon, eFREquipmentType.Necklace, eFREquipmentType.Glove, eFREquipmentType.Armor, eFREquipmentType.Belt, eFREquipmentType.Shose })
{
var it = WLInventory.EquippedInSlot(t);
sb.Append(WLEquipTables.SlotName(t)).Append(": ").Append(it != null ? it.itemJson.Item_name + " · " + WLPartStats.ItemStatLine(it, true) : "-").AppendLine();
}
return sb.ToString();
}
}