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/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json new file mode 100644 index 000000000..23f683d01 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_BossArena_Draft.json @@ -0,0 +1,124 @@ +{ + "_meta": { + "발주": "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..f503af3c6 --- /dev/null +++ b/Assets/ResWork/Table/Export/_WL813b_MonsterAppear_Draft.json @@ -0,0 +1,130 @@ +{ + "_meta": { + "발주": "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/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: