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

+138
View File
@@ -0,0 +1,138 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// The pose selection menu (the "Pose UI" canvas in Main): a modal pause overlay (same pattern as
/// <see cref="MatchResultScreen"/>) opened via the overview menu's Pose button, closed by Esc through
/// <see cref="OverviewMenu"/> (all Esc handling lives there). On every open it rebuilds a radial menu
/// of Icon Button prefab instances from the local player's <see cref="PlayerPoseController.Poses"/>
/// (icon + title come from each <see cref="PlayerPose"/>); clicking an item applies that pose and
/// closes the menu. The active pose's button is disabled (its hover ring marks it, like the overview
/// menu's mode buttons). Only the visual <see cref="panelRoot"/> is toggled - this binder stays alive
/// on its (always-active) canvas root.
/// </summary>
[DisallowMultipleComponent]
public class PoseMenu : MonoBehaviour
{
[Tooltip("Mode manager supplying the local player (whose PlayerPoseController is driven) and the " +
"pause hook. Empty = found automatically.")]
[SerializeField] private GameModeManager gameMode;
[Header("Overlay")]
[Tooltip("Visual container toggled via Open/Close. Must be a CHILD of this binder's object so the " +
"binder stays alive while the panel is hidden.")]
[SerializeField] private GameObject panelRoot;
[Header("Radial menu")]
[Tooltip("Center the pose items are arranged around (typically a centered child of the panel).")]
[SerializeField] private RectTransform itemContainer;
[Tooltip("Icon Button prefab instantiated per pose (Icon child image + Label child TMP are bound).")]
[SerializeField] private GameObject itemPrefab;
[Tooltip("Distance of each item from the container center, in canvas units.")]
[Min(0f)]
[SerializeField] private float radius = 220f;
private readonly List<GameObject> _items = new();
private PlayerPoseController _poseController; // controller the open menu is bound to
private bool _open;
private void Awake()
{
if (!gameMode) gameMode = FindFirstObjectByType<GameModeManager>();
if (panelRoot) panelRoot.SetActive(false);
}
/// <summary>True while the modal overlay is open. The overview menu yields Esc to this overlay
/// (close-only) while it is open.</summary>
public bool IsOpen => _open;
/// <summary>True while the local player has selectable poses - the overview menu hides its Pose
/// button otherwise (no player bound, or an empty pose list).</summary>
public bool CanOpen => LocalPoseController() && LocalPoseController().Poses.Count > 0;
/// <summary>Opens the modal overlay and (re)builds the radial items; false when there is nothing
/// to show, so the caller can drop the pause instead of stranding it.</summary>
public bool Open()
{
PlayerPoseController pose = LocalPoseController();
if (!pose || pose.Poses.Count == 0 || !itemContainer || !itemPrefab) return false;
_poseController = pose;
BuildItems(pose);
_open = true;
if (panelRoot) panelRoot.SetActive(true);
if (gameMode) gameMode.SetPaused(true);
return true;
}
/// <summary>Closes the modal overlay and resumes gameplay (Esc handling in <see cref="OverviewMenu"/>).</summary>
public void Close()
{
_open = false;
if (panelRoot) panelRoot.SetActive(false);
if (gameMode) gameMode.SetPaused(false);
}
private void Update()
{
// The bound player went away underneath the open menu (despawn/disconnect) -> don't linger
// as a stale pause overlay.
if (_open && !_poseController)
Close();
}
private PlayerPoseController LocalPoseController()
{
PlayerCharacterController player = gameMode ? gameMode.Player : null;
return player ? player.GetComponent<PlayerPoseController>() : null;
}
/// <summary>Instantiates one Icon Button per pose, evenly on a circle starting at 12 o'clock going
/// clockwise. Rebuilt on every open - pose count is tiny and the list can differ per bound player.</summary>
private void BuildItems(PlayerPoseController pose)
{
for (int i = 0; i < _items.Count; i++)
if (_items[i]) Destroy(_items[i]);
_items.Clear();
IReadOnlyList<PlayerPose> poses = pose.Poses;
for (int i = 0; i < poses.Count; i++)
{
GameObject item = Instantiate(itemPrefab, itemContainer);
float angle = (90f - i * 360f / poses.Count) * Mathf.Deg2Rad;
((RectTransform)item.transform).anchoredPosition =
new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
PlayerPose p = poses[i];
Transform iconChild = item.transform.Find("Icon");
Image icon = iconChild ? iconChild.GetComponent<Image>() : null;
if (icon)
{
icon.sprite = p ? p.Icon : null;
icon.preserveAspect = true;
icon.enabled = icon.sprite; // no icon authored -> no solid white square
}
TMP_Text label = item.GetComponentInChildren<TMP_Text>(true);
if (label) label.text = p && !string.IsNullOrEmpty(p.Title) ? p.Title : (p ? p.name : "");
Button button = item.GetComponent<Button>();
if (button)
{
int index = i;
button.onClick.AddListener(() => OnItem(index));
// Disabled = current pose; its hover ring (IconButtonHover) marks it as active.
button.interactable = pose.CurrentIndex != i;
}
_items.Add(item);
}
}
private void OnItem(int index)
{
if (_poseController) _poseController.SetPoseIndex(index);
Close();
}
}