415 lines
23 KiB
C#
415 lines
23 KiB
C#
// PD 지시 #757 — WL_Start 프리팹에 힘민지 방식 몬스터 스포너 19개를 배치한다.
|
|
// 실행: unity command run_script --file AgentScripts/WL_MonsterSpawnerBuilder.cs --entry WL_MonsterSpawnerBuilder.Run --timeout 300
|
|
// CLAUDE.md §1 준수: using 금지 · 네임스페이스 풀어 쓰기
|
|
//
|
|
// 방식 (힘민지 실측)
|
|
// · 스포너 오브젝트 = Map_C01.prefab 의 maplv/1 (Transform + MobControlMgr, 자식 lv = TextMeshPro 바닥 라벨) 을 그대로 복제
|
|
// · 컨테이너 = WL_Start 루트 아래 'maplv' (루트에 MobControlMgrList 가 있어 GetComponentsInChildren 로 잡힌다)
|
|
// · SpawnerID = 1~19 (MonsterAppear.json 기존 행 재사용 · PD 결정 #757)
|
|
// · 위치 = 베이크된 NavMeshData(Assets/WL/Settings/WL_Start_NavMesh.asset) 위에서 최원점 표집(farthest-point sampling)
|
|
//
|
|
// 수치는 전부 데이터에서 읽는다 (C45): MobGenRange·회전 = Map_C01 템플릿 / CamRot = WL_Start MapData / 레벨·몬스터수 = MonsterAppear.json
|
|
//
|
|
// 롤백: WL_Start.prefab 의 'maplv' 자식을 통째로 삭제 (kRemoveOnly = true 로 실행해도 된다)
|
|
|
|
public static class WL_MonsterSpawnerBuilder
|
|
{
|
|
// ── 실행 스위치 ────────────────────────────────────────────
|
|
const bool kDryRun = false; // true = 아무것도 쓰지 않고 계획만 기록
|
|
static bool kRemoveOnly = false; // true = 기존 maplv 만 제거하고 종료 (롤백용)
|
|
|
|
// ── 경로 (#771 인자화 · 기본값 = WL_Start 그대로) ─────────
|
|
const string kTemplatePrefab = "Assets/Res_Addr/Map/Map_C01.prefab";
|
|
const string kTemplatePath = "maplv/1"; // 템플릿 스포너 (SpawnerID 1)
|
|
static string kTargetPrefab = "Assets/Res_Addr/Map/WL_Start.prefab";
|
|
static string kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset";
|
|
const string kAppearJson = "Assets/ResWork/Table/Export/MonsterAppear.json";
|
|
const string kContainerName = "maplv";
|
|
static string kLogPath = "AgentScripts/staging/WL_Monsters/BUILDER_LOG.txt";
|
|
|
|
// ── 배치 제약 (발주 #757) ──────────────────────────────────
|
|
const int kSpawnerCount = 19; // PD 지정
|
|
const int kFirstSpawnerID = 1; // PD 지정 (1~19)
|
|
static float kMinDistFromPC = 15f; // 플레이어 스폰 지점에서 최소 거리
|
|
static float kMinMutual = 12f; // 스포너 간 최소 거리
|
|
const float kFlatProbeRadius = 3f; // 근접 평탄도 확인 반경
|
|
const float kFlatMaxDrop = 2.0f; // 근접 반경 내 높이 편차 상한
|
|
const float kAreaProbeRadius = 8f; // MobGenRange 확보 확인 반경 (템플릿 MobGenRange 10 의 80%)
|
|
const int kAreaProbeHits = 6; // 8방향 중 최소 통과 수
|
|
const float kAreaMaxDrop = 4.0f; // 확보 반경 내 높이 편차 상한 (절벽 배제)
|
|
const float kWaterMargin = 0.5f; // 수면 위 최소 여유
|
|
static float kGridStep = 4f; // 후보 격자 간격
|
|
const float kLabelLift = 0.05f; // 바닥 라벨 z-fighting 여유
|
|
|
|
// 플레이 영역 한정 (#771 · <=0 이면 제한 없음 = 기존 동작)
|
|
static float kAreaCX = 0f, kAreaCZ = 0f, kAreaHalf = -1f;
|
|
|
|
static System.Text.StringBuilder s_log = new System.Text.StringBuilder();
|
|
|
|
static void ResetDefaults()
|
|
{
|
|
kRemoveOnly = false;
|
|
kTargetPrefab = "Assets/Res_Addr/Map/WL_Start.prefab";
|
|
kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset";
|
|
kLogPath = "AgentScripts/staging/WL_Monsters/BUILDER_LOG.txt";
|
|
kMinDistFromPC = 15f; kMinMutual = 12f; kGridStep = 4f;
|
|
kAreaCX = 0f; kAreaCZ = 0f; kAreaHalf = -1f;
|
|
}
|
|
|
|
// #771 인자화 진입점 — 임의의 맵 프리팹 · 플레이 영역 한정 · 안전거리 지정
|
|
public static object RunFor(string targetPrefab, string navAsset, string logPath,
|
|
float minDistFromPC, float minMutual, float gridStep,
|
|
float areaCX, float areaCZ, float areaHalf, bool removeOnly)
|
|
{
|
|
ResetDefaults();
|
|
kTargetPrefab = targetPrefab; kNavAsset = navAsset; kLogPath = logPath;
|
|
kMinDistFromPC = minDistFromPC; kMinMutual = minMutual; kGridStep = gridStep;
|
|
kAreaCX = areaCX; kAreaCZ = areaCZ; kAreaHalf = areaHalf; kRemoveOnly = removeOnly;
|
|
Core();
|
|
return "done — log=" + kLogPath;
|
|
}
|
|
|
|
// 기존 진입점 — 기본값(WL_Start) 복원 후 실행 · 동작 무변경
|
|
public static void Run() { ResetDefaults(); Core(); }
|
|
|
|
static void Core()
|
|
{
|
|
s_log.Length = 0;
|
|
L("===== WL_MonsterSpawnerBuilder dryRun=" + kDryRun + " removeOnly=" + kRemoveOnly + " =====");
|
|
|
|
// ── 1. 템플릿 실측 ────────────────────────────────────
|
|
var srcRoot = UnityEditor.PrefabUtility.LoadPrefabContents(kTemplatePrefab);
|
|
UnityEngine.GameObject template = null;
|
|
float tplRange = 10f;
|
|
try
|
|
{
|
|
var t = srcRoot.transform.Find(kTemplatePath);
|
|
if (t == null) { E("템플릿을 찾지 못했다: " + kTemplatePath); Flush(); return; }
|
|
template = t.gameObject;
|
|
var mcm = template.GetComponent<MobControlMgr>();
|
|
if (mcm == null) { E("템플릿에 MobControlMgr 이 없다"); Flush(); return; }
|
|
tplRange = mcm.MobGenRange;
|
|
L("템플릿 = " + kTemplatePrefab + " / " + kTemplatePath
|
|
+ " MobGenRange=" + tplRange + " layer=" + UnityEngine.LayerMask.LayerToName(template.layer)
|
|
+ " 자식lv=" + (mcm.text_lv == null ? "NULL" : mcm.text_lv.name));
|
|
|
|
// ── 2. 테이블 확인 (C45 — SOT 직접 읽기) ──────────
|
|
var lv = new System.Collections.Generic.Dictionary<int, string>();
|
|
var mc = new System.Collections.Generic.Dictionary<int, int>();
|
|
ReadAppearTable(lv, mc);
|
|
int missing = 0, totalMobs = 0;
|
|
for (int id = kFirstSpawnerID; id < kFirstSpawnerID + kSpawnerCount; id++)
|
|
{
|
|
if (!lv.ContainsKey(id)) { E("MonsterAppear 에 SpawnerID " + id + " 행이 없다"); missing++; }
|
|
else totalMobs += mc[id];
|
|
}
|
|
if (missing > 0) { Flush(); return; }
|
|
L("MonsterAppear 확인 OK — SpawnerID " + kFirstSpawnerID + "~" + (kFirstSpawnerID + kSpawnerCount - 1)
|
|
+ " · n_MaxMonsterCount 합계 = " + totalMobs);
|
|
|
|
// ── 3. 대상 프리팹 열기 ───────────────────────────
|
|
var dstRoot = UnityEditor.PrefabUtility.LoadPrefabContents(kTargetPrefab);
|
|
try
|
|
{
|
|
// 기존 컨테이너 제거 (재실행 가능하게)
|
|
var old = dstRoot.transform.Find(kContainerName);
|
|
if (old != null)
|
|
{
|
|
L("기존 '" + kContainerName + "' 발견 (자식 " + old.childCount + "개) → 제거");
|
|
if (!kDryRun) UnityEngine.Object.DestroyImmediate(old.gameObject);
|
|
}
|
|
if (kRemoveOnly)
|
|
{
|
|
if (!kDryRun) { UnityEditor.PrefabUtility.SaveAsPrefabAsset(dstRoot, kTargetPrefab); L("제거 후 저장 완료"); }
|
|
Flush(); return;
|
|
}
|
|
|
|
// 스폰 지점 (LoadMapMgr 은 Portal_Start + Vector3.forward 에 세운다)
|
|
var portal = dstRoot.transform.Find("Portal_Start");
|
|
if (portal == null) { E("Portal_Start 가 없다"); Flush(); return; }
|
|
var pcSpawn = portal.position + UnityEngine.Vector3.forward;
|
|
L("플레이어 스폰 지점 = " + pcSpawn.ToString("F3"));
|
|
|
|
// 카메라 초기 yaw (바닥 라벨 방향 결정용)
|
|
float camYaw = 180f;
|
|
var md = dstRoot.GetComponent<MapData>();
|
|
if (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0)
|
|
camYaw = md.m_MapZoneData[0].CamRot_Portal;
|
|
L("MapData.CamRot_Portal = " + camYaw + " → 스포너 yaw 로 사용 (바닥 라벨이 초기 카메라에 정립)");
|
|
|
|
// 수면 (전역 수위가 아니라 개별 수역 — 바운즈 목록으로 판정)
|
|
var water = FindWaterVolumes(dstRoot);
|
|
|
|
// ── 4. NavMesh 후보 산출 ──────────────────────
|
|
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
|
if (navData == null) { E("NavMeshData 에셋 없음: " + kNavAsset); Flush(); return; }
|
|
var navInst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
|
|
System.Collections.Generic.List<UnityEngine.Vector3> chosen;
|
|
try { chosen = Choose(pcSpawn, water); }
|
|
finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst); }
|
|
|
|
if (chosen == null || chosen.Count < kSpawnerCount)
|
|
{
|
|
E("후보가 부족하다 — 선택 " + (chosen == null ? 0 : chosen.Count) + " / 필요 " + kSpawnerCount);
|
|
Flush(); return;
|
|
}
|
|
|
|
// 스폰 지점에서 가까운 순 → SpawnerID 1..19 (Lv.1 이 가장 가깝다)
|
|
chosen.Sort((a, b) => UnityEngine.Vector3.Distance(a, pcSpawn).CompareTo(UnityEngine.Vector3.Distance(b, pcSpawn)));
|
|
|
|
L("");
|
|
L("── 배치표 (ID · 위치 · 스폰지점거리 · 최근접스포너 · Lv · 몬스터수 · MobGenRange) ──");
|
|
for (int i = 0; i < chosen.Count; i++)
|
|
{
|
|
int id = kFirstSpawnerID + i;
|
|
float dPC = UnityEngine.Vector3.Distance(chosen[i], pcSpawn);
|
|
float dMin = float.MaxValue;
|
|
for (int j = 0; j < chosen.Count; j++)
|
|
if (j != i) dMin = UnityEngine.Mathf.Min(dMin, UnityEngine.Vector3.Distance(chosen[i], chosen[j]));
|
|
L(string.Format(" {0,2} | ({1,8:F2},{2,7:F2},{3,8:F2}) | PC {4,6:F1}m | 인접 {5,5:F1}m | Lv.{6,-3} | {7,2}마리 | range {8}",
|
|
id, chosen[i].x, chosen[i].y, chosen[i].z, dPC, dMin, lv[id], mc[id], tplRange));
|
|
}
|
|
L("");
|
|
|
|
if (kDryRun) { L("--- dryRun: 여기서 중단. kDryRun=false 로 재실행 ---"); Flush(); return; }
|
|
|
|
// ── 5. 배치 ────────────────────────────────────
|
|
var container = new UnityEngine.GameObject(kContainerName);
|
|
container.transform.SetParent(dstRoot.transform, false);
|
|
container.layer = template.layer;
|
|
|
|
for (int i = 0; i < chosen.Count; i++)
|
|
{
|
|
int id = kFirstSpawnerID + i;
|
|
var clone = UnityEngine.Object.Instantiate(template, container.transform, false);
|
|
clone.name = id.ToString();
|
|
clone.transform.position = chosen[i] + UnityEngine.Vector3.up * kLabelLift;
|
|
clone.transform.rotation = UnityEngine.Quaternion.Euler(0f, camYaw, 0f);
|
|
clone.transform.localScale = UnityEngine.Vector3.one;
|
|
|
|
var comp = clone.GetComponent<MobControlMgr>();
|
|
comp.SpawnerID = id;
|
|
comp.MobGenRange = tplRange;
|
|
if (comp.text_lv == null)
|
|
{
|
|
var lvt = clone.transform.Find("lv");
|
|
if (lvt != null) comp.text_lv = lvt.GetComponent<TMPro.TextMeshPro>();
|
|
W("#" + id + " text_lv 자동 복원 = " + (comp.text_lv == null ? "실패" : "성공"));
|
|
}
|
|
UnityEditor.EditorUtility.SetDirty(comp);
|
|
}
|
|
L("스포너 " + chosen.Count + "개 배치 완료 (컨테이너 '" + kContainerName + "')");
|
|
|
|
// 검증
|
|
var verify = dstRoot.GetComponentsInChildren<MobControlMgr>(true);
|
|
L("검증: GetComponentsInChildren<MobControlMgr>(루트) = " + verify.Length + "개");
|
|
int nullLv = 0;
|
|
for (int i = 0; i < verify.Length; i++) if (verify[i].text_lv == null) nullLv++;
|
|
L("검증: text_lv null = " + nullLv + "개");
|
|
var lst = dstRoot.GetComponent<MobControlMgrList>();
|
|
L("검증: 루트 MobControlMgrList = " + (lst == null ? "없음 🔴" : "있음"));
|
|
|
|
UnityEditor.PrefabUtility.SaveAsPrefabAsset(dstRoot, kTargetPrefab);
|
|
L("프리팹 저장: " + kTargetPrefab);
|
|
}
|
|
finally { UnityEditor.PrefabUtility.UnloadPrefabContents(dstRoot); }
|
|
}
|
|
finally { UnityEditor.PrefabUtility.UnloadPrefabContents(srcRoot); }
|
|
|
|
UnityEditor.AssetDatabase.SaveAssets();
|
|
UnityEditor.AssetDatabase.Refresh();
|
|
L("===== 완료 =====");
|
|
Flush();
|
|
}
|
|
|
|
// ── 후보 산출 + 최원점 표집 ────────────────────────────────
|
|
static System.Collections.Generic.List<UnityEngine.Vector3> Choose(
|
|
UnityEngine.Vector3 pcSpawn, System.Collections.Generic.List<UnityEngine.Bounds> water)
|
|
{
|
|
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
|
|
L("NavMesh 삼각형 " + (tri.indices.Length / 3) + "개");
|
|
if (tri.vertices.Length == 0) { E("NavMesh 가 비어 있다"); return null; }
|
|
|
|
var min = tri.vertices[0]; var max = tri.vertices[0];
|
|
for (int i = 1; i < tri.vertices.Length; i++)
|
|
{ min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); }
|
|
L("NavMesh 범위 min=" + min.ToString("F1") + " max=" + max.ToString("F1"));
|
|
|
|
// 경로 판정 기준점은 NavMesh 에 스냅한 실제 플레이어 위치
|
|
UnityEngine.AI.NavMeshHit pcHit;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(pcSpawn, out pcHit, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{ E("플레이어 스폰 지점이 NavMesh 위가 아니다"); return null; }
|
|
var pcOnNav = pcHit.position;
|
|
L("스폰 지점 NavMesh 스냅 = " + pcOnNav.ToString("F3") + " (원점에서 " + UnityEngine.Vector3.Distance(pcSpawn, pcOnNav).ToString("F2") + "m)");
|
|
|
|
// 격자 표집 — 삼각형 무게중심보다 훨씬 고르게 섬 전역을 덮는다
|
|
// 플레이 영역 한정 (#771)
|
|
if (kAreaHalf > 0f)
|
|
{
|
|
min.x = UnityEngine.Mathf.Max(min.x, kAreaCX - kAreaHalf);
|
|
max.x = UnityEngine.Mathf.Min(max.x, kAreaCX + kAreaHalf);
|
|
min.z = UnityEngine.Mathf.Max(min.z, kAreaCZ - kAreaHalf);
|
|
max.z = UnityEngine.Mathf.Min(max.z, kAreaCZ + kAreaHalf);
|
|
L("플레이 영역 한정: x " + min.x.ToString("F1") + "~" + max.x.ToString("F1")
|
|
+ " / z " + min.z.ToString("F1") + "~" + max.z.ToString("F1"));
|
|
}
|
|
|
|
var cand = new System.Collections.Generic.List<UnityEngine.Vector3>();
|
|
int nGrid = 0, rejOff = 0, rejWater = 0, rejPC = 0, rejFlat = 0, rejArea = 0, rejPath = 0, nReach = 0, nOnNav = 0;
|
|
for (float x = min.x; x <= max.x; x += kGridStep)
|
|
for (float z = min.z; z <= max.z; z += kGridStep)
|
|
{
|
|
nGrid++;
|
|
UnityEngine.AI.NavMeshHit h;
|
|
// 높이를 모르므로 상단에서 넉넉한 반경으로 투영한다
|
|
var probe = new UnityEngine.Vector3(x, (min.y + max.y) * 0.5f, z);
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(probe, out h, (max.y - min.y) * 0.5f + kGridStep, UnityEngine.AI.NavMesh.AllAreas))
|
|
{ rejOff++; continue; }
|
|
var p = h.position;
|
|
if (UnityEngine.Mathf.Abs(p.x - x) > kGridStep || UnityEngine.Mathf.Abs(p.z - z) > kGridStep) { rejOff++; continue; }
|
|
nOnNav++;
|
|
|
|
bool reach = Reachable(pcOnNav, p);
|
|
if (reach) nReach++;
|
|
if (InWater(p, water)) { rejWater++; continue; }
|
|
if (UnityEngine.Vector3.Distance(p, pcSpawn) < kMinDistFromPC) { rejPC++; continue; }
|
|
if (!Flat(p, kFlatProbeRadius, 8, 8, kFlatMaxDrop)) { rejFlat++; continue; }
|
|
if (!Flat(p, kAreaProbeRadius, 8, kAreaProbeHits, kAreaMaxDrop)) { rejArea++; continue; }
|
|
if (!reach) { rejPath++; continue; } // 플레이어 스폰에서 도달 가능해야 한다
|
|
cand.Add(p);
|
|
}
|
|
L("격자 " + nGrid + "점(" + kGridStep + "m) → 후보 " + cand.Count
|
|
+ "개 (제외: NavMesh밖 " + rejOff + " · 수중 " + rejWater
|
|
+ " · 스폰근접 " + rejPC + " · 비평탄 " + rejFlat + " · 협소 " + rejArea + " · 경로없음 " + rejPath + ")");
|
|
L("연결성 실측: NavMesh 위 격자점 " + nOnNav + "개 중 스폰 지점에서 도달 가능 " + nReach
|
|
+ "개 (" + (nOnNav == 0 ? 0 : nReach * 100 / nOnNav) + "%)");
|
|
if (cand.Count == 0) return null;
|
|
|
|
// 최원점 표집: 플레이어 스폰에서 가장 먼 후보로 시작해, 매번 기선택집합과의 최소거리가 최대인 후보를 추가
|
|
var sel = new System.Collections.Generic.List<UnityEngine.Vector3>();
|
|
int first = 0; float best = -1f;
|
|
for (int i = 0; i < cand.Count; i++)
|
|
{
|
|
float d = UnityEngine.Vector3.Distance(cand[i], pcSpawn);
|
|
if (d > best) { best = d; first = i; }
|
|
}
|
|
sel.Add(cand[first]);
|
|
|
|
while (sel.Count < kSpawnerCount)
|
|
{
|
|
int pick = -1; float pickD = -1f;
|
|
for (int i = 0; i < cand.Count; i++)
|
|
{
|
|
float dmin = float.MaxValue;
|
|
for (int j = 0; j < sel.Count; j++)
|
|
dmin = UnityEngine.Mathf.Min(dmin, UnityEngine.Vector3.Distance(cand[i], sel[j]));
|
|
if (dmin > pickD) { pickD = dmin; pick = i; }
|
|
}
|
|
if (pick < 0 || pickD < kMinMutual)
|
|
{
|
|
W("최원점 표집 중단 — 남은 최대 이격 " + pickD.ToString("F1") + "m < 하한 " + kMinMutual + "m (선택 " + sel.Count + "개)");
|
|
break;
|
|
}
|
|
sel.Add(cand[pick]);
|
|
}
|
|
L("최원점 표집 결과 " + sel.Count + "개");
|
|
return sel;
|
|
}
|
|
|
|
// 플레이어 스폰 지점에서 완전 경로가 나오는지 (고립된 섬·바위 배제)
|
|
static bool Reachable(UnityEngine.Vector3 from, UnityEngine.Vector3 to)
|
|
{
|
|
var path = new UnityEngine.AI.NavMeshPath();
|
|
if (!UnityEngine.AI.NavMesh.CalculatePath(from, to, UnityEngine.AI.NavMesh.AllAreas, path)) return false;
|
|
return path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete;
|
|
}
|
|
|
|
// 반경 r 의 8방향 표본 중 minHits 이상이 NavMesh 위이고 높이 편차가 maxDrop 이하이면 true
|
|
static bool Flat(UnityEngine.Vector3 p, float r, int rays, int minHits, float maxDrop)
|
|
{
|
|
int hits = 0; float ymin = p.y, ymax = p.y;
|
|
for (int k = 0; k < rays; k++)
|
|
{
|
|
float ang = k * (360f / rays) * UnityEngine.Mathf.Deg2Rad;
|
|
var q = p + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang) * r, 0f, UnityEngine.Mathf.Sin(ang) * r);
|
|
UnityEngine.AI.NavMeshHit h;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(q, out h, 1.5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
hits++;
|
|
if (h.position.y < ymin) ymin = h.position.y;
|
|
if (h.position.y > ymax) ymax = h.position.y;
|
|
}
|
|
return hits >= minHits && (ymax - ymin) <= maxDrop;
|
|
}
|
|
|
|
// ── 수역 실측 (전역 수위가 아니라 개별 렌더러 바운즈) ───────
|
|
static System.Collections.Generic.List<UnityEngine.Bounds> FindWaterVolumes(UnityEngine.GameObject root)
|
|
{
|
|
var list = new System.Collections.Generic.List<UnityEngine.Bounds>();
|
|
var rs = root.GetComponentsInChildren<UnityEngine.Renderer>(true);
|
|
for (int i = 0; i < rs.Length; i++)
|
|
{
|
|
var mats = rs[i].sharedMaterials;
|
|
bool isWater = false;
|
|
for (int m = 0; m < mats.Length; m++)
|
|
{
|
|
if (mats[m] == null || mats[m].shader == null) continue;
|
|
if (mats[m].shader.name.ToLowerInvariant().Contains("water")
|
|
|| mats[m].name.ToLowerInvariant().Contains("water")) { isWater = true; break; }
|
|
}
|
|
if (!isWater) continue;
|
|
list.Add(rs[i].bounds);
|
|
L(" 수역: " + rs[i].name + " center=" + rs[i].bounds.center.ToString("F1")
|
|
+ " size=" + rs[i].bounds.size.ToString("F1") + " topY=" + rs[i].bounds.max.y.ToString("F2"));
|
|
}
|
|
if (list.Count == 0) W("수역 렌더러를 찾지 못했다 — 수중 판정을 건너뛴다");
|
|
else L("수역 렌더러 " + list.Count + "개 (여유 " + kWaterMargin + "m)");
|
|
return list;
|
|
}
|
|
|
|
// 후보가 어떤 수역의 XZ 범위 안이고 그 수면보다 낮으면 수중으로 본다
|
|
static bool InWater(UnityEngine.Vector3 p, System.Collections.Generic.List<UnityEngine.Bounds> water)
|
|
{
|
|
for (int i = 0; i < water.Count; i++)
|
|
{
|
|
var b = water[i];
|
|
if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
|
|
if (p.y < b.max.y + kWaterMargin) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── MonsterAppear.json 직접 파싱 (C45) ─────────────────────
|
|
static void ReadAppearTable(System.Collections.Generic.Dictionary<int, string> lv,
|
|
System.Collections.Generic.Dictionary<int, int> maxCount)
|
|
{
|
|
if (!System.IO.File.Exists(kAppearJson)) { E("테이블 없음: " + kAppearJson); return; }
|
|
var txt = System.IO.File.ReadAllText(kAppearJson);
|
|
// 행 단위로 n_SpawnerID / n_AppearMonsterLv / n_MaxMonsterCount 를 뽑는다
|
|
var rx = new System.Text.RegularExpressions.Regex(
|
|
"\"n_SpawnerID\"\\s*:\\s*\"?(-?\\d+)\"?.*?\"n_AppearMonsterLv\"\\s*:\\s*\"?(-?\\d+)\"?.*?\"n_MaxMonsterCount\"\\s*:\\s*\"?(-?\\d+)\"?",
|
|
System.Text.RegularExpressions.RegexOptions.Singleline);
|
|
int idx = 0;
|
|
while (true)
|
|
{
|
|
var m = rx.Match(txt, idx);
|
|
if (!m.Success) break;
|
|
int id = int.Parse(m.Groups[1].Value);
|
|
if (!lv.ContainsKey(id)) { lv[id] = m.Groups[2].Value; maxCount[id] = int.Parse(m.Groups[3].Value); }
|
|
idx = m.Index + 1;
|
|
}
|
|
L("MonsterAppear 파싱: " + lv.Count + "행");
|
|
}
|
|
|
|
// ── 로그 ───────────────────────────────────────────────────
|
|
static void L(string s) { s_log.AppendLine(s); }
|
|
static void W(string s) { s_log.AppendLine("[경고] " + s); }
|
|
static void E(string s) { s_log.AppendLine("[오류] " + s); }
|
|
static void Flush()
|
|
{
|
|
var dir = System.IO.Path.GetDirectoryName(kLogPath);
|
|
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
|
System.IO.File.WriteAllText(kLogPath, s_log.ToString());
|
|
UnityEngine.Debug.Log("[SpawnerBuilder] 로그: " + kLogPath);
|
|
}
|
|
}
|