Add radial pose menu with hotkeys, remove C cycle hotkey

- PlayerPose: Icon (Sprite) + Title (string) UI properties
- PoseMenu: modal pause overlay building a radial menu from Icon Button
  prefab instances; active pose disabled (hover ring marks it); click
  applies pose via SetPoseIndex and closes
- OverviewMenu: pose button wiring with pause hand-off, Esc/Tab chain
  extended; hotkeys C (toggle pose menu) and V (paint mode) mirroring
  the buttons incl. identical gating
- PlayerPoseController: C cycle hotkey and NextPose() removed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-12 16:17:40 +02:00
1 parent c3d9c06118
commit 3a2e8a7057
5 files changed
+216 -23

No files matched your search

+61 -3
View File
@@ -9,11 +9,12 @@ using UnityEngine.InputSystem;
/// 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 or match results overlay is open instead, Esc closes that, and while paint
/// while the session browser, match results or pose menu overlay is open instead, Esc closes that, and while paint
/// mode is active Esc exits to the camera mode (Player Control, or First Person for the hunter). 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. Attach to the
/// (<see cref="GameModeManager.Modes"/>); the active mode's button is disabled. Hotkeys mirror the
/// buttons: C toggles the pose menu, 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
@@ -29,6 +30,10 @@ public class OverviewMenu : MonoBehaviour
"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.")]
@@ -45,6 +50,8 @@ public class OverviewMenu : MonoBehaviour
[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. Only shown while the local player has selectable poses.")]
[SerializeField] private Button poseButton;
private bool _open;
@@ -53,6 +60,7 @@ public class OverviewMenu : MonoBehaviour
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);
}
@@ -65,6 +73,7 @@ public class OverviewMenu : MonoBehaviour
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()
@@ -76,18 +85,20 @@ public class OverviewMenu : MonoBehaviour
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)
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.
@@ -95,12 +106,16 @@ public class OverviewMenu : MonoBehaviour
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,
@@ -157,6 +172,13 @@ public class OverviewMenu : MonoBehaviour
gameMode.SetPaused(false);
}
private void OnPose()
{
Close(resume: false);
if (!(poseMenu && poseMenu.Open()) && gameMode)
gameMode.SetPaused(false);
}
private void OnTaunt()
{
Close(resume: true);
@@ -196,6 +218,10 @@ public class OverviewMenu : MonoBehaviour
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);
@@ -213,6 +239,38 @@ public class OverviewMenu : MonoBehaviour
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()