261 lines
14 KiB
C#
261 lines
14 KiB
C#
// PD 지시 #813 · 발주서 WL-813h §1-4 — 보스 패턴 로더 · 페이즈 통지 에디트 모드 프로브 (읽기 전용)
|
||
//
|
||
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기 · 첫 Roslyn 컴파일은 --timeout 60)
|
||
// unity command run_script --file AgentScripts/WL813h_Probe.cs --entry WL813h_Probe.Run --timeout 180
|
||
//
|
||
// 무엇을 확인하나 (발주서 §3 ⓒ ⓓ ⓔ)
|
||
// ① 로더 상태(JSON 행 수 · 파싱) 와 12행 임계 조회표
|
||
// ② Anubis(10006) 프리팹 인스턴스에 행 값이 적용되는지 — 적용 전/후 필드 덤프
|
||
// ③ 행 없음(가짜 ID) → 값 무변경(원본 유지)
|
||
// ④ BossPhase 통지 — 가짜 HP 100 → 70 → 40 % 에서 이벤트 3회 + 페이로드
|
||
// ⑤ 디스패치 GC 할당(1000회 Raise 전후 GC.GetTotalMemory 차)
|
||
// ⑥ C8 — RuntimeDisabled = true 면 로더/통지 무동작
|
||
//
|
||
// Play 하지 않는다. 프리팹은 LoadPrefabContents → (저장 없이) UnloadPrefabContents 라 에셋 무변경.
|
||
// 수치는 전부 JSON/프리팹에서 읽는다(C45). 프로브의 반복 횟수·가짜 HP 는 "측정 조건"이지 게임 데이터가 아니다.
|
||
|
||
public static class WL813h_Probe
|
||
{
|
||
const string kAnubisPrefab = "Assets/Res_Addr/Mobs/FieldBoss/Anubis.prefab";
|
||
const int kAnubisId = 10006;
|
||
const int kMissingId = 99999; // JSON 에 없는 ID (행 없음 경로 검증)
|
||
const int kGcIterations = 1000; // 측정 조건
|
||
const string kLog = "AgentScripts/WL813h_PROBE.txt"; // 발주서 §4 허용 경로(AgentScripts/WL813h_*)
|
||
|
||
static System.Text.StringBuilder s;
|
||
static int s_eventCount;
|
||
static string s_eventLog;
|
||
|
||
public static string Run()
|
||
{
|
||
s = new System.Text.StringBuilder();
|
||
s_eventCount = 0; s_eventLog = "";
|
||
|
||
L("===== WL-813h 보스 패턴 로더 · 페이즈 통지 프로브 =====");
|
||
L("playMode=" + UnityEngine.Application.isPlaying + " time=" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
|
||
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
|
||
WL.Combat.Boss.BossPatternTable.ClearCache();
|
||
WL.Combat.Boss.BossEvents.ResetDiagnostics();
|
||
|
||
Section1_Table();
|
||
Section2_Apply(); // ②③ 안에서 ④⑤(페이즈·GC)까지 같은 인스턴스로 이어서 돈다
|
||
Section4_Rollback();
|
||
|
||
L("");
|
||
L("===== 끝 =====");
|
||
return Flush();
|
||
}
|
||
|
||
// ───────────────────────────────────────── ① 로더 상태 + 임계 조회표
|
||
static void Section1_Table()
|
||
{
|
||
L("");
|
||
L("── ① 로더 상태");
|
||
L(" 코어 Enabled = " + WL.Combat.Core.WLCombatCoreSettings.Enabled);
|
||
L(" 로더 Enabled = " + WL.Combat.Boss.BossPatternTable.Enabled);
|
||
L(" RowCount = " + WL.Combat.Boss.BossPatternTable.RowCount);
|
||
L(" ParseFailed = " + WL.Combat.Boss.BossPatternTable.ParseFailed +
|
||
(WL.Combat.Boss.BossPatternTable.LoadError.Length > 0 ? (" (" + WL.Combat.Boss.BossPatternTable.LoadError + ")") : ""));
|
||
|
||
L("");
|
||
L("── ① 임계 조회표 (skill1 / skill2 = 해금 임계 · phases = 페이즈 임계)");
|
||
L(" ID 이름 skill1 skill2 phases 필드수");
|
||
var f = WL.Combat.Boss.BossPatternTable.File;
|
||
if (f == null || f.rows == null) { L(" (JSON 없음)"); return; }
|
||
for (int i = 0; i < f.rows.Count; i++)
|
||
{
|
||
var r = f.rows[i];
|
||
float t1, t2;
|
||
WL.Combat.Boss.BossPatternTable.GetSkillThresholds(r.id, out t1, out t2);
|
||
var th = WL.Combat.Boss.BossPatternTable.GetPhaseThresholds(r.id);
|
||
L(string.Format(" {0,-7} {1,-20} {2,-7} {3,-7} {4,-21} {5}",
|
||
r.id, r.name, t1.ToString("0.###"), t2.ToString("0.###"),
|
||
"[" + string.Join(", ", System.Array.ConvertAll(th, x => x.ToString("0.##"))) + "]",
|
||
r.fields != null ? r.fields.Count : 0));
|
||
}
|
||
}
|
||
|
||
// ───────────────────────────────────────── ② 행 적용 · ③ 행 없음
|
||
static void Section2_Apply()
|
||
{
|
||
L("");
|
||
L("── ② Anubis(10006) 프리팹 인스턴스에 행 적용 (LoadPrefabContents · 저장 없음)");
|
||
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
|
||
if (root == null) { L(" [에러] 프리팹 로드 실패: " + kAnubisPrefab); return; }
|
||
try
|
||
{
|
||
var boss = root.GetComponentInChildren<BossMobActor>(true);
|
||
if (boss == null) { L(" [에러] BossMobActor 컴포넌트 없음"); return; }
|
||
L(" 대상 = " + boss.GetType().Name + " on '" + boss.gameObject.name + "'");
|
||
|
||
var before = Dump(boss);
|
||
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
|
||
var after = Dump(boss);
|
||
L(" ApplyById(10006) → 적용 필드 " + applied + "개 · 미지원 키 " + WL.Combat.Boss.BossPatternTable.LastUnknownKeys + "개");
|
||
L("");
|
||
L(" 필드 프리팹값 → 적용후 (변화)");
|
||
foreach (var kv in before)
|
||
{
|
||
string a = after.ContainsKey(kv.Key) ? after[kv.Key] : "?";
|
||
L(string.Format(" {0,-24} {1,-9} → {2,-9} {3}", kv.Key, kv.Value, a, kv.Value == a ? "" : "<<< 변경"));
|
||
}
|
||
|
||
L("");
|
||
L("── ③ 행 없음(ID " + kMissingId + ") → 값 유지 검증");
|
||
int reverted = WL.Combat.Boss.BossPatternTable.Revert(boss, kAnubisId);
|
||
L(" Revert(10006) = " + reverted + "개 되돌림 (프리팹 원본 값으로 복구)");
|
||
var baseline = Dump(boss);
|
||
int applied2 = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kMissingId);
|
||
var afterMissing = Dump(boss);
|
||
int changed = 0;
|
||
foreach (var kv in baseline) if (!afterMissing.ContainsKey(kv.Key) || afterMissing[kv.Key] != kv.Value) changed++;
|
||
L(" ApplyById(" + kMissingId + ") = " + applied2 + " (−1 = 무동작) · 변경된 필드 " + changed + "개");
|
||
L(" 판정 = " + (applied2 == -1 && changed == 0 ? "PASS (행 없으면 프리팹 값 그대로)" : "FAIL"));
|
||
|
||
// 페이즈 검사는 같은 인스턴스로 이어서 한다.
|
||
s_probeBoss = boss;
|
||
Section3_PhaseInner();
|
||
}
|
||
finally
|
||
{
|
||
s_probeBoss = null;
|
||
UnityEditor.PrefabUtility.UnloadPrefabContents(root); // 저장하지 않으므로 프리팹 에셋 무변경
|
||
}
|
||
}
|
||
|
||
static BossMobActor s_probeBoss;
|
||
|
||
// ───────────────────────────────────────── ④ 페이즈 통지 · ⑤ GC
|
||
static void Section3_PhaseInner()
|
||
{
|
||
var boss = s_probeBoss;
|
||
L("");
|
||
L("── ④ BossPhase 통지 (가짜 HP 100 → 70 → 40 %)");
|
||
|
||
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> h = OnPhase;
|
||
WL.Combat.Boss.BossEvents.BossPhase.Add(h);
|
||
WL.Combat.Boss.BossEvents.ResetDiagnostics();
|
||
s_eventCount = 0; s_eventLog = "";
|
||
try
|
||
{
|
||
WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId); // 스폰 = 페이즈 1
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 1.00f);
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.85f);
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.70f);
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.55f);
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.40f);
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.90f); // 회복 — 되돌아가지 않아야 한다
|
||
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.05f);
|
||
L(s_eventLog);
|
||
L(" 이벤트 " + s_eventCount + "회 · Raised=" + WL.Combat.Boss.BossEvents.BossPhase.Raised +
|
||
" Dispatched=" + WL.Combat.Boss.BossEvents.BossPhase.Dispatched +
|
||
" 최종 페이즈=" + WL.Combat.Boss.BossPatternTable.CurrentPhase(boss));
|
||
L(" 판정 = " + (s_eventCount == 3 ? "PASS (100→70→40 · 3회 · 회복 시 되돌림 없음)" : "FAIL (기대 3회)"));
|
||
|
||
L("");
|
||
L("── ④ 스킬 해금 임계 (원본 0.9 / 0.7 · 행이 값을 주면 그 값)");
|
||
bool c1 = false, c2 = false;
|
||
float[] hps = { 1.00f, 0.95f, 0.90f, 0.80f, 0.70f, 0.50f };
|
||
for (int i = 0; i < hps.Length; i++)
|
||
{
|
||
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, hps[i], ref c1, ref c2);
|
||
L(string.Format(" HP {0,5:P0} → CanUseSkill1={1,-5} CanUseSkill2={2}", hps[i], c1, c2));
|
||
}
|
||
|
||
L("");
|
||
L("── ⑤ 디스패치 GC (Raise " + kGcIterations + "회 · 구독자는 카운터만 = 로그 문자열 할당 배제)");
|
||
WL.Combat.Boss.BossEvents.BossPhase.Remove(h);
|
||
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> hc = OnPhaseCount;
|
||
WL.Combat.Boss.BossEvents.BossPhase.Add(hc);
|
||
s_gcCount = 0;
|
||
System.GC.Collect();
|
||
long m0 = System.GC.GetTotalMemory(true);
|
||
for (int i = 0; i < kGcIterations; i++)
|
||
WL.Combat.Boss.BossEvents.RaiseBossPhase(boss, kAnubisId, 1, 2, 3, 0.7f, 0.7f, false);
|
||
long m1 = System.GC.GetTotalMemory(false);
|
||
WL.Combat.Boss.BossEvents.BossPhase.Remove(hc);
|
||
L(" 구독자 1명 · 수신 " + s_gcCount + "회 · 할당 델타 = " + (m1 - m0) + " byte (회당 " +
|
||
((m1 - m0) / (double)kGcIterations).ToString("0.##") + " byte)");
|
||
}
|
||
finally { WL.Combat.Boss.BossEvents.BossPhase.Remove(h); }
|
||
}
|
||
|
||
static int s_gcCount;
|
||
|
||
/// GC 측정용 — 할당이 전혀 없는 구독자(문자열 조립 없음).
|
||
static void OnPhaseCount(in WL.Combat.Boss.BossPhaseEvent e) { s_gcCount++; }
|
||
|
||
static void OnPhase(in WL.Combat.Boss.BossPhaseEvent e)
|
||
{
|
||
s_eventCount++;
|
||
s_eventLog += string.Format(" #{0} phase {1}→{2}/{3} id={4} hp={5:P0} 임계={6:0.##} spawn={7}\n",
|
||
s_eventCount, e.prevPhase, e.phase, e.phaseCount, e.monsterId, e.hpPercent, e.threshold, e.isSpawn);
|
||
}
|
||
|
||
// ───────────────────────────────────────── ⑥ C8 롤백
|
||
static void Section4_Rollback()
|
||
{
|
||
L("");
|
||
L("── ⑥ C8 롤백 (RuntimeDisabled = true → 로더·통지 무동작)");
|
||
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
|
||
try
|
||
{
|
||
var boss = root.GetComponentInChildren<BossMobActor>(true);
|
||
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = true;
|
||
WL.Combat.Boss.BossEvents.ResetDiagnostics();
|
||
var before = Dump(boss);
|
||
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
|
||
var after = Dump(boss);
|
||
int changed = 0;
|
||
foreach (var kv in before) if (after[kv.Key] != kv.Value) changed++;
|
||
bool c1 = false, c2 = false;
|
||
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, 0.10f, ref c1, ref c2);
|
||
int ph = WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.10f);
|
||
L(" 로더 Enabled=" + WL.Combat.Boss.BossPatternTable.Enabled + " · ApplyById=" + applied +
|
||
" · 변경 필드=" + changed + " · 게이트(HP 10%)=" + c1 + "/" + c2 +
|
||
" · UpdatePhase=" + ph + " · 이벤트=" + WL.Combat.Boss.BossEvents.TotalRaised);
|
||
L(" 판정 = " + (applied == -1 && changed == 0 && !c1 && !c2 && ph == 0 &&
|
||
WL.Combat.Boss.BossEvents.TotalRaised == 0
|
||
? "PASS (프리팹 값 · 원본 게이트 · 통지 0)" : "FAIL"));
|
||
}
|
||
finally
|
||
{
|
||
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
|
||
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
|
||
WL.Combat.Boss.BossPatternTable.ClearCache();
|
||
}
|
||
}
|
||
|
||
// ───────────────────────────────────────── 유틸
|
||
static System.Collections.Generic.Dictionary<string, string> Dump(BossMobActor boss)
|
||
{
|
||
var d = new System.Collections.Generic.Dictionary<string, string>();
|
||
var fields = boss.GetType().GetFields(System.Reflection.BindingFlags.Public |
|
||
System.Reflection.BindingFlags.Instance |
|
||
System.Reflection.BindingFlags.DeclaredOnly);
|
||
for (int i = 0; i < fields.Length; i++)
|
||
{
|
||
var t = fields[i].FieldType;
|
||
if (t == typeof(float) || t == typeof(int) || t == typeof(bool) || t == typeof(string))
|
||
d[fields[i].Name] = System.Convert.ToString(fields[i].GetValue(boss));
|
||
}
|
||
return d;
|
||
}
|
||
|
||
static void L(string line) { s.Append(line).Append("\n"); }
|
||
|
||
static string Flush()
|
||
{
|
||
var text = s.ToString();
|
||
try
|
||
{
|
||
var dir = System.IO.Path.GetDirectoryName(kLog);
|
||
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
||
System.IO.File.WriteAllText(kLog, text, new System.Text.UTF8Encoding(false));
|
||
}
|
||
catch (System.Exception ex) { text += "\n[로그 저장 실패] " + ex.Message; }
|
||
return text;
|
||
}
|
||
}
|