using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; /// /// The pose selection menu (the "Pose UI" canvas in Main): a modal pause overlay (same pattern as /// ) opened via the overview menu's Pose button, closed by Esc through /// (all Esc handling lives there). On every open it rebuilds a radial menu /// of Icon Button prefab instances from the local player's /// (icon + title come from each ); 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 is toggled - this binder stays alive /// on its (always-active) canvas root. /// [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 _items = new(); private PlayerPoseController _poseController; // controller the open menu is bound to private bool _open; private void Awake() { if (!gameMode) gameMode = FindFirstObjectByType(); if (panelRoot) panelRoot.SetActive(false); } /// True while the modal overlay is open. The overview menu yields Esc to this overlay /// (close-only) while it is open. public bool IsOpen => _open; /// True while the local player has selectable poses - the overview menu hides its Pose /// button otherwise (no player bound, or an empty pose list). public bool CanOpen => LocalPoseController() && LocalPoseController().Poses.Count > 0; /// With 2 or fewer poses (the hunter's set) a radial menu is pointless - the pose button /// and the C hotkey cycle directly via instead of opening the overlay. public bool CyclesDirectly { get { PlayerPoseController pose = LocalPoseController(); return pose && pose.Poses.Count > 0 && pose.Poses.Count <= 2; } } /// Advances to the next pose (wrapping) without opening the overlay. public void CyclePose() { PlayerPoseController pose = LocalPoseController(); if (pose && pose.Poses.Count > 0) pose.SetPoseIndex(pose.CurrentIndex + 1); } /// 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. 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; } /// Closes the modal overlay and resumes gameplay (Esc handling in ). 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() : null; } /// 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. private void BuildItems(PlayerPoseController pose) { for (int i = 0; i < _items.Count; i++) if (_items[i]) Destroy(_items[i]); _items.Clear(); IReadOnlyList 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() : null; if (icon) { icon.sprite = p ? p.Icon : null; icon.preserveAspect = true; icon.enabled = icon.sprite; // no icon authored -> no solid white square icon.rectTransform.localScale = Vector3.one * (p ? p.IconScale : 1f); } TMP_Text label = item.GetComponentInChildren(true); if (label) label.text = p && !string.IsNullOrEmpty(p.Title) ? p.Title : (p ? p.name : ""); Button button = item.GetComponent