[WL-813b] 보스·군중 콘텐츠 실측·초안 (#813)

- 보스 12종(10001~10012) 실측: 프리팹/스크립트/애니 상태/셰이더/인디케이터/이펙트/투사체 전수 검사 — 누락 0
- WL_Nature 군중 현황 실측: 스포너 19개 · 동시 175마리 · 인스턴스 375 · 스포너 최근접 44.0m
- 데이터 초안 2건(군중 존 리듬 · 보스 아레나) — 게임이 읽지 않는 _WL813b_* 별도 파일 · xlsm 동기화 필요
- 로드 검증 프로브 AgentScripts/WL813b_BossProbe.cs (Play 없음)
- 게임이 읽는 테이블·프리팹·씬·코드 무변경

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-08 23:30:11 +09:00
parent abcd038656
commit a4a053c445
7 changed files with 719 additions and 0 deletions

View File

@ -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<Row> ReadBossRows()
{
var list = new System.Collections.Generic.List<Row>();
string txt = System.IO.File.ReadAllText(kTableJson);
var objs = System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}");
for (int i = 0; i < objs.Count; i++)
{
string o = objs[i].Value;
if (F(o, "e_MonsterType") != "Boss") continue;
var r = new Row();
r.id = F(o, "n_MonsterID"); r.prefab = F(o, "s_MonsterPrefab");
r.scale = F(o, "f_DefaultScale"); r.hp = F(o, "f_BaseHPRate");
r.atk = F(o, "f_BaseATKRate"); r.def = F(o, "f_BaseDEFRate");
r.atkspd = F(o, "f_BaseATKSpeed"); r.movspd = F(o, "f_BaseMOVSpeed");
r.range = F(o, "f_BaseATKRange"); r.chase = F(o, "f_BaseChaseRange");
r.p1 = F(o, "s_Porjectile1"); r.p2 = F(o, "s_Porjectile2"); r.p3 = F(o, "s_Porjectile3");
r.lt1 = F(o, "f_ProjectileLifeTime1"); r.lt2 = F(o, "f_ProjectileLifeTime2"); r.lt3 = F(o, "f_ProjectileLifeTime3");
r.passive = F(o, "n_PassiveSkillID"); r.name = F(o, "n_MonsterName");
list.Add(r);
}
return list;
}
static string F(string obj, string key)
{
var m = System.Text.RegularExpressions.Regex.Match(obj, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
return m.Success ? m.Groups[1].Value : "";
}
// ── 보스 1종 검사 ────────────────────────────────────────────────────────
static bool ProbeOne(Row r)
{
bool ok = true;
string path = string.Format(kMobFmt, r.prefab);
L("── [" + r.id + "] " + r.prefab + " (scale " + r.scale + " · HPx" + r.hp + " · ATKx" + r.atk
+ " · MS" + r.movspd + " · RNG" + r.range + " · CHASE" + r.chase + " · passive " + r.passive + ")");
L(" key = " + path);
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) { L(" [X] 프리팹 없음 — 로드 불가"); return false; }
// Addressables 커버리지 (엔트리 직접 or 상위 폴더 엔트리)
L(" addressable = " + AddrState(path));
// 컴포넌트 / missing script
int missing = 0;
var comps = go.GetComponents<UnityEngine.Component>();
for (int i = 0; i < comps.Length; i++) if (comps[i] == null) missing++;
var allComps = go.GetComponentsInChildren<UnityEngine.Component>(true);
int missingAll = 0;
for (int i = 0; i < allComps.Length; i++) if (allComps[i] == null) missingAll++;
var mob = go.GetComponent<MobActor>();
string mobType = mob == null ? "(없음)" : mob.GetType().Name;
if (mob == null) { ok = false; L(" [X] MobActor 파생 컴포넌트 없음"); }
else L(" actor = " + mobType + (mobType.StartsWith("Boss_") ? "" : " [!] Boss_* 아님"));
if (missing > 0 || missingAll > 0) { ok = false; L(" [X] missing script 루트 " + missing + " · 전체 " + missingAll); }
else L(" missing script = 0");
// 스케일 (세로 화면 판정 · 발주서 localScale>2)
var sc = go.transform.localScale;
L(" localScale = " + sc.x.ToString("F2") + " (테이블 f_DefaultScale=" + r.scale + ")"
+ (sc.x > 2f ? " [!] >2" : ""));
// 애니메이터
var anim = go.GetComponentInChildren<UnityEngine.Animator>(true);
if (anim == null) { ok = false; L(" [X] Animator 없음"); }
else
{
var rac = anim.runtimeAnimatorController;
if (rac == null) { ok = false; L(" [X] AnimatorController 비어 있음"); }
else L(" animator = " + rac.name + " (clips " + rac.animationClips.Length + ")");
}
// 렌더러 · 셰이더 · 삼각형
var rends = go.GetComponentsInChildren<UnityEngine.Renderer>(true);
int tri = 0, matN = 0, mkToon = 0, badShader = 0;
var shaders = new System.Collections.Generic.List<string>();
for (int i = 0; i < rends.Length; i++)
{
var mats = rends[i].sharedMaterials;
for (int j = 0; j < mats.Length; j++)
{
matN++;
if (mats[j] == null) { badShader++; continue; }
var sh = mats[j].shader;
if (sh == null || sh.name.Contains("InternalErrorShader")) { badShader++; continue; }
if (sh.name.IndexOf("MK", System.StringComparison.OrdinalIgnoreCase) >= 0
&& sh.name.IndexOf("Toon", System.StringComparison.OrdinalIgnoreCase) >= 0) mkToon++;
if (!shaders.Contains(sh.name)) shaders.Add(sh.name);
}
var smr = rends[i] as UnityEngine.SkinnedMeshRenderer;
if (smr != null && smr.sharedMesh != null) tri += smr.sharedMesh.triangles.Length / 3;
var mf = rends[i].GetComponent<UnityEngine.MeshFilter>();
if (mf != null && mf.sharedMesh != null) tri += mf.sharedMesh.triangles.Length / 3;
}
if (badShader > 0) { ok = false; L(" [X] 셰이더 없음/에러 머티리얼 " + badShader + "개"); }
L(" renderer " + rends.Length + " · material " + matN + " · tri " + tri
+ " · MKToon " + mkToon + " · shader = " + string.Join(", ", shaders.ToArray()));
// 투사체 3종
ok &= CheckKey("P1", r.p1, kProjFmt, r.lt1);
ok &= CheckKey("P2", r.p2, kProjFmt, r.lt2);
ok &= CheckKey("P3", r.p3, kProjFmt, r.lt3);
// 스크립트 소스에서 뽑은 인디케이터 / 이펙트
string src = FindSrc(mobType);
if (src == null) L(" 소스 없음 — 인디케이터/이펙트 검사 생략");
else
{
ok &= CheckSet("indicator", src, "Load_Indicator\\(\\s*\"([^\"]+)\"", kIndiFmt);
ok &= CheckSet("indicator(필드기본값)", src, "Indicator\\s*=\\s*\"([^\"]+)\"", kIndiFmt);
ok &= CheckSet("effect", src, "Show_Effect\\(\\s*\"([^\"]+)\"", kEffFmt);
ok &= CheckSet("projectile(직접)", src, "Shoot_Projectile\\(\\s*\"([^\"]+)\"", kProjFmt);
}
L(" => " + (ok ? "PASS" : "FAIL"));
return ok;
}
static bool CheckKey(string label, string name, string fmt, string lifetime)
{
if (string.IsNullOrEmpty(name) || name == "None") { L(" " + label + " = None"); return true; }
string p = string.Format(fmt, name);
var a = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
L(" " + label + " = " + name + " (life " + lifetime + ") " + (a != null ? "OK" : "[X] 없음: " + p));
return a != null;
}
static bool CheckSet(string label, string src, string pattern, string fmt)
{
var ms = System.Text.RegularExpressions.Regex.Matches(src, pattern);
var seen = new System.Collections.Generic.List<string>();
var miss = new System.Collections.Generic.List<string>();
for (int i = 0; i < ms.Count; i++)
{
string n = ms[i].Groups[1].Value;
if (seen.Contains(n)) continue;
seen.Add(n);
if (UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(string.Format(fmt, n)) == null) miss.Add(n);
}
if (seen.Count == 0) return true;
L(" " + label + " " + seen.Count + "종 · 누락 " + miss.Count + (miss.Count > 0 ? " -> " + string.Join(", ", miss.ToArray()) : ""));
return miss.Count == 0;
}
static string FindSrc(string typeName)
{
if (string.IsNullOrEmpty(typeName) || typeName == "(없음)") return null;
var files = System.IO.Directory.GetFiles(kBossSrcDir, "*.cs");
for (int i = 0; i < files.Length; i++)
{
string t = System.IO.File.ReadAllText(files[i]);
if (t.Contains("class " + typeName)) return t;
}
return null;
}
static string AddrState(string assetPath)
{
var st = UnityEditor.AddressableAssets.AddressableAssetSettingsDefaultObject.Settings;
if (st == null) return "(Addressables 설정 없음)";
string guid = UnityEditor.AssetDatabase.AssetPathToGUID(assetPath);
var e = st.FindAssetEntry(guid);
if (e != null) return "직접 엔트리 · address=" + e.address + " · group=" + e.parentGroup.Name;
// 상위 폴더 엔트리 탐색
string dir = System.IO.Path.GetDirectoryName(assetPath).Replace("\\", "/");
while (!string.IsNullOrEmpty(dir) && dir != "Assets")
{
var fe = st.FindAssetEntry(UnityEditor.AssetDatabase.AssetPathToGUID(dir));
if (fe != null) return "폴더 엔트리 " + fe.address + " (group=" + fe.parentGroup.Name + ") 하위";
dir = System.IO.Path.GetDirectoryName(dir).Replace("\\", "/");
}
return "[X] Addressables 미등록";
}
static string Flush()
{
string dir = System.IO.Path.GetDirectoryName(kOutLog);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kOutLog, s.ToString());
return s.ToString();
}
}

View File

@ -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)"
}
]
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9d4686c83015e4e4b8ebea4f0055fb49
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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·포탈 수·티켓)과 무관하다. 보스 아레나에도 새 맵 행이 필요 없다(같은 맵 안이므로)."
}
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cf5c35821912d4749af3b23eb0d96e7f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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 동기화 후 재익스포트**가 선행돼야 한다.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d41a8168846fb9e419083d09fdb58b36
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: