377 lines
18 KiB
C#
377 lines
18 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; } }
|
|
|
|
/// <summary>씬을 넘어 사는 코루틴 러너(WL-816g 의 발판 흐름도 이것을 쓴다).</summary>
|
|
public static WLIslandRunner Runner { get { EnsureRunner(); return s_runner; } }
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// 설치 — 씬 하나 없이 자동으로 걸린다(원본 훅 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);
|
|
// WL-816g — 밭을 숨기고 그 자리에 「골드 납품 발판」 1개를 놓는다(꺼져 있으면 무동작).
|
|
WLIslandGateFlow.Apply(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;
|
|
|
|
// 🔴 WL-816g: 초기에는 던전이 **하나도** 보이지 않는다. 발판에서 골드를 낸 만큼만 열린다.
|
|
// `gateFlowEnabled = 0` 이면 UnlockedCount 가 int.MaxValue = 지금까지처럼 전부 배치.
|
|
int limit = WLIslandGateFlow.UnlockedCount(cfg);
|
|
int made = 0;
|
|
|
|
for (int i = 0; i < cfg.dungeons.Length; i++)
|
|
{
|
|
var d = cfg.dungeons[i];
|
|
if (d == null || d.enabled_ == 0) continue;
|
|
if (made >= limit) break;
|
|
var gate = WLDungeonGate.Create(cfg, d, scene);
|
|
if (gate != null) { s_gates.Add(gate); GatesSpawned++; made++; }
|
|
}
|
|
Log(cfg, "던전 입구 " + s_gates.Count + "개 배치(해금 " +
|
|
(limit == int.MaxValue ? "전체" : limit.ToString()) + ")");
|
|
}
|
|
|
|
/// <summary>발판에서 값을 치렀을 때 던전 입구 1개를 추가로 켠다(표 한 행 = 던전 하나).</summary>
|
|
public static void SpawnOneGate(WLIslandSettings cfg, WLDungeonDef d, Scene scene)
|
|
{
|
|
if (cfg == null || d == null) return;
|
|
for (int i = 0; i < s_gates.Count; i++)
|
|
if (s_gates[i] != null && s_gates[i].Def == d) return; // 이미 있다
|
|
|
|
var gate = WLDungeonGate.Create(cfg, d, scene);
|
|
if (gate == null) return;
|
|
s_gates.Add(gate); GatesSpawned++;
|
|
Log(cfg, "던전 입구 추가 — " + d.displayName + " @ " + d.gatePosition.ToString("F1"));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// 던전 입장 / 복귀
|
|
// ─────────────────────────────────────────────────────────────────
|
|
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;
|
|
}
|
|
|
|
/// <summary>보류 보상을 비운다(WL-816g 세이브 초기화가 부른다 — 초기 골드가 10 이 되도록).</summary>
|
|
public static void ClearPending()
|
|
{
|
|
s_pendingGold = 0;
|
|
var cfg = WLIslandSettings.Instance;
|
|
if (cfg != null) PlayerPrefs.SetInt(cfg.pendingPrefsKey, 0);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|