[WL-816d] 섬 카메라 소유권 + WL PC 비동기 생성 대응 + NavMesh 기본값 사용 (#816)

- NavMeshSurface 기본값(collectObjects=All · useGeometry=RenderMeshes)을 그대로 쓴다(게이트 PASS)
- 섬 카메라 소유권 SO(islandCameraOwner 기본 1 = FI 카메라) · 떠날 때 되살림
- WL 전투 PC 는 Addressables 콜백으로 늦게 생긴다 → hidePcWatchSeconds 동안 감시해 숨김
- 프로브 전용 DebugSceneOverride(던전 복사본 씬 미등록 구간 검증용)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-13 12:34:24 +09:00
parent c23fdeb91a
commit 6fa7a421d3
6 changed files with 106 additions and 15 deletions

View File

@ -19,6 +19,8 @@ MonoBehaviour:
islandSceneName: Level01
hideWlPcOnIsland: 1
fixIslandDuplicates: 1
islandCameraOwner: 1
hidePcWatchSeconds: 6
islandSaveKey:
swapIslandPlayer: 1
pcPrefabPath: Assets/Res_Addr/PC/LH_M05.prefab

View File

@ -20,7 +20,6 @@ using System.Collections.Generic;
using TMPro;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
using WL.Combat.Stage;
@ -100,8 +99,8 @@ namespace WL.Island
var go = new GameObject("~WL_DungeonNavMesh");
SceneManager.MoveGameObjectToScene(go, scene);
var surf = go.AddComponent<NavMeshSurface>();
surf.collectObjects = CollectObjects.All;
surf.useGeometry = NavMeshCollectGeometry.RenderMeshes; // 아레나 바닥에 콜라이더가 없을 수 있다
// collectObjects = All · useGeometry = RenderMeshes 는 NavMeshSurface 기본값 그대로 쓴다
// (아레나 바닥에 콜라이더가 없어도 렌더 메시로 베이크된다 · 패키지 실측 NavMeshSurface.cs:54·66)
surf.BuildNavMesh();
NavBakes++;
WLIslandBridge.Log(cfg, "던전 NavMesh 런타임 베이크 완료");

View File

@ -35,6 +35,13 @@ namespace WL.Island
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;
@ -123,6 +130,15 @@ namespace WL.Island
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;
}
}
// ─────────────────────────────────────────────────────────────────
@ -153,6 +169,7 @@ namespace WL.Island
EnsureRunner();
WLDungeonSceneSetup.Pending = d;
WLIslandSceneSetup.RestoreDuplicates();
if (InGameInfo.Ins != null)
{
@ -162,8 +179,9 @@ namespace WL.Island
else
{
// 에디터에서 섬 씬만 열고 Play 한 경우(로그인 없는 검증 경로).
Log(cfg, "던전 입장(단독) — " + d.sceneName);
SceneManager.LoadScene(d.sceneName, LoadSceneMode.Single);
string scn = string.IsNullOrEmpty(DebugSceneOverride) ? d.sceneName : DebugSceneOverride;
Log(cfg, "던전 입장(단독) — " + scn);
SceneManager.LoadScene(scn, LoadSceneMode.Single);
}
}
@ -268,6 +286,33 @@ namespace WL.Island
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)
{

View File

@ -76,26 +76,63 @@ namespace WL.Island
{
if (cfg.fixIslandDuplicates == 0) return;
// WL 전투 PC 는 섬에서 숨긴다(플레이어 2명 방지) — 던전에 가면 다시 만들어진다.
if (cfg.hideWlPcOnIsland != 0 && MyValue.bMyPC && MyValue.MyPC != null)
{
MyValue.MyPC.gameObject.SetActive(false);
PcHidden++;
}
HideWlPc(cfg);
// FI 카메라·AudioListener 중복 — Additive 로 붙었을 때만 정리한다.
// · 섬 씬만 단독으로 열려 있으면(검증 Play) FI 카메라가 유일한 카메라라 끄면 안 된다.
if (SceneManager.sceneCount <= 1) return;
// 카메라 소유권 — 섬은 FarmingIsland 처럼 보여야 한다(PD) → 기본은 FI 카메라가 이긴다.
if (cfg.islandCameraOwner != 0)
{
bool fiWins = cfg.islandCameraOwner == 1;
var cams = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None);
for (int i = 0; i < cams.Length; i++)
{
var c = cams[i];
if (c == null) continue;
bool inIsland = c.gameObject.scene == scene;
bool want = fiWins ? inIsland : !inIsland;
if (c.enabled == want) continue;
if (!want) s_disabledCams.Add(c); // 섬을 떠날 때 되살린다
c.enabled = want;
CamerasFixed++;
}
}
var listeners = Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None);
int alive = 0;
for (int i = 0; i < listeners.Length; i++)
{
if (listeners[i] == null || !listeners[i].enabled) continue;
alive++;
if (alive > 1 && listeners[i].gameObject.scene == scene) { listeners[i].enabled = false; ListenersFixed++; }
if (alive > 1) { listeners[i].enabled = false; s_disabledListeners.Add(listeners[i]); ListenersFixed++; }
}
WLIslandBridge.Log(cfg, "중복 정리 — PC 숨김 " + PcHidden + " · AudioListener 해제 " + ListenersFixed);
WLIslandBridge.Log(cfg, "중복 정리 — PC 숨김 " + PcHidden + " · 카메라 " + CamerasFixed +
" · AudioListener 해제 " + ListenersFixed);
}
static readonly List<Camera> s_disabledCams = new List<Camera>(4);
static readonly List<AudioListener> s_disabledListeners = new List<AudioListener>(4);
/// <summary>섬을 떠날 때 우리가 끈 것만 되살린다(C8 — 원래대로).</summary>
public static void RestoreDuplicates()
{
for (int i = 0; i < s_disabledCams.Count; i++) if (s_disabledCams[i] != null) s_disabledCams[i].enabled = true;
for (int i = 0; i < s_disabledListeners.Count; i++) if (s_disabledListeners[i] != null) s_disabledListeners[i].enabled = true;
s_disabledCams.Clear();
s_disabledListeners.Clear();
}
/// <summary>WL 전투 PC 를 섬에서 숨긴다(플레이어 2명 방지). 던전에 가면 다시 만들어진다.</summary>
public static bool HideWlPc(WLIslandSettings cfg)
{
if (cfg.hideWlPcOnIsland == 0) return true;
if (!MyValue.bMyPC || MyValue.MyPC == null) return false;
if (!MyValue.MyPC.gameObject.activeSelf) return true;
MyValue.MyPC.gameObject.SetActive(false);
PcHidden++;
return true;
}
// ─────────────────────────────────────────────────────────────────

View File

@ -109,6 +109,14 @@ namespace WL.Island
[Tooltip("섬 씬의 FI 카메라/AudioListener 중복을 정리한다(Additive 로 붙었을 때만).")]
public int fixIslandDuplicates = 1;
[Tooltip("섬에서 어느 카메라가 화면을 그릴지. 0 = 아무것도 안 건드림(카메라 2대 = 겹쳐 그려짐) · " +
"1 = FI 카메라가 이긴다(섬을 FarmingIsland 처럼 · 기본) · 2 = WL 카메라가 이긴다. " +
"🔴 Additive 경로는 로그인이 필요해 실측하지 못했다 — 실기 첫 실행에서 확인할 값.")]
public int islandCameraOwner = 1;
[Tooltip("WL 전투 PC 는 Addressables 로 비동기 생성된다 — 이 초 동안 지켜보며 나타나면 숨긴다.")]
public float hidePcWatchSeconds = 6f;
[Tooltip("FI 세이브 파일 이름을 이 값으로 고정한다(빈 문자열 = 원본 그대로 = 활성 씬 이름). " +
"816c §A-3: 섹터마다 세이브가 갈리는 문제의 최소 수정 — 다만 원본 0줄 원칙 때문에 이번엔 기본 빈칸(미적용).")]
public string islandSaveKey = "";

View File

@ -40,8 +40,8 @@ namespace WL.Island
if (LitShader == null) return null;
m = new Material(LitShader) { name = "WL_GateDummy", hideFlags = HideFlags.DontSave };
m.SetColor("_BaseColor", c);
m.SetColor("_Color", c);
if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", c);
if (m.HasProperty("_Color")) m.SetColor("_Color", c);
if (m.HasProperty("_Smoothness")) m.SetFloat("_Smoothness", 0.15f);
s_cache[key] = m;
return m;