- PoseIconStage: permanent editor capture pipeline (context menu 'Capture All Icons') — renders each entry at 4096x4096, alpha-crops, writes Icon_pose_*.png and wires sprites onto matching PlayerPose assets - PlayerPose: new iconScale property (0.1-2), applied to the icon child in PoseMenu's radial layout - OverviewMenu: paint-mode special-case removed — Esc/Tab opens the overview menu in every mode Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
286 lines
12 KiB
C#
286 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
#if ENABLE_INPUT_SYSTEM
|
|
using UnityEngine.InputSystem;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// The Esc overview menu (the "Overview UI" canvas in Main). Owns ALL Escape handling (Tab acts as an
|
|
/// alternative to Esc): Esc toggles
|
|
/// this panel as a modal pause overlay (gameplay suspended via <see cref="GameModeManager.SetPaused"/>);
|
|
/// while the session browser, match results or pose menu overlay is open instead, Esc closes that. Buttons
|
|
/// switch game modes (via <see cref="GameModeManager.TrySwitchTo{T}"/>) or open the session browser /
|
|
/// match results overlay, and every click closes the panel. Mode buttons are only shown for modes present in the current role-scoped set
|
|
/// (<see cref="GameModeManager.Modes"/>); the active mode's button is disabled. Hotkeys mirror the
|
|
/// buttons: C toggles the pose menu (or cycles the pose directly with 2 or fewer poses), V switches to
|
|
/// paint mode (E = taunt lives in GameModeManager). Attach to the
|
|
/// always-active canvas root; <see cref="panelRoot"/> is the toggled visual child.
|
|
/// </summary>
|
|
public class OverviewMenu : MonoBehaviour
|
|
{
|
|
[Tooltip("Mode manager driving pause + mode switches. Empty = found automatically.")]
|
|
[SerializeField] private GameModeManager gameMode;
|
|
|
|
[Tooltip("Session browser binder. While its overlay is open it owns Esc (close-only) and this menu " +
|
|
"stays closed. Empty = found automatically.")]
|
|
[SerializeField] private OnlineSessionBrowserUI sessionBrowser;
|
|
|
|
[Tooltip("Match results screen binder. While its overlay is open it owns Esc (close-only) and this " +
|
|
"menu stays closed. Empty = found automatically.")]
|
|
[SerializeField] private MatchResultScreen matchResultScreen;
|
|
|
|
[Tooltip("Pose selection menu binder. While its overlay is open it owns Esc (close-only) and this " +
|
|
"menu stays closed. Empty = found automatically.")]
|
|
[SerializeField] private PoseMenu poseMenu;
|
|
|
|
[Header("Panel")]
|
|
[Tooltip("Visual container toggled by Esc. Must be a CHILD of this binder's object so the binder " +
|
|
"keeps reading Esc while the panel is hidden.")]
|
|
[SerializeField] private GameObject panelRoot;
|
|
|
|
[Header("Buttons")]
|
|
[SerializeField] private Button sessionButton;
|
|
[SerializeField] private Button matchResultButton;
|
|
[Tooltip("Enters Player Control (third-person orbit) mode.")]
|
|
[SerializeField] private Button playerControlButton;
|
|
[SerializeField] private Button paintButton;
|
|
[SerializeField] private Button firstPersonButton;
|
|
[SerializeField] private Button freeCamButton;
|
|
[Tooltip("Triggers the hider's manual radar ping (same as the E key) and closes the menu. Only shown " +
|
|
"for a hider during Seek.")]
|
|
[SerializeField] private Button tauntButton;
|
|
[Tooltip("Opens the pose selection menu (or cycles the pose directly with 2 or fewer poses). Only " +
|
|
"shown while the local player has selectable poses.")]
|
|
[SerializeField] private Button poseButton;
|
|
|
|
private bool _open;
|
|
|
|
private void Awake()
|
|
{
|
|
if (!gameMode) gameMode = FindFirstObjectByType<GameModeManager>();
|
|
if (!sessionBrowser) sessionBrowser = FindFirstObjectByType<OnlineSessionBrowserUI>();
|
|
if (!matchResultScreen) matchResultScreen = FindFirstObjectByType<MatchResultScreen>();
|
|
if (!poseMenu) poseMenu = FindFirstObjectByType<PoseMenu>();
|
|
if (panelRoot) panelRoot.SetActive(false);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (sessionButton) sessionButton.onClick.AddListener(OnSession);
|
|
if (matchResultButton) matchResultButton.onClick.AddListener(OnMatchResult);
|
|
if (playerControlButton) playerControlButton.onClick.AddListener(OnPlayerControl);
|
|
if (paintButton) paintButton.onClick.AddListener(OnPaint);
|
|
if (firstPersonButton) firstPersonButton.onClick.AddListener(OnFirstPerson);
|
|
if (freeCamButton) freeCamButton.onClick.AddListener(OnFreeCam);
|
|
if (tauntButton) tauntButton.onClick.AddListener(OnTaunt);
|
|
if (poseButton) poseButton.onClick.AddListener(OnPose);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (sessionButton) sessionButton.onClick.RemoveListener(OnSession);
|
|
if (matchResultButton) matchResultButton.onClick.RemoveListener(OnMatchResult);
|
|
if (playerControlButton) playerControlButton.onClick.RemoveListener(OnPlayerControl);
|
|
if (paintButton) paintButton.onClick.RemoveListener(OnPaint);
|
|
if (firstPersonButton) firstPersonButton.onClick.RemoveListener(OnFirstPerson);
|
|
if (freeCamButton) freeCamButton.onClick.RemoveListener(OnFreeCam);
|
|
if (tauntButton) tauntButton.onClick.RemoveListener(OnTaunt);
|
|
if (poseButton) poseButton.onClick.RemoveListener(OnPose);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
bool browserOpen = sessionBrowser && sessionBrowser.IsOpen;
|
|
bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen;
|
|
bool poseOpen = poseMenu && poseMenu.IsOpen;
|
|
|
|
if (_open)
|
|
{
|
|
// Another overlay took over (results Resolution auto-open) -> hide this panel; the pause
|
|
// is that overlay's now, so don't release it.
|
|
if (browserOpen || resultsOpen || poseOpen)
|
|
HidePanel();
|
|
// An external state reset (respawn/bind, disconnect) dropped the pause underneath the
|
|
// open panel -> don't linger as a stale overlay over live gameplay.
|
|
else if (gameMode && !gameMode.IsPaused)
|
|
HidePanel();
|
|
}
|
|
|
|
if (HandleHotkeys(browserOpen, resultsOpen, poseOpen)) return;
|
|
|
|
if (!EscapePressed()) return;
|
|
|
|
if (browserOpen)
|
|
sessionBrowser.Close();
|
|
else if (resultsOpen)
|
|
matchResultScreen.Close();
|
|
else if (poseOpen)
|
|
poseMenu.Close();
|
|
else if (_open)
|
|
Close(resume: true);
|
|
// Loading (joining a session, or the pre-match Loading barrier): the scene/avatar is mid-setup,
|
|
// pausing or switching modes here corrupts the camera rigs - Esc/Tab do nothing.
|
|
else if (MatchManager.Instance && MatchManager.Instance.IsLoadingLocally)
|
|
{ }
|
|
// No active/paused mode = the inert no-player state (pre-lobby Main, where the session panel
|
|
// is always shown) - nothing to pause, don't open.
|
|
else if (gameMode && gameMode.EffectiveMode != null)
|
|
Open();
|
|
}
|
|
|
|
private void Open()
|
|
{
|
|
_open = true;
|
|
if (panelRoot) panelRoot.SetActive(true);
|
|
RefreshButtons();
|
|
if (gameMode) gameMode.SetPaused(true);
|
|
}
|
|
|
|
/// <summary>Hides the panel; <paramref name="resume"/> = false hands the pause over (session
|
|
/// browser opening) instead of resuming gameplay.</summary>
|
|
private void Close(bool resume)
|
|
{
|
|
HidePanel();
|
|
if (resume && gameMode) gameMode.SetPaused(false);
|
|
}
|
|
|
|
private void HidePanel()
|
|
{
|
|
_open = false;
|
|
if (panelRoot) panelRoot.SetActive(false);
|
|
}
|
|
|
|
private void OnSession()
|
|
{
|
|
Close(resume: false);
|
|
// Not connected (defensive - the menu should not be openable then): don't strand the pause.
|
|
if (!(sessionBrowser && sessionBrowser.Open()) && gameMode)
|
|
gameMode.SetPaused(false);
|
|
}
|
|
|
|
private void OnMatchResult()
|
|
{
|
|
Close(resume: false);
|
|
if (!(matchResultScreen && matchResultScreen.Open()) && gameMode)
|
|
gameMode.SetPaused(false);
|
|
}
|
|
|
|
private void OnPose()
|
|
{
|
|
// Small pose set (hunter): no radial menu, the button/hotkey just cycles to the next pose.
|
|
if (poseMenu && poseMenu.CyclesDirectly)
|
|
{
|
|
Close(resume: true);
|
|
poseMenu.CyclePose();
|
|
return;
|
|
}
|
|
Close(resume: false);
|
|
if (!(poseMenu && poseMenu.Open()) && gameMode)
|
|
gameMode.SetPaused(false);
|
|
}
|
|
|
|
private void OnTaunt()
|
|
{
|
|
Close(resume: true);
|
|
if (MatchManager.Instance) MatchManager.Instance.RequestManualPing();
|
|
}
|
|
|
|
private void OnPlayerControl() => SwitchMode<PlayerControlMode>();
|
|
private void OnPaint() => SwitchMode<PaintMode>();
|
|
private void OnFirstPerson() => SwitchMode<FirstPersonMode>();
|
|
private void OnFreeCam() => SwitchMode<FreeCamMode>();
|
|
|
|
private void SwitchMode<T>() where T : GameMode
|
|
{
|
|
// Resume first so the switch runs on a live cycle (SwitchTo on a paused manager would fight
|
|
// the pause bookkeeping); picking the already-active mode is a no-op inside SwitchTo.
|
|
Close(resume: true);
|
|
if (gameMode) gameMode.TrySwitchTo<T>();
|
|
}
|
|
|
|
/// <summary>Shows only the buttons whose mode exists in the current role-scoped set and disables
|
|
/// the active mode's button. Ran on every open - the set only changes with a (re)bind, which
|
|
/// force-closes the panel anyway.</summary>
|
|
private void RefreshButtons()
|
|
{
|
|
if (!gameMode) return;
|
|
IReadOnlyList<GameMode> modes = gameMode.Modes;
|
|
GameMode active = gameMode.EffectiveMode;
|
|
|
|
// Match results only exist from the first Hide phase on - hide the button until then
|
|
// (same pattern as the role-scoped mode buttons).
|
|
if (matchResultButton)
|
|
matchResultButton.gameObject.SetActive(matchResultScreen && matchResultScreen.HasResults);
|
|
|
|
// Taunt/ping only does something for a hider during Seek (the server re-gates found hiders) -
|
|
// hide it otherwise, same pattern as the role-scoped mode buttons.
|
|
if (tauntButton)
|
|
tauntButton.gameObject.SetActive(gameMode.LocalPlayerIsHider && MatchManager.Instance &&
|
|
MatchManager.Instance.LocalSubPhase == MatchManager.MatchSubPhase.Seek);
|
|
|
|
// Pose selection needs a bound local player with poses - hide the button otherwise.
|
|
if (poseButton)
|
|
poseButton.gameObject.SetActive(poseMenu && poseMenu.CanOpen);
|
|
|
|
SetModeButton<PlayerControlMode>(playerControlButton, modes, active);
|
|
SetModeButton<PaintMode>(paintButton, modes, active);
|
|
SetModeButton<FirstPersonMode>(firstPersonButton, modes, active);
|
|
SetModeButton<FreeCamMode>(freeCamButton, modes, active);
|
|
}
|
|
|
|
private static void SetModeButton<T>(Button button, IReadOnlyList<GameMode> modes, GameMode active)
|
|
where T : GameMode
|
|
{
|
|
if (!button) return;
|
|
bool present = false;
|
|
for (int i = 0; i < modes.Count; i++)
|
|
if (modes[i] is T) { present = true; break; }
|
|
button.gameObject.SetActive(present);
|
|
button.interactable = !(active is T);
|
|
}
|
|
|
|
/// <summary>Hotkeys mirroring the overlay buttons: C = pose menu (toggle), V = paint mode.
|
|
/// Same gating as the buttons: inert while another overlay owns input, while loading, and in the
|
|
/// no-player state. Returns true when a hotkey acted this frame.</summary>
|
|
private bool HandleHotkeys(bool browserOpen, bool resultsOpen, bool poseOpen)
|
|
{
|
|
if (browserOpen || resultsOpen) return false;
|
|
if (MatchManager.Instance && MatchManager.Instance.IsLoadingLocally) return false;
|
|
if (!gameMode || gameMode.EffectiveMode == null) return false;
|
|
|
|
#if ENABLE_INPUT_SYSTEM
|
|
bool posePressed = InputUtil.GameplayKeyPressed(Key.C);
|
|
bool paintPressed = InputUtil.GameplayKeyPressed(Key.V);
|
|
#else
|
|
bool posePressed = InputUtil.GameplayKeyPressed(KeyCode.C);
|
|
bool paintPressed = InputUtil.GameplayKeyPressed(KeyCode.V);
|
|
#endif
|
|
|
|
if (posePressed)
|
|
{
|
|
if (poseOpen) poseMenu.Close();
|
|
else if (poseMenu && poseMenu.CanOpen) OnPose();
|
|
return true;
|
|
}
|
|
|
|
if (paintPressed && !poseOpen)
|
|
{
|
|
SwitchMode<PaintMode>();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Esc is NOT TypingGuard-gated: it must work even while a text field has focus.
|
|
// Tab (alternative binding) IS gated so it stays usable for typing/field navigation.
|
|
private static bool EscapePressed()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM
|
|
return InputUtil.KeyPressedThisFrame(Key.Escape) || InputUtil.GameplayKeyPressed(Key.Tab);
|
|
#else
|
|
return InputUtil.KeyPressedThisFrame(KeyCode.Escape) || InputUtil.GameplayKeyPressed(KeyCode.Tab);
|
|
#endif
|
|
}
|
|
}
|