Project_WL/Assets/WL/Island/WLDungeonSceneSetup.cs

209 lines
12 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLDungeonSceneSetup.cs — 던전 씬(아레나 복사본)을 **런타임**에 전투 가능한 맵으로 만든다
//
// PD 지시 「던전은 지금 만든 데모 씬을 쓰면 될 것 같아」 + Lead 「WL_ArenaProto 복사본 2개, 원본은 룩 개발용」
//
// ■ 아레나 데모 씬에 없는 것(실측) — 전부 여기서 런타임에 채운다. 씬 파일은 룩 그대로 둔다.
// ① NavMesh : 룩 씬이라 베이크 데이터가 없다 → `NavMeshSurface` 를 붙여 런타임 베이크
// ② 몬스터 스포너: `StageSpawner` 는 `MobControlMgr.SpawnerID` 로 몹을 찾는다(StageSpawner.cs:20)
// → 스테이지 표가 요구하는 SpawnerID 로 스포너를 만든다(테이블 수정 0 · 몹 종류는 MonsterAppear 표가 정한다)
// ③ LoadMapMgr : `PCInfo.Make_Actors:24` 가 부른다 → 섬과 같은 파생 노드로 채운다
// ④ 중복 정리 : 데모 리그(ArenaWalkerRig)·여분 카메라/AudioListener 해제
//
// ■ 그 다음 `StageDirector.Enter(stageIndex, "dungeon")` — #815 자산을 그대로 쓴다(코드 0줄).
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.SceneManagement;
using WL.Combat.Stage;
namespace WL.Island
{
/// <summary>던전 씬 준비 + 스테이지 입장. 정적.</summary>
public static class WLDungeonSceneSetup
{
/// <summary>입장 요청이 걸린 던전(씬 로드 전에 세팅된다).</summary>
public static WLDungeonDef Pending;
/// <summary>지금 돌고 있는 던전(스테이지 이벤트를 우리 것으로 가려내는 기준).</summary>
public static WLDungeonDef Current;
public static int Prepared, NavBakes, SpawnersMade, EnterCalls;
public static string LastLog = "";
static readonly List<MobControlMgr> s_made = new List<MobControlMgr>(4);
public static IEnumerator Co_Prepare(WLIslandSettings cfg, Scene scene, WLDungeonDef d)
{
Prepared++;
Current = d;
Pending = null;
yield return null; // 씬 안 오브젝트들의 Awake 를 끝까지 돌린다
WLIslandSceneSetup.EnsureDungeonMapNodes(cfg, scene, d);
FixDuplicates(cfg, scene);
// 🔴 WL-816i — 던전 카메라를 **섬 구도**(각 45° · 거리 22 m · FOV 60)로 맞춘다.
// 씬이 내려가면 이 오브젝트도 사라지고 OnDestroy 가 원래 값을 돌려놓는다(C8).
if (Object.FindFirstObjectByType<WLDungeonCameraRig>() == null)
WLDungeonCameraRig.Create(cfg, scene);
// ① NavMesh
if (cfg.bakeDungeonNavMesh != 0) BakeNavMesh(cfg, scene);
// 🔴 단독 Play(씬만 열고 Play = 룩 확인 경로)에는 테이블·ServerData 가 없다.
// 스포너·스테이지는 원본 테이블을 타므로 여기서 멈춘다 — 씬 룩만 확인한다.
if (InGameInfo.Ins == null)
{
LastLog = "던전 씬 단독 Play — 룩만 확인(스포너·스테이지 건너뜀)";
WLIslandBridge.Log(cfg, LastLog);
yield break;
}
// ② 스포너 — 스테이지 표가 요구하는 ID 로만 만든다
s_made.Clear();
var def = StageDefOf(d);
if (cfg.spawnDungeonSpawners != 0 && def != null) MakeSpawners(cfg, scene, def);
// ③ 스포너 로딩(첫 스폰)이 끝나기를 기다린다 — 안 기다리면 리젠 차단이 첫 스폰을 죽인다(StageSpawner.cs 가드)
float t = 0f;
while (t < 8f && !AllLoaded()) { t += Time.unscaledDeltaTime; yield return null; }
// ④ PC 가 생길 때까지 기다린다(Make_Actors 는 Addressables 비동기다)
t = 0f;
while (t < 8f && !(MyValue.bMyPC && MyValue.MyPC != null)) { t += Time.unscaledDeltaTime; yield return null; }
if (cfg.dungeonEnterDelay > 0f) yield return new WaitForSecondsRealtime(cfg.dungeonEnterDelay);
EnterCalls++;
bool ok = StageDirector.Enter(d.stageIndex, "dungeon " + d.id);
LastLog = "던전 준비 완료 — " + d.displayName + " · 스포너 " + s_made.Count +
" · NavMesh " + NavBakes + " · Enter(" + d.stageIndex + ")=" + ok;
WLIslandBridge.Log(cfg, LastLog);
}
public static StageDef StageDefOf(WLDungeonDef d)
{
var t = WLStageTable.Instance;
return t != null ? t.Get(d.stageIndex) : null;
}
// ─────────────────────────────────────────────────────────────────
// ① NavMesh 런타임 베이크
// ─────────────────────────────────────────────────────────────────
static void BakeNavMesh(WLIslandSettings cfg, Scene scene)
{
var surf = Object.FindFirstObjectByType<NavMeshSurface>();
if (surf == null)
{
var go = new GameObject("~WL_DungeonNavMesh");
SceneManager.MoveGameObjectToScene(go, scene);
surf = go.AddComponent<NavMeshSurface>();
}
// 🔴 기본값(RenderMeshes)은 메시마다 Read/Write 를 요구해 콘솔 에러 100건이 난다(실측).
// 콜라이더 기준으로 베이크하면 임포트 설정을 하나도 건드리지 않고 조용히 끝난다.
surf.useGeometry = UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders;
surf.BuildNavMesh();
NavBakes++;
UnityEngine.AI.NavMeshHit hit;
var def = StageDefOf(WLDungeonSceneSetup.Current);
Vector3 probe = def != null ? def.anchor : Vector3.zero;
bool onMesh = UnityEngine.AI.NavMesh.SamplePosition(probe, out hit, 6f, UnityEngine.AI.NavMesh.AllAreas);
WLIslandBridge.Log(cfg, "던전 NavMesh 런타임 베이크 완료 — 앵커 " + probe.ToString("F1") +
" 위 NavMesh " + (onMesh ? ("있음(" + hit.position.ToString("F2") + ")") : "🔴 없음"));
}
// ─────────────────────────────────────────────────────────────────
// ② 스포너 생성 (테이블 수정 0 — MonsterAppear 표의 기존 SpawnerID 를 그대로 쓴다)
// ─────────────────────────────────────────────────────────────────
static void MakeSpawners(WLIslandSettings cfg, Scene scene, StageDef def)
{
if (def.waves == null) return;
for (int i = 0; i < def.waves.Length; i++)
{
var w = def.waves[i];
if (w == null || w.spawnerId == 0) continue;
if (StageSpawner.Find(w.spawnerId) != null) continue; // 이미 씬에 있으면 그대로 쓴다
var go = new GameObject("~WL_Spawner_" + w.spawnerId);
go.SetActive(false); // 🔴 필드부터 채우고 깨운다
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.position = def.anchor;
var labelGo = new GameObject("lv");
labelGo.transform.SetParent(go.transform, false);
labelGo.transform.localPosition = new Vector3(0f, 2.2f, 0f);
var tmp = labelGo.AddComponent<TextMeshPro>();
tmp.text = "Lv.???";
tmp.fontSize = 2.4f;
tmp.alignment = TextAlignmentOptions.Center;
labelGo.SetActive(false); // 던전 바닥에 레벨표가 뜨지 않게
var mgr = go.AddComponent<MobControlMgr>();
mgr.SpawnerID = w.spawnerId;
mgr.MobGenRange = cfg.dungeonSpawnerRange;
mgr.text_lv = tmp;
go.SetActive(true);
s_made.Add(mgr);
SpawnersMade++;
}
WLIslandBridge.Log(cfg, "던전 스포너 " + s_made.Count + "개 생성(앵커 " + def.anchor + ")");
}
static bool AllLoaded()
{
for (int i = 0; i < s_made.Count; i++)
if (s_made[i] == null || !s_made[i].Get_LoadComplete()) return false;
return s_made.Count > 0;
}
// ─────────────────────────────────────────────────────────────────
// ④ 중복 정리 — 데모 리그·여분 카메라
// ─────────────────────────────────────────────────────────────────
public static void FixDuplicates(WLIslandSettings cfg, Scene scene)
{
if (cfg.fixDungeonDuplicates == 0) return;
if (InGameInfo.Ins == null) return; // 단독 Play(룩 확인)에서는 데모 리그가 유일한 볼거리다
int rigs = 0, cams = 0, listeners = 0;
var walkers = Object.FindObjectsByType<WL.Look.Arena.ArenaWalker>(FindObjectsSortMode.None);
for (int i = 0; i < walkers.Length; i++)
if (walkers[i] != null) { walkers[i].gameObject.SetActive(false); rigs++; }
// 🔴 WL-816i — 전에는 `c == Camera.main` 을 건너뛰었는데, 아레나 복사본 씬의 `Main Camera`(직교 · orthoSize 10)가
// 바로 그 MainCamera 태그를 달고 있어서 **룩 개발용 직교 카메라가 그대로 살아 전투 화면을 차지**했다.
// 던전 씬 안의 카메라는 전부 룩용이다 → 전투 카메라(RealCamera · Ingame 씬)만 남기고 모두 끈다.
// 단 전투 카메라를 못 찾으면 화면이 까매지므로 아무것도 끄지 않는다(안전).
var combat = Object.FindFirstObjectByType<RealCamera>();
var combatCam = combat != null ? combat.GetComponent<Camera>() : null;
if (combatCam != null)
{
if (!combatCam.enabled) { combatCam.enabled = true; cams++; }
var allCams = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None);
for (int i = 0; i < allCams.Length; i++)
{
var c = allCams[i];
if (c == null || c == combatCam || c.gameObject.scene != scene) continue;
if (c.enabled) { c.enabled = false; cams++; }
var al = c.GetComponent<AudioListener>();
if (al != null && al.enabled) { al.enabled = false; listeners++; }
}
}
else
{
WLIslandBridge.Log(cfg, "🔴 던전 전투 카메라(RealCamera)를 못 찾았다 — 씬 카메라를 그대로 둔다");
}
WLIslandBridge.Log(cfg, "던전 중복 정리 — 데모 리그 " + rigs + " · 카메라 " + cams + " · 리스너 " + listeners);
}
}
}