244 lines
12 KiB
C#
244 lines
12 KiB
C#
// 발주서 WL-813b — 힘민지 보스 12종 로드 검증 프로브 (읽기 전용 · Play 없음)
|
|
//
|
|
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
|
// unity command run_script --file AgentScripts/WL813b_BossProbe.cs --entry WL813b_BossProbe.Edit --timeout 300
|
|
//
|
|
// 무엇을 재나 (전부 에디트 모드 AssetDatabase · 씬/프리팹 변경 0):
|
|
// 1) MonsterList.json 의 e_MonsterType=Boss 행 → s_MonsterPrefab
|
|
// 2) Addressables 키 = "Assets/Res_Addr/Mobs/{s_MonsterPrefab}.prefab"
|
|
// (table_monsterlist.cs:191 이 붙이는 실제 런타임 키와 동일한 규칙)
|
|
// 3) 프리팹 로드 · MobActor 파생 컴포넌트 이름 · missing script 수
|
|
// 4) Animator / RuntimeAnimatorController · 루트 localScale
|
|
// 5) 렌더러·머티리얼·셰이더(MK Toon · missing 판정) · 삼각형 수(모바일 예산)
|
|
// 6) 투사체 3종 = "Assets/Res_Addr/Projectile/{s_Porjectile#}.prefab"
|
|
// 7) 보스 스크립트 소스에서 뽑은 인디케이터/이펙트 키 존재 여부
|
|
// Load_Indicator("X") -> Assets/Res_Addr/Obj/X.prefab
|
|
// Show_Effect("X" -> Assets/Res_Addr/Effect/X.prefab
|
|
//
|
|
// 경로·수치는 전부 프로젝트 데이터·소스에서 읽는다(C45). 프로브 상수는 "어디를 보나"일 뿐이다.
|
|
|
|
public static class WL813b_BossProbe
|
|
{
|
|
const string kTableJson = "Assets/ResWork/Table/Export/MonsterList.json";
|
|
const string kBossSrcDir = "Assets/Script/Character/Mob/Boss";
|
|
const string kMobFmt = "Assets/Res_Addr/Mobs/{0}.prefab";
|
|
const string kProjFmt = "Assets/Res_Addr/Projectile/{0}.prefab";
|
|
const string kIndiFmt = "Assets/Res_Addr/Obj/{0}.prefab";
|
|
const string kEffFmt = "Assets/Res_Addr/Effect/{0}.prefab";
|
|
const string kOutLog = "AgentScripts/staging/WL813b/BOSS_PROBE.txt";
|
|
|
|
static System.Text.StringBuilder s;
|
|
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813b] " + t); }
|
|
|
|
public static string Edit()
|
|
{
|
|
s = new System.Text.StringBuilder();
|
|
L("===== WL-813b 보스 프리팹 로드 검증 =====");
|
|
L("playMode=" + UnityEngine.Application.isPlaying + " / unity=" + UnityEngine.Application.unityVersion);
|
|
L("buildTarget=" + UnityEditor.EditorUserBuildSettings.activeBuildTarget);
|
|
|
|
var rows = ReadBossRows();
|
|
L("MonsterList Boss 행 = " + rows.Count);
|
|
L("");
|
|
|
|
int okAll = 0;
|
|
for (int i = 0; i < rows.Count; i++)
|
|
{
|
|
var r = rows[i];
|
|
bool ok = ProbeOne(r);
|
|
if (ok) okAll++;
|
|
L("");
|
|
}
|
|
L("===== 요약: 전부 통과 " + okAll + " / " + rows.Count + " =====");
|
|
return Flush();
|
|
}
|
|
|
|
// ── MonsterList.json 에서 Boss 행만 (JsonUtility 없이 얕은 파싱) ──────────
|
|
class Row
|
|
{
|
|
public string id, prefab, scale, hp, atk, def, atkspd, movspd, range, chase, p1, p2, p3, lt1, lt2, lt3, passive, name;
|
|
}
|
|
|
|
static System.Collections.Generic.List<Row> ReadBossRows()
|
|
{
|
|
var list = new System.Collections.Generic.List<Row>();
|
|
string txt = System.IO.File.ReadAllText(kTableJson);
|
|
var objs = System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}");
|
|
for (int i = 0; i < objs.Count; i++)
|
|
{
|
|
string o = objs[i].Value;
|
|
if (F(o, "e_MonsterType") != "Boss") continue;
|
|
var r = new Row();
|
|
r.id = F(o, "n_MonsterID"); r.prefab = F(o, "s_MonsterPrefab");
|
|
r.scale = F(o, "f_DefaultScale"); r.hp = F(o, "f_BaseHPRate");
|
|
r.atk = F(o, "f_BaseATKRate"); r.def = F(o, "f_BaseDEFRate");
|
|
r.atkspd = F(o, "f_BaseATKSpeed"); r.movspd = F(o, "f_BaseMOVSpeed");
|
|
r.range = F(o, "f_BaseATKRange"); r.chase = F(o, "f_BaseChaseRange");
|
|
r.p1 = F(o, "s_Porjectile1"); r.p2 = F(o, "s_Porjectile2"); r.p3 = F(o, "s_Porjectile3");
|
|
r.lt1 = F(o, "f_ProjectileLifeTime1"); r.lt2 = F(o, "f_ProjectileLifeTime2"); r.lt3 = F(o, "f_ProjectileLifeTime3");
|
|
r.passive = F(o, "n_PassiveSkillID"); r.name = F(o, "n_MonsterName");
|
|
list.Add(r);
|
|
}
|
|
return list;
|
|
}
|
|
static string F(string obj, string key)
|
|
{
|
|
var m = System.Text.RegularExpressions.Regex.Match(obj, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
|
|
return m.Success ? m.Groups[1].Value : "";
|
|
}
|
|
|
|
// ── 보스 1종 검사 ────────────────────────────────────────────────────────
|
|
static bool ProbeOne(Row r)
|
|
{
|
|
bool ok = true;
|
|
string path = string.Format(kMobFmt, r.prefab);
|
|
L("── [" + r.id + "] " + r.prefab + " (scale " + r.scale + " · HPx" + r.hp + " · ATKx" + r.atk
|
|
+ " · MS" + r.movspd + " · RNG" + r.range + " · CHASE" + r.chase + " · passive " + r.passive + ")");
|
|
L(" key = " + path);
|
|
|
|
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
|
|
if (go == null) { L(" [X] 프리팹 없음 — 로드 불가"); return false; }
|
|
|
|
// Addressables 커버리지 (엔트리 직접 or 상위 폴더 엔트리)
|
|
L(" addressable = " + AddrState(path));
|
|
|
|
// 컴포넌트 / missing script
|
|
int missing = 0;
|
|
var comps = go.GetComponents<UnityEngine.Component>();
|
|
for (int i = 0; i < comps.Length; i++) if (comps[i] == null) missing++;
|
|
var allComps = go.GetComponentsInChildren<UnityEngine.Component>(true);
|
|
int missingAll = 0;
|
|
for (int i = 0; i < allComps.Length; i++) if (allComps[i] == null) missingAll++;
|
|
|
|
var mob = go.GetComponent<MobActor>();
|
|
string mobType = mob == null ? "(없음)" : mob.GetType().Name;
|
|
if (mob == null) { ok = false; L(" [X] MobActor 파생 컴포넌트 없음"); }
|
|
else L(" actor = " + mobType + (mobType.StartsWith("Boss_") ? "" : " [!] Boss_* 아님"));
|
|
if (missing > 0 || missingAll > 0) { ok = false; L(" [X] missing script 루트 " + missing + " · 전체 " + missingAll); }
|
|
else L(" missing script = 0");
|
|
|
|
// 스케일 (세로 화면 판정 · 발주서 localScale>2)
|
|
var sc = go.transform.localScale;
|
|
L(" localScale = " + sc.x.ToString("F2") + " (테이블 f_DefaultScale=" + r.scale + ")"
|
|
+ (sc.x > 2f ? " [!] >2" : ""));
|
|
|
|
// 애니메이터
|
|
var anim = go.GetComponentInChildren<UnityEngine.Animator>(true);
|
|
if (anim == null) { ok = false; L(" [X] Animator 없음"); }
|
|
else
|
|
{
|
|
var rac = anim.runtimeAnimatorController;
|
|
if (rac == null) { ok = false; L(" [X] AnimatorController 비어 있음"); }
|
|
else L(" animator = " + rac.name + " (clips " + rac.animationClips.Length + ")");
|
|
}
|
|
|
|
// 렌더러 · 셰이더 · 삼각형
|
|
var rends = go.GetComponentsInChildren<UnityEngine.Renderer>(true);
|
|
int tri = 0, matN = 0, mkToon = 0, badShader = 0;
|
|
var shaders = new System.Collections.Generic.List<string>();
|
|
for (int i = 0; i < rends.Length; i++)
|
|
{
|
|
var mats = rends[i].sharedMaterials;
|
|
for (int j = 0; j < mats.Length; j++)
|
|
{
|
|
matN++;
|
|
if (mats[j] == null) { badShader++; continue; }
|
|
var sh = mats[j].shader;
|
|
if (sh == null || sh.name.Contains("InternalErrorShader")) { badShader++; continue; }
|
|
if (sh.name.IndexOf("MK", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
&& sh.name.IndexOf("Toon", System.StringComparison.OrdinalIgnoreCase) >= 0) mkToon++;
|
|
if (!shaders.Contains(sh.name)) shaders.Add(sh.name);
|
|
}
|
|
var smr = rends[i] as UnityEngine.SkinnedMeshRenderer;
|
|
if (smr != null && smr.sharedMesh != null) tri += smr.sharedMesh.triangles.Length / 3;
|
|
var mf = rends[i].GetComponent<UnityEngine.MeshFilter>();
|
|
if (mf != null && mf.sharedMesh != null) tri += mf.sharedMesh.triangles.Length / 3;
|
|
}
|
|
if (badShader > 0) { ok = false; L(" [X] 셰이더 없음/에러 머티리얼 " + badShader + "개"); }
|
|
L(" renderer " + rends.Length + " · material " + matN + " · tri " + tri
|
|
+ " · MKToon " + mkToon + " · shader = " + string.Join(", ", shaders.ToArray()));
|
|
|
|
// 투사체 3종
|
|
ok &= CheckKey("P1", r.p1, kProjFmt, r.lt1);
|
|
ok &= CheckKey("P2", r.p2, kProjFmt, r.lt2);
|
|
ok &= CheckKey("P3", r.p3, kProjFmt, r.lt3);
|
|
|
|
// 스크립트 소스에서 뽑은 인디케이터 / 이펙트
|
|
string src = FindSrc(mobType);
|
|
if (src == null) L(" 소스 없음 — 인디케이터/이펙트 검사 생략");
|
|
else
|
|
{
|
|
ok &= CheckSet("indicator", src, "Load_Indicator\\(\\s*\"([^\"]+)\"", kIndiFmt);
|
|
ok &= CheckSet("indicator(필드기본값)", src, "Indicator\\s*=\\s*\"([^\"]+)\"", kIndiFmt);
|
|
ok &= CheckSet("effect", src, "Show_Effect\\(\\s*\"([^\"]+)\"", kEffFmt);
|
|
ok &= CheckSet("projectile(직접)", src, "Shoot_Projectile\\(\\s*\"([^\"]+)\"", kProjFmt);
|
|
}
|
|
L(" => " + (ok ? "PASS" : "FAIL"));
|
|
return ok;
|
|
}
|
|
|
|
static bool CheckKey(string label, string name, string fmt, string lifetime)
|
|
{
|
|
if (string.IsNullOrEmpty(name) || name == "None") { L(" " + label + " = None"); return true; }
|
|
string p = string.Format(fmt, name);
|
|
var a = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
|
|
L(" " + label + " = " + name + " (life " + lifetime + ") " + (a != null ? "OK" : "[X] 없음: " + p));
|
|
return a != null;
|
|
}
|
|
|
|
static bool CheckSet(string label, string src, string pattern, string fmt)
|
|
{
|
|
var ms = System.Text.RegularExpressions.Regex.Matches(src, pattern);
|
|
var seen = new System.Collections.Generic.List<string>();
|
|
var miss = new System.Collections.Generic.List<string>();
|
|
for (int i = 0; i < ms.Count; i++)
|
|
{
|
|
string n = ms[i].Groups[1].Value;
|
|
if (seen.Contains(n)) continue;
|
|
seen.Add(n);
|
|
if (UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(string.Format(fmt, n)) == null) miss.Add(n);
|
|
}
|
|
if (seen.Count == 0) return true;
|
|
L(" " + label + " " + seen.Count + "종 · 누락 " + miss.Count + (miss.Count > 0 ? " -> " + string.Join(", ", miss.ToArray()) : ""));
|
|
return miss.Count == 0;
|
|
}
|
|
|
|
static string FindSrc(string typeName)
|
|
{
|
|
if (string.IsNullOrEmpty(typeName) || typeName == "(없음)") return null;
|
|
var files = System.IO.Directory.GetFiles(kBossSrcDir, "*.cs");
|
|
for (int i = 0; i < files.Length; i++)
|
|
{
|
|
string t = System.IO.File.ReadAllText(files[i]);
|
|
if (t.Contains("class " + typeName)) return t;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static string AddrState(string assetPath)
|
|
{
|
|
var st = UnityEditor.AddressableAssets.AddressableAssetSettingsDefaultObject.Settings;
|
|
if (st == null) return "(Addressables 설정 없음)";
|
|
string guid = UnityEditor.AssetDatabase.AssetPathToGUID(assetPath);
|
|
var e = st.FindAssetEntry(guid);
|
|
if (e != null) return "직접 엔트리 · address=" + e.address + " · group=" + e.parentGroup.Name;
|
|
// 상위 폴더 엔트리 탐색
|
|
string dir = System.IO.Path.GetDirectoryName(assetPath).Replace("\\", "/");
|
|
while (!string.IsNullOrEmpty(dir) && dir != "Assets")
|
|
{
|
|
var fe = st.FindAssetEntry(UnityEditor.AssetDatabase.AssetPathToGUID(dir));
|
|
if (fe != null) return "폴더 엔트리 " + fe.address + " (group=" + fe.parentGroup.Name + ") 하위";
|
|
dir = System.IO.Path.GetDirectoryName(dir).Replace("\\", "/");
|
|
}
|
|
return "[X] Addressables 미등록";
|
|
}
|
|
|
|
static string Flush()
|
|
{
|
|
string dir = System.IO.Path.GetDirectoryName(kOutLog);
|
|
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
|
System.IO.File.WriteAllText(kOutLog, s.ToString());
|
|
return s.ToString();
|
|
}
|
|
}
|