diff --git a/AgentScripts/WL813b_BossProbe.cs b/AgentScripts/WL813b_BossProbe.cs new file mode 100644 index 000000000..37b39c2b5 --- /dev/null +++ b/AgentScripts/WL813b_BossProbe.cs @@ -0,0 +1,243 @@ +// 발주서 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 ReadBossRows() + { + var list = new System.Collections.Generic.List(); + 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(path); + if (go == null) { L(" [X] 프리팹 없음 — 로드 불가"); return false; } + + // Addressables 커버리지 (엔트리 직접 or 상위 폴더 엔트리) + L(" addressable = " + AddrState(path)); + + // 컴포넌트 / missing script + int missing = 0; + var comps = go.GetComponents(); + for (int i = 0; i < comps.Length; i++) if (comps[i] == null) missing++; + var allComps = go.GetComponentsInChildren(true); + int missingAll = 0; + for (int i = 0; i < allComps.Length; i++) if (allComps[i] == null) missingAll++; + + var mob = go.GetComponent(); + 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(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(true); + int tri = 0, matN = 0, mkToon = 0, badShader = 0; + var shaders = new System.Collections.Generic.List(); + 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(); + 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(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(); + var miss = new System.Collections.Generic.List(); + 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(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(); + } +} diff --git a/AgentScripts/WL813e_Zones.cs b/AgentScripts/WL813e_Zones.cs new file mode 100644 index 000000000..a87d0b966 --- /dev/null +++ b/AgentScripts/WL813e_Zones.cs @@ -0,0 +1,480 @@ +// 발주서 WL-813e — 군중 존 5개 + 보스 아레나(Anubis) 데이터 적용 검증 프로브 +// +// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) +// 에디트 모드 : unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Edit --timeout 300 +// Play 60s : unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Watch (Play 중에만) +// unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Result (감시 종료 후 결과) +// +// Edit() 가 재는 것 — 전부 AssetDatabase 읽기 전용, 씬/프리팹 변경 0: +// (a) MonsterAppear.json 새 행 6개(813001~813005 · 813010) 존재·값 +// (b) WL_Nature.prefab 의 MobControlMgr 19개 → SpawnerID 가 실제 행에 붙는가 · 존 5개 재바인딩 확인 +// (c) s_AppearMonster 의 몬스터 ID 가 MonsterList 에 있고 Addressables 프리팹이 실존하는가 +// (d) FieldBossData 1기 — n_MonsterID/n_SpawnerId/RegenTime/isChapterBoss · MapData.m_ChapterPortal null 정합 +// (e) 동시 생존 수 · 인스턴스 풀 총계 (MobControlMgr.Get_Mobs 규칙 재현: Random/Order = ×2 · Phase* = 종류×수) +// (f) 보스 아레나 좌표의 NavMesh 도달성 (WL_Nature_NavMesh.asset · SamplePosition + CalculatePath) — 813b 미확인 ③ +// +// 수치·경로는 전부 프로젝트 데이터에서 읽는다(C45). 프로브 상수는 "어디를 보나" 뿐이다. + +public static class WL813e_Zones +{ + const string kAppearJson = "Assets/ResWork/Table/Export/MonsterAppear.json"; + const string kListJson = "Assets/ResWork/Table/Export/MonsterList.json"; + const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; + const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset"; + const string kMobFmt = "Assets/Res_Addr/Mobs/{0}.prefab"; + const string kOutDir = "AgentScripts/staging/WL813e"; + const string kEditLog = kOutDir + "/ZONES_EDIT.txt"; + const string kPlayLog = kOutDir + "/ZONES_PLAY.txt"; + + static readonly int[] kZoneIds = { 813001, 813002, 813003, 813004, 813005 }; + const int kBossRowId = 813010; + + static System.Text.StringBuilder s; + static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813e] " + t); } + + /// 텍스트 편집으로 바꾼 프리팹·JSON 을 에디터 캐시에 반영한다(상주 에디터에서 필수). + static UnityEngine.GameObject LoadMap() + { + UnityEditor.AssetDatabase.ImportAsset(kMapPrefab, UnityEditor.ImportAssetOptions.ForceUpdate); + UnityEditor.AssetDatabase.ImportAsset(kAppearJson, UnityEditor.ImportAssetOptions.ForceUpdate); + return UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); + } + + static string Abs(string rel) + { + return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), + rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); + } + + static string Flush(string path) + { + try + { + System.IO.Directory.CreateDirectory(Abs(kOutDir)); + System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true)); + } + catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813e] 로그 저장 실패: " + e.Message); } + return s.ToString(); + } + + // ── 아주 얕은 JSON 배열 파서 (행 = {"k": "v", ...}) ─────────────────────── + static System.Collections.Generic.List> ReadRows(string relPath) + { + var list = new System.Collections.Generic.List>(); + var txt = System.IO.File.ReadAllText(Abs(relPath), new System.Text.UTF8Encoding(true)); + foreach (System.Text.RegularExpressions.Match m in + System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}")) + { + var d = new System.Collections.Generic.Dictionary(); + foreach (System.Text.RegularExpressions.Match kv in + System.Text.RegularExpressions.Regex.Matches(m.Value, "\"([^\"]+)\"\\s*:\\s*\"([^\"]*)\"")) + d[kv.Groups[1].Value] = kv.Groups[2].Value; + if (d.Count > 0) list.Add(d); + } + return list; + } + + static string G(System.Collections.Generic.Dictionary d, string k) + { + string v; return d.TryGetValue(k, out v) ? v : ""; + } + + // ══════════════════════════════════════════════════════════════════════ + public static string Edit() + { + s = new System.Text.StringBuilder(); + L("===== WL-813e 존 리듬 + 보스 아레나 데이터 검증 ====="); + L("playMode=" + UnityEngine.Application.isPlaying + " · unity=" + UnityEngine.Application.unityVersion + + " · buildTarget=" + UnityEditor.EditorUserBuildSettings.activeBuildTarget); + L(""); + + var appear = ReadRows(kAppearJson); + var mlist = ReadRows(kListJson); + var byAppear = new System.Collections.Generic.Dictionary>(); + foreach (var r in appear) byAppear[G(r, "n_SpawnerID")] = r; + var byMob = new System.Collections.Generic.Dictionary>(); + foreach (var r in mlist) byMob[G(r, "n_MonsterID")] = r; + + // ── (a) 새 행 ──────────────────────────────────────────────────── + L("── (a) MonsterAppear 새 행 (전체 " + appear.Count + "행) ──"); + int newOk = 0; + var wanted = new System.Collections.Generic.List(kZoneIds); wanted.Add(kBossRowId); + foreach (var id in wanted) + { + System.Collections.Generic.Dictionary r; + if (!byAppear.TryGetValue(id.ToString(), out r)) { L(" [에러] 행 없음 " + id); continue; } + newOk++; + L(string.Format(" {0} Lv{1} ATK {2} DEF {3} HP {4} · {5} {6} · 최대 {7}마리 · 리젠 {8}s · 순찰 {9} · Exp {10} Gold {11}", + id, G(r, "n_AppearMonsterLv"), G(r, "n_MonsterATK"), G(r, "n_MonsterDEF"), G(r, "n_MonsterHP"), + G(r, "e_SpawnerType"), G(r, "s_AppearMonster"), G(r, "n_MaxMonsterCount"), G(r, "f_SpawnerDelay"), + G(r, "f_PatrolRange"), G(r, "n_DropExp"), G(r, "n_DropGold"))); + } + L(" 새 행 " + newOk + " / " + wanted.Count); + L(""); + + // ── (b)(c)(e) 맵 프리팹의 스포너 ──────────────────────────────── + var root = LoadMap(); + if (root == null) { L("[에러] 맵 프리팹 로드 실패: " + kMapPrefab); return Flush(kEditLog); } + + var mgrs = root.GetComponentsInChildren(true); + L("── (b) " + kMapPrefab + " 스포너 " + mgrs.Length + "개 ──"); + int bound = 0, unbound = 0, zoneHit = 0, alive = 0, pool = 0, mobMissing = 0; + for (int i = 0; i < mgrs.Length; i++) + { + var m = mgrs[i]; + var p = m.transform.position; + System.Collections.Generic.Dictionary r; + bool has = byAppear.TryGetValue(m.SpawnerID.ToString(), out r); + if (!has) { unbound++; L(string.Format(" [에러] {0} SpawnerID {1} → MonsterAppear 행 없음 (MobActor.cs:62 null → MyValue 에서 NullReference)", m.name, m.SpawnerID)); continue; } + bound++; + bool isZone = System.Array.IndexOf(kZoneIds, m.SpawnerID) >= 0; + if (isZone) zoneHit++; + + int cnt = 0; int.TryParse(G(r, "n_MaxMonsterCount"), out cnt); + var ids = G(r, "s_AppearMonster").Split('|'); + string type = G(r, "e_SpawnerType"); + int inst = (type == "Phase" || type == "PhaseAllKill") ? ids.Length * cnt : cnt * 2; + alive += cnt; pool += inst; + + int miss = 0; + for (int k = 0; k < ids.Length; k++) + { + System.Collections.Generic.Dictionary mr; + if (!byMob.TryGetValue(ids[k], out mr)) { miss++; L(" [에러] MonsterList 에 없는 ID " + ids[k]); continue; } + var path = string.Format(kMobFmt, G(mr, "s_MonsterPrefab")); + if (UnityEditor.AssetDatabase.LoadAssetAtPath(path) == null) + { miss++; L(" [에러] 프리팹 없음 " + path); } + } + mobMissing += miss; + L(string.Format(" {0,-3} SpawnerID {1,-7} pos=({2,6:F1},{3,5:F2},{4,6:F1}) range {5} · {6,-12} {7,2}마리 · 종류 {8} · 인스턴스 {9}{10}", + m.name, m.SpawnerID, p.x, p.y, p.z, m.MobGenRange, type, cnt, ids.Length, inst, isZone ? " ← 새 존" : "")); + } + L(string.Format(" 행 연결 {0} / 미연결 {1} · 새 존 {2}/5 · 몹 자산 누락 {3}", bound, unbound, zoneHit, mobMissing)); + L(string.Format("── (e) 동시 생존 {0}마리 · 인스턴스 풀 {1}개 (MobControlMgr.Get_Mobs 규칙 재현)", alive, pool)); + L(""); + + // ── (d) FieldBossData ─────────────────────────────────────────── + var bosses = root.GetComponentsInChildren(true); + L("── (d) FieldBossData " + bosses.Length + "기 ──"); + var mapCfg = root.GetComponentInChildren(true); + L(" MapData.m_ChapterPortal = " + (mapCfg == null ? "MapData 없음" : (mapCfg.m_ChapterPortal == null ? "null → isChapterBoss 는 반드시 0" : "있음")) + + " · spawnRequireReachableFromPlayer = " + (mapCfg == null ? "?" : mapCfg.spawnRequireReachableFromPlayer.ToString())); + for (int i = 0; i < bosses.Length; i++) + { + var b = bosses[i]; + var p = b.transform.position; + System.Collections.Generic.Dictionary mr, ar; + bool hasMob = byMob.TryGetValue(b.n_MonsterID.ToString(), out mr); + bool hasRow = byAppear.TryGetValue(b.n_SpawnerId.ToString(), out ar); + string prefab = hasMob ? string.Format(kMobFmt, G(mr, "s_MonsterPrefab")) : "(MonsterList 행 없음)"; + bool hasPrefab = hasMob && UnityEditor.AssetDatabase.LoadAssetAtPath(prefab) != null; + L(string.Format(" {0} pos=({1:F1},{2:F1},{3:F1}) MonsterID {4} · SpawnerId {5} · Regen {6}s · isChapterBoss {7}", + b.name, p.x, p.y, p.z, b.n_MonsterID, b.n_SpawnerId, b.RegenTime, b.isChapterBoss ? 1 : 0)); + L(" MonsterList 행 " + (hasMob ? "O" : "[에러] X") + " · 프리팹 " + (hasPrefab ? "O " + prefab : "[에러] X " + prefab) + + " · MonsterAppear 행 " + (hasRow ? "O" : "[에러] X")); + if (hasMob) + { + float hpRate = 0f, atkRate = 0f, chase = 0f; + float.TryParse(G(mr, "f_BaseHPRate"), out hpRate); + float.TryParse(G(mr, "f_BaseATKRate"), out atkRate); + float.TryParse(G(mr, "f_BaseChaseRange"), out chase); + float hp = 0f, atk = 0f; + if (hasRow) { float.TryParse(G(ar, "n_MonsterHP"), out hp); float.TryParse(G(ar, "n_MonsterATK"), out atk); } + L(string.Format(" 실 HP = {0} × {1} = {2:N0} · 실 ATK = {3} × {4} = {5:N0} · 추적 사거리 {6} (MyValue.cs:370-371)", + hpRate, hp, hpRate * hp, atkRate, atk, atkRate * atk, chase)); + } + if (b.isChapterBoss && mapCfg != null && mapCfg.m_ChapterPortal == null) + L(" [에러] isChapterBoss=1 인데 m_ChapterPortal 이 null → AfterDie 에서 NullReference (FieldBossData.cs:47-49)"); + } + L(""); + + // ── (f) 아레나 NavMesh 도달성 ─────────────────────────────────── + L("── (f) 아레나 NavMesh 도달성 (813b 미확인 ③) ──"); + var navData = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); + if (navData == null) L(" [미확인] NavMeshData 없음: " + kNavAsset); + else + { + var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData); + try + { + var start = root.transform.Find("Portal_Start"); + UnityEngine.Vector3 pcPos = start != null ? start.position : new UnityEngine.Vector3(115f, 0f, 10f); + L(" 플레이어 시작 = " + (start != null ? "Portal_Start " : "(폴백 상수) ") + pcPos.ToString("F1")); + UnityEngine.AI.NavMeshHit ph; + bool pcOn = UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 5f, UnityEngine.AI.NavMesh.AllAreas); + L(" 시작 지점 NavMesh 스냅 = " + (pcOn ? ph.position.ToString("F2") : "[에러] NavMesh 밖")); + for (int i = 0; i < bosses.Length; i++) + { + var bp = bosses[i].transform.position; + UnityEngine.AI.NavMeshHit bh; + bool on = UnityEngine.AI.NavMesh.SamplePosition(bp, out bh, 5f, UnityEngine.AI.NavMesh.AllAreas); + L(" 보스 " + bosses[i].name + " " + bp.ToString("F1") + " → NavMesh " + + (on ? "O 스냅 " + bh.position.ToString("F2") + " (거리 " + UnityEngine.Vector3.Distance(bp, bh.position).ToString("F2") + "m)" : "[에러] X")); + if (on && pcOn) + { + var path = new UnityEngine.AI.NavMeshPath(); + bool okc = UnityEngine.AI.NavMesh.CalculatePath(ph.position, bh.position, UnityEngine.AI.NavMesh.AllAreas, path); + float len = 0f; + for (int k = 1; k < path.corners.Length; k++) len += UnityEngine.Vector3.Distance(path.corners[k - 1], path.corners[k]); + L(" 시작→보스 경로 " + (okc ? path.status.ToString() : "CalculatePath 실패") + + " · 코너 " + path.corners.Length + " · 경로장 " + len.ToString("F1") + "m · 직선 " + + UnityEngine.Vector3.Distance(ph.position, bh.position).ToString("F1") + "m"); + } + } + } + finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } + } + + L(""); + L("===== 요약: 새 행 " + newOk + "/6 · 스포너 연결 " + bound + "/" + mgrs.Length + + " · 새 존 " + zoneHit + "/5 · 보스 " + bosses.Length + "기 · 몹 자산 누락 " + mobMissing + " ====="); + return Flush(kEditLog); + } + + // ══════════════════════════════════════════════════════════════════════ + /// + /// 아레나 좌표만 문제인지, 이 맵의 NavMesh 가 원래 섬으로 끊겨 있는지 가른다. + /// 시작 지점 → 스포너 19 원점 · 보스 1 좌표의 CalculatePath 상태를 전부 찍는다. + /// (MapData.cs:36 — WL_Start 는 NavMesh 위 1144점 중 37% 만 도달 가능하다는 실측이 있다) + /// + public static string Nav() + { + s = new System.Text.StringBuilder(); + L("===== WL-813e NavMesh 도달성 스윕 (" + kNavAsset + ") ====="); + var root = LoadMap(); + var navData = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); + if (root == null || navData == null) { L("[에러] 프리팹 또는 NavMeshData 로드 실패"); return Flush(kEditLog); } + var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData); + try + { + var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); + L("NavMesh 삼각형 " + (tri.indices.Length / 3) + " · 정점 " + tri.vertices.Length); + var start = root.transform.Find("Portal_Start"); + UnityEngine.Vector3 pcPos = start != null ? start.position : new UnityEngine.Vector3(115f, 0f, 10f); + UnityEngine.AI.NavMeshHit ph; + if (!UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 5f, UnityEngine.AI.NavMesh.AllAreas)) + { L("[에러] 시작 지점이 NavMesh 밖"); return Flush(kEditLog); } + L("시작 " + pcPos.ToString("F1") + " → 스냅 " + ph.position.ToString("F2")); + L(""); + int comp = 0, part = 0, invalid = 0; + var targets = new System.Collections.Generic.List(); + var pts = new System.Collections.Generic.List(); + var mgrs = root.GetComponentsInChildren(true); + for (int i = 0; i < mgrs.Length; i++) { targets.Add("스포너 " + mgrs[i].name + " (ID " + mgrs[i].SpawnerID + ")"); pts.Add(mgrs[i].transform.position); } + var bs = root.GetComponentsInChildren(true); + for (int i = 0; i < bs.Length; i++) { targets.Add("보스 " + bs[i].name + " (Mob " + bs[i].n_MonsterID + ")"); pts.Add(bs[i].transform.position); } + for (int i = 0; i < pts.Count; i++) + { + UnityEngine.AI.NavMeshHit h; + bool on = UnityEngine.AI.NavMesh.SamplePosition(pts[i], out h, 5f, UnityEngine.AI.NavMesh.AllAreas); + string st = "NavMesh 밖"; + float len = 0f; + if (on) + { + var path = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(ph.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path); + st = path.status.ToString(); + for (int k = 1; k < path.corners.Length; k++) len += UnityEngine.Vector3.Distance(path.corners[k - 1], path.corners[k]); + if (path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) comp++; + else if (path.status == UnityEngine.AI.NavMeshPathStatus.PathPartial) part++; + else invalid++; + } + else invalid++; + L(string.Format(" {0,-28} {1,-22} 경로장 {2,6:F1}m · 직선 {3,6:F1}m · 스냅거리 {4:F2}m", + targets[i], st, len, UnityEngine.Vector3.Distance(ph.position, pts[i]), on ? UnityEngine.Vector3.Distance(pts[i], h.position) : -1f)); + } + L(""); + L("PathComplete " + comp + " · PathPartial " + part + " · 불가 " + invalid + " / " + pts.Count); + } + finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } + return Flush(kOutDir + "/ZONES_NAV.txt"); + } + + // ══════════════════════════════════════════════════════════════════════ + /// + /// 아레나 후보 탐색 — 초안 후보A (137, −22) 주변 격자에서 + /// (1) 시작 지점에서 PathComplete 이고 (2) SP1·SP4 원점에서 15m 이상 떨어진 점을 + /// 후보A 로부터 가까운 순으로 찍는다. 초안의 "군집 간섭 없음" 조건을 유지한 최소 이동을 고른다. + /// + public static string Arena() + { + s = new System.Text.StringBuilder(); + var root = LoadMap(); + var navData = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); + if (root == null || navData == null) { L("[에러] 로드 실패"); return Flush(kOutDir + "/ZONES_ARENA.txt"); } + var ideal = new UnityEngine.Vector3(137f, 0.6f, -22f); + const float kStep = 2f, kSpan = 16f, kClear = 15f; + var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData); + try + { + var start = root.transform.Find("Portal_Start"); + UnityEngine.Vector3 pcPos = start != null ? start.position : new UnityEngine.Vector3(115f, 0f, 10f); + UnityEngine.AI.NavMeshHit ph; + if (!UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 5f, UnityEngine.AI.NavMesh.AllAreas)) + { L("[에러] 시작 지점이 NavMesh 밖"); return Flush(kOutDir + "/ZONES_ARENA.txt"); } + + var mgrs = root.GetComponentsInChildren(true); + L("===== 아레나 후보 탐색 · 이상점 " + ideal.ToString("F1") + " · ±" + kSpan + "m / " + kStep + "m 격자 ====="); + L("조건: 시작에서 PathComplete · 모든 스포너 원점에서 " + kClear + "m 이상 · MobGenRange 10 기준 여유 5m"); + var res = new System.Collections.Generic.List(); + var keys = new System.Collections.Generic.List(); + for (float dx = -kSpan; dx <= kSpan; dx += kStep) + for (float dz = -kSpan; dz <= kSpan; dz += kStep) + { + var q = new UnityEngine.Vector3(ideal.x + dx, ideal.y, ideal.z + dz); + UnityEngine.AI.NavMeshHit h; + if (!UnityEngine.AI.NavMesh.SamplePosition(q, out h, 3f, UnityEngine.AI.NavMesh.AllAreas)) continue; + float nearest = 1e9f; string who = ""; + for (int i = 0; i < mgrs.Length; i++) + { + float d = UnityEngine.Vector3.Distance(new UnityEngine.Vector3(h.position.x, 0f, h.position.z), + new UnityEngine.Vector3(mgrs[i].transform.position.x, 0f, mgrs[i].transform.position.z)); + if (d < nearest) { nearest = d; who = mgrs[i].name + "(ID " + mgrs[i].SpawnerID + ")"; } + } + if (nearest < kClear) continue; + var path = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(ph.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path); + if (path.status != UnityEngine.AI.NavMeshPathStatus.PathComplete) continue; + float len = 0f; + for (int k = 1; k < path.corners.Length; k++) len += UnityEngine.Vector3.Distance(path.corners[k - 1], path.corners[k]); + float off = UnityEngine.Vector3.Distance(new UnityEngine.Vector3(ideal.x, 0f, ideal.z), new UnityEngine.Vector3(h.position.x, 0f, h.position.z)); + keys.Add(off); + res.Add(string.Format(" ({0,6:F1},{1,5:F2},{2,6:F1}) 후보A 로부터 {3,4:F1}m · 시작에서 경로 {4,5:F1}m · 최근접 스포너 {5,5:F1}m {6} · 스냅 {7:F2}m", + h.position.x, h.position.y, h.position.z, off, len, nearest, who, + UnityEngine.Vector3.Distance(q, h.position))); + } + var idx = new System.Collections.Generic.List(); + for (int i = 0; i < res.Count; i++) idx.Add(i); + idx.Sort(delegate (int a, int b) { return keys[a].CompareTo(keys[b]); }); + L("후보 " + res.Count + "개 (가까운 순 상위 12)"); + for (int i = 0; i < idx.Count && i < 12; i++) L(res[idx[i]]); + } + finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } + return Flush(kOutDir + "/ZONES_ARENA.txt"); + } + + // ══════════════════════════════════════════════════════════════════════ + // Play 슬롯 60s 실측 — 절두체 내 몹 수 · 처치 간격 · 보스 스폰 · fps + // ══════════════════════════════════════════════════════════════════════ + static System.Text.StringBuilder r; + static bool s_on; + static float s_t0, s_dur, s_next, s_lastKill, s_bossAt; + static int s_ticks, s_frames, s_peakInView, s_sumInView, s_kills, s_peakAlive; + static System.Collections.Generic.List s_gaps; + static System.Collections.Generic.HashSet s_prev; + static System.Collections.Generic.Dictionary s_killBySpawner; + static float s_frameAcc; + + const float kWatchSeconds = 60f; + const float kSample = 0.2f; + + static void R(string t) { r.AppendLine(t); UnityEngine.Debug.Log("[WL813e] " + t); } + + public static string Watch() + { + if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다"; + if (s_on) return "이미 감시 중 — 남은 " + (s_dur - (UnityEngine.Time.realtimeSinceStartup - s_t0)).ToString("F0") + "s"; + r = new System.Text.StringBuilder(); + s_gaps = new System.Collections.Generic.List(); + s_prev = new System.Collections.Generic.HashSet(); + s_killBySpawner = new System.Collections.Generic.Dictionary(); + s_t0 = UnityEngine.Time.realtimeSinceStartup; s_dur = kWatchSeconds; s_next = 0f; + s_lastKill = -1f; s_bossAt = -1f; + s_ticks = 0; s_frames = 0; s_frameAcc = 0f; + s_peakInView = 0; s_sumInView = 0; s_kills = 0; s_peakAlive = 0; + s_on = true; + UnityEditor.EditorApplication.update -= Tick; + UnityEditor.EditorApplication.update += Tick; + R("===== WL-813e Play 실측 시작 (" + kWatchSeconds + "s · " + kSample + "s 간격) ====="); + R("맵 = " + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name + + " · PC = " + (MyValue.MyPC == null ? "null" : MyValue.MyPC.transform.position.ToString("F1"))); + return "감시 시작 " + kWatchSeconds + "s — 끝나면 Result 로 결과를 받는다"; + } + + static void Tick() + { + if (!s_on) { UnityEditor.EditorApplication.update -= Tick; return; } + if (!UnityEngine.Application.isPlaying) { R("[중단] Play 가 멈췄다"); Stop(); return; } + s_frames++; s_frameAcc += UnityEngine.Time.unscaledDeltaTime; + float el = UnityEngine.Time.realtimeSinceStartup - s_t0; + if (el < s_next) return; + s_next = el + kSample; + s_ticks++; + + var cam = UnityEngine.Camera.main; + UnityEngine.Plane[] planes = null; + if (cam != null) planes = UnityEngine.GeometryUtility.CalculateFrustumPlanes(cam); + + var mobs = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None); + var now = new System.Collections.Generic.HashSet(); + int inView = 0; + for (int i = 0; i < mobs.Length; i++) + { + var m = mobs[i]; + now.Add(m.GetInstanceID()); + if (planes != null) + { + var b = new UnityEngine.Bounds(m.transform.position + UnityEngine.Vector3.up, UnityEngine.Vector3.one * 2f); + if (UnityEngine.GeometryUtility.TestPlanesAABB(planes, b)) inView++; + } + // 보스 판별 = FieldBossData 가 넘긴 스포너 ID (MobActor.Get_Group() → m_SpawnerId · MobActor.cs:635) + if (s_bossAt < 0f && m.Get_Group() == kBossRowId) + { + s_bossAt = el; + R(string.Format(" [보스] t={0:F1}s {1} pos={2} HP={3:N0}/{4:N0}", el, m.name, m.transform.position.ToString("F1"), m.Get_HP(), m.Get_MaxHP())); + } + } + if (mobs.Length > s_peakAlive) s_peakAlive = mobs.Length; + if (inView > s_peakInView) s_peakInView = inView; + s_sumInView += inView; + + // 처치 = 활성 몹 목록에서 사라진 인스턴스 (811b Killed 이벤트가 이 브랜치에 없어 쓰는 프록시) + foreach (var id in s_prev) + { + if (now.Contains(id)) continue; + s_kills++; + if (s_lastKill >= 0f) s_gaps.Add(el - s_lastKill); + s_lastKill = el; + } + s_prev = now; + + if (s_ticks % 25 == 0) + R(string.Format(" t={0,4:F0}s 활성 {1,3} · 절두체 내 {2,2} · 누적 처치 {3,3} · fps {4:F1}", + el, mobs.Length, inView, s_kills, s_frames / System.Math.Max(0.001f, s_frameAcc))); + + if (el >= s_dur) Stop(); + } + + static void Stop() + { + s_on = false; + UnityEditor.EditorApplication.update -= Tick; + float el = UnityEngine.Time.realtimeSinceStartup - s_t0; + R(""); + R("===== 결과 (" + el.ToString("F1") + "s · 샘플 " + s_ticks + ") ====="); + R("절두체 내 몹 수: 평균 " + (s_ticks > 0 ? ((float)s_sumInView / s_ticks) : 0f).ToString("F1") + " · 최대 " + s_peakInView + + " (기준서 §B 요소1 = 8~15)"); + R("동시 활성 몹(맵 전체) 최대 = " + s_peakAlive); + float avg = 0f; + for (int i = 0; i < s_gaps.Count; i++) avg += s_gaps[i]; + if (s_gaps.Count > 0) avg /= s_gaps.Count; + R("처치 " + s_kills + "회 · 처치 간격 평균 " + avg.ToString("F2") + "s · 표본 " + s_gaps.Count + " (기준서 §B 요소1 = ≤3s)"); + R("보스 최초 관측 = " + (s_bossAt < 0f ? "관측 안 됨(절두체·거리 밖일 수 있다)" : s_bossAt.ToString("F1") + "s")); + R("평균 fps = " + (s_frames / System.Math.Max(0.001f, s_frameAcc)).ToString("F1") + " (프레임 " + s_frames + ")"); + try + { + System.IO.Directory.CreateDirectory(Abs(kOutDir)); + System.IO.File.WriteAllText(Abs(kPlayLog), r.ToString(), new System.Text.UTF8Encoding(true)); + } + catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813e] 로그 저장 실패: " + e.Message); } + } + + public static string Result() + { + if (r == null) return "아직 감시한 적이 없다 (Watch 먼저)"; + if (s_on) return "감시 중 — 남은 " + (s_dur - (UnityEngine.Time.realtimeSinceStartup - s_t0)).ToString("F0") + "s"; + return r.ToString(); + } + + public static string StopNow() { if (s_on) { Stop(); return "중단"; } return "감시 중이 아니다"; } +} diff --git a/Assets/ResWork/Table/Export/MonsterAppear.json b/Assets/ResWork/Table/Export/MonsterAppear.json index 5915a4c5a..a4e461bc5 100644 --- a/Assets/ResWork/Table/Export/MonsterAppear.json +++ b/Assets/ResWork/Table/Export/MonsterAppear.json @@ -1 +1 @@ -[{"n_SpawnerID": "1", "n_AppearMonsterLv": "1", "n_MonsterATK": "10", "n_MonsterDEF": "-10", "n_MonsterMDEF": "-10", "n_MonsterHP": "300", "s_AppearMonster": "101|102|103|104", "e_SpawnerType": "Random", "f_SpawnerDelay": "4", "n_MaxMonsterCount": "15", "f_PatrolRange": "10", "n_DropExp": "10", "n_DropGold": "1", "n_DropReward": "9700001"}, {"n_SpawnerID": "2", "n_AppearMonsterLv": "2", "n_MonsterATK": "10", "n_MonsterDEF": "2", "n_MonsterMDEF": "2", "n_MonsterHP": "99999", "s_AppearMonster": "105|106|107", "e_SpawnerType": "Order", "f_SpawnerDelay": "3.8", "n_MaxMonsterCount": "10", "f_PatrolRange": "4", "n_DropExp": "15", "n_DropGold": "2", "n_DropReward": "9700001"}, {"n_SpawnerID": "3", "n_AppearMonsterLv": "3", "n_MonsterATK": "99999", "n_MonsterDEF": "3", "n_MonsterMDEF": "3", "n_MonsterHP": "10000", "s_AppearMonster": "1014|1015", "e_SpawnerType": "Order", "f_SpawnerDelay": "3.6", "n_MaxMonsterCount": "10", "f_PatrolRange": "5", "n_DropExp": "20", "n_DropGold": "3", "n_DropReward": "9700001"}, {"n_SpawnerID": "4", "n_AppearMonsterLv": "4", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "111|112|113", "e_SpawnerType": "PhaseAllKill", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "5", "n_AppearMonsterLv": "5", "n_MonsterATK": "5", "n_MonsterDEF": "1", "n_MonsterMDEF": "1", "n_MonsterHP": "100", "s_AppearMonster": "114|115|116|117", "e_SpawnerType": "Phase", "f_SpawnerDelay": "4", "n_MaxMonsterCount": "10", "f_PatrolRange": "3", "n_DropExp": "10", "n_DropGold": "1", "n_DropReward": "9700001"}, {"n_SpawnerID": "6", "n_AppearMonsterLv": "6", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "118|119|120", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "7", "n_AppearMonsterLv": "7", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "121|122|123", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "8", "n_AppearMonsterLv": "8", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "124|125|126|127|128", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "9", "n_AppearMonsterLv": "9", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "129|130|131|132|133", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "10", "n_AppearMonsterLv": "10", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "134|135|136", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "11", "n_AppearMonsterLv": "11", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "137|138|139|140", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "12", "n_AppearMonsterLv": "12", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "141|142|143|144|145", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "13", "n_AppearMonsterLv": "13", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "70", "s_AppearMonster": "146|147|148|149|150|151|152|153|154", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "14", "n_AppearMonsterLv": "14", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "155|156|157", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "15", "n_AppearMonsterLv": "15", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "158|159|160", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "16", "n_AppearMonsterLv": "16", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "161|162|163|164", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "17", "n_AppearMonsterLv": "17", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "165|166|167", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "18", "n_AppearMonsterLv": "18", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "168|169|170|171|172", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "19", "n_AppearMonsterLv": "19", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "173|174|175", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "20", "n_AppearMonsterLv": "20", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "176|177|178|179|180|181", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "21", "n_AppearMonsterLv": "21", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "182|183|184|185|186", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "22", "n_AppearMonsterLv": "22", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "187|188|189|190|191", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "23", "n_AppearMonsterLv": "23", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "192|193|194|195|196|197", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "24", "n_AppearMonsterLv": "24", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "198|199|200|201|202|203", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "25", "n_AppearMonsterLv": "25", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "204|205|206|207|208|209|210", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "26", "n_AppearMonsterLv": "26", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "211|212|213|214|215|216|217|218", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "27", "n_AppearMonsterLv": "27", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "219|220|221|222|223|224|225", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "28", "n_AppearMonsterLv": "28", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "226|227|228|229|230|231|232", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "29", "n_AppearMonsterLv": "29", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "233|234|235|236|237|238|239", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "30", "n_AppearMonsterLv": "30", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "240|241|242|243|244|245|246", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "31", "n_AppearMonsterLv": "31", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "247|248|249|250", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "32", "n_AppearMonsterLv": "32", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "251|252|253|254|255|256|257|258|259", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "33", "n_AppearMonsterLv": "33", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "260|261|262|263|264|265|266", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "34", "n_AppearMonsterLv": "34", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "267|268|269|270|271|272|273", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "35", "n_AppearMonsterLv": "35", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "274|275|276|277|278|279|280|281", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "36", "n_AppearMonsterLv": "36", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "282|283|284|285|286|287|288|289|290", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "37", "n_AppearMonsterLv": "37", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "291|292|293|294|295|296|297|298", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "38", "n_AppearMonsterLv": "38", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "299|300|301|302|303|304", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "39", "n_AppearMonsterLv": "39", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "305|306|307|308|309|310|311", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "40", "n_AppearMonsterLv": "40", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "312|313|314|315|316|317|318|319", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "41", "n_AppearMonsterLv": "41", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "320|321|322|323", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "42", "n_AppearMonsterLv": "42", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "324|325|326|327|328|329|330|331|332", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "40", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "43", "n_AppearMonsterLv": "43", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "333|334|335|336|337|338|339|340|341|342|343|344", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "40", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "44", "n_AppearMonsterLv": "44", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "345|346|347", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "45", "n_AppearMonsterLv": "45", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "2000", "s_AppearMonster": "348|349|350|351", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "46", "n_AppearMonsterLv": "46", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "3000", "s_AppearMonster": "1031", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "1", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}] \ No newline at end of file +[{"n_SpawnerID": "1", "n_AppearMonsterLv": "1", "n_MonsterATK": "10", "n_MonsterDEF": "-10", "n_MonsterMDEF": "-10", "n_MonsterHP": "300", "s_AppearMonster": "101|102|103|104", "e_SpawnerType": "Random", "f_SpawnerDelay": "4", "n_MaxMonsterCount": "15", "f_PatrolRange": "10", "n_DropExp": "10", "n_DropGold": "1", "n_DropReward": "9700001"}, {"n_SpawnerID": "2", "n_AppearMonsterLv": "2", "n_MonsterATK": "10", "n_MonsterDEF": "2", "n_MonsterMDEF": "2", "n_MonsterHP": "99999", "s_AppearMonster": "105|106|107", "e_SpawnerType": "Order", "f_SpawnerDelay": "3.8", "n_MaxMonsterCount": "10", "f_PatrolRange": "4", "n_DropExp": "15", "n_DropGold": "2", "n_DropReward": "9700001"}, {"n_SpawnerID": "3", "n_AppearMonsterLv": "3", "n_MonsterATK": "99999", "n_MonsterDEF": "3", "n_MonsterMDEF": "3", "n_MonsterHP": "10000", "s_AppearMonster": "1014|1015", "e_SpawnerType": "Order", "f_SpawnerDelay": "3.6", "n_MaxMonsterCount": "10", "f_PatrolRange": "5", "n_DropExp": "20", "n_DropGold": "3", "n_DropReward": "9700001"}, {"n_SpawnerID": "4", "n_AppearMonsterLv": "4", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "111|112|113", "e_SpawnerType": "PhaseAllKill", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "5", "n_AppearMonsterLv": "5", "n_MonsterATK": "5", "n_MonsterDEF": "1", "n_MonsterMDEF": "1", "n_MonsterHP": "100", "s_AppearMonster": "114|115|116|117", "e_SpawnerType": "Phase", "f_SpawnerDelay": "4", "n_MaxMonsterCount": "10", "f_PatrolRange": "3", "n_DropExp": "10", "n_DropGold": "1", "n_DropReward": "9700001"}, {"n_SpawnerID": "6", "n_AppearMonsterLv": "6", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "118|119|120", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "7", "n_AppearMonsterLv": "7", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "121|122|123", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "8", "n_AppearMonsterLv": "8", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "124|125|126|127|128", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "9", "n_AppearMonsterLv": "9", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "129|130|131|132|133", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "10", "n_AppearMonsterLv": "10", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "134|135|136", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "11", "n_AppearMonsterLv": "11", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "137|138|139|140", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "12", "n_AppearMonsterLv": "12", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "141|142|143|144|145", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "13", "n_AppearMonsterLv": "13", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "70", "s_AppearMonster": "146|147|148|149|150|151|152|153|154", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "14", "n_AppearMonsterLv": "14", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "155|156|157", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "15", "n_AppearMonsterLv": "15", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "158|159|160", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "16", "n_AppearMonsterLv": "16", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "161|162|163|164", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "17", "n_AppearMonsterLv": "17", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "165|166|167", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "18", "n_AppearMonsterLv": "18", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "168|169|170|171|172", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "19", "n_AppearMonsterLv": "19", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "173|174|175", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "20", "n_AppearMonsterLv": "20", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "176|177|178|179|180|181", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "21", "n_AppearMonsterLv": "21", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "182|183|184|185|186", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "22", "n_AppearMonsterLv": "22", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "187|188|189|190|191", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "23", "n_AppearMonsterLv": "23", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "192|193|194|195|196|197", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "24", "n_AppearMonsterLv": "24", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "198|199|200|201|202|203", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "20", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "25", "n_AppearMonsterLv": "25", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "204|205|206|207|208|209|210", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "26", "n_AppearMonsterLv": "26", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "211|212|213|214|215|216|217|218", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "27", "n_AppearMonsterLv": "27", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "219|220|221|222|223|224|225", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "28", "n_AppearMonsterLv": "28", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "226|227|228|229|230|231|232", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "29", "n_AppearMonsterLv": "29", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "233|234|235|236|237|238|239", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "30", "n_AppearMonsterLv": "30", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "240|241|242|243|244|245|246", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "31", "n_AppearMonsterLv": "31", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "247|248|249|250", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "32", "n_AppearMonsterLv": "32", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "251|252|253|254|255|256|257|258|259", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "33", "n_AppearMonsterLv": "33", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "260|261|262|263|264|265|266", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "34", "n_AppearMonsterLv": "34", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "267|268|269|270|271|272|273", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "25", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "35", "n_AppearMonsterLv": "35", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "274|275|276|277|278|279|280|281", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "36", "n_AppearMonsterLv": "36", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "282|283|284|285|286|287|288|289|290", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "37", "n_AppearMonsterLv": "37", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "291|292|293|294|295|296|297|298", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "38", "n_AppearMonsterLv": "38", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "299|300|301|302|303|304", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "39", "n_AppearMonsterLv": "39", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "305|306|307|308|309|310|311", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "40", "n_AppearMonsterLv": "40", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "312|313|314|315|316|317|318|319", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "30", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "41", "n_AppearMonsterLv": "41", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "320|321|322|323", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "42", "n_AppearMonsterLv": "42", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "324|325|326|327|328|329|330|331|332", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "40", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "43", "n_AppearMonsterLv": "43", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "333|334|335|336|337|338|339|340|341|342|343|344", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "40", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "44", "n_AppearMonsterLv": "44", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "4", "s_AppearMonster": "345|346|347", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "10", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "45", "n_AppearMonsterLv": "45", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "2000", "s_AppearMonster": "348|349|350|351", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "15", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "46", "n_AppearMonsterLv": "46", "n_MonsterATK": "4", "n_MonsterDEF": "4", "n_MonsterMDEF": "4", "n_MonsterHP": "3000", "s_AppearMonster": "1031", "e_SpawnerType": "Random", "f_SpawnerDelay": "3.4", "n_MaxMonsterCount": "1", "f_PatrolRange": "6", "n_DropExp": "25", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "813001", "n_AppearMonsterLv": "1", "n_MonsterATK": "10", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "300", "s_AppearMonster": "101|102|103|104", "e_SpawnerType": "Random", "f_SpawnerDelay": "1.5", "n_MaxMonsterCount": "12", "f_PatrolRange": "8", "n_DropExp": "10", "n_DropGold": "1", "n_DropReward": "9700001"}, {"n_SpawnerID": "813002", "n_AppearMonsterLv": "2", "n_MonsterATK": "12", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "450", "s_AppearMonster": "168|169|170|171|172", "e_SpawnerType": "Random", "f_SpawnerDelay": "1.2", "n_MaxMonsterCount": "15", "f_PatrolRange": "8", "n_DropExp": "15", "n_DropGold": "2", "n_DropReward": "9700001"}, {"n_SpawnerID": "813003", "n_AppearMonsterLv": "3", "n_MonsterATK": "14", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "600", "s_AppearMonster": "173|174|175|146|147|151", "e_SpawnerType": "Random", "f_SpawnerDelay": "1.2", "n_MaxMonsterCount": "14", "f_PatrolRange": "8", "n_DropExp": "20", "n_DropGold": "3", "n_DropReward": "9700001"}, {"n_SpawnerID": "813004", "n_AppearMonsterLv": "4", "n_MonsterATK": "16", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "800", "s_AppearMonster": "118|119|120", "e_SpawnerType": "PhaseAllKill", "f_SpawnerDelay": "2.0", "n_MaxMonsterCount": "8", "f_PatrolRange": "6", "n_DropExp": "30", "n_DropGold": "4", "n_DropReward": "9700001"}, {"n_SpawnerID": "813005", "n_AppearMonsterLv": "5", "n_MonsterATK": "20", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "1500", "s_AppearMonster": "1014|1015|1016", "e_SpawnerType": "Order", "f_SpawnerDelay": "3.0", "n_MaxMonsterCount": "5", "f_PatrolRange": "6", "n_DropExp": "60", "n_DropGold": "8", "n_DropReward": "9700001"}, {"n_SpawnerID": "813010", "n_AppearMonsterLv": "6", "n_MonsterATK": "18", "n_MonsterDEF": "0", "n_MonsterMDEF": "0", "n_MonsterHP": "450", "s_AppearMonster": "10006", "e_SpawnerType": "Random", "f_SpawnerDelay": "60", "n_MaxMonsterCount": "1", "f_PatrolRange": "4", "n_DropExp": "500", "n_DropGold": "100", "n_DropReward": "9700001"}] \ No newline at end of file diff --git a/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json new file mode 100644 index 000000000..96da470b4 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json @@ -0,0 +1,125 @@ +{ + "_meta": { + "적용": "WL-813e 에서 채택. 다만 (1) 아레나 좌표는 후보A (137,0.6,-22) 가 시작 지점에서 NavMesh PathPartial(끊긴 구역)로 실측돼 가장 가까운 도달 가능점 (139,0.8,-20) 으로 2.8m 옮겼고, (2) monsterappear_rows 의 n_MonsterHP 는 1200 대신 450(실 HP 9,000 = 기준서 §B 보스 90s)으로 넣었다. 기록 = Assets/WL/Settings/WL813e_ZoneApply.md. xlsm 동기화는 아직 필요하다.", + "발주": "WL-813b (PD #813) · 트랙 S", + "무엇": "WL_Nature 안에 보스 1종(추천 10006 Anubis)을 세우는 아레나 데이터 초안 — 좌표 · 반경 · 진입 방식 · 필요한 MonsterAppear 행", + "상태": "초안 · 게임이 읽지 않는다. 테이블 로더는 프리팹의 TextAsset 참조만 읽는다(table_missionbase.cs:5,12).", + "SOT": "butler.xlsm (PD 소유) → **xlsm 동기화 필요** (아래 monsterappear_rows 를 MonsterAppear 시트에 넣고 재익스포트).", + "게임 원본 무변경": "MonsterList.json · MonsterAppear.json · BattleMapConfig.json · WL_Nature.prefab 전부 무변경.", + "코드 변경": "이 초안은 **코드 0** 으로 성립한다. 기존 Assets/Script/Map/FieldBossData.cs 를 그대로 쓴다." + }, + + "보스_선정": { + "1순위": { "id": 10006, "이름": "Anubis", "프리팹": "FieldBoss/Anubis", "scale": 1.5, + "근거": "박진감 9/9(회전 차징 → 모델 은신 후 5초 추격 폭풍 → 3방향 점프 6연사) · 12종 중 가장 작은 축이라 세로 1080×1920 에 적합 · 인디케이터를 안 써서 누락 위험 0 · 이펙트 5/투사체 3/애니 상태 4 전부 존재" }, + "2순위": { "id": 10005, "이름": "Golem_Child", "프리팹": "FieldBoss/Golem_Child", "scale": 2, + "근거": "패턴 다양성 최고(2단 낙석 + 워프 후 5연속 장판 + 패시브 순간이동). 아레나 2호로." }, + "제외": { "id": 10003, "이름": "Mega_Ice_Golem", "이유": "돌진 2종으로 액션감은 최고지만 f_DefaultScale = 8 이라 세로 화면을 덮는다. 카메라 작업 이후로." } + }, + + "등장_방식": { + "채택": "FieldBossData 컴포넌트 (Assets/Script/Map/FieldBossData.cs · 코드 변경 0)", + "동작": "Start() 에서 MonsterList 행을 읽어 Addressables 로 프리팹 1기를 자기 transform.position 에 스폰하고, 사망 시 RegenTime 초 뒤 On_Regen 한다.", + "선례": "Map_C01 / Map_C02 에 각각 FieldBossData 9개가 이미 들어 있다(10001~10009 · 전부 n_SpawnerId=1 · RegenTime=10 · 마지막 10009 만 isChapterBoss=1). WL_Nature 에는 0개다.", + "필수_주의": "WL_Nature 의 MapData.m_ChapterPortal 이 {fileID: 0}(null) 이다 → **isChapterBoss 는 반드시 0**. 1로 두면 AfterDie 에서 MapData.Ins.m_ChapterPortal 접근으로 NullReference 가 난다(FieldBossData.cs:47-49).", + "Phase_모드_활용_여부": { + "결론": "쓸 수 없다(데이터만으로는).", + "근거": "MobControlMgr 의 PhaseAllKill 은 n_MaxMonsterCount 를 **모든 페이즈에 똑같이** 적용한다(MobControlMgr.cs:123-128). s_AppearMonster 를 '잡몹|잡몹|10006' 으로 써도 마지막 페이즈에서 보스가 n_MaxMonsterCount 마리 생성된다. '잡몹 8마리 → 보스 1마리' 는 페이즈별 마릿수 지원이 필요하다 → 아래 코드_변경_필요 1번." + } + }, + + "아레나_배치": { + "맵": "WL_Nature (BattleMapConfig n_MapID = -1)", + "플레이어_시작": [115.0, 0.0, 10.0], + "후보A_권장": { + "좌표": [137.0, 0.6, -22.0], + "근거": "SP1 원점(115,-22)과 SP4 원점(159,-22)의 정중앙 — 두 군집(반경 10) 어느 쪽과도 12m 여유. 시작 지점에서 38.8m 로 '존1(32m)을 지나면 바로 보스' 동선이 된다. y 는 두 스포너 원점 높이(0.4 / 0.9)의 중간값.", + "미확인": "이 좌표의 NavMesh 도달성은 **미검증**. WL_Nature_NavMesh.asset 은 존재하나 정적 파일로는 판정 못 한다 — #764 의 AgentScripts/WL764_Probe.cs 방식(에디트 모드 NavMesh.AddNavMeshData + CalculatePath)으로 1회 확인해야 한다." + }, + "후보B_대안": { + "좌표": [159.0, 0.9, -22.0], + "근거": "SP4 스포너 원점 그대로. 이미 배치된 지점이라 NavMesh 도달성이 사실상 보장된다(MapData.spawnRequireReachableFromPlayer = 1 로 몹이 실제로 여기서 스폰·이동 중). 대신 존4(웨이브 관문)와 자리가 겹치므로 SP4 를 보스 전용으로 비우거나, 존4 를 다른 스포너로 옮겨야 한다.", + "시작에서_거리": 54.4 + }, + "아레나_반경_권장": 20.0, + "반경_근거": "보스 추적 사거리(f_BaseChaseRange)를 15~20 으로 낮춘 값 + 여유. Anubis 는 현재 50 이라 아레나 밖 존1/존4 까지 따라온다 → xlsm 에서 낮추는 편이 좋다. 스킬2 의 점프 거리 5m × 3방향, 스킬1 폭풍 추격 5s 를 담으려면 최소 반경 15m 는 필요하다.", + "군집_간섭_검사": "후보A 는 SP1·SP4 군집(반경 10)에서 각각 12m 떨어져 있어 잡몹이 아레나 안으로 들어오지 않는다(스포너는 원점 반경 MobGenRange 안에서만 스폰하고, PatrolRange 6~10 으로 배회한다)." + }, + + "FieldBossData_설정값": { + "_설명": "WL_Nature.prefab 에 빈 GameObject 를 만들고 이 컴포넌트를 붙일 때 넣을 값. (프리팹 편집은 후속 태스크)", + "GameObject": "WL_Nature/FieldBoss/1", + "position": [137.0, 0.6, -22.0], + "n_MonsterID": 10006, + "n_SpawnerId": 813010, + "RegenTime": 60, + "isChapterBoss": false + }, + + "monsterappear_rows": [ + { + "n_SpawnerID": "813010", + "n_AppearMonsterLv": "6", + "n_MonsterATK": "18", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "1200", + "s_AppearMonster": "10006", + "e_SpawnerType": "Random", + "f_SpawnerDelay": "60", + "n_MaxMonsterCount": "1", + "f_PatrolRange": "4", + "n_DropExp": "500", + "n_DropGold": "100", + "n_DropReward": "9700001", + "_주석": "보스 아레나용 스탯 행. FieldBossData 는 스폰을 직접 하므로 e_SpawnerType/n_MaxMonsterCount 는 실제로 안 쓰이지만, MyValue.cs:370-371 이 이 행의 n_MonsterATK/n_MonsterHP 로 보스 스탯을 만든다.", + "_계산": "Anubis 는 f_BaseHPRate=20, f_BaseATKRate=30 → 실 HP = 20 × 1200 = 24,000 · 실 ATK = 30 × 18 = 540. 존1 잡몹(300)의 80배 = 3s 리듬 기준 약 240초. 목표 보스전 60~90초로 맞추려면 n_MonsterHP 를 300~450 으로 낮춰야 한다(= 실 HP 6,000~9,000).", + "_미확인": "절대값은 플레이어 실 DPS 를 재야 정해진다. 규칙: n_MonsterHP = (목표 보스전 초 × 플레이어 DPS) / f_BaseHPRate. 참고로 Map_C01/C02 의 보스 9종은 전부 n_SpawnerId=1(Lv1 · ATK 10 · HP 300)을 쓴다 → Anubis 실 HP 6,000 · ATK 300. 이 값이 '기존 힘민지 기준'이다." + } + ], + + "코드_변경_필요": [ + { + "번호": 1, + "제목": "스포너 페이즈별 마릿수", + "무엇": "MobControlMgr 의 Phase/PhaseAllKill 이 페이즈마다 다른 마릿수를 쓰게 한다(예: '8|8|1'). 지금은 n_MaxMonsterCount 하나를 모든 페이즈에 적용한다(MobControlMgr.cs:123-128, 213-224).", + "왜": "'잡몹 웨이브 → 보스 1기' 를 스포너 하나로 만들려면 필요. 없으면 보스가 8마리 나온다.", + "트랙": "G(Gameplay) 또는 S" + }, + { + "번호": 2, + "제목": "보스 진입 트리거", + "무엇": "FieldBossData 는 Start() 에서 무조건 스폰한다. '아레나에 들어가면 그때 등장' 을 만들려면 트리거 콜라이더 + 스폰 지연 + (선택) 카메라 컷인 + 입구 봉쇄가 필요하다.", + "왜": "기획안 v2 슬라이드 21 '보스 개입 창' · 맵 로드 즉시 보스가 서 있으면 존 리듬이 깨진다.", + "트랙": "G" + }, + { + "번호": 3, + "제목": "보스 추적 사거리 아레나 클램프", + "무엇": "f_BaseChaseRange 를 테이블로 낮추거나(데이터), 아레나 반경 밖으로 나가면 원위치로 귀환시키는 코드(리쉬).", + "왜": "Anubis·Golem_Child 는 chase 50 이라 보스가 잡몹 존까지 끌려 나온다. 테이블만으로도 절반은 해결된다.", + "트랙": "데이터(xlsm) 우선 · 리쉬는 G" + }, + { + "번호": 4, + "제목": "보스 처치 보상", + "무엇": "보스 12종 전부 MonsterList.n_DropItem = 0. 드랍 테이블 연결이 필요하다.", + "왜": "PD #813 '쉽고 빠른 성장' — 보스를 잡을 이유가 데이터에 없다.", + "트랙": "데이터(xlsm)" + }, + { + "번호": 5, + "제목": "보스 HP 바 · 등장 연출 UI", + "무엇": "세로 화면 상단 보스 HP 게이지 · 이름 · 페이즈 표시.", + "왜": "현재 몹 공통 HUD 만 있다(InGameInfo 의 MobMark/HUDUI). 「미확인」 — 보스 전용 UI 존재 여부는 UI 트랙 소관이라 확인하지 않았다.", + "트랙": "U(UI)" + }, + { + "번호": 6, + "제목": "Angez_B 테이블 보정", + "무엇": "10009 Angez_B 의 f_BaseHPRate = 1 (다른 보스 9~20). 보스로 쓰려면 올려야 한다.", + "왜": "실측. 지금은 잡몹보다 약하다.", + "트랙": "데이터(xlsm)" + } + ] +} diff --git a/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json.meta b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json.meta new file mode 100644 index 000000000..dec1601b4 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9d4686c83015e4e4b8ebea4f0055fb49 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json b/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json new file mode 100644 index 000000000..618d68e05 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json @@ -0,0 +1,131 @@ +{ + "_meta": { + "적용": "WL-813e 에서 rows 5행을 MonsterAppear.json 에 그대로 추가하고 WL_Nature.prefab 의 maplv/1~5 SpawnerID 를 813001~813005 로 재바인딩했다 → 이 초안은 이제 이력 문서다. 적용 기록·롤백·다음 조정 지점 = Assets/WL/Settings/WL813e_ZoneApply.md. xlsm 동기화는 아직 필요하다.", + "발주": "WL-813b (PD #813) · 트랙 S", + "무엇": "WL_Nature 군중 전투 '존 리듬' 프리셋 — MonsterAppear 추가 행 초안", + "상태": "초안 · 게임이 읽지 않는다. 테이블 로더는 폴더를 훑지 않고 프리팹의 TextAsset 참조만 읽는다(Assets/Script/Table/Mission/table_missionbase.cs:5,12) — 이 파일은 어떤 table_* 도 참조하지 않는다.", + "SOT": "butler.xlsm (PD 소유). 아래 rows 를 실제로 쓰려면 **xlsm 의 MonsterAppear 시트에 먼저 넣고 재익스포트**해야 한다 → xlsm 동기화 필요.", + "게임 원본 무변경": "Assets/ResWork/Table/Export/MonsterAppear.json 은 손대지 않았다.", + "설계 근거": [ + "동시 생존 수 = n_MaxMonsterCount (MobControlMgr.cs:52-53) · 풀 = ×2 (Random/Order, :102/:108)", + "리젠 = 사망 후 f_SpawnerDelay 초 (MobControlMgr.cs:170-225)", + "몹 최종 스탯 = MonsterList.f_Base*Rate × MonsterAppear.n_Monster* (MyValue.cs:370-371)", + "스폰 반경은 테이블이 아니라 맵 프리팹의 MobControlMgr.MobGenRange (WL_Nature 전부 10)", + "기존 SpawnerID 1~19 는 13개 맵이 공유하므로 절대 덮어쓰지 않고 새 ID(813001~)를 쓴다" + ], + "핵심 미확인": "3초 처치 리듬의 절대 HP 값은 **플레이어 실 DPS 를 재야** 정해진다(Play 필요 · 이번 태스크 범위 밖). 아래 n_MonsterHP 는 '기준서 확정 전 제안값'이며, 규칙은 n_MonsterHP = (목표 TTK 3s × 플레이어 DPS) / f_BaseHPRate 다. 현재 유일하게 튜닝된 행 SP1(Lv1 · HP 300 · f_BaseHPRate 1 → 실 HP 300)을 1존 기준점으로 잡고 존별 배수만 설계했다." + }, + + "존_리듬_설계": { + "목표": "PD #813 — 쉽고 빠른 성장 · 화려한 전투. 시작 지점(115,0,10)에서 바깥으로 걸어 나갈수록 밀도와 체력이 계단식으로 오르는 5개 존.", + "밀집 정의": "군집 반지름 10m(MobGenRange) 안에 8~15마리 → 1마리당 약 21~39 m². 세로 화면 한 화면에 한 존만 들어온다(스포너 최근접 44.0m 실측).", + "리듬": "처치 → f_SpawnerDelay 초 뒤 1마리 리젠. 목표 처치 간격 3s 보다 리젠을 짧게 두어야 밀도가 유지된다 → 1.0~2.0s.", + "몹 선정 규칙": "세로 화면 밀집이므로 f_DefaultScale 2.0 이하만 쓴다. Platopo(scale 5 · SP17)·Golem Elite(2.5 · SP3)는 밀집 존에서 제외하고 '한 마리짜리 강적' 존에만 쓴다." + }, + + "rows": [ + { + "n_SpawnerID": "813001", + "n_AppearMonsterLv": "1", + "n_MonsterATK": "10", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "300", + "s_AppearMonster": "101|102|103|104", + "e_SpawnerType": "Random", + "f_SpawnerDelay": "1.5", + "n_MaxMonsterCount": "12", + "f_PatrolRange": "8", + "n_DropExp": "10", + "n_DropGold": "1", + "n_DropReward": "9700001", + "_주석": "존1 '첫 교전' · SP1 자리(115,0.4,-22 · 시작에서 32m). Batty 4종(scale 1.5). 기존 SP1(15마리/4.0s/DEF -10)에서 밀도 12·리젠 1.5s·DEF 0 으로 조정 — 리젠이 4s면 3s 리듬에서 밀도가 계속 깎인다." + }, + { + "n_SpawnerID": "813002", + "n_AppearMonsterLv": "2", + "n_MonsterATK": "12", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "450", + "s_AppearMonster": "168|169|170|171|172", + "e_SpawnerType": "Random", + "f_SpawnerDelay": "1.2", + "n_MaxMonsterCount": "15", + "f_PatrolRange": "8", + "n_DropExp": "15", + "n_DropGold": "2", + "n_DropReward": "9700001", + "_주석": "존2 '밀집 최대' · SP2 자리(147,2.6,30 · 시작에서 37.7m). Porin 5종(scale 1.3 · 작아서 15마리도 화면에 들어온다). 기존 SP2 는 Order + HP 99999(테스트 값)라 벽이 된다 → Random 15마리로 교체." + }, + { + "n_SpawnerID": "813003", + "n_AppearMonsterLv": "3", + "n_MonsterATK": "14", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "600", + "s_AppearMonster": "173|174|175|146|147|151", + "e_SpawnerType": "Random", + "f_SpawnerDelay": "1.2", + "n_MaxMonsterCount": "14", + "f_PatrolRange": "8", + "n_DropExp": "20", + "n_DropGold": "3", + "n_DropReward": "9700001", + "_주석": "존3 '혼합' · SP3 자리(71,-0.3,18 · 44.7m). Racco 3종(1.1) + Rabby 3종(1.5). 기존 SP3 은 Elite Golem 2종 + ATK 99999 라 초반 동선에 두면 즉사 구간이 된다 → 일반 몹으로 교체하고 Elite 는 존5로 뺀다." + }, + { + "n_SpawnerID": "813004", + "n_AppearMonsterLv": "4", + "n_MonsterATK": "16", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "800", + "s_AppearMonster": "118|119|120", + "e_SpawnerType": "PhaseAllKill", + "f_SpawnerDelay": "2.0", + "n_MaxMonsterCount": "8", + "f_PatrolRange": "6", + "n_DropExp": "30", + "n_DropGold": "4", + "n_DropReward": "9700001", + "_주석": "존4 '웨이브 관문' · SP4 자리(159,0.9,-22 · 54.4m). Devilu 3종 × 8마리 = 3웨이브. PhaseAllKill 은 그 종류를 전멸시켜야 다음 웨이브가 2.0s 뒤 일괄 생성된다(MobControlMgr.cs:213-224) → 8마리×3s = 웨이브당 약 24s, 총 72s. 보스 아레나 바로 앞 관문으로 쓴다." + }, + { + "n_SpawnerID": "813005", + "n_AppearMonsterLv": "5", + "n_MonsterATK": "20", + "n_MonsterDEF": "0", + "n_MonsterMDEF": "0", + "n_MonsterHP": "1500", + "s_AppearMonster": "1014|1015|1016", + "e_SpawnerType": "Order", + "f_SpawnerDelay": "3.0", + "n_MaxMonsterCount": "5", + "f_PatrolRange": "6", + "n_DropExp": "60", + "n_DropGold": "8", + "n_DropReward": "9700001", + "_주석": "존5 '엘리트' · SP5 자리(103,0.2,74 · 65.1m). Golem_A/B/C(Elite · scale 2.5)를 5마리만. 밀집이 아니라 '한 마리씩 무겁게' 담당 — 존2/3 의 15마리 난타와 리듬을 대비시킨다." + } + ], + + "스포너_바인딩_필요": { + "왜": "MonsterAppear 에는 맵 컬럼이 없다. 맵 프리팹의 MobControlMgr.SpawnerID 가 행을 가리킨다.", + "무엇": "Assets/Res_Addr/Map/WL_Nature.prefab 의 아래 GameObject 5개의 SpawnerID 를 새 값으로 바꿔야 이 행들이 적용된다.", + "표": [ + { "GameObject": "WL_Nature/maplv/1", "현재": 1, "→": 813001, "좌표": [115.0, 0.4, -22.0] }, + { "GameObject": "WL_Nature/maplv/2", "현재": 2, "→": 813002, "좌표": [147.0, 2.6, 30.0] }, + { "GameObject": "WL_Nature/maplv/3", "현재": 3, "→": 813003, "좌표": [71.0, -0.3, 18.0] }, + { "GameObject": "WL_Nature/maplv/4", "현재": 4, "→": 813004, "좌표": [159.0, 0.9, -22.0] }, + { "GameObject": "WL_Nature/maplv/5", "현재": 5, "→": 813005, "좌표": [103.0, 0.2, 74.0] } + ], + "주의": "**이번 태스크에서는 프리팹을 건드리지 않았다.** 프리팹 수정은 Gameplay 트랙 후속. 기존 ID 1~5 를 그대로 두고 행만 바꾸면 WL_Nature02~10 · WL_Start · Map_C01 까지 13개 맵이 함께 바뀐다." + }, + + "BattleMapConfig": { + "변경": "없음", + "이유": "WL_Nature 행(n_MapID -1 · n_Chapter -1 · s_Scene WL_Nature)은 이미 존재하고, 존 리듬은 BattleMapConfig 컬럼(씬·BGM·포탈 수·티켓)과 무관하다. 보스 아레나에도 새 맵 행이 필요 없다(같은 맵 안이므로)." + } +} diff --git a/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json.meta b/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json.meta new file mode 100644 index 000000000..cfdc8c0e5 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cf5c35821912d4749af3b23eb0d96e7f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Res_Addr/Map/WL_Nature.prefab b/Assets/Res_Addr/Map/WL_Nature.prefab index 7fa28ab83..64b9c5298 100644 --- a/Assets/Res_Addr/Map/WL_Nature.prefab +++ b/Assets/Res_Addr/Map/WL_Nature.prefab @@ -45,7 +45,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 06269e6c1653db34090895cd16244df9, type: 3} m_Name: m_EditorClassIdentifier: - SpawnerID: 1 + SpawnerID: 813001 MobGenRange: 10 text_lv: {fileID: 875760665853817538} --- !u!1 &28255864051587328 @@ -515,7 +515,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 06269e6c1653db34090895cd16244df9, type: 3} m_Name: m_EditorClassIdentifier: - SpawnerID: 2 + SpawnerID: 813002 MobGenRange: 10 text_lv: {fileID: 1148498265225467706} --- !u!1 &1273559770134254925 @@ -2649,6 +2649,7 @@ Transform: - {fileID: 6072744329692217702} - {fileID: 5625234380910883685} - {fileID: 8168532711777316576} + - {fileID: 8130050000000000002} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &5695711385560286233 @@ -2794,7 +2795,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 06269e6c1653db34090895cd16244df9, type: 3} m_Name: m_EditorClassIdentifier: - SpawnerID: 5 + SpawnerID: 813005 MobGenRange: 10 text_lv: {fileID: 7565486426041407482} --- !u!1 &4255569283510092827 @@ -3103,7 +3104,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 06269e6c1653db34090895cd16244df9, type: 3} m_Name: m_EditorClassIdentifier: - SpawnerID: 4 + SpawnerID: 813004 MobGenRange: 10 text_lv: {fileID: 7694301954248919502} --- !u!1 &5292814964321027778 @@ -3975,7 +3976,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 06269e6c1653db34090895cd16244df9, type: 3} m_Name: m_EditorClassIdentifier: - SpawnerID: 3 + SpawnerID: 813003 MobGenRange: 10 text_lv: {fileID: 1399851639559242293} --- !u!1 &6825731264015073366 @@ -68605,3 +68606,83 @@ Transform: type: 3} m_PrefabInstance: {fileID: 9221781064863107696} m_PrefabAsset: {fileID: 0} +--- !u!1 &8130050000000000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8130050000000000002} + m_Layer: 3 + m_Name: FieldBoss + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8130050000000000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8130050000000000001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8130050000000000004} + m_Father: {fileID: 4502284306717827522} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &8130050000000000003 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8130050000000000004} + - component: {fileID: 8130050000000000005} + m_Layer: 3 + m_Name: 1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8130050000000000004 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8130050000000000003} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 139, y: 0.8, z: -20} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8130050000000000002} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8130050000000000005 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8130050000000000003} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 935c913087b349141aa632be4f8c2689, type: 3} + m_Name: + m_EditorClassIdentifier: + isChapterBoss: 0 + n_MonsterID: 10006 + n_SpawnerId: 813010 + RegenTime: 60 diff --git a/Assets/WL/Settings/WL813b_Boss12_Measured.md b/Assets/WL/Settings/WL813b_Boss12_Measured.md new file mode 100644 index 000000000..fe8a8a856 --- /dev/null +++ b/Assets/WL/Settings/WL813b_Boss12_Measured.md @@ -0,0 +1,201 @@ +# WL-813b — 힘민지 보스 12종 · WL_Nature 군중 전투 실측 (코드 0 · 읽기 전용 조사) + +> 발주 WL-813b(PD #813) · worktree `E:\NerdNavis\WL_wt\systems` · base `main@abcd03865` +> **이 문서는 초안·실측 기록이다. 게임이 읽는 테이블·프리팹·씬·코드는 한 줄도 바꾸지 않았다.** +> 모든 수치는 이 worktree 의 파일에서 직접 읽은 값이다. 읽지 못한 것은 「미확인」으로 적었다. + +## 0. 실측 방법 · 이 문서를 읽을 때 알아야 할 규칙 3가지 + +| 규칙 | 근거 (파일:줄) | +|---|---| +| 보스 프리팹 Addressables 키 = `Assets/Res_Addr/Mobs/{s_MonsterPrefab}.prefab` — 테이블의 `FieldBoss/Anubis` 에 코드가 접두/확장자를 붙인다 | `Assets/Script/Table/IngameStage/table_monsterlist.cs:191` | +| 런타임 크기 = **테이블 `f_DefaultScale` 가 프리팹 루트 스케일을 덮어쓴다** | `Assets/Script/Character/Mob/MobActor.cs:87` (`transform.localScale = f_DefaultScale * Vector3.one`) | +| 몹 최종 스탯 = `MonsterList.f_Base*Rate` × `MonsterAppear.n_Monster*` — 보스 HP/ATK 는 **스포너 행이 있어야 정해진다** | `Assets/Script/Util/MyValue.cs:370-371` | + +부속 경로 규칙(전부 코드에서 확인): 투사체 `Assets/Res_Addr/Projectile/{name}.prefab`(`ProjectileInfo.cs:48`) · 인디케이터 `Assets/Res_Addr/Obj/{name}.prefab`(`IndicatorInfo.cs:40`) · 이펙트 `Assets/Res_Addr/Effect/{name}.prefab`(`InGameInfo.cs:345`). + +--- + +## 1. A — 보스 12종 실측 표 + +`MonsterList.json` 의 `e_MonsterType == "Boss"` 행은 **정확히 12행(10001~10012)** 이고, 12개 모두 +`Assets/Res_Addr/Mobs/FieldBoss/` 에 프리팹이 있으며 해당 `Boss_*.cs` 가 붙어 있다. + +### 1-1. 테이블 · 프리팹 (실측) + +| ID | 프리팹 (Addressables 키 접미) | 스크립트 | scale | HP배율 | ATK배율 | 사거리/추적 | 애니메이터 | 패시브 | +|---|---|---|---|---|---|---|---|---| +| 10001 | FieldBoss/Lusif | Boss_LusifActor | 3 | ×10 | ×2 | 5 / 16 | Lusif_Animations_Check | 710031 DMG_TO_HP(흡혈 0.5%) | +| 10002 | FieldBoss/Lusif_B | Boss_LusifB_Actor | 3 | ×10 | ×50 | 5 / 16 | Lusif_Animations_B | 710032 PerHPBuff_AASPD | +| 10003 | FieldBoss/Mega_Ice_Golem | Boss_IceGolem | **8** | ×10 | ×50 | 10 / 16 | Ice_Golem_Animations_Check | 710033 DMG_DECREASE(피해 15%↓) | +| 10004 | FieldBoss/Minotos | Boss_Minotos | 3 | ×15 | ×25 | 3.5 / 16 | Mintos_Animations_Check | 710034 Revive30(부활 HP30%) | +| 10005 | FieldBoss/Golem_Child | Boss_GolemChild | 2 | ×20 | ×30 | 3.5 / **50** | Golem_Child_Animations_Check | 710035 StartleBehind(뒤로 순간이동+스턴) | +| 10006 | FieldBoss/Anubis | Boss_Anubis | **1.5** | ×20 | ×30 | 3.5 / **50** | Anubis_Animations_Check | 710036 PerHPBuff_AMSPD | +| 10007 | FieldBoss/Queen_Undead | Boss_QueenUndead | 1.5 | ×20 | ×30 | 2.5 / 20 | Queen_Undead_Animations_Check | 710037 DeadSkin(스킬피해 50%↓) | +| 10008 | FieldBoss/Flower Dryad | Boss_FlowerDryad | 1 | ×16 | ×16 | 6 / 15 | Flower Dryad_Animations_Check | 710038 RageOfFairy | +| 10009 | FieldBoss/Angez_B | Boss_Angez_B | 3 | **×1** ⚠ | ×5 | 6 / 15 | Angez_Boss | 710039 SummonMob(ID253 ×3) | +| 10010 | FieldBoss/Angez_C | Boss_Angez_C | 3 | ×12 | ×7 | 6 / 15 | Angez_Boss | 710040 HellfireFlames(상시 화염 장판) | +| 10011 | FieldBoss/Brother_Penguin_C | Boss_BrotherPenguin | 3 | ×13 | ×11 | 4 / 15 | Brother_Penguin_Boss | 710041 SummonMob(ID290 ×3) | +| 10012 | FieldBoss/Shaga_B | Boss_Shaga | 5 | ×9 | ×11 | 6 / 15 | Forest_Shaga_Boss | 710042 AcidicLiquid | + +⚠ **10009 Angez_B 만 `f_BaseHPRate = 1`** (나머지 9~20). 테이블 미조정으로 보인다 — 보스로 쓰려면 xlsm 수정 필요. +공통: **12종 전부 `n_DropItem = 0`** (처치 보상 없음) · `f_BaseATKSpeed = 1` · `f_BaseMOVSpeed = 2` · `f_BaseDEFRate = 1`. + +### 1-2. 패턴 요약 (6훅 `Set / Init / Update / Get_Skill#Dmg / Get_Skill#Pos / Set_Skill#Projectile` 에서 읽은 실제 행동) + +공통 골격 = `BossMobActor.cs` — 스킬1 은 **HP 90% 이하**, 스킬2 는 **HP 70% 이하**에서 해금되고(`Get_Damage`), 둘 다 준비되면 랜덤 선택. 스킬 종료 시 반대편 스킬 쿨을 최소 0.5s 로 강제해 연속 발동을 막는다. + +| ID | 스킬1 | 스킬2 | 이동성 | 예고(인디케이터/차징) | +|---|---|---|---|---| +| 10001 Lusif | 전방 2.5m 인디케이터 1s → 관통 장판 (쿨 8s, 배율 3.5) | 거리 10m 초과 시만 · 0.8s 간격 **5연사** (쿨 1.5s, 1.5) | 없음 | 인디케이터 1 | +| 10002 Lusif_B | 4s 차징 + 인디케이터 → 광역 (쿨 12s, 3.5) | 4s 차징 → 인디케이터 2-1 → 1s → 2-2 **2단 광역** (쿨 20s, 5.0) | 없음 | 인디케이터 3 + 차징 4s | +| 10003 IceGolem | 타깃 2m 앞까지 **돌진(속도 5)** → 점프 → 착지 광역 + **스턴 2s** (쿨 8s, 3.5) | 3s 차징(인디케이터가 타깃 추적 회전) → **전방 18m 직선 돌진** + 넉백/스턴 (쿨 10s, 3.5) | **최상** (돌진 2종) | 인디케이터 2 + 차징 3s | +| 10004 Minotos | 1s 차징 → 근접 광역 (쿨 15s, 1.2) | 1s → **자버프 이속+50% 공속+50% 15s** (쿨 **10s** ※프리팹값 · 코드 기본은 25s) | 중 (자버프) | 없음(차징 이펙트만) | +| 10005 GolemChild | 타깃 위치 인디케이터 2s → 바위 낙하 → 2s 뒤 **후속 폭발**(2단) (쿨 8s, 3.8/1.2) | 1s 차징 장판 → **뒤 15m 순간이동** → 전방 3m 간격 **5연속 장판**(0.5s) (쿨 8s, 3.0/2.0) | **최상** (워프 2종 + 패시브 순간이동) | 인디케이터 4 | +| 10006 Anubis | 3s 회전 차징(가속) → **모델 은신 후 칼날 폭풍으로 타깃 추적 이동 5s** (쿨 18s, 2.2) | 3s 차징 → **뒤·우·앞 5m 점프 3회 × 2연사 = 6발** (쿨 15s, **3.0** ※프리팹값 · 코드 2.8, 투사체 속도 15) | **최상** (은신 추격 + 점프 3회) | 이펙트 5 + 차징 3s | +| 10007 QueenUndead | 거리 15m 이상일 때만 · **침묵/마나번**(SilenceCurse 5s, 마나 2%/s) (쿨 6s) | 2s 차징 + 인디케이터 → 광역 → 피격 시 **타임스케일 연출 + 플레이어 7.5m 밀어냄** (쿨 18s, 3.2) | 낮음 | 인디케이터 1 + 차징 2s | +| 10008 FlowerDryad | 3s 차징 → 주변 3m 아군 몹 회복. **프리팹에서 `Skill1Heal = 0`** 이라 즉시 회복은 꺼져 있고 6s 도트힐(1s당 최대HP 1%)만 돈다 (쿨 20s) — `s_Porjectile2 = None` | 3s 차징 + 인디케이터 → **독 장판(5s) + 에어본 1.5s** (쿨 18s, 5.2) | 없음 | 인디케이터 1 + 차징 3s | +| 10009 Angez_B | 3s 차징 → **플레이어 추적 낙뢰 10회**(0.5s 예고 → 0.5s 발동, 총 10s) + 스턴 0.3s (쿨 20s, 2.8) | 3s 차징 + 방향 인디케이터 → 전방 광역 + 스턴 0.2s (쿨 15s, 1.2) | 없음 | 인디케이터 2 + 차징 3s | +| 10010 Angez_C | 3s 차징 → **추적 메테오 8회** + 화상 (쿨 12s, 3.2) | 3s 차징 + 방향 인디케이터 → 전방 화염 광역 (쿨 18s, 1.5) | 없음 | 인디케이터 2 + 차징 3s + **패시브가 1s마다 랜덤 위치에 화염 장판** | +| 10011 BrotherPenguin | 3s 차징 + 인디케이터 → **광역 냉기 + 넉백**(공속/이속 30%↓ 3s) (쿨 12s, 3.2) | 3s 차징 → **보호막 최대HP 25% · 12s**, 보호막 도는 동안 쿨 정지 = 딜체크 구간 (쿨 20s, 5.0) | 없음 | 인디케이터 2 + 차징 3s | +| 10012 Shaga | **곡선 투사체**가 근처 랜덤 낙하 → 지면 냉기 장판 6s (쿨 18s, 1.6) | 3s 차징 → **무적 소환물 2기**(ID176 Shaga_A · 스탯 50% 계승 · 12s) (쿨 20s) — `s_Porjectile3 = None` | 낮음 | 인디케이터 1 + 차징 3s | + +### 1-3. WL 로드 가능 여부 (실측 · 파일시스템 정적 검사) + +전 항목 **12/12 통과 · 누락 0**: + +| 검사 | 결과 | +|---|---| +| 프리팹 존재 (`Assets/Res_Addr/Mobs/FieldBoss/*.prefab`) | 12/12 존재 | +| `Boss_*.cs` 컴포넌트 부착 | 12/12 부착 (Shaga 는 `NoSeeLongDMob` 도 함께) | +| **missing script (해결 불가 GUID)** | **0개** — 걷어낸 SDK 참조 없음 | +| Animator + Controller | 12/12 연결 | +| **스크립트가 부르는 애니 상태 ↔ 컨트롤러 상태** | **누락 0** (`AnimationPlay` 인자 전수 대조 · `Actor.cs:2027` 은 `Animator.Play(이름)`) | +| 머티리얼 셰이더 | 전부 `MK/Toon/URP/Standard/Simple` (`Assets/_MK/MKToon/Shader/URP/Standard/StandardSimple.shader`) — **URP 전용 패스 보유**(`RenderPipeline=UniversalPipeline` · `UniversalForward`) → URP 17.3 에서 정상. 셰이더 없음/에러 머티리얼 0 | +| 투사체 `s_Porjectile1~3` + 코드 직접 호출분 | 전부 존재 | +| 인디케이터 (`Load_Indicator` 인자) | 전부 존재 | +| 이펙트 (`Show_Effect` 인자) | 전부 존재 | +| 소환 대상 몹 (176 Shaga_A · 253 Teruru_Ac · 290 Ice_Cube_B) | 전부 MonsterList 에 존재 | +| Addressables 등록 | 그룹 `Mobs` 의 폴더 엔트리 `Assets/Res_Addr/Mobs` 하위 — 12종 전부 포함 | + +추가 실측 — **스크립트가 null 검사 없이 쓰는 오브젝트 참조가 전부 할당돼 있다**: `Boss_GolemChild.tf_Skill1Pos`(Set 에서 `.position` 접근) · `Boss_QueenUndead.go_attackeffect`(Set 에서 `.SetActive`) · `Boss_Minotos.gos_attackeffect`(2개). 셋 다 비어 있으면 스폰 즉시 NullReference 가 나는 자리인데, 프리팹에 fileID 가 들어 있다. + +**Addressables 주의**: `Mobs` 그룹은 폴더 1개를 통째 등록한 형태(`AssetGroups/Mobs.asset` 의 `m_SerializeEntries` 1건)라서 개별 주소가 아니라 **폴더 경로 기반 주소**로 빌드된다 — 코드가 만드는 키(`Assets/Res_Addr/Mobs/....prefab`)와 형태가 일치한다. 런타임 카탈로그로 최종 확인한 것은 아니므로 이 항목만 「정적 근거 · 런타임 미검증」. + +### 1-4. 크기 · 박진감 · 준비 비용 + +박진감 = 패턴 다양성(0~3) + 예고(0~3) + 이동성(0~3) = **/9**. 크기는 `f_DefaultScale`(런타임 확정값)과 프리팹 스킨메시 AABB×누적스케일로 낸 **높이 「추정」**. + +| ID | 보스 | scale | 추정 높이 | NavMeshAgent 반경(×scale) | 박진감 | 준비 비용 | +|---|---|---|---|---|---|---| +| 10006 | **Anubis** | 1.5 | 「미확인」(AABB 이상치 20.0m — 스킬1 폭풍용 보조 스킨메시로 추정) | 0.32 → **0.48** | **9/9** | **S** | +| 10005 | **Golem_Child** | 2 | ~2.3 m | 0.25 → 0.50 | **9/9** | **S** | +| 10003 | Mega_Ice_Golem | 8 | ~2.4 m | 0.11 → 0.88 | 8/9 | S | +| 10010 | Angez_C | 3 | ~1.2 m | 0.25 → 0.75 | 6/9 | S | +| 10007 | Queen_Undead | 1.5 | ~3.6 m | 0.46 → 0.69 | 6/9 | S (내장 파티클 28) | +| 10002 | Lusif_B | 3 | ~3.9 m | 0.35 → 1.05 | 5/9 | S | +| 10009 | Angez_B | 3 | ~1.2 m | 0.10 → 0.30 | 5/9 | **M** (HP배율 1 → 테이블 수정 필요) | +| 10011 | Brother_Penguin_C | 3 | ~3.4 m | 0.37 → 1.11 | 5/9 | S | +| 10012 | Shaga_B | 5 | ~1.9 m | 0.28 → 1.40 | 5/9 | S | +| 10001 | Lusif | 3 | ~3.9 m | 0.35 → 1.05 | 4/9 | S | +| 10008 | Flower Dryad | 1 | ~2.2 m | 0.50 → 0.50 | 4/9 | M (단독 보스전이면 스킬1 힐이 무의미 — 군중 동반 전제) | +| 10004 | Minotos | 3 | ~2.5 m | 0.23 → 0.69 | 3/9 | S (내장 파티클 16) | + +「미확인」 — **삼각형 수/드로우콜**: FBX 메시 통계는 Unity 없이 못 읽는다. `AgentScripts/WL813b_BossProbe.cs` 가 에디트 모드에서 보스별 tri/머티리얼 수를 찍도록 만들어 뒀다(실행은 다음 태스크). 렌더러 수만 실측: Brother_Penguin_C·Golem_Child 가 SkinnedMeshRenderer **5개**(나머지는 1~2개)라 드로우콜이 가장 크다. + +### 1-4b. ⚠ 보스 스킬 수치는 **테이블이 아니라 프리팹 인스펙터 값**이다 + +`Boss_*.cs` 의 `public float Skill1CoolTime = 18f;` 같은 필드는 전부 `[SerializeField]` 대상이라 **프리팹에 직렬화된 값이 코드 기본값을 덮는다**. 12종 프리팹의 직렬화 값을 코드 기본값과 전수 대조한 결과, **어긋나는 곳 4군데**를 찾았다. + +| 보스 | 필드 | 프리팹(실제 동작) | 코드 기본 | 의미 | +|---|---|---|---|---| +| 10004 Minotos | `Skill2CoolTime` | **10** | 25 | 자버프가 2.5배 자주 — 실제로는 거의 상시 버프 상태 | +| 10006 Anubis | `Skill2Dmg` | **3.0** | 2.8 | 점프 연사 배율 상향 | +| 10007 Queen_Undead | `Skill2BackDuration` | **2** | 1.5 | 플레이어를 밀어내는 시간이 더 길다 | +| 10008 Flower Dryad | `Skill1Heal` | **0** | 0.1 | **즉시 회복이 꺼져 있다** — 힐 스킬이 도트힐만 남았다 | + +또, 프리팹이 필드 추가 이전에 저장돼 **직렬화 자체가 없는 필드**들이 있다(이 경우 코드 기본값이 그대로 쓰인다): `Boss_GolemChild.Skill1AfterIndicator / Skill2AfterIndicator / DmgAfterSkill2` · `Boss_FlowerDryad.Skill1ChargingTime / Skill2AirborneTime` · `Boss_BrotherPenguin.Skill1KnockBackTime / Skill1KnockBackSpeed / Skill2Dmg / Skill2ChargingTime`. 전부 코드 기본값이 유효한 자산을 가리켜 문제는 없지만, **튜닝 시 프리팹을 저장하면 값이 고정된다**는 점을 알고 만져야 한다. + +→ **보스 튜닝은 xlsm 이 아니라 프리팹 수정**이다(C45 예외 구간). 데이터 값을 테이블로 옮기는 것은 별도 과제. + +### 1-5. 첫 보스 후보 2종 (추천) + +1. **10006 Anubis — 1순위.** + 근거: ① 박진감 9/9 — 3s 회전 차징 → **모델을 지우고 칼날 폭풍이 되어 플레이어를 5초간 추격** → 3방향 점프 6연사. PD 가 말한 "다이나믹하고 박진감 넘치는" 에 그대로 맞는다. ② `f_DefaultScale = 1.5` 로 **12종 중 가장 작은 축** — 세로 1080×1920 에서 보스가 화면을 가리지 않는다. ③ 준비 비용 S — 인디케이터를 아예 안 쓰고(누락 위험 0) 이펙트 5종·투사체 3종·애니 상태 4종이 전부 존재. ④ 스킬2 가 제자리 연사가 아니라 **점프로 위치를 바꾸며** 쏴서 한 손 조작에서도 회피 동선이 생긴다. + 주의: `f_BaseChaseRange = 50` — 아레나 밖까지 따라온다. 아레나에 쓰려면 15~20 으로 낮추는 편이 좋다(xlsm). + +2. **10005 Golem_Child — 2순위.** + 근거: ① 박진감 9/9, **패턴 다양성은 12종 중 최고** — 2단 낙석(예고 → 낙하 → 후속 폭발), 순간이동 후 전방 5연속 장판, 패시브(StartleBehind)로 플레이어 뒤 순간이동+스턴까지 3층. ② scale 2 로 세로 화면에 적합. ③ 인디케이터 4종·투사체 4종 전부 존재, 애니 상태 4/4. ④ 인디케이터 기반 예고가 촘촘해 "피하는 재미"를 데이터만으로 조절할 수 있다(모든 시간값이 `public float`). + 주의: `f_BaseChaseRange = 50` 동일 · SkinnedMeshRenderer 5개(드로우콜). + +*(참고 · 3순위)* **10003 Mega_Ice_Golem** 은 돌진 2종으로 액션감이 가장 크지만 `f_DefaultScale = 8` 이라 세로 화면에서 카메라 조정 없이는 화면을 다 덮는다 — 카메라 작업이 붙는 뒤 순번으로 둔다. + +--- + +## 2. B — 군중 전투 현황 (WL_Nature) + +### 2-1. 스포너 시스템 (`Assets/Script/Mgr/MobControlMgr.cs`) + +| 항목 | 실측 | +|---|---| +| 스폰 모드 4종 | `Random`(지정 몹을 max 마리 랜덤) · `Order`(순서 반복) · `Phase`(한 종류씩, 1마리 죽으면 다음 종류 1마리) · `PhaseAllKill`(그 종류를 전멸시켜야 다음 종류 일괄 생성) — `MobControlMgr.cs:99-132` | +| 풀 크기 | Random/Order = `n_MaxMonsterCount × 2` 를 미리 생성 · Phase 계열 = `몹 종류 수 × n_MaxMonsterCount` (`:102, :108, :123-128`) | +| 초기 활성 | 어떤 모드든 `dic_actors[0]` 에서 `n_MaxMonsterCount` 마리만 켠다 (`:52-53`) | +| 리젠 | 사망 후 `f_SpawnerDelay` 초 뒤 (`:170-225`) · Random 은 죽은 것 중 **랜덤 1마리** 부활, Order 는 순번, Phase 는 다음 종류 1마리, PhaseAllKill 은 `killmobcount == n_MaxMonsterCount` 일 때 다음 종류 일괄 | +| 스폰 위치 | 스포너 원점 반경 `MobGenRange`(WL_Nature 전부 **10**) 안 NavMesh 랜덤 (`:65-75`) · `MapData.spawnRequireReachableFromPlayer = 1` 이라 WL_Nature 는 **도달 가능 표집**(#764) 사용 | +| 맵 ↔ 스포너 연결 | `MonsterAppear.json` 에 맵 컬럼이 **없다**. 맵 프리팹의 `MobControlMgr.SpawnerID` 가 행을 가리킨다 | + +### 2-2. WL_Nature (BattleMapConfig `n_MapID = -1` · `s_Scene = WL_Nature` · 프리팹 `Assets/Res_Addr/Map/WL_Nature.prefab`) + +- **스포너 19개**(`SpawnerID 1~19`, 전부 `MobGenRange = 10`) · `MapData` 1개 · **`FieldBossData` 0개 → 지금 WL_Nature 에는 보스가 한 마리도 안 나온다.** +- 플레이어 시작 지점 `Portal_Start` = **(115, 0, 10)** (부활 지점도 동일 좌표). +- NavMesh = `Assets/WL/Settings/WL_Nature_NavMesh.asset` (존재). +- 스포너 좌표(x, y, z) 와 구성: + +| SP | 좌표 | 모드 | 동시 | 리젠 | 몹 | +|---|---|---|---|---|---| +| 1 | (115, 0.4, −22) | Random | 15 | 4.0s | Batty_A~D (101~104, scale 1.5) | +| 2 | (147, 2.6, 30) | Order | 10 | 3.8s | Cute_Spider_A~C (105~107, 2.0) | +| 3 | (71, −0.3, 18) | Order | 10 | 3.6s | **Golem_A/B (1014·1015 — Elite)**, 2.5 | +| 4 | (159, 0.9, −22) | PhaseAllKill | 5 | 3.4s | Prizon_01~03 (111~113, 0.9) | +| 5 | (103, 0.2, 74) | Phase | 10 | 4.0s | Whispa_A~D (114~117, 1.25) | +| 6 | (55, 0.2, −38) | Random | 5 | 3.4s | Devilu_A~C (118~120) | +| 7 | (191, 0.3, 42) | Random | 5 | 3.4s | Magic_Mirror_A~C (121~123) | +| 8 | (199, 0.7, −2) | Random | 5 | 3.4s | Magicbook_A~E (124~128) | +| 9 | (47, 0.1, 62) | Random | 5 | 3.4s | Mimix_A~E (129~133) | +| 10 | (155, 1.3, −70) | Random | 5 | 3.4s | Beez ×3 (134~136) | +| 11 | (19, 0.2, 10) | Random | 5 | 3.4s | Devil_Tree ×4 (137~140, 0.35) | +| 12 | (195, 0.3, −50) | Random | 15 | 3.4s | Planta ×5 (141~145) | +| 13 | (143, 0.4, 110) | Random | 20 | 3.4s | Rabby ×9 (146~154) | +| 14 | (91, 0.2, −90) | Random | 5 | 3.4s | Turnipa ×3 (155~157, 0.45) | +| 15 | (67, 0.1, 110) | Random | 5 | 3.4s | Big_Boar ×3 (158~160) | +| 16 | (195, 0.1, 90) | Random | 10 | 3.4s | Forest_Butterfly ×4 (161~164) | +| 17 | (19, −0.6, −90) | Random | 10 | 3.4s | **Platopo ×3 (165~167, scale 5)** | +| 18 | (15, 0.1, 110) | Random | 15 | 3.4s | Porin ×5 (168~172) | +| 19 | (215, 1.0, −90) | Random | 15 | 3.4s | Racco ×3 (173~175) | + +- **동시 생존 몹 = 175마리** (각 스포너 `n_MaxMonsterCount` 합) · **인스턴스 생성 = 375개** (풀 포함). +- 시작 지점에서 가까운 스포너: **SP1 32.0m** → SP2 37.7m → SP3 44.7m → SP4 54.4m. + +### 2-3. 화면 내 적 수 「추정」 + +스포너 간 **최근접 거리 44.0m**(SP1↔SP4), 스폰 반경 10 → **군집 반지름 10m 원끼리 최소 24m 떨어져 절대 겹치지 않는다**. 따라서 화면에 동시에 보이는 적 = **그 자리 스포너 1개의 `n_MaxMonsterCount`, 즉 5 / 10 / 15 / 20 마리**. 시작 직후 첫 교전은 SP1 의 **Batty 15마리**다. +「미확인」 — 실제 카메라 FOV·거리(세로 화면 화각)는 `RealCamera` 값이 런타임 결정이라 정적으로 못 읽었다. 위 수치는 스포너 기하만으로 낸 상한이다. + +### 2-4. 지금 데이터의 문제 (실측 · 그대로 쓰면 리듬이 안 나온다) + +1. **SP4~SP45 의 `n_MonsterATK / n_MonsterDEF / n_MonsterHP` 가 전부 `4`** — 자리표시자 값이다. 몹 최종 HP = `f_BaseHPRate × 4` 라 거의 즉사한다. +2. **SP2 `n_MonsterHP = 99999`, SP3 `n_MonsterATK = 99999`** — 테스트 값이 남아 있다. 시작에서 37.7m 거리의 SP2 는 사실상 못 죽이는 벽이다. +3. **SP1 만 정상 튜닝**(Lv1 · ATK 10 · HP 300 · `n_DropReward 9700001`). SP1 의 `n_MonsterDEF = -10` 도 음수다. +4. **스포너 ID 1~19 는 13개 맵이 공유한다** — `WL_Nature`, `WL_Nature02~10`, `WL_Start`, `Map_C01`(1~13 중복 사용), `Map_C02`(14~24). **기존 1~19 행을 고치면 13개 맵이 전부 바뀐다.** WL_Nature 전용 리듬은 **새 SpawnerID** 로 가야 한다. +5. **보스를 참조하는 `MonsterAppear` 행이 0개** — 46행 중 보스 ID(10001~10012)를 쓰는 행이 없다. 보스가 등장하는 곳은 `Map_C01` / `Map_C02` 의 `FieldBossData` 컴포넌트뿐이고(각 9개 · 10001~10009 · 전부 `n_SpawnerId = 1` · `RegenTime = 10` · 마지막 10009 만 `isChapterBoss = 1`), **10010·10011·10012 는 어디에도 배치돼 있지 않다.** + +--- + +## 3. 다음 (초안 파일) + +- 군중 프리셋 → `Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json` +- 보스 아레나(+ 필요한 코드 변경 6건 목록) → `Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json` +- 로드 검증 프로브 → `AgentScripts/WL813b_BossProbe.cs` (`unity command run_script --file AgentScripts/WL813b_BossProbe.cs --entry WL813b_BossProbe.Edit --timeout 300`) + +두 초안 모두 **게임이 읽지 않는다**. 테이블 로더는 폴더를 훑지 않고 프리팹의 `TextAsset` 참조만 읽는다(`Assets/Script/Table/Mission/table_missionbase.cs:5,12`) — `_WL813b_*` 를 참조하는 `table_*` 는 없다. 실제 적용은 **butler.xlsm 동기화 후 재익스포트**가 선행돼야 한다. diff --git a/Assets/WL/Settings/WL813b_Boss12_Measured.md.meta b/Assets/WL/Settings/WL813b_Boss12_Measured.md.meta new file mode 100644 index 000000000..584d1dfbc --- /dev/null +++ b/Assets/WL/Settings/WL813b_Boss12_Measured.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d41a8168846fb9e419083d09fdb58b36 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Settings/WL813e_ZoneApply.md b/Assets/WL/Settings/WL813e_ZoneApply.md new file mode 100644 index 000000000..39567e319 --- /dev/null +++ b/Assets/WL/Settings/WL813e_ZoneApply.md @@ -0,0 +1,109 @@ +# WL-813e — 군중 존 5개 + 보스 아레나(Anubis) 데이터 적용 기록 + +> 발주 WL-813e (PD #813 · 트랙 S) · 브랜치 `wl/systems/WL-813e-zones-boss` (base `wl/systems/WL-813b-boss-content` @ `a4a053c44`) +> 규격 = 813b 완료보고 §6 + 초안 2건(`_WL813b_MonsterAppear_Draft.json` · `_WL813b_BossArena_Draft.json`) + 핵앤슬래시 기준서 v1 §B 요소 1·6 · §D-1 813e 행 +> **코드 변경 0** — `Assets/Script/**` 무수정. 보스 프리팹(`Res_Addr/Mobs/FieldBoss/Anubis.prefab`) 인스펙터 값 무수정(813h 가 테이블화). + +## 🔴 SOT — xlsm 동기화 필요 + +데이터 SOT 는 **butler.xlsm (PD 소유)** 다. 아래 6행은 `MonsterAppear.json` 에 직접 추가했으므로, +**xlsm 의 MonsterAppear 시트에 같은 6행을 넣고 재익스포트하기 전까지는 다음 익스포트에서 사라진다.** +`WL_Nature.prefab` 의 스포너 재바인딩(1~5 → 813001~813005)도 같이 살아 있어야 짝이 맞는다. + +## 1. 반영한 것 (전수) + +### (a) `Assets/ResWork/Table/Export/MonsterAppear.json` — 새 행 6개 (기존 46행 **바이트 무변경**) + +| SpawnerID | 존 | Lv | ATK | DEF/MDEF | HP | s_AppearMonster | e_SpawnerType | 리젠 s | 최대 | 순찰 | Exp/Gold | +|---|---|---|---|---|---|---|---|---|---|---|---| +| 813001 | 존1 첫 교전 | 1 | 10 | 0 | 300 | 101\|102\|103\|104 (Batty 4 · scale 1.5) | Random | 1.5 | 12 | 8 | 10/1 | +| 813002 | 존2 밀집 최대 | 2 | 12 | 0 | 450 | 168~172 (Porin 5 · scale 1.3) | Random | 1.2 | 15 | 8 | 15/2 | +| 813003 | 존3 혼합 | 3 | 14 | 0 | 600 | 173\|174\|175\|146\|147\|151 (Racco 3 + Rabby 3) | Random | 1.2 | 14 | 8 | 20/3 | +| 813004 | 존4 웨이브 관문 | 4 | 16 | 0 | 800 | 118\|119\|120 (Devilu 3) | PhaseAllKill | 2.0 | 8 | 6 | 30/4 | +| 813005 | 존5 엘리트 | 5 | 20 | 0 | 1500 | 1014\|1015\|1016 (Golem Elite 3 · scale 2.5) | Order | 3.0 | 5 | 6 | 60/8 | +| 813010 | 보스 아레나 | 6 | 18 | 0 | **450** | 10006 (Anubis) | Random | 60 | 1 | 4 | 500/100 | + +- `n_DropReward` 는 6행 모두 기존 백 `9700001`(전 스포너 공용). 새 백은 813f 소관. +- **813010 만 초안 값에서 바꿨다**: 초안 행은 `n_MonsterHP 1200`(실 HP 20×1200 = 24,000 ≈ 240s)인데 + 초안 자신의 `_계산` 과 기준서 §B 요소6(보스 **60~120 s** · 첫 보스 90 s 목표)에 어긋난다. + → 존1 의 3 s 처치(HP 300)가 함의하는 DPS 100 으로 역산: `450 = 90 s × 100 / f_BaseHPRate 20` → **실 HP 9,000**. + ATK 18 은 초안대로(실 ATK 30×18 = 540). 플레이어 무적 유지 중이라 ATK 는 아직 체감에 영향이 없다. + +### (b) `Assets/Res_Addr/Map/WL_Nature.prefab` — 6줄 변경 + 새 노드 1기 (총 +86 / −5 줄) + +| 무엇 | 변경 | +|---|---| +| `WL_Nature/maplv/1~5` 의 `MobControlMgr.SpawnerID` | 1·2·3·4·5 → **813001~813005** (각 1줄) | +| `WL_Nature` 루트 Transform `m_Children` | FieldBoss 컨테이너 1줄 추가 | +| `WL_Nature/FieldBoss/1` (신규) | GameObject 2 + Transform 2 + `FieldBossData` 1 = YAML 문서 5개(80줄) | + +`FieldBossData` 값: `n_MonsterID 10006` · `n_SpawnerId 813010` · `RegenTime 60` · **`isChapterBoss 0`** · pos **(139, 0.8, −20)** +- **`isChapterBoss` 는 반드시 0.** WL_Nature 의 `MapData.m_ChapterPortal` 이 null 이라 1이면 `AfterDie` 에서 NullReference (`FieldBossData.cs:47-49`) — 실측 확인. +- 스포너 ID 6~19 와 다른 12개 맵(WL_Nature02~10 · WL_Start · Map_C01/02)은 **손대지 않았다**(ID 1~19 는 13개 맵 공용 · 813b 교훈). + +## 2. 실측 (헤드리스 에디터 · `AgentScripts/WL813e_Zones.cs`) + +| 항목 | 값 | +|---|---| +| 새 행 인식 | **6 / 6** | +| 스포너 → 행 연결 | **19 / 19** (미연결 0 = `MobActor.cs:62` null 로 인한 NullReference 자리 0) | +| 새 존 재바인딩 | **5 / 5** | +| 몹 프리팹 자산 누락 | **0** (존 21종 + 보스 1종 전부 `Res_Addr/Mobs` 에 실존) | +| FieldBossData | **1기** · MonsterList 행 O · `Res_Addr/Mobs/FieldBoss/Anubis.prefab` O · MonsterAppear 행 O | +| 보스 실 스탯 | 실 HP = 20 × 450 = **9,000** · 실 ATK = 30 × 18 = **540** (`MyValue.cs:370-371`) | +| 동시 생존 · 풀 | **179마리 · 366개** (813b 기준 175 · 375 → 생존 +4 · 인스턴스 −9) | +| NavMesh 도달성 | 스포너 19 + 보스 1 = **PathComplete 20 / 20** · PathPartial 0 | +| 컴파일 게이트 | PASS · `error CS` 0 · 콘솔 error **0** | + +### 🔴 아레나 좌표를 초안에서 옮긴 이유 (813b 미확인 ③ 해소) + +초안 후보A **(137, 0.6, −22)** 는 NavMesh 위(스냅 0.33 m)이긴 하나 +**플레이어 시작 지점(`Portal_Start` 115,0,10)에서 `NavMesh.CalculatePath` = `PathPartial`** 이다 — 끊긴 구역이다. +같은 스윕에서 스포너 원점 19개는 전부 `PathComplete` 였으므로 프로브 문제가 아니다. +→ 후보A 주변 ±16 m 를 2 m 격자로 훑어 «시작에서 PathComplete + 모든 스포너 원점에서 15 m 이상» 을 만족하는 92개 중 +**후보A 에서 가장 가까운 (139, 0.8, −20)** 을 채택했다(후보A 로부터 **2.8 m** · 경로장 38.4 m · 스냅 0.01 m · 최근접 스포너 20.1 m). +초안의 설계 의도(SP1·SP4 사이 · 시작에서 약 38 m · 군집 반경 10 밖)는 그대로 유지된다. + +### 미확인 + +- **Play 실측(절두체 내 몹 수 · 처치 간격 · 보스 스폰 시각 · fps) = 「미확인」.** + 이 worktree 헤드리스 에디터에서 `Assets/Resources/PlayFabSharedSettings.asset` 이 임포트되지 않아 + (`Resources.LoadAll` = **0** · `AssetDatabase.LoadAssetAtPath` 도 null · 강제 재임포트·Refresh 후에도 동일) + `ServerInfo.Awake`(`ServerInfo.cs:45`)가 `IndexOutOfRangeException` 으로 죽고 `OnClick_DevLogin` 이 진행되지 않는다. + 파일·meta·스크립트 GUID 는 디스크에 정상 존재 → **Library 시딩(robocopy) 잔재**로 보이며 이번 변경과 무관하다. + Play 는 Title 에서 시작·정지했고 씬 dirty 0 으로 되돌렸다. +- 존 리듬의 **절대 HP 값**은 여전히 「제안값」이다. 플레이어 실 DPS 미측정(813m/813 S 트랙) — DPS 가 정해지면 + `n_MonsterHP = 목표 TTK × DPS / f_BaseHPRate` 로 6행을 한 번에 다시 계산한다. +- 보스 등장 방식은 **맵 로드 즉시 상시 배치**다(`FieldBossData.Start()`). "존 전멸 → 보스 스폰" 게이트는 코드가 필요해 813d(G)로 넘겼다. + +## 3. 롤백 + +1. `MonsterAppear.json` 에서 `n_SpawnerID` 가 `813001`~`813005`·`813010` 인 **6행 삭제**(기존 46행은 바이트 그대로라 앞부분은 손댈 필요 없다). +2. `WL_Nature.prefab`: `maplv/1~5` 의 `SpawnerID` 를 `1~5` 로 되돌리고, `FieldBoss` 컨테이너(`&8130050000000000001`~`&8130050000000000005` 문서 5개)와 + 루트 Transform `m_Children` 의 `{fileID: 8130050000000000002}` 줄을 지운다. +3. 또는 병합 커밋을 `git revert -m 1`. 다른 맵·다른 스포너 행에 영향 0. + +## 4. 다음 바퀴 값 조정 지점 + +| # | 무엇 | 근거 | +|---|---|---| +| 1 | **리젠 방식** — 기준서 §B 요소1 은 «존 전멸 후 4~6 s 일괄 재등장(`PhaseAllKill`)» 인데 이번엔 발주서 §1 이 지정한 초안 값(`Random` 1.2~1.5 s 개별 리젠)을 그대로 넣었다. 존1~3 을 `PhaseAllKill` + `f_SpawnerDelay 4~6` 으로 바꾸면 "비었다 → 다시 찬다" 페이싱이 생긴다. **단** `PhaseAllKill` 은 인스턴스 = 종류×마릿수라 존3(6종×14=84)은 풀이 커진다 → 종류를 3~4 로 줄여야 한다 | 기준서 §B · `MobControlMgr.cs:118-128, 213-224` | +| 2 | **보스 추적 사거리** — Anubis `f_BaseChaseRange` **50**. 아레나 반경 20 을 넘어 존1(24 m)·존4(20 m)까지 끌려 나온다 → xlsm `MonsterList` 에서 15~20 으로 | 실측 · 초안 코드변경목록 3 | +| 3 | **드랍** — 존·보스 6행 전부 `n_DropReward 9700001` 공용 백, 보스 `MonsterList.n_DropItem 0`. 보스 3~5건 폭발 드랍은 새 백 필요 | 기준서 §B 요소4 · 813f | +| 4 | **존5 마릿수** — 5마리는 기준서 «밀집 8~15» 밖이다. 의도적(엘리트 = «8~12 s 짜리 벽»)이지만 기준서 채점 시 요소1 로 세지 말 것 | 기준서 §B 요소1 | +| 5 | **보스 HP 재계산** — 450 은 DPS 100 「제안」 기반. 실 DPS 측정 후 재산출 | 위 미확인 | +| 6 | **존4 PhaseAllKill 총 길이** — 3웨이브 × 8마리 × 3 s ≈ 72 s + 웨이브 간 2 s. 모바일 3분 런 기준으로 길면 마릿수를 6 으로 | `MobControlMgr.cs:213-224` | + +## 5. 검증 도구 + +`AgentScripts/WL813e_Zones.cs` (Assets 밖 · 게임 빌드에 안 들어간다) + +``` +unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Edit # (a)~(f) 전수 +unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Nav # 스포너·보스 도달성 스윕 +unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Arena # 아레나 후보 재탐색 +unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Watch # Play 60 s 실측(로그인 후) +unity command run_script --file AgentScripts/WL813e_Zones.cs --entry WL813e_Zones.Result # 결과 +``` + +출력 원본 = `AgentScripts/staging/WL813e/{ZONES_EDIT,ZONES_NAV,ZONES_ARENA}.txt` diff --git a/Assets/WL/Settings/WL813e_ZoneApply.md.meta b/Assets/WL/Settings/WL813e_ZoneApply.md.meta new file mode 100644 index 000000000..5f06aec27 --- /dev/null +++ b/Assets/WL/Settings/WL813e_ZoneApply.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ef642f4f45ea4c0fa763c0852f384fcc +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: