// ───────────────────────────────────────────────────────────────────────────── // WLIslandBridge.cs — 섬 ↔ 던전 왕복의 배선 (원본 코드 0줄 · 새 Manager/Singleton 0) // // PD 지시 #816 · 발주서 WL-816d §1-④ / §1-B · 816c 설계서 §B-1 표(1~6단계) // // ■ 한 바퀴 (설계서 §B-1 을 그대로 구현) // ① 섬 진입 : Load_Map(islandMapId) → 섬 씬(Level01)이 Additive 로 붙는다 // ② 던전 입구 : WLDungeonGate(더미)에 다가가면 「들어가기」 // ③ 던전 입장 : Load_Map(dungeon.mapId) → 던전 씬 → StageDirector.Enter(stageIndex) // ④ 보상 확정 : StageEvents.StageCleared 구독 → StageDef.goldMultiplier 로 코인 계산 → **보류** // ⑤ 섬 복귀 : StageExited(toLobby) → Load_Map(islandMapId) // ⑥ 지급 : 섬 씬 로드 후 IslandManager.Instance.Coin += 보류분 // (816c 실측: IslandManager.Awake 에서 로드가 끝나 있어 Start 전에 더해도 안전) // // ■ 중복 정리(설계서 §B-2) — 전부 **런타임**에 한다. FI 씬·코드는 0줄. // 섬 : WL 전투 PC(Actor) 숨김 · FI 카메라/AudioListener 중복 해제 // 던전: 아레나 데모 리그(ArenaWalkerRig)·여분 카메라 해제 // // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다. // 🔴 어셈블리 주의: Assets/WL/Island/ 에 .asmdef 를 만들지 말 것. // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using WL.Combat.Stage; namespace WL.Island { /// 섬 ↔ 던전 왕복. 정적 진입 + 숨김 러너 1개(코루틴·입력용). public static class WLIslandBridge { // ── 진단(프로브·보고가 읽는다) public static int IslandLoads, DungeonLoads, ClearGrants, FailGrants, CoinGranted, GatesSpawned; public static string LastLog = ""; /// /// 프로브 전용 — 비어 있지 않으면 단독 Play 경로에서 이 씬 이름으로 대신 들어간다. /// 🔴 던전 복사본 씬이 아직 빌드 목록에 없어(등록은 Lead·PD 승인 사항) 로그인 없는 검증에 쓴다. /// 실기(InGameInfo 가 있는 흐름)에는 영향이 없다 — 그쪽은 Load_Map(mapId) 를 탄다. /// public static string DebugSceneOverride = ""; static bool s_installed; static int s_pendingGold; static WLIslandRunner s_runner; static readonly List s_gates = new List(8); /// 보류 중인 보상(섬에 도착하면 더해진다). public static int PendingGold { get { return s_pendingGold; } } /// 현재 씬에 살아 있는 던전 입구들. public static List Gates { get { return s_gates; } } // ───────────────────────────────────────────────────────────────── // 설치 — 씬 하나 없이 자동으로 걸린다(원본 훅 0줄) // ───────────────────────────────────────────────────────────────── [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] public static void Install() { if (s_installed) return; var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; s_installed = true; s_pendingGold = PlayerPrefs.GetInt(cfg.pendingPrefsKey, 0); SceneManager.sceneLoaded += OnSceneLoaded; StageEvents.StageCleared.Add(OnStageCleared); StageEvents.StageFailed.Add(OnStageFailed); StageEvents.StageExited.Add(OnStageExited); EnsureRunner(); // 이미 열려 있는 씬(에디터에서 씬을 직접 열고 Play 한 경우)도 한 번 훑는다. for (int i = 0; i < SceneManager.sceneCount; i++) { var sc = SceneManager.GetSceneAt(i); if (sc.isLoaded) OnSceneLoaded(sc, LoadSceneMode.Additive); } Log(cfg, "설치 완료 — 보류 보상 " + s_pendingGold); } static void EnsureRunner() { if (s_runner != null) return; var go = new GameObject("~WLIslandRunner"); Object.DontDestroyOnLoad(go); s_runner = go.AddComponent(); } // ───────────────────────────────────────────────────────────────── // 씬 로드 // ───────────────────────────────────────────────────────────────── static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; if (!string.IsNullOrEmpty(cfg.islandSceneName) && scene.name == cfg.islandSceneName) { IslandLoads++; EnsureRunner(); // 🔴 LoadMapMgr 는 **PCInfo.Make_Actors 보다 먼저** 있어야 한다(같은 프레임). WLIslandSceneSetup.EnsureMapNodes(cfg, scene); s_runner.StartCoroutine(Co_IslandReady(cfg, scene)); return; } var dg = cfg.FindBySceneName(scene.name); if (dg != null) { DungeonLoads++; EnsureRunner(); s_runner.StartCoroutine(WLDungeonSceneSetup.Co_Prepare(cfg, scene, dg)); } } static IEnumerator Co_IslandReady(WLIslandSettings cfg, Scene scene) { // 같은 프레임에는 FI 매니저들의 Awake 가 아직 안 끝났을 수 있다 — 한 프레임 기다린다. yield return null; WLIslandSceneSetup.FixDuplicates(cfg, scene); WLIslandSceneSetup.SwapPlayerVisual(cfg, scene); SpawnGates(cfg, scene); // 보류 보상 지급(⑥) — IslandManager 가 살아날 때까지 잠깐 기다린다. float t = 0f; while (t < 5f && !WLIslandCoin.Ready) { t += Time.unscaledDeltaTime; yield return null; } GrantPending(cfg); // 🔴 WL 전투 PC 는 Addressables 콜백으로 **나중에** 생긴다(PCInfo.cs:16) — 나타나면 그때 숨긴다. t = 0f; while (t < cfg.hidePcWatchSeconds) { if (WLIslandSceneSetup.HideWlPc(cfg)) break; t += Time.unscaledDeltaTime; yield return null; } } // ───────────────────────────────────────────────────────────────── // 던전 입구 배치 // ───────────────────────────────────────────────────────────────── static void SpawnGates(WLIslandSettings cfg, Scene scene) { s_gates.Clear(); if (cfg.dungeons == null) return; for (int i = 0; i < cfg.dungeons.Length; i++) { var d = cfg.dungeons[i]; if (d == null || d.enabled_ == 0) continue; var gate = WLDungeonGate.Create(cfg, d, scene); if (gate != null) { s_gates.Add(gate); GatesSpawned++; } } Log(cfg, "던전 입구 " + s_gates.Count + "개 배치"); } // ───────────────────────────────────────────────────────────────── // 던전 입장 / 복귀 // ───────────────────────────────────────────────────────────────── public static void EnterDungeon(WLDungeonDef d) { var cfg = WLIslandSettings.Instance; if (cfg == null || d == null) return; EnsureRunner(); WLIslandPrompt.Hide(); WLDungeonSceneSetup.Pending = d; WLIslandSceneSetup.RestoreDuplicates(); if (InGameInfo.Ins != null) { Log(cfg, "던전 입장 — " + d.displayName + " (map " + d.mapId + " · stage " + d.stageIndex + ")"); InGameInfo.Ins.Load_Map(d.mapId); } else { // 에디터에서 섬 씬만 열고 Play 한 경우(로그인 없는 검증 경로). string scn = string.IsNullOrEmpty(DebugSceneOverride) ? d.sceneName : DebugSceneOverride; Log(cfg, "던전 입장(단독) — " + scn); SceneManager.LoadScene(scn, LoadSceneMode.Single); } } /// 던전에서 섬으로 돌아간다(클리어·실패·중단 공통). public static void ReturnToIsland(string reason) { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; if (InGameInfo.Ins != null) { Log(cfg, "섬으로 복귀(" + reason + ") — 보류 보상 " + s_pendingGold); InGameInfo.Ins.Load_Map(cfg.islandMapId); } else { Log(cfg, "섬으로 복귀(단독 · " + reason + ")"); SceneManager.LoadScene(cfg.islandSceneName, LoadSceneMode.Single); } } // ───────────────────────────────────────────────────────────────── // 보상 (④ 확정 · ⑥ 지급) // ───────────────────────────────────────────────────────────────── static void OnStageCleared(in StageClearResult r) { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.rewardEnabled == 0) return; if (WLDungeonSceneSetup.Current == null) return; // 던전이 아닌 스테이지(#815 데모)는 건드리지 않는다 int gold = ComputeGold(cfg, r); SetPending(cfg, gold); ClearGrants++; Log(cfg, "클리어 보상 확정 — " + gold + " 코인 (처치 " + r.killed + "/" + r.total + " · 배수 " + MultiplierOf(r.index).ToString("F2") + ")"); } static void OnStageFailed(in StageFailedEvent e) { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; if (WLDungeonSceneSetup.Current == null) return; SetPending(cfg, cfg.goldOnFail); FailGrants++; Log(cfg, "실패(" + e.reason + ") — 보상 " + cfg.goldOnFail); } static void OnStageExited(in StageExitedEvent e) { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; if (!e.toLobby) return; if (WLDungeonSceneSetup.Current == null) return; WLDungeonSceneSetup.Current = null; ReturnToIsland(e.reason); } /// 표의 goldMultiplier(WLStageTable.cs:103) — 소비처가 없던 필드의 첫 소비처. public static float MultiplierOf(int stageIndex) { var t = WLStageTable.Instance; if (t == null) return 1f; var def = t.Get(stageIndex); if (def == null || def.goldMultiplier <= 0f) return 1f; return def.goldMultiplier; } public static int ComputeGold(WLIslandSettings cfg, in StageClearResult r) { if (!r.win) return cfg.goldOnFail; float g = cfg.goldClearBonus + cfg.goldPerKill * r.killed; if (r.isBoss) g += cfg.goldBossBonus; if (cfg.applyGoldMultiplier != 0) g *= MultiplierOf(r.index); int v = Mathf.RoundToInt(g); return v < 0 ? 0 : v; } static void SetPending(WLIslandSettings cfg, int gold) { s_pendingGold = gold < 0 ? 0 : gold; PlayerPrefs.SetInt(cfg.pendingPrefsKey, s_pendingGold); } /// 보류분을 섬 코인에 더한다. 성공하면 보류를 비운다. public static bool GrantPending(WLIslandSettings cfg) { if (cfg == null) cfg = WLIslandSettings.Instance; if (cfg == null || s_pendingGold <= 0) return false; int before; if (!WLIslandCoin.TryGet(out before)) { Log(cfg, "보상 지급 보류 — IslandManager 없음(" + s_pendingGold + ")"); return false; } if (!WLIslandCoin.Add(s_pendingGold)) return false; CoinGranted += s_pendingGold; int after; WLIslandCoin.TryGet(out after); Log(cfg, "🔴 보상 지급 — 코인 " + before + " → " + after + " (+" + s_pendingGold + ")"); s_pendingGold = 0; PlayerPrefs.SetInt(cfg.pendingPrefsKey, 0); return true; } /// /// 검증용: 클리어 1회를 **실제 계산식 그대로** 태운다(전투만 건너뛴다). /// `ComputeGold`(표의 goldMultiplier 소비) → 보류 설정까지 실기와 같은 코드가 돈다. /// public static int DebugSimulateClear(int stageIndex, int killed, bool isBoss) { var cfg = WLIslandSettings.Instance; if (cfg == null) return 0; var t = WLStageTable.Instance; var def = t != null ? t.Get(stageIndex) : null; var r = new StageClearResult { index = stageIndex, stageId = def != null ? def.id : 0, win = true, killed = killed, total = def != null ? def.TotalCount : killed, isBoss = isBoss }; int gold = ComputeGold(cfg, r); SetPending(cfg, gold); ClearGrants++; Log(cfg, "검증 클리어 — 스테이지 " + stageIndex + " · 처치 " + killed + " · 배수 " + MultiplierOf(stageIndex).ToString("F2") + " → 보류 " + gold + " 코인"); return gold; } /// 검증용: 보상분을 직접 주입한다(전투 없이 ④⑤⑥ 경로만 태운다). public static void DebugSetPending(int gold) { var cfg = WLIslandSettings.Instance; if (cfg == null) return; SetPending(cfg, gold); Log(cfg, "검증 주입 — 보류 보상 " + gold); } internal static void Log(WLIslandSettings cfg, string msg) { LastLog = msg; if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-816d] " + msg); } } /// 코루틴·입력을 도맡는 숨김 러너(새 Manager 가 아니다 · 씬에 남지 않는다). public sealed class WLIslandRunner : MonoBehaviour { void Update() { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; WLDungeonGate.TickAll(cfg); } } }