Project_WL/Assets/WL/Look/Toggle/LookModeHub.cs

279 lines
15 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// LookModeHub.cs — 인게임 룩 모드 허브: 카메라 모드 순환 + 배경 룩 플래그 (WL-814d · #814)
//
// PD 제안(2026-09-10) 「토글 형태로 카메라를 바꿔서 볼 수 있게」 · 발주서 WL-814d §1-1·§1-2·§1-4.
//
// ■ 이 파일이 하는 것 — 원본 훅 0 · 새 Manager/Singleton 0(정적 + 러너 GameObject 1개)
// ① 앱 세션 동안 「지금 어떤 룩인가」를 들고 있는다(CameraMode 0~N · EnvLookOn).
// ② 모드가 바뀌면 값을 **두 기존 기능에 런타임으로 얹는다**(둘 다 이번에 추가한 API · 기존 경로 무변경):
// · 811o `CameraFramingOverride.SetRuntimePreset(h, d, up)` / `ClearRuntimePreset()`
// → 원본 `RealCamera.arr_cCamData[0]` 구도를 SO 값 대신 프리셋 값으로 (폴링 0.25 s 안에 반영 ·
// 킬캠/보스 캠이 도는 동안에는 그 뒤로 미뤄진다 = 811o 의 SkippedBusy 규칙 그대로)
// · 814b `PixelCameraRig.SetRuntimeOverride(on, orthoSize, pixelHeight)`
// → SO `enabled 0`(F-23) 이어도 on 이면 리그를 조립·유지, off 면 해체
// ③ 배경 룩(814c)은 **플래그 + 이벤트만** 여기서 든다 — 렌더러/에셋은 814c 소유라 이 태스크가 건드리지 않는다.
// 814c `EnvLook` 이 `EnvLookChanged` 를 구독하면 그대로 이어진다(발주서 §1-4 · 완료보고 §연동 규약).
//
// ■ 왜 「값을 얹기」인가
// 814b 리그와 811o 구도는 각자 SO 를 갖고 각자 맵 진입 경계에서 값을 적용한다. 허브가 카메라를 직접
// 만지면 그 둘과 싸운다. 그래서 허브는 **두 기능의 입력값만 바꾸고** 적용 시점·복원은 원래 주인에게 맡긴다.
// → 맵 재진입·런 재시작에서 리그 재조립·구도 재적용이 저절로 일어난다(각 기능의 PollBoundary).
//
// ■ 로비/타이틀
// `defaultInLobby` 가 켜져 있으면 로비에서는 **적용 모드만** 0 으로 내리고 고른 모드(CameraMode)는 그대로 둔다
// → 인게임에 다시 들어가면 저절로 복원된다. (814b 리그도 `assembleInLobby 0` 이라 로비에서 스스로 걷는다.)
//
// ■ GC 0 — 틱은 비교뿐이다(문자열·람다·클로저 0). 로그 문자열은 만들기 전에 verboseLog 로 막는다(811b FIX-3).
//
// ■ C8 롤백 — `WLLookModeSettings.enabled_ = 0` → 적용 모드 0 · 신규 API 호출 0 = 814b/811o 원래 동작 100 %.
//
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.CameraFraming;
using WL.PixelArt;
namespace WL.Look.Toggle
{
/// <summary>룩 모드(카메라 프리셋 + 배경 룩) 허브. 정적 · 상태는 앱 세션 동안 유지된다.</summary>
public static class LookModeHub
{
static WLLookModeSettings St { get { return WLLookModeSettings.Instance; } }
// ── 상태 ──────────────────────────────────────────────────────────────
static int s_mode = -1; // PD 가 고른 모드(로비에 가도 유지)
static int s_applied = 0; // 실제로 두 기능에 얹혀 있는 모드
static bool s_env;
static bool s_booted;
/// <summary>PD 가 고른 카메라 모드(0 = 기본 원근 · 1~ = 도트 프리셋). 로비에 가도 이 값은 그대로.</summary>
public static int CameraMode { get { EnsureBoot(); return s_mode; } }
/// <summary>지금 실제로 카메라에 얹혀 있는 모드(로비에서는 0 으로 내려간다).</summary>
public static int AppliedMode { get { return s_applied; } }
/// <summary>배경 룩(814c) on/off 플래그 — 이 태스크는 플래그와 이벤트만 든다.</summary>
public static bool EnvLookOn { get { EnsureBoot(); return s_env; } }
/// <summary>지금 모드가 도트(직교 리그) 모드인가.</summary>
public static bool IsPixelMode
{
get { var p = St != null ? St.Preset(CameraMode) : null; return p != null && p.pixelRig; }
}
/// <summary>버튼에 찍을 지금 모드 이름.</summary>
public static string CameraModeName { get { var s = St; return s != null ? s.ModeName(CameraMode) : "기본"; } }
// ── 이벤트 (814c · UI 가 구독한다 · 인자 = 바뀐 값) ────────────────────
/// <summary>카메라 모드가 바뀌었다(PD 선택 기준 · 로비 강등은 알리지 않는다).</summary>
public static event System.Action<int> CameraModeChanged;
/// <summary>배경 룩 플래그가 바뀌었다 — 🔴 814c `EnvLook` 이 여기에 붙는다(발주서 §1-4).</summary>
public static event System.Action<bool> EnvLookChanged;
/// <summary>UI(WLLookToggleUi)가 매 프레임 얻어가는 훅 — 러너를 두 개 만들지 않기 위한 배선.</summary>
public static System.Action UiTick;
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
public static int Applies, ModeChanges, EnvChanges, LobbyDemotes, Restores;
public static string LastLog = "";
// ─────────────────────────────────────────────────────────────────────
// 공개 조작
// ─────────────────────────────────────────────────────────────────────
/// <summary>모드를 고른다(범위를 넘으면 순환). 같은 모드면 아무 것도 하지 않는다.</summary>
public static string SetCameraMode(int mode)
{
EnsureBoot();
var s = St;
if (s == null) return "WLLookModeSettings 에셋 없음";
int m = s.Wrap(mode);
if (m == s_mode) return "모드 변화 없음(" + m + ")";
s_mode = m;
ModeChanges++;
if (s.envFollowsCameraMode)
{
var p = s.Preset(m);
if (p != null) SetEnvLook(p.envLookOn);
}
var cb = CameraModeChanged;
if (cb != null) cb(m);
return Push(WantedMode(), "모드 선택");
}
/// <summary>다음 모드로 순환한다(버튼이 부르는 것 — 0 → 1 → 2 → 3 → 0).</summary>
public static string NextCameraMode() { return SetCameraMode(CameraMode + 1); }
/// <summary>배경 룩 플래그를 바꾼다(814c 가 구독한다). 이 태스크에서 렌더러는 건드리지 않는다.</summary>
public static string SetEnvLook(bool on)
{
EnsureBoot();
if (on == s_env) return "배경 룩 변화 없음(" + on + ")";
s_env = on;
EnvChanges++;
var cb = EnvLookChanged;
if (cb != null) cb(on);
return Log("배경 룩 " + (on ? "ON" : "OFF"));
}
/// <summary>배경 룩을 뒤집는다(버튼이 부르는 것).</summary>
public static string ToggleEnvLook() { return SetEnvLook(!EnvLookOn); }
// ─────────────────────────────────────────────────────────────────────
// 틱 · 적용
// ─────────────────────────────────────────────────────────────────────
/// <summary>러너가 매 프레임 부른다(에디트 모드에서는 프로브가 직접 부를 수 있다). 비교뿐 = GC 0.</summary>
public static void Tick()
{
var s = St;
if (s == null || !WLLookModeSettings.Enabled)
{
if (s_applied != 0) Push(0, "off"); // C8 — 즉시 원래 동작으로
return;
}
EnsureBoot();
int want = WantedMode();
if (want != s_applied) Push(want, want == 0 ? "로비" : "인게임");
var ui = UiTick;
if (ui != null) ui();
}
/// <summary>지금 얹혀야 하는 모드 — 로비/타이틀에서는 0(고른 모드는 그대로 둔다).</summary>
static int WantedMode()
{
var s = St;
if (s == null) return 0;
if (!s.defaultInLobby) return s_mode;
return InLobby ? 0 : s_mode;
}
/// <summary>로비·타이틀인가. `InGameInfo.Ins` 는 순수 static 필드라 에디트 모드에서도 부작용 0(실측).</summary>
public static bool InLobby
{
get
{
var info = InGameInfo.Ins;
if (DSUtil.CheckNull(info)) return true; // 아직 인게임이 아니다
return info.Get_GameMode() == eGameMode.Lobby;
}
}
/// <summary>모드 값을 두 기존 기능에 얹는다(적용 시점·복원은 그 둘이 원래 하던 대로).</summary>
static string Push(int mode, string why)
{
var s = St;
if (s == null) return "WLLookModeSettings 에셋 없음";
bool on = WLLookModeSettings.Enabled;
var p = on ? s.Preset(mode) : null;
if (!on) mode = 0;
if (p == null || p.useSoFraming) CameraFramingOverride.ClearRuntimePreset();
else CameraFramingOverride.SetRuntimePreset(p.cameraHeight, p.distance, p.distanceUp);
string rig = p != null && p.pixelRig
? PixelCameraRig.SetRuntimeOverride(true, p.orthoSize, p.pixelHeight)
: PixelCameraRig.SetRuntimeOverride(false, 0f, 0);
if (mode == 0 && s_applied != 0) Restores++;
if (mode == 0 && why == "로비") LobbyDemotes++;
s_applied = mode;
Applies++;
if (s.verboseLog)
return Log("모드 " + mode + "(" + s.ModeName(mode) + ") 적용 — " + why + " · 리그: " + rig);
LastLog = why;
return "모드 " + mode + " 적용(" + why + ")";
}
static void EnsureBoot()
{
if (s_booted) return;
s_booted = true;
var s = St;
s_mode = s != null ? s.Wrap(s.startMode) : 0;
s_env = s != null && s.envLookOnAtStart;
}
static string Log(string msg)
{
LastLog = msg;
var s = St;
if (s != null && s.verboseLog) Debug.Log("[LookMode] " + msg);
return msg;
}
// ─────────────────────────────────────────────────────────────────────
// 프로브 · 러너
// ─────────────────────────────────────────────────────────────────────
/// <summary>프로브: 상태·카운터를 시작값으로 되돌리고 두 기능의 오버라이드도 해제한다.</summary>
public static string ResetAll()
{
s_booted = false;
EnsureBoot();
s_applied = -1;
Applies = ModeChanges = EnvChanges = LobbyDemotes = Restores = 0;
LastLog = "";
CameraFramingOverride.ClearRuntimePreset();
PixelCameraRig.SetRuntimeOverride(false, 0f, 0);
s_applied = 0;
return "리셋 — 모드 " + s_mode + " · 배경 " + s_env;
}
/// <summary>프로브: 로비 판정을 건너뛰고 지금 고른 모드를 그대로 얹는다.</summary>
public static string ForceApply() { EnsureBoot(); return Push(s_mode, "probe"); }
public static string Dump()
{
var s = St;
return "[LookMode] 고른모드=" + s_mode + "(" + (s != null ? s.ModeName(s_mode) : "-") + ")"
+ " 적용모드=" + s_applied + " 배경룩=" + s_env
+ " · 리그조립=" + PixelCameraRig.IsAssembled + " 리그오버라이드=" + PixelCameraRig.RuntimeOverrideOn
+ " · 구도적용=" + CameraFramingOverride.IsApplied
+ " 런타임프리셋=" + CameraFramingOverride.RuntimePresetOn
+ "(h " + CameraFramingOverride.AppliedHeight.ToString("0.##")
+ " · d " + CameraFramingOverride.AppliedDistance.ToString("0.##")
+ " · up " + CameraFramingOverride.AppliedUp.ToString("0.##") + ")"
+ " · 적용 " + Applies + "회 · 모드변경 " + ModeChanges + " · 배경변경 " + EnvChanges
+ " · 로비강등 " + LobbyDemotes;
}
static GameObject s_runner;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (s_runner != null) return;
if (!WLLookModeSettings.Enabled) return; // C8 — 러너 자체가 뜨지 않는다
EnsureBoot();
s_runner = new GameObject("[WL814d] LookModeRunner");
s_runner.hideFlags = HideFlags.HideAndDontSave;
s_runner.AddComponent<LookModeRunner>();
Object.DontDestroyOnLoad(s_runner);
}
internal static void OnRunnerGone()
{
CameraFramingOverride.ClearRuntimePreset();
PixelCameraRig.SetRuntimeOverride(false, 0f, 0);
s_applied = 0;
}
}
/// <summary>룩 토글의 시간 축(허브 + UI). GameObject 1개 · 코루틴 0.</summary>
internal sealed class LookModeRunner : MonoBehaviour
{
void Update() { LookModeHub.Tick(); }
void OnApplicationQuit() { LookModeHub.OnRunnerGone(); }
void OnDestroy() { LookModeHub.OnRunnerGone(); }
}
}