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:
1 parent
c3d9c06118
commit
3a2e8a7057
5 files changed
+216
-23
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user