Project_WL/AgentScripts/WL813h_Probe.cs

261 lines
14 KiB
C#
Raw Normal View History

[WL-813h] 보스 페이즈·예고·쿨 데이터화 (#813) - Assets/WL/Combat/Boss/BossEvents.cs (NEW): BossPhase 통지 허브. 코어 CombatEvents.cs 는 무수정 — 811d~j 와 충돌 방지. 같은 규약(정적 · struct 페이로드 in 전달 · 사전 할당 구독자 배열 · 예외 격리 · WLCombatCoreSettings enabled/RuntimeDisabled 준수). 실측 GC = 1000회 디스패치 0 byte. - Assets/WL/Combat/Boss/BossPatternTable.cs (NEW): 보스 패턴 JSON 로더. 스폰(On_Regen) 시 그 Boss_*.cs 가 직접 선언한 public float/int/bool 필드에만 대입(DeclaredOnly) → Boss_*.cs 수정 0 · 프리팹 값 변경 0. 행이 없으면 무동작. - Assets/WL/Combat/Settings/Resources/WL/WL813h_BossPattern.json (NEW): 보스 12행(10001~10012). 813b 실측(프리팹 인스펙터) 값 그대로이며, Anubis(10006) 의 예고(차징) 3 s → 1.2 s 만 발주서 제안값으로 데이터에서 바꾼다. 페이즈 임계 기본 [1.0, 0.7, 0.4] · 스킬 해금 임계 기본 0.9 / 0.7(원본과 동일). - Assets/Script/Character/Mob/BossMobActor.cs: 훅 3줄(로더 적용 1 · 임계 조회 1 · 페이즈 통지 1). 원본 라인은 그대로 둬서 C8 시 100% 원본 동작. - AgentScripts/WL813h_Probe.cs + WL813h_PROBE.txt: 에디트 모드 프로브와 실행 덤프 (로더 적용 · 행 없음 원복 · 페이즈 3회 · GC 0 · RuntimeDisabled 롤백 전부 PASS). 게이트 A PASS(oneshot · exit 0 · error CS 0줄 · 0.8분) · 콘솔 error 0 · Play 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 15:19:49 +00:00
// 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;
}
}