Project_WL/Assets/WL/Island/WLIslandBridge.cs

344 lines
16 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// <summary>섬 ↔ 던전 왕복. 정적 진입 + 숨김 러너 1개(코루틴·입력용).</summary>
public static class WLIslandBridge
{
// ── 진단(프로브·보고가 읽는다)
public static int IslandLoads, DungeonLoads, ClearGrants, FailGrants, CoinGranted, GatesSpawned;
public static string LastLog = "";
/// <summary>
/// 프로브 전용 — 비어 있지 않으면 단독 Play 경로에서 이 씬 이름으로 대신 들어간다.
/// 🔴 던전 복사본 씬이 아직 빌드 목록에 없어(등록은 Lead·PD 승인 사항) 로그인 없는 검증에 쓴다.
/// 실기(InGameInfo 가 있는 흐름)에는 영향이 없다 — 그쪽은 Load_Map(mapId) 를 탄다.
/// </summary>
public static string DebugSceneOverride = "";
static bool s_installed;
static int s_pendingGold;
static WLIslandRunner s_runner;
static readonly List<WLDungeonGate> s_gates = new List<WLDungeonGate>(8);
/// <summary>보류 중인 보상(섬에 도착하면 더해진다).</summary>
public static int PendingGold { get { return s_pendingGold; } }
/// <summary>현재 씬에 살아 있는 던전 입구들.</summary>
public static List<WLDungeonGate> 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<WLIslandRunner>();
}
// ─────────────────────────────────────────────────────────────────
// 씬 로드
// ─────────────────────────────────────────────────────────────────
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);
}
}
/// <summary>던전에서 섬으로 돌아간다(클리어·실패·중단 공통).</summary>
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);
}
/// <summary>표의 goldMultiplier(WLStageTable.cs:103) — 소비처가 없던 필드의 첫 소비처.</summary>
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);
}
/// <summary>보류분을 섬 코인에 더한다. 성공하면 보류를 비운다.</summary>
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;
}
/// <summary>
/// 검증용: 클리어 1회를 **실제 계산식 그대로** 태운다(전투만 건너뛴다).
/// `ComputeGold`(표의 goldMultiplier 소비) → 보류 설정까지 실기와 같은 코드가 돈다.
/// </summary>
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;
}
/// <summary>검증용: 보상분을 직접 주입한다(전투 없이 ④⑤⑥ 경로만 태운다).</summary>
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);
}
}
/// <summary>코루틴·입력을 도맡는 숨김 러너(새 Manager 가 아니다 · 씬에 남지 않는다).</summary>
public sealed class WLIslandRunner : MonoBehaviour
{
void Update()
{
var cfg = WLIslandSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
WLDungeonGate.TickAll(cfg);
}
}
}