Project_WL/AgentScripts/WL879_Probe.cs

423 lines
28 KiB
C#
Raw Permalink Normal View History

// 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();
}
}