Project_WL/AgentScripts/WL815f_Probe.cs

699 lines
41 KiB
C#
Raw 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.

// WL-815f 프로브 — 적 공격 예고(텔레그래프) 1차 · 에디트 모드 전용(Play 0 · 로그인 0 · 커밋 0)
//
// MakeAsset — WLTelegraphSettings.asset 생성/갱신(손으로 YAML 쓰지 않는다)
// Run — A 클립 히트 시각 표(몹 12종) · B 콜라이더 실측 · C 가짜 왕복(3종+보스) · D 동시 상한
// E 보스 무변경 · F GC 0 · G C8 · H 애니메이터 실측(데미지도 늦어짐 증명) · I 캡처 3장
//
// 산출물 = AgentScripts/staging/WL815f/EDIT_PROBE.txt · 캡처 = Screenshots_WL/WL815f/
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using WL.Combat.Telegraph;
public static class WL815f_Probe
{
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL815f";
const string kSnap = "AgentScripts/staging/WL815f/EDIT_PROBE.txt";
const string kSo = "Assets/WL/Combat/Settings/Resources/WL/WLTelegraphSettings.asset";
const string kMobRoot = "Assets/Res_Addr/Mobs/";
const string kProjRoot = "Assets/Res_Addr/Projectile/";
const int W = 1080, H = 1920;
static System.Text.StringBuilder sb;
static int pass, fail;
static readonly List<string> s_errors = new List<string>();
static void L(string s) { sb.Append(s).Append('\n'); }
static void Chk(bool ok, string label, string detail)
{
if (ok) pass++; else fail++;
sb.Append(ok ? " PASS " : " FAIL ").Append(label);
if (!string.IsNullOrEmpty(detail)) sb.Append(" ").Append(detail);
sb.Append('\n');
}
static string F(float v, int d) { return v.ToString(d == 3 ? "F3" : d == 2 ? "F2" : "F4"); }
// ─────────────────────────────────────────────────────────────────────────
public static string MakeAsset()
{
sb = new System.Text.StringBuilder();
var dir = System.IO.Path.GetDirectoryName(kSo).Replace('\\', '/');
if (!System.IO.Directory.Exists(dir)) { L("FAIL 폴더 없음 " + dir); return sb.ToString(); }
var so = AssetDatabase.LoadAssetAtPath<WLTelegraphSettings>(kSo);
bool created = false;
if (so == null)
{
so = ScriptableObject.CreateInstance<WLTelegraphSettings>();
AssetDatabase.CreateAsset(so, kSo);
created = true;
}
// 캡처 실측으로 조정한 값(몹이 통째로 노랗게 보여 에미션을 낮췄다) — 코드 기본값과 같게 유지한다
so.tintAlbedoAvoidable = new Color(1.20f, 1.05f, 0.60f, 1f);
so.tintEmissionAvoidable = new Color(0.55f, 0.34f, 0.02f, 1f);
so.tintAlbedoUnavoidable = new Color(1.30f, 0.60f, 0.55f, 1f);
so.tintEmissionUnavoidable = new Color(0.75f, 0.08f, 0.04f, 1f);
so.defaultProjectileSpeed = 5f;
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
L((created ? "CREATED " : "EXISTS ") + kSo);
L(" enabled=" + so.enabled + " 잡몹 " + so.telegraphSecondsNormal + "s · 원거리 " + so.telegraphSecondsRanged
+ "s · 엘리트 " + so.telegraphSecondsElite + "s · 하한 k " + so.minSpeedMultiplier
+ " · 상한 " + so.maxConcurrent + " · 펄스 " + so.pulseScale + " · SFX idx " + so.sfxIndex);
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
public static string Run()
{
sb = new System.Text.StringBuilder();
pass = fail = 0; s_errors.Clear();
Application.logMessageReceived += OnLog;
// 배치 에디터의 현재(제목 없는·저장 안 된) 씬을 그대로 작업장으로 쓴다.
// 만드는 오브젝트는 전부 HideFlags.DontSave 이고 끝에서 파기한다 → 저장 0 · 커밋 0.
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
try
{
L("# WL-815f 프로브 — 적 공격 예고 1차 (에디트 모드 · Play 0)");
L("생성 " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L("");
WLTelegraphSettings.ClearCache();
var st = WLTelegraphSettings.Instance;
if (st == null) { L("FAIL — SO 를 못 읽었다: " + kSo); return Finish(); }
Telegraph.EnsureSubscribed();
Telegraph.ResetDiagnostics();
Telegraph.ClearClipCache();
TelegraphShape.ClearCache(); TelegraphShape.ResetDiagnostics();
TelegraphDecal.ResetDiagnostics();
WL.Combat.Reaction.ImpactTier.ResetDiagnostics();
TelegraphRunner.BeginForceClock();
SectionA(st);
SectionB(st);
SectionC(st, scene);
SectionD(st, scene);
SectionE(st, scene);
SectionF(st, scene);
SectionG(st, scene);
SectionH(scene);
SectionJ(st, scene);
SectionI(st, scene);
}
catch (System.Exception ex)
{
L("EXCEPTION " + ex.GetType().Name + " : " + ex.Message);
L(ex.StackTrace);
fail++;
}
finally
{
Telegraph.ClearAll();
TelegraphDecal.DestroyAll();
TelegraphRunner.ResetForceClock();
WLTelegraphSettings.RuntimeDisabled = false;
CleanupStrays();
Application.logMessageReceived -= OnLog;
}
return Finish();
}
static string Finish()
{
L("");
L("## 콘솔 Error/Exception " + s_errors.Count + "건");
for (int i = 0; i < s_errors.Count && i < 12; i++) L(" " + s_errors[i]);
L("");
L("RESULT PASS " + pass + " / FAIL " + fail);
var dir = System.IO.Path.GetDirectoryName(kSnap);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kSnap, sb.ToString());
return sb.ToString();
}
/// <summary>프로브가 만든 오브젝트가 씬에 남지 않게 한다(이름 접두 PROBE_ · 저장 0).</summary>
static void CleanupStrays()
{
var all = Object.FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
int n = 0;
for (int i = 0; i < all.Length; i++)
{
var g = all[i];
if (g == null || g.transform.parent != null) continue;
if (g.name.StartsWith("PROBE_") || g.name.StartsWith("__WLTelegraph")) { Object.DestroyImmediate(g); n++; }
}
if (n > 0) L(" (정리) 프로브 오브젝트 " + n + "개 파기");
}
static void OnLog(string cond, string stack, LogType t)
{
if (t == LogType.Error || t == LogType.Exception || t == LogType.Assert)
if (s_errors.Count < 40) s_errors.Add(t + " : " + cond);
}
// ── 몹 표(기준서 §G-0 실측표와 대조할 12종) ────────────────────────────
struct MobRow { public string label, prefab, proj; public float lifetime; public bool elite, boss; }
static readonly MobRow[] s_mobs =
{
new MobRow{ label="Batty_A(101) 존1 잡몹", prefab="Mob/Batty_A", proj="Mob/P_Batty_Meele", lifetime=0.1f },
new MobRow{ label="Batty_B(102) 존1 원거리", prefab="Mob/Batty_B", proj="Mob/P_Batty_Range", lifetime=1.5f },
new MobRow{ label="Porin_A(168) 존2 잡몹", prefab="Mob/Porin_A", proj="Mob/P_Porin_Meele", lifetime=0.1f },
new MobRow{ label="Racco_A(173) 존3 잡몹", prefab="Mob/Racco_A", proj="Mob/P_Racco_Meele", lifetime=0.1f },
new MobRow{ label="Rabby_Brown(146) 존3 잡몹", prefab="Mob/Rabby_Brown", proj="Mob/P_Rabby_Meele", lifetime=0.1f },
new MobRow{ label="Devilu_A(118) 존4 잡몹", prefab="Mob/Devilu_A", proj="Mob/P_Devilu_Meele", lifetime=0.1f },
new MobRow{ label="Golem_A(1014) 엘리트", prefab="Elite/Golem_A", proj="Elite/P_Golem_A", lifetime=0.1f, elite=true },
new MobRow{ label="Aspethis_D(1023) 엘리트", prefab="Elite/Aspethis_D", proj="Elite/P_Aspethis_D", lifetime=0.1f, elite=true },
new MobRow{ label="Beeto_A(1009) 엘리트", prefab="Elite/Beeto_A", proj="Elite/P_Beeto_A", lifetime=0.1f, elite=true },
new MobRow{ label="Cute_Crab_C(1028) 엘리트", prefab="Elite/Cute_Crab_C_Blue", proj="Elite/P_Cute_Crab_C_Blue",lifetime=0.1f, elite=true },
new MobRow{ label="Mummy_King(1024) 엘리트", prefab="Elite/Mummy_King", proj="Elite/P_Mummy_King", lifetime=1.5f, elite=true },
new MobRow{ label="Anubis(10006) 보스", prefab="FieldBoss/Anubis", proj="FieldBoss/Anubis_Attack", lifetime=0.1f, boss=true },
};
// ─────────────────────────────────────────────────────────── A. 클립 히트 시각 표
static void SectionA(WLTelegraphSettings st)
{
L("## A. 예고 시간 전/후 표 (몹 12종 · 클립 히트 이벤트 실측 · 목표 = SO 값)");
L("| 몹 | 컨트롤러 | 히트 이벤트 s(전) | 등급 목표 s | k(= 적용 속도) | 하한 걸림 | 예고 s(후) | 배수 |");
L("|---|---|---|---|---|---|---|---|");
int measured = 0;
for (int i = 0; i < s_mobs.Length; i++)
{
var m = s_mobs[i];
var go = AssetDatabase.LoadAssetAtPath<GameObject>(kMobRoot + m.prefab + ".prefab");
if (go == null) { L("| " + m.label + " | (프리팹 없음) | | | | | | |"); continue; }
var anim = go.GetComponentInChildren<Animator>(true);
var rac = anim != null ? anim.runtimeAnimatorController : null;
string racName = rac != null ? rac.name : "(없음)";
var tbl = Telegraph.HitSecondsTable(rac);
float before = tbl != null && tbl.Length > 0 ? tbl[0] : -1f;
if (before > 0f) measured++;
bool ranged = m.lifetime >= st.rangedLifetimeThreshold;
float target = m.boss ? -1f : (m.elite ? st.telegraphSecondsElite : (ranged ? st.telegraphSecondsRanged : st.telegraphSecondsNormal));
if (m.boss)
{
L("| " + m.label + " | " + racName + " | " + (before > 0f ? F(before, 3) : "-")
+ " | **무변경**(코드 3.0 s 차징) | 1.000 | - | " + (before > 0f ? F(before, 3) : "-") + " | ×1.00 |");
continue;
}
if (before <= 0f) { L("| " + m.label + " | " + racName + " | (히트 이벤트 못 찾음) | " + F(target, 2) + " | | | | |"); continue; }
float desired = before / target;
float floorSpeed = 1f * Mathf.Clamp(st.minSpeedMultiplier, 0.01f, 1f);
float applied = Mathf.Clamp(desired, floorSpeed, 1f);
bool clamped = desired < floorSpeed - 0.0001f;
float after = before / applied;
L("| " + m.label + " | " + racName + " | " + F(before, 3) + " | " + F(target, 2) + " | " + F(applied, 3)
+ " | " + (clamped ? "🔴 O" : "-") + " | **" + F(after, 3) + "** | ×" + F(after / before, 2) + " |");
}
L("");
Chk(measured >= 10, "12종 중 클립 히트 이벤트 실측 성공", measured + " / " + s_mobs.Length);
L("→ 🔴 속도 곡선만으로는 목표 0.5/0.6/0.8 s 에 도달하지 못한다(하한 k=" + st.minSpeedMultiplier + ").");
L(" 나머지 시간은 **바닥 판이 채워지는 " + F(st.telegraphSecondsNormal, 2) + " s** 와 몹 크기/색/소리가 채운다(기준서 §G-2 ⓐ 한계 그대로).");
L("");
}
// ─────────────────────────────────────────────────────────── B. 콜라이더 실측
static void SectionB(WLTelegraphSettings st)
{
L("## B. 데칼 크기의 출처 = 투사체 프리팹 BoxCollider (신규 데이터 0)");
L("| 투사체 | 읽은 size | 읽은 center | 속도 | 출처 | 기준서 §G-0 실측 |");
L("|---|---|---|---|---|---|");
string[] expect = { "3×3×3 c(0,0,0)", "1×3×1 c(0,0,0)", "3×3×3 c(0,0,0)", "3×3×3 c(0,0,1.5)", "2×2×4 c(0,0,2)" };
string[] names = { "Mob/P_Batty_Meele", "Mob/P_Batty_Range", "Mob/P_Porin_Meele", "Elite/P_Golem_A", "FieldBoss/Anubis_Attack" };
Vector3[] wantSize = { new Vector3(3, 3, 3), new Vector3(1, 3, 1), new Vector3(3, 3, 3), new Vector3(3, 3, 3), new Vector3(2, 2, 4) };
Vector3[] wantCenter = { Vector3.zero, Vector3.zero, Vector3.zero, new Vector3(0, 0, 1.5f), new Vector3(0, 0, 2f) };
int ok = 0;
for (int i = 0; i < names.Length; i++)
{
var b = TelegraphShape.Get(names[i]);
bool match = b.resolved && (b.size - wantSize[i]).sqrMagnitude < 0.0001f && (b.center - wantCenter[i]).sqrMagnitude < 0.0001f;
if (match) ok++;
L("| " + names[i] + " | " + b.size + " | " + b.center + " | " + F(b.speed, 2) + " | " + b.source + " | " + expect[i] + (match ? " ✅" : " ❌") + " |");
}
Chk(ok == names.Length, "콜라이더 실측 = 기준서 §G-0 표와 완전 일치", ok + " / " + names.Length);
L("");
}
// ─────────────────────────────────────────────────────────── C. 가짜 왕복
static GameObject MakeMob(MobRow m, Vector3 pos, UnityEngine.SceneManagement.Scene scene, out MobActor actor)
{
actor = null;
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(kMobRoot + m.prefab + ".prefab");
if (prefab == null) return null;
var go = Object.Instantiate(prefab);
go.name = "PROBE_" + m.prefab.Replace('/', '_');
go.hideFlags = HideFlags.DontSave;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(go, scene);
go.transform.position = pos;
go.transform.rotation = Quaternion.identity;
actor = go.GetComponent<MobActor>();
if (actor == null) { Object.DestroyImmediate(go); return null; }
actor.m_Role = eRole.Mob;
actor.m_SubRole = m.boss ? eSubRol.Boss : (m.elite ? eSubRol.Elite : eSubRol.None);
actor.m_animation = go.GetComponentInChildren<Animator>(true);
var td = new MonsterTableData();
td.e_MonsterType = actor.m_SubRole;
td.s_Porjectile1 = m.proj;
td.f_ProjectileLifeTime1 = m.lifetime;
td.e_AttackType = eAttackType.Physics;
td.f_DefaultScale = go.transform.localScale.x;
var fi = typeof(MobActor).GetField("m_MonsterTableData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (fi != null) fi.SetValue(actor, td);
return go;
}
static void SectionC(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## C. 가짜 AttackStarted → HitboxSpawned 왕복 (몹 3종 + 보스 · 실측)");
int[] pick = { 0, 2, 6, 11 }; // Batty_A · Porin_A · Golem_A(엘리트) · Anubis(보스)
for (int p = 0; p < pick.Length; p++)
{
var m = s_mobs[pick[p]];
MobActor actor;
var go = MakeMob(m, new Vector3(p * 20f, 0f, 0f), scene, out actor);
if (go == null) { Chk(false, "C 몹 인스턴스 " + m.label, "프리팹/MobActor 없음"); continue; }
float scale0 = go.transform.localScale.x;
float speed0 = actor.m_animation != null ? 1f : -1f;
if (actor.m_animation != null) actor.m_animation.speed = 1f;
Telegraph.ResetDiagnostics();
TelegraphDecal.ResetDiagnostics();
WL.Combat.Core.CombatEvents.RaiseAttackStarted(actor, 0, 1f, null);
L("");
L("### " + m.label);
if (m.boss)
{
bool untouched = Telegraph.AppliedSpeedOf(actor) < 0f && (actor.m_animation == null || Mathf.Abs(actor.m_animation.speed - 1f) < 0.0001f)
&& Mathf.Abs(go.transform.localScale.x - scale0) < 0.0001f && Telegraph.DecalIndexOf(actor) < 0;
Chk(untouched, "보스 무변경(속도·크기·판 전부 원본)",
"speed=" + (actor.m_animation != null ? F(actor.m_animation.speed, 3) : "-") + " scale=" + F(go.transform.localScale.x, 3)
+ " 판=" + Telegraph.DecalIndexOf(actor) + " SkippedBoss=" + Telegraph.SkippedBoss);
Object.DestroyImmediate(go);
continue;
}
float planned = Telegraph.PlannedSecondsOf(actor);
float applied = Telegraph.AppliedSpeedOf(actor);
int decal = Telegraph.DecalIndexOf(actor);
L(" 히트 시각 " + F(Telegraph.LastHitSeconds, 3) + " s → 목표 " + F(Telegraph.LastTargetSeconds, 2)
+ " s · 속도 " + F(Telegraph.LastOrigSpeed, 3) + "→" + F(applied, 3)
+ (Telegraph.LastWasClamped ? " (🔴 하한 " + st.minSpeedMultiplier + " 로 잘림)" : "")
+ " · 실제 예고 " + F(planned, 3) + " s");
Chk(applied > 0f && actor.m_animation != null && Mathf.Abs(actor.m_animation.speed - applied) < 0.0001f,
"animator.speed 가 실제로 낮아졌다", "anim.speed=" + (actor.m_animation != null ? F(actor.m_animation.speed, 3) : "-"));
Chk(Mathf.Abs(applied - Mathf.Max(Telegraph.LastHitSeconds / Telegraph.LastTargetSeconds, st.minSpeedMultiplier)) < 0.001f,
"속도 = max(히트시각 ÷ 목표, 하한)", "기대 " + F(Mathf.Max(Telegraph.LastHitSeconds / Telegraph.LastTargetSeconds, st.minSpeedMultiplier), 3));
// 판 크기 = 콜라이더 크기인가
Vector3 psize, pcenter; float yaw;
var box = TelegraphShape.Get(m.proj);
bool ranged = m.lifetime >= st.rangedLifetimeThreshold;
if (TelegraphDecal.TryGetPlate(decal, out psize, out pcenter, out yaw))
{
float wantW = box.size.x, wantL = ranged ? Mathf.Clamp(box.speed * m.lifetime, box.size.z, st.rangedMaxLength) : box.size.z;
bool sizeOk = Mathf.Abs(psize.x - wantW) < 0.01f && Mathf.Abs(psize.z - wantL) < 0.01f;
Chk(sizeOk, "판 크기 = 투사체 콜라이더 크기", "판 " + F(psize.x, 3) + "×" + F(psize.z, 3) + " · 콜라이더 " + F(wantW, 3) + "×" + F(wantL, 3));
float wantZ = st.forwardOffset + box.center.z + (ranged ? wantL * 0.5f : 0f);
bool cenOk = Mathf.Abs((pcenter.z - go.transform.position.z) - wantZ) < 0.01f;
Chk(cenOk, "판 중심 = 발사 위치 + collider.center", "판 z=" + F(pcenter.z - go.transform.position.z, 3) + " 기대 " + F(wantZ, 3));
}
else Chk(false, "판이 켜졌다", "decalIdx=" + decal);
// 회전 추종
go.transform.rotation = Quaternion.Euler(0f, 90f, 0f);
TelegraphRunner.ForceTick(0.02f);
if (TelegraphDecal.TryGetPlate(decal, out psize, out pcenter, out yaw))
Chk(Mathf.Abs(Mathf.DeltaAngle(yaw, 90f)) < 0.5f, "몹이 돌면 판도 따라 돈다", "판 yaw=" + F(yaw, 2));
// 채워지는 진행 + 펄스
float half = Mathf.Max(0.02f, planned * 0.5f - 0.02f);
TelegraphRunner.ForceTick(half);
float midScale = go.transform.localScale.x;
Chk(midScale > scale0 * 1.0005f && midScale < scale0 * st.pulseScale, "예고 중 크기가 커진다(펄스)",
"기본 " + F(scale0, 3) + " → 중간 " + F(midScale, 3) + " (상한 " + F(scale0 * st.pulseScale, 3) + ")");
Chk(WL.Combat.Reaction.MobHitFlash.HasEliteTint(actor) || !st.tintEnabled, "예고 중 몹 색(MPB)이 걸렸다",
"TintApplied=" + Telegraph.TintApplied);
// 811gh 히트스톱 스킵 조건
WL.Combat.Reaction.ImpactTier.ClearStamp();
var tier = WL.Combat.Reaction.ImpactTier.Resolve(actor, null, false);
float hs = WL.Combat.Reaction.ImpactTier.HitStopSeconds(tier);
Chk(!WL.Combat.Reaction.ImpactTier.Active || hs <= 0.0001f, "예고 중 히트스톱 = 0 (기준서 §G-4)",
"tier=" + tier + " hitStop=" + F(hs, 4) + "s skip=" + WL.Combat.Reaction.ImpactTier.LastSkipHitStop
+ " (티어 활성=" + WL.Combat.Reaction.ImpactTier.Active + ")");
// 히트 → 원복
WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(actor, m.proj, 1f, m.lifetime, go.transform.position);
Chk(actor.m_animation != null && Mathf.Abs(actor.m_animation.speed - 1f) < 0.0001f, "히트에서 animator.speed 원복",
"speed=" + (actor.m_animation != null ? F(actor.m_animation.speed, 3) : "-") + " (원본 " + F(speed0, 3) + ")");
Chk(Mathf.Abs(go.transform.localScale.x - scale0) < 0.0001f, "히트에서 크기 원복",
"scale=" + F(go.transform.localScale.x, 4) + " 기본 " + F(scale0, 4));
Chk(TelegraphDecal.InUse == 0, "판 반납", "InUse=" + TelegraphDecal.InUse);
// 흰 섬광 만료 후 색 원복
TelegraphRunner.ForceTick(st.flashSeconds + 0.02f);
TelegraphRunner.ForceTick(st.pulseRestoreGuardSeconds + 0.02f);
Chk(!WL.Combat.Reaction.MobHitFlash.HasEliteTint(actor), "섬광 뒤 몹 색 원복(MPB 잔류 0)",
"HasTint=" + WL.Combat.Reaction.MobHitFlash.HasEliteTint(actor) + " HasBlock=" + WL.Combat.Reaction.MobHitFlash.HasAnyBlock(actor));
Chk(Telegraph.SpeedRestored >= 1 && Telegraph.PulseRestored >= 1, "원복 카운터",
"SpeedRestored=" + Telegraph.SpeedRestored + " PulseRestored=" + Telegraph.PulseRestored);
Object.DestroyImmediate(go);
}
L("");
}
// ─────────────────────────────────────────────────────────── D. 동시 상한
static void SectionD(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## D. 동시 상한 " + st.maxConcurrent + " (초과 = 판 생략 · 811s 예산기 방식)");
Telegraph.ResetDiagnostics(); TelegraphDecal.ResetDiagnostics(); Telegraph.ClearAll();
int n = st.maxConcurrent + 3;
var gos = new GameObject[n];
var actors = new MobActor[n];
for (int i = 0; i < n; i++) gos[i] = MakeMob(s_mobs[0], new Vector3(i * 6f, 0f, 0f), scene, out actors[i]);
for (int i = 0; i < n; i++)
if (actors[i] != null) WL.Combat.Core.CombatEvents.RaiseAttackStarted(actors[i], 0, 1f, null);
Chk(TelegraphDecal.InUse == st.maxConcurrent, "동시에 켜진 판 = 상한", "InUse=" + TelegraphDecal.InUse + " / 상한 " + st.maxConcurrent);
Chk(TelegraphDecal.BudgetSkipCount == n - st.maxConcurrent, "초과분은 판 생략", "생략 " + TelegraphDecal.BudgetSkipCount + " / 기대 " + (n - st.maxConcurrent));
int granted = Telegraph.SfxPlayed + Telegraph.SfxNoManager;
Chk(granted == st.sfxMaxConcurrent && Telegraph.SfxThrottled == n - st.sfxMaxConcurrent,
"예고 SFX 동시 상한 " + st.sfxMaxConcurrent + " (기준서 §G-4)",
"토큰 " + granted + " · 스킵 " + Telegraph.SfxThrottled + " / " + n + "회 요청 · 실재생 " + Telegraph.SfxPlayed
+ " (에디트 모드라 SoundInfo 인스턴스=" + SoundInfo.isIns + " → 실재생은 「미확인」)");
L(" 풀 크기 " + TelegraphDecal.PoolSize + " · 풀 성장 " + TelegraphDecal.PoolGrowCount + "회 · 셰이더 " + TelegraphDecal.ShaderName);
// 전부 반납
for (int i = 0; i < n; i++) if (actors[i] != null) WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(actors[i], s_mobs[0].proj, 1f, 0.1f, Vector3.zero);
Chk(TelegraphDecal.InUse == 0, "전부 반납(풀 누수 0)", "InUse=" + TelegraphDecal.InUse);
for (int i = 0; i < n; i++) if (gos[i] != null) Object.DestroyImmediate(gos[i]);
Telegraph.ClearAll();
L("");
}
// ─────────────────────────────────────────────────────────── E. 보스 + 타임아웃
static void SectionE(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## E. 보스 무변경 · 히트가 안 올 때 강제 원복");
Telegraph.ResetDiagnostics(); Telegraph.ClearAll();
MobActor boss;
var bgo = MakeMob(s_mobs[11], new Vector3(-30f, 0f, 0f), scene, out boss);
if (bgo != null)
{
float s0 = bgo.transform.localScale.x;
if (boss.m_animation != null) boss.m_animation.speed = 1f;
for (int i = 0; i < 3; i++) WL.Combat.Core.CombatEvents.RaiseAttackStarted(boss, 0, 1f, null);
Chk(Telegraph.SkippedBoss == 3 && Telegraph.StartCount == 0, "보스 AttackStarted 3회 → 예고 0",
"SkippedBoss=" + Telegraph.SkippedBoss + " Start=" + Telegraph.StartCount);
Chk(boss.m_animation == null || Mathf.Abs(boss.m_animation.speed - 1f) < 0.0001f, "보스 animator.speed 무변경",
"speed=" + (boss.m_animation != null ? F(boss.m_animation.speed, 3) : "-"));
Chk(Mathf.Abs(bgo.transform.localScale.x - s0) < 0.0001f, "보스 크기 무변경", "scale=" + F(bgo.transform.localScale.x, 4));
Object.DestroyImmediate(bgo);
}
Telegraph.ResetDiagnostics();
MobActor a;
var go = MakeMob(s_mobs[0], new Vector3(-60f, 0f, 0f), scene, out a);
if (go != null)
{
float s0 = go.transform.localScale.x;
if (a.m_animation != null) a.m_animation.speed = 1f;
WL.Combat.Core.CombatEvents.RaiseAttackStarted(a, 0, 1f, null);
TelegraphRunner.ForceTick(st.maxTelegraphSeconds + 0.1f); // 히트가 안 온다(피격·사망)
Chk(Telegraph.TimeoutCount == 1, "타임아웃 감지", "Timeout=" + Telegraph.TimeoutCount);
Chk(a.m_animation != null && Mathf.Abs(a.m_animation.speed - 1f) < 0.0001f, "타임아웃 → 속도 원복", "speed=" + F(a.m_animation.speed, 3));
TelegraphRunner.ForceTick(st.pulseRestoreGuardSeconds + 0.05f);
Chk(Mathf.Abs(go.transform.localScale.x - s0) < 0.0001f, "타임아웃 → 크기 원복", "scale=" + F(go.transform.localScale.x, 4));
Chk(TelegraphDecal.InUse == 0, "타임아웃 → 판 반납", "InUse=" + TelegraphDecal.InUse);
Object.DestroyImmediate(go);
}
Telegraph.ClearAll();
L("");
}
// ─────────────────────────────────────────────────────────── F. GC
static void SectionF(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## F. GC (워밍 후 차분 · GC.GetAllocatedBytesForCurrentThread)");
Telegraph.ClearAll(); Telegraph.ResetDiagnostics();
MobActor a;
var go = MakeMob(s_mobs[0], new Vector3(-90f, 0f, 0f), scene, out a);
if (go == null) { Chk(false, "F 몹 인스턴스", "없음"); return; }
if (a.m_animation != null) a.m_animation.speed = 1f;
for (int i = 0; i < 30; i++) // 워밍(캐시·풀·머티리얼)
{
WL.Combat.Core.CombatEvents.RaiseAttackStarted(a, 0, 1f, null);
TelegraphRunner.ForceTick(0.02f);
WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(a, s_mobs[0].proj, 1f, 0.1f, Vector3.zero);
TelegraphRunner.ForceTick(0.3f);
}
long b0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 200; i++)
{
WL.Combat.Core.CombatEvents.RaiseAttackStarted(a, 0, 1f, null);
TelegraphRunner.ForceTick(0.02f);
WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(a, s_mobs[0].proj, 1f, 0.1f, Vector3.zero);
TelegraphRunner.ForceTick(0.3f);
}
long d1 = System.GC.GetAllocatedBytesForCurrentThread() - b0;
long b1 = System.GC.GetAllocatedBytesForCurrentThread();
WL.Combat.Core.CombatEvents.RaiseAttackStarted(a, 0, 1f, null);
for (int i = 0; i < 5000; i++) TelegraphRunner.ForceTick(0.0001f);
long d2 = System.GC.GetAllocatedBytesForCurrentThread() - b1;
WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(a, s_mobs[0].proj, 1f, 0.1f, Vector3.zero);
Chk(d1 == 0, "왕복 ×200 할당 0 B", d1 + " B");
Chk(d2 == 0, "틱 ×5000 할당 0 B", d2 + " B");
Object.DestroyImmediate(go);
Telegraph.ClearAll();
L("");
}
// ─────────────────────────────────────────────────────────── G. C8
static void SectionG(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## G. C8 — enabled = 0 이면 전부 원본");
Telegraph.ClearAll(); Telegraph.ResetDiagnostics(); TelegraphDecal.ResetDiagnostics();
WL.Combat.Reaction.ImpactTier.ResetDiagnostics();
WLTelegraphSettings.RuntimeDisabled = true; // 에셋 enabled = 0 과 같은 게이트
MobActor a;
var go = MakeMob(s_mobs[0], new Vector3(-120f, 0f, 0f), scene, out a);
if (go != null)
{
float s0 = go.transform.localScale.x;
if (a.m_animation != null) a.m_animation.speed = 1f;
WL.Combat.Core.CombatEvents.RaiseAttackStarted(a, 0, 1f, null);
TelegraphRunner.ForceTick(0.2f);
Chk(Telegraph.StartCount == 0 && Telegraph.SkippedDisabled >= 1, "예고 0", "Start=" + Telegraph.StartCount + " SkippedDisabled=" + Telegraph.SkippedDisabled);
Chk(a.m_animation != null && Mathf.Abs(a.m_animation.speed - 1f) < 0.0001f, "animator.speed 원본", "speed=" + F(a.m_animation.speed, 3));
Chk(Mathf.Abs(go.transform.localScale.x - s0) < 0.0001f, "크기 원본", "scale=" + F(go.transform.localScale.x, 4));
Chk(TelegraphDecal.InUse == 0 && !WL.Combat.Reaction.MobHitFlash.HasEliteTint(a), "판 0 · MPB 0", "InUse=" + TelegraphDecal.InUse);
Chk(!WLTelegraphSettings.SkipHitStop, "히트스톱 스킵 조건도 꺼진다(811gh 기존 동작)", "SkipHitStop=" + WLTelegraphSettings.SkipHitStop);
Object.DestroyImmediate(go);
}
WLTelegraphSettings.RuntimeDisabled = false;
Telegraph.ClearAll();
L("");
}
// ─────────────────────────────────────────────────────────── H. 애니메이터 실측
static void SectionH(UnityEngine.SceneManagement.Scene scene)
{
L("## H. 🔴 「데미지도 같이 늦어진다」 실측 — animator.speed 를 낮추면 클립 진행이 정확히 반비례한다");
L(" (히트 = 클립 안의 애니메이션 이벤트 `Projectile` 이므로 클립이 늦어지면 데미지도 늦어진다)");
L("| 몹 | 속도 | 0.2 s 동안 normalizedTime 진행 | 기대(= 0.2 ÷ 길이 × 속도) | 오차 |");
L("|---|---|---|---|---|");
int[] pick = { 0, 2, 6 };
int okCount = 0;
for (int p = 0; p < pick.Length; p++)
{
var m = s_mobs[pick[p]];
MobActor actor;
var go = MakeMob(m, new Vector3(p * 20f, 0f, 40f), scene, out actor);
if (go == null || actor.m_animation == null) { if (go != null) Object.DestroyImmediate(go); continue; }
var anim = actor.m_animation;
anim.enabled = true;
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
float len = ClipLength(anim, "attack");
for (int k = 0; k < 2; k++)
{
float sp = k == 0 ? 1f : 0.35f;
anim.speed = 1f;
anim.Play("attack", 0, 0f);
anim.Update(0f);
anim.speed = sp;
for (int i = 0; i < 20; i++) anim.Update(0.01f); // 0.2 s
float nt = anim.GetCurrentAnimatorStateInfo(0).normalizedTime;
float want = len > 0f ? 0.2f / len * sp : -1f;
float err = want > 0f ? Mathf.Abs(nt - want) : -1f;
bool ok = want > 0f && err < 0.02f;
if (ok) okCount++;
L("| " + m.label + " | " + F(sp, 2) + " | " + F(nt, 4) + " | " + F(want, 4) + " | " + (err >= 0f ? F(err, 4) : "-") + (ok ? " ✅" : " ❌") + " |");
}
anim.speed = 1f;
Object.DestroyImmediate(go);
}
Chk(okCount >= 4, "속도 ↔ 클립 진행 반비례 실측", okCount + " / " + (pick.Length * 2));
L("");
}
static float ClipLength(Animator anim, string state)
{
var rac = anim.runtimeAnimatorController;
if (rac == null) return -1f;
anim.Play(state, 0, 0f);
anim.Update(0f);
var infos = anim.GetCurrentAnimatorClipInfo(0);
if (infos != null && infos.Length > 0 && infos[0].clip != null) return infos[0].clip.length;
return -1f;
}
// ─────────────────────────────────────────────────────────── J. 보유 에셋 실측(SFX 클립 · 인디케이터 20종)
static void SectionJ(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## J. ④ 예고 SFX 클립 · ① IndicatorInfo 재사용 경로 (보유 에셋 실측)");
var sound = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/ResWork/UIPrefabs/Title/SoundInfo.prefab");
var si = sound != null ? sound.GetComponent<SoundInfo>() : null;
if (si != null && si.arr_clip != null)
{
bool inRange = st.sfxIndex >= 0 && st.sfxIndex < si.arr_clip.Length;
var clip = inRange ? si.arr_clip[st.sfxIndex] : null;
L(" SoundInfo.arr_clip 길이 " + si.arr_clip.Length + " · eSound.Max = " + (int)eSound.Max);
Chk(clip != null, "예고 SFX 클립 실존 (index " + st.sfxIndex + " = " + (eSound)st.sfxIndex + ")",
clip != null ? clip.name + " · " + F(clip.length, 3) + " s" : "null");
}
else Chk(false, "SoundInfo 프리팹/클립 배열", sound == null ? "프리팹 없음" : "arr_clip null");
// ① Show_Indicator 재사용 — 원본 공개 API(Make_Indicator) 경로가 살아 있는지
int indicators = 0;
var guids = AssetDatabase.FindAssets("_Indicator t:GameObject", new[] { "Assets/Res_Addr/Obj" });
if (guids != null) indicators = guids.Length;
L(" Res_Addr/Obj 인디케이터 프리팹 " + indicators + "종 (기준서 §G-1 = 20종)");
Chk(indicators >= 20, "보유 인디케이터 20종 실존", indicators + "종");
string save = st.indicatorPrefabName;
st.indicatorPrefabName = "Golem_Child_Skill1_Indicator";
int before = TelegraphDecal.IndicatorMissCount;
MobActor ia;
var igo = MakeMob(s_mobs[0], new Vector3(-150f, 0f, 0f), scene, out ia);
var box = TelegraphShape.Get("Mob/P_Batty_Meele");
int idx = igo != null ? TelegraphDecal.Acquire(ia, in box, false, 0.1f, TelegraphDanger.Avoidable) : -1;
st.indicatorPrefabName = save;
Chk(TelegraphDecal.IndicatorMissCount > before || TelegraphDecal.IndicatorBorrowCount > 0,
"IndicatorInfo 재사용 경로가 안전하게 분기한다(에디트 모드 = 싱글턴 없음)",
"isIns=" + IndicatorInfo.isIns + " miss=" + TelegraphDecal.IndicatorMissCount + " borrow=" + TelegraphDecal.IndicatorBorrowCount
+ " → 실제 표시는 Play 전용 「미확인」");
if (idx >= 0) TelegraphDecal.Release(idx);
if (igo != null) Object.DestroyImmediate(igo);
L(" 🔴 기본값은 빈 문자열 = 인디케이터 덧칠 off. 보유 20종은 전부 **원형 파티클**이라 사각 콜라이더와 100 % 일치가 안 된다 →");
L(" 판정 일치는 런타임 쿼드가 담당하고, 덧칠이 필요하면 SO 한 줄(indicatorPrefabName)로 켠다.");
L("");
}
// ─────────────────────────────────────────────────────────── I. 캡처
static void SectionI(WLTelegraphSettings st, UnityEngine.SceneManagement.Scene scene)
{
L("## I. 캡처 3장 (1080×1920)");
if (!System.IO.Directory.Exists(kOutDir)) System.IO.Directory.CreateDirectory(kOutDir);
Telegraph.ClearAll(); Telegraph.ResetDiagnostics();
// 조명 + 지면(판이 보이게)
var lightGo = new GameObject("PROBE_Light"); lightGo.hideFlags = HideFlags.DontSave;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(lightGo, scene);
var light = lightGo.AddComponent<Light>(); light.type = LightType.Directional; light.intensity = 1.1f;
lightGo.transform.rotation = Quaternion.Euler(45f, -30f, 0f);
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.name = "PROBE_Ground"; ground.hideFlags = HideFlags.DontSave;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(ground, scene);
ground.transform.localScale = new Vector3(6f, 1f, 6f);
var camGo = new GameObject("PROBE_Cam"); camGo.hideFlags = HideFlags.DontSave;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(camGo, scene);
var cam = camGo.AddComponent<Camera>();
cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(0.18f, 0.2f, 0.24f);
cam.fieldOfView = 60f; cam.nearClipPlane = 0.05f; cam.farClipPlane = 200f;
int[] pick = { 0, 6, 11 };
string[] tags = { "1_잡몹_예고중", "2_엘리트_예고중", "3_보스_무변경" };
for (int p = 0; p < pick.Length; p++)
{
var m = s_mobs[pick[p]];
MobActor actor;
var go = MakeMob(m, Vector3.zero, scene, out actor);
if (go == null) { L(" (프리팹 없음) " + m.label); continue; }
if (actor.m_animation != null) { actor.m_animation.speed = 1f; actor.m_animation.cullingMode = AnimatorCullingMode.AlwaysAnimate; }
WL.Combat.Core.CombatEvents.RaiseAttackStarted(actor, 0, 1f, null);
float planned = Telegraph.PlannedSecondsOf(actor);
if (planned > 0f) TelegraphRunner.ForceTick(planned * 0.6f); // 60 % 채워진 순간
else TelegraphRunner.ForceTick(0.02f);
// 판(3×3 또는 3×3+center 1.5)과 몹이 화면에 꽉 차게 — 세로 1080×1920 구도
Vector3 focus = new Vector3(0f, 0f, m.boss ? 0.6f : 1.0f);
float span = Mathf.Max(3.2f, go.transform.localScale.x * 1.8f);
cam.transform.position = focus + new Vector3(0f, span * 1.15f, -span * 1.45f);
cam.transform.LookAt(focus + new Vector3(0f, span * 0.18f, 0f));
string path = Shot(cam, tags[p]);
L(" " + tags[p] + " — " + m.label + " · 예고 " + F(planned, 3) + " s · 판 " + (Telegraph.DecalIndexOf(actor) >= 0 ? "O" : "X") + " → " + path);
WL.Combat.Core.CombatEvents.RaiseHitboxSpawned(actor, m.proj, 1f, m.lifetime, Vector3.zero);
TelegraphRunner.ForceTick(0.5f);
Object.DestroyImmediate(go);
}
Object.DestroyImmediate(ground); Object.DestroyImmediate(camGo); Object.DestroyImmediate(lightGo);
int n = System.IO.Directory.GetFiles(kOutDir, "*.png").Length;
Chk(n >= 3, "캡처 3장", n + "장 · " + kOutDir);
L("");
}
static string Shot(Camera cam, string tag)
{
var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32);
cam.aspect = (float)W / H;
cam.targetTexture = rt;
cam.Render();
var prev = RenderTexture.active;
RenderTexture.active = rt;
var tex = new Texture2D(W, H, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, W, H), 0, 0);
tex.Apply();
RenderTexture.active = prev;
cam.targetTexture = null;
var path = System.IO.Path.Combine(kOutDir, tag + ".png");
System.IO.File.WriteAllBytes(path, ImageConversion.EncodeToPNG(tex));
Object.DestroyImmediate(tex);
rt.Release(); Object.DestroyImmediate(rt);
return path;
}
}