Project_WL/AgentScripts/WL764_Probe.cs

383 lines
22 KiB
C#
Raw Permalink Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// PD 지시 #764 — 몬스터 스폰/이동 지형 실측 프로브 (읽기 전용)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// 에디트 모드 스폰 도달성 + NavMesh 섬 통계
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Edit --timeout 300
// Play 중 몹 이탈 감시 시작 / 상태 / 결과
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Watch
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Status
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Dump
//
// 수치는 전부 프로젝트 데이터에서 읽는다 (C45):
// 스포너 위치·MobGenRange = WL_Start.prefab / 플레이어 시작 지점 = MapData.m_MapZoneData[].go_portal (+Vector3.forward)
// NavMesh = Assets/WL/Settings/WL_Start_NavMesh.asset
// 프로브 자체의 격자 간격·표집 수는 "측정 해상도"이지 게임 데이터가 아니다.
public static class WL764_Probe
{
const string kTargetPrefab = "Assets/Res_Addr/Map/WL_Start.prefab";
const string kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset";
const string kEditLog = "AgentScripts/staging/WL_Spawn/EDIT_PROBE.txt";
const string kRunLog = "AgentScripts/staging/WL_Spawn/RUNTIME_PROBE.txt";
// 측정 해상도 (게임 데이터 아님)
const float kGridStep = 4f; // 섬 통계 격자
const int kSamplesPerSpawner = 30;
const int kSeed = 764;
const float kOffMeshRadius = 0.3f; // 발주서 지정 — 이 반경 안에 NavMesh 가 없으면 "이탈"
static System.Text.StringBuilder s;
// ─────────────────────────────────────────────────────────────────────
// 1. 에디트 모드: 스포너별 도달률 + 섬 통계
// ─────────────────────────────────────────────────────────────────────
public static string Edit()
{
s = new System.Text.StringBuilder();
L("===== WL764 에디트 모드 실측 =====");
L("playMode=" + UnityEngine.Application.isPlaying);
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
if (navData == null) { L("[에러] NavMeshData 없음: " + kNavAsset); return Flush(kEditLog); }
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kTargetPrefab);
var navInst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
try
{
// 플레이어 시작 지점 — MapData 의 포탈 (런타임 LoadMapMgr.Get_PortalPos 와 동일 식)
var md = root.GetComponent<MapData>();
UnityEngine.Vector3 pcSpawn;
if (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0 && md.m_MapZoneData[0].go_portal != null)
{
pcSpawn = md.m_MapZoneData[0].go_portal.transform.position + UnityEngine.Vector3.forward;
L("플레이어 시작 = MapData.m_MapZoneData[0].go_portal(" + md.m_MapZoneData[0].go_portal.name + ") + forward = " + pcSpawn.ToString("F2"));
}
else
{
var p = root.transform.Find("Portal_Start");
if (p == null) { L("[에러] 포탈을 찾지 못했다"); return Flush(kEditLog); }
pcSpawn = p.position + UnityEngine.Vector3.forward;
L("플레이어 시작 = Portal_Start + forward = " + pcSpawn.ToString("F2") + " (MapData 경로 실패 → 폴백)");
}
UnityEngine.AI.NavMeshHit pcHit;
if (!UnityEngine.AI.NavMesh.SamplePosition(pcSpawn, out pcHit, 5f, UnityEngine.AI.NavMesh.AllAreas))
{ L("[에러] 시작 지점이 NavMesh 위가 아니다"); return Flush(kEditLog); }
var pcOnNav = pcHit.position;
L("시작 지점 NavMesh 스냅 = " + pcOnNav.ToString("F2"));
// ── (d) 섬 통계 ──────────────────────────────────────────────
IslandStats(pcOnNav);
// ── (a) 스포너별 도달률 ──────────────────────────────────────
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
L("");
L("── (a) 스포너 " + mgrs.Length + "개 · 스포너당 " + kSamplesPerSpawner + "점 표집 ──");
L(" ID | 스포너위치 | range | 스포너도달 | 현행표집 도달 | 신방식 도달 | 표집반경초과");
System.Array.Sort(mgrs, delegate (MobControlMgr a, MobControlMgr b) { return a.SpawnerID.CompareTo(b.SpawnerID); });
int totBadOld = 0, totBadNew = 0, badSpawners = 0, overRange = 0, totFallback = 0; float sumSpread = 0f;
for (int i = 0; i < mgrs.Length; i++)
{
var m = mgrs[i];
var org = m.transform.position;
bool spawnerReach = Reachable(pcOnNav, org);
if (!spawnerReach) badSpawners++;
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
int okOld = 0, over = 0;
for (int k = 0; k < kSamplesPerSpawner; k++)
{
var p = DSUtil.Get_RandomPos_onNavMesh(org, 0f, m.MobGenRange);
if (Reachable(pcOnNav, p)) okOld++;
if (UnityEngine.Vector3.Distance(org, p) > m.MobGenRange + 0.01f) over++;
}
overRange += over;
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
int okNew = 0, fallback = 0; float sumD = 0f;
for (int k = 0; k < kSamplesPerSpawner; k++)
{
s_fellBack = false;
var p = SampleStrict(org, m.MobGenRange, pcOnNav);
if (s_fellBack) fallback++;
sumD += UnityEngine.Vector3.Distance(org, p);
if (Reachable(pcOnNav, p)) okNew++;
}
totFallback += fallback; sumSpread += sumD / kSamplesPerSpawner;
totBadOld += (kSamplesPerSpawner - okOld);
totBadNew += (kSamplesPerSpawner - okNew);
L(string.Format(" {0,2} | ({1,8:F1},{2,6:F1},{3,8:F1}) | {4,5:F1} | {5,-6} | {6,3}/{7} ({8,3}%) | {9,3}/{10} ({11,3}%) | {12,2}",
m.SpawnerID, org.x, org.y, org.z, m.MobGenRange, spawnerReach ? "OK" : "🔴불가",
okOld, kSamplesPerSpawner, okOld * 100 / kSamplesPerSpawner,
okNew, kSamplesPerSpawner, okNew * 100 / kSamplesPerSpawner, over));
}
L("");
L("합계: 스포너 원점 도달불가 " + badSpawners + "/" + mgrs.Length
+ " · 현행 표집 도달불가 " + totBadOld + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " (" + Pct(totBadOld, mgrs.Length * kSamplesPerSpawner) + "%)"
+ " · 신방식 도달불가 " + totBadNew + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " (" + Pct(totBadNew, mgrs.Length * kSamplesPerSpawner) + "%)");
L("표집점이 MobGenRange 를 벗어난 횟수(현행) = " + overRange
+ " — SamplePosition 의 스냅 반경이 MobGenRange 라 최대 2×range 까지 튄다");
L("신방식 산포: 원점 폴백 " + totFallback + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " · 스포너별 평균 이격 " + (sumSpread / mgrs.Length).ToString("F2") + "m (MobGenRange 10 기준 · 몹이 원점에 뭉치지 않는지 확인)");
}
finally
{
UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst);
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
}
return Flush(kEditLog);
}
static void IslandStats(UnityEngine.Vector3 pcOnNav)
{
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
if (tri.vertices.Length == 0) { L("[에러] NavMesh 가 비어 있다"); return; }
var min = tri.vertices[0]; var max = tri.vertices[0];
for (int i = 1; i < tri.vertices.Length; i++)
{ min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); }
int nGrid = 0, nOnNav = 0, nReach = 0;
for (float x = min.x; x <= max.x; x += kGridStep)
for (float z = min.z; z <= max.z; z += kGridStep)
{
nGrid++;
UnityEngine.AI.NavMeshHit h;
var probe = new UnityEngine.Vector3(x, (min.y + max.y) * 0.5f, z);
if (!UnityEngine.AI.NavMesh.SamplePosition(probe, out h, (max.y - min.y) * 0.5f + kGridStep, UnityEngine.AI.NavMesh.AllAreas)) continue;
if (UnityEngine.Mathf.Abs(h.position.x - x) > kGridStep || UnityEngine.Mathf.Abs(h.position.z - z) > kGridStep) continue;
nOnNav++;
if (Reachable(pcOnNav, h.position)) nReach++;
}
L("");
L("── (d) NavMesh 섬 통계 (" + kGridStep + "m 격자) ──");
L(" 삼각형 " + (tri.indices.Length / 3) + "개 · 범위 min=" + min.ToString("F0") + " max=" + max.ToString("F0"));
L(" 격자 " + nGrid + "점 → NavMesh 위 " + nOnNav + "점 → 시작 지점에서 도달 가능 " + nReach
+ "점 (" + Pct(nReach, nOnNav) + "%) · 도달 불가 " + (nOnNav - nReach) + "점 (" + Pct(nOnNav - nReach, nOnNav) + "%)");
}
static bool s_fellBack;
/// <summary>신방식 표집 — 스냅 반경을 좁히고 완전 경로(PathComplete)를 요구한다. MobControlMgr 에 넣을 로직과 동일.</summary>
public static UnityEngine.Vector3 SampleStrict(UnityEngine.Vector3 origin, float range, UnityEngine.Vector3 reachFrom)
{
float snap = range * 0.2f; // Edit 프로브 기본 — 실제 값은 MapData.spawnSnapRadius
for (int i = 0; i < 50; i++)
{
var dir = UnityEngine.Random.insideUnitSphere * range + origin;
UnityEngine.AI.NavMeshHit hit;
if (!UnityEngine.AI.NavMesh.SamplePosition(dir, out hit, snap, UnityEngine.AI.NavMesh.AllAreas)) continue;
if (UnityEngine.Vector3.Distance(origin, hit.position) > range) continue;
if (!Reachable(origin, hit.position)) continue;
if (!Reachable(reachFrom, hit.position)) continue;
return hit.position;
}
s_fellBack = true;
return origin;
}
static bool Reachable(UnityEngine.Vector3 from, UnityEngine.Vector3 to)
{
var path = new UnityEngine.AI.NavMeshPath();
if (!UnityEngine.AI.NavMesh.CalculatePath(from, to, UnityEngine.AI.NavMesh.AllAreas, path)) return false;
return path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete;
}
// ─────────────────────────────────────────────────────────────────────
// 2. Play 모드: 몹 이탈 감시
// ─────────────────────────────────────────────────────────────────────
static bool s_watching;
static float s_next, s_t0, s_dur;
static int s_ticks;
static System.Text.StringBuilder s_run;
static System.Collections.Generic.Dictionary<int, int> s_offCount; // instanceID → 이탈 틱 수
static System.Collections.Generic.Dictionary<int, string> s_offName;
static int s_peakMobs, s_offEvents;
const float kWatchSeconds = 60f; // 발주서 지정 감시 시간(측정 조건)
public static string Watch()
{
float seconds = kWatchSeconds;
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
if (s_watching) return "이미 감시 중 — 남은 " + (s_dur - (UnityEngine.Time.realtimeSinceStartup - s_t0)).ToString("F0") + "s";
s_run = new System.Text.StringBuilder();
s_offCount = new System.Collections.Generic.Dictionary<int, int>();
s_offName = new System.Collections.Generic.Dictionary<int, string>();
s_peakMobs = 0; s_offEvents = 0; s_ticks = 0;
s_t0 = UnityEngine.Time.realtimeSinceStartup; s_dur = seconds; s_next = 0f;
s_watching = true;
UnityEditor.EditorApplication.update -= Tick;
UnityEditor.EditorApplication.update += Tick;
R("===== 런타임 이탈 감시 시작 (" + seconds + "s · 1s 간격 · SamplePosition 반경 " + kOffMeshRadius + ") =====");
ReachSweep("시작");
return "감시 시작 " + seconds + "s";
}
static void Tick()
{
if (!s_watching) { UnityEditor.EditorApplication.update -= Tick; return; }
if (!UnityEngine.Application.isPlaying) { R("[중단] Play 가 멈췄다"); StopWatch(); return; }
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
if (el < s_next) return;
s_next = el + 1f;
s_ticks++;
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
if (mobs.Length > s_peakMobs) s_peakMobs = mobs.Length;
int off = 0;
for (int i = 0; i < mobs.Length; i++)
{
var m = mobs[i];
var p = m.transform.position;
UnityEngine.AI.NavMeshHit h;
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) continue;
off++; s_offEvents++;
int id = m.GetInstanceID();
if (!s_offCount.ContainsKey(id)) { s_offCount[id] = 0; s_offName[id] = m.name; }
s_offCount[id]++;
if (s_offCount[id] <= 3) // 몹당 3회까지만 상세 기록
{
var ag = m.GetComponent<UnityEngine.AI.NavMeshAgent>();
R(string.Format(" [이탈] t={0,5:F1}s {1} pos=({2:F1},{3:F1},{4:F1}) agent.enabled={5} isOnNavMesh={6} CC={7}",
el, m.name, p.x, p.y, p.z,
ag == null ? "null" : ag.enabled.ToString(),
ag == null ? "null" : (ag.enabled ? ag.isOnNavMesh.ToString() : "-"),
m.IsCC()));
}
}
if (s_ticks % 10 == 0) R(" t=" + el.ToString("F0") + "s 활성몹=" + mobs.Length + " 이탈=" + off);
if (el >= s_dur) { ReachSweep("종료"); StopWatch(); }
}
static void ReachSweep(string tag)
{
var pc = MyValue.MyPC;
if (pc == null) { R("[" + tag + "] PC 없음 — 도달성 스윕 생략"); return; }
var pcPos = pc.transform.position;
UnityEngine.AI.NavMeshHit ph;
if (!UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
{ R("[" + tag + "] PC 가 NavMesh 위가 아니다 pos=" + pcPos.ToString("F1")); return; }
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
int bad = 0; var names = new System.Text.StringBuilder();
for (int i = 0; i < mobs.Length; i++)
{
if (Reachable(ph.position, mobs[i].transform.position)) continue;
bad++;
if (bad <= 15) { if (names.Length > 0) names.Append(", "); names.Append(mobs[i].name + mobs[i].transform.position.ToString("F0")); }
}
R("[" + tag + "] PC=" + ph.position.ToString("F1") + " 활성몹 " + mobs.Length
+ " 중 도달 불가 " + bad + " (" + Pct(bad, mobs.Length) + "%)" + (bad > 0 ? " 예: " + names : ""));
}
static void StopWatch()
{
s_watching = false;
UnityEditor.EditorApplication.update -= Tick;
R("");
R("===== 감시 종료 · 틱 " + s_ticks + " · 최대 활성몹 " + s_peakMobs
+ " · 이탈 이벤트 " + s_offEvents + " · 이탈 몹 " + (s_offCount == null ? 0 : s_offCount.Count) + "종 =====");
if (s_offCount != null)
foreach (var kv in s_offCount) R(" 이탈몹 " + s_offName[kv.Key] + " ×" + kv.Value + "틱");
WriteFile(kRunLog, s_run.ToString());
}
public static string Stop() { if (s_watching) StopWatch(); return "중지"; }
/// <summary>즉시 1회 측정 — 활성 몹 전원의 (a) PC 로부터의 도달성 (b) NavMesh 이탈 여부.</summary>
public static string Sweep()
{
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
var pc = MyValue.MyPC;
if (pc == null) return "[에러] PC 없음";
UnityEngine.AI.NavMeshHit ph;
if (!UnityEngine.AI.NavMesh.SamplePosition(pc.transform.position, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
return "[에러] PC 가 NavMesh 위가 아니다 " + pc.transform.position.ToString("F1");
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
int unreach = 0, off = 0;
var bad = new System.Text.StringBuilder();
for (int i = 0; i < mobs.Length; i++)
{
var p = mobs[i].transform.position;
UnityEngine.AI.NavMeshHit h;
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) off++;
if (Reachable(ph.position, p)) continue;
unreach++;
if (unreach <= 12) { if (bad.Length > 0) bad.Append(", "); bad.Append(mobs[i].name + p.ToString("F0")); }
}
return "활성몹 " + mobs.Length + " · PC(" + ph.position.ToString("F1") + ") 에서 도달 불가 " + unreach
+ " (" + Pct(unreach, mobs.Length) + "%) · NavMesh 이탈(" + kOffMeshRadius + "m) " + off
+ (unreach > 0 ? " · 예: " + bad : "");
}
public static string Status()
{
if (!s_watching) return "감시 중 아님 (틱 " + s_ticks + " · 이탈 " + s_offEvents + ")";
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
return "감시 중 " + el.ToString("F0") + "/" + s_dur + "s · 틱 " + s_ticks + " · 현재 활성몹 " + mobs.Length
+ " · 누적 이탈 " + s_offEvents + " (" + (s_offCount == null ? 0 : s_offCount.Count) + "종)";
}
public static string Dump()
{
if (s_run == null) return "기록 없음";
WriteFile(kRunLog, s_run.ToString());
return s_run.ToString();
}
/// <summary>#763 펫 판정 — 활성 PetActor 수와 펫 카드 상태.</summary>
public static string PetCheck()
{
var sb = new System.Text.StringBuilder();
var all = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);
var act = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
sb.Append("PetActor 전체=" + all.Length + " 활성=" + act.Length);
for (int i = 0; i < all.Length; i++) sb.Append(" | " + all[i].name + " active=" + all[i].gameObject.activeInHierarchy);
var pets = ActorInfo.Ins != null ? ActorInfo.Ins.Get_Actors(eRole.Pet, 0, true) : null;
sb.Append(" || ActorInfo eRole.Pet(includeDie) = " + (pets == null ? "n/a" : pets.Count.ToString()));
// 계정에 실제로 펫이 장착돼 있는지 — 장착 0 이면 이 검증은 무의미하다
if (ServerInfo.Ins != null && ServerInfo.Ins.m_ServerData != null)
{
int equipped = ServerInfo.Ins.m_ServerData.Get_EqiupmentEquip(eItem.Pet);
sb.Append(" || 장착 펫ID = " + equipped + (equipped > 0 ? " (장착됨 — 차단이 실제로 동작해야 함)" : " (미장착 🔴 검증 무의미)"));
}
// 펫 카드 UI 상태
if (NewGameUI.isIns && NewGameUI.Ins.arr_PetCard != null && NewGameUI.Ins.arr_PetCard.Length > 0)
{
var card = NewGameUI.Ins.arr_PetCard[0];
sb.Append(" || arr_PetCard[0] = " + (card == null ? "null" : card.name + " active=" + card.gameObject.activeInHierarchy));
}
else sb.Append(" || NewGameUI/arr_PetCard 없음");
sb.Append(" || MyPC.Get_MyPet() = " + (MyValue.MyPC == null ? "PC없음" : (MyValue.MyPC.Get_MyPet() == null ? "null(OK)" : MyValue.MyPC.Get_MyPet().name)));
// 설정 에셋은 리플렉션으로 읽는다 — 이 프로브가 WLGameplaySettings 존재 여부와 무관하게 컴파일되도록.
var t = System.Type.GetType("WL.Settings.WLGameplaySettings, Assembly-CSharp");
if (t == null) sb.Append(" || WLGameplaySettings 타입 없음");
else
{
var inst = t.GetProperty("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).GetValue(null, null);
if (inst == null) sb.Append(" || WLGameplaySettings 에셋 없음");
else sb.Append(" || WLGameplaySettings.spawnPlayerPet=" + t.GetField("spawnPlayerPet").GetValue(inst));
}
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────
static string Pct(int a, int b) { return b == 0 ? "0" : (a * 100 / b).ToString(); }
static void L(string t) { s.Append(t); s.Append("\n"); UnityEngine.Debug.Log("[WL764] " + t); }
static void R(string t) { s_run.Append(t); s_run.Append("\n"); }
static string Flush(string path) { WriteFile(path, s.ToString()); return s.ToString(); }
static void WriteFile(string path, string body)
{
var full = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), path);
var dir = System.IO.Path.GetDirectoryName(full);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(full, body, new System.Text.UTF8Encoding(true));
}
}