279 lines
15 KiB
C#
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(); }
|
|
}
|
|
}
|