feat: runtime pause menu (continuar/reiniciar/opciones/menu) + Mi Memorial in main menu

This commit is contained in:
2026-08-25 17:41:07 -03:00
parent 97677fbfff
commit bcc590f621
2 changed files with 367 additions and 14 deletions
@@ -1,8 +1,10 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using AjedrezPurgatorio.Meta;
using AjedrezPurgatorio.Audio;
using AjedrezPurgatorio.Data;
namespace AjedrezPurgatorio.UI
{
@@ -32,6 +34,8 @@ namespace AjedrezPurgatorio.UI
[SerializeField] private bool _enableDebugLogging = false;
private Button _alternateCampaignButton;
private Button _memorialButton;
private GameObject _memorialPanel;
#endregion
@@ -57,6 +61,7 @@ namespace AjedrezPurgatorio.UI
{
UpdateContinueButtonVisibility();
CreateAlternateCampaignButton();
CreateMemorialButton();
// Tema propio del menú; además reemplaza el BGM de capítulo
// que sigue sonando al volver (AudioManager es persistente).
@@ -81,6 +86,9 @@ namespace AjedrezPurgatorio.UI
if (_alternateCampaignButton != null)
_alternateCampaignButton.onClick.RemoveListener(OnAlternateCampaignClick);
if (_memorialButton != null)
_memorialButton.onClick.RemoveListener(OnMemorialClick);
}
#endregion
@@ -186,6 +194,25 @@ namespace AjedrezPurgatorio.UI
#endif
}
/// <summary>
/// Called when "Mi Memorial" is clicked.
/// Shows the list of fallen kings (name + last words) or closes it.
/// </summary>
private void OnMemorialClick()
{
if (_enableDebugLogging)
Debug.Log("[MainMenu] Mi Memorial clicked.");
if (_memorialPanel != null)
{
Destroy(_memorialPanel);
_memorialPanel = null;
return;
}
_memorialPanel = BuildMemorialPanel();
}
#endregion
#region Private Methods
@@ -256,6 +283,206 @@ namespace AjedrezPurgatorio.UI
Debug.Log($"[MainMenu] Botón de campaña alternativa creado: '{_alternateCampaignLabel}'.");
}
/// <summary>
/// Crea en runtime el botón "Mi Memorial" debajo del último botón del menú.
/// </summary>
private void CreateMemorialButton()
{
if (_newCampaignButton == null || _memorialButton != null)
return;
GameObject clone = Instantiate(
_newCampaignButton.gameObject,
_newCampaignButton.transform.parent);
clone.name = "MemorialButton";
clone.SetActive(true);
TMP_Text[] tmpTexts = clone.GetComponentsInChildren<TMP_Text>(true);
if (tmpTexts.Length > 0)
{
foreach (TMP_Text t in tmpTexts)
t.text = "Mi Memorial";
}
else
{
Text[] legacyTexts = clone.GetComponentsInChildren<Text>(true);
foreach (Text t in legacyTexts)
t.text = "Mi Memorial";
}
RectTransform referenceRect = _alternateCampaignButton != null
? _alternateCampaignButton.GetComponent<RectTransform>()
: _newCampaignButton.GetComponent<RectTransform>();
RectTransform cloneRect = clone.GetComponent<RectTransform>();
float height = referenceRect.rect.height > 0f ? referenceRect.rect.height : referenceRect.sizeDelta.y;
cloneRect.anchoredPosition = referenceRect.anchoredPosition + new Vector2(0f, -(height + _buttonSpacing));
_memorialButton = clone.GetComponent<Button>();
_memorialButton.onClick.AddListener(OnMemorialClick);
if (_enableDebugLogging)
Debug.Log("[MainMenu] Botón 'Mi Memorial' creado.");
}
/// <summary>
/// Construye el panel overlay con la lista completa de Reyes Muertos
/// (nombre + últimas palabras), leída del pool persistente.
/// </summary>
private GameObject BuildMemorialPanel()
{
var pool = DeadKingPool.Instance;
List<DeadKingData> kings = pool != null ? pool.GetAllDeadKings() : new List<DeadKingData>();
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
var canvasGO = new GameObject("MemorialRuntimeCanvas");
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 350;
var scaler = canvasGO.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0f;
canvasGO.AddComponent<GraphicRaycaster>();
var bg = canvasGO.AddComponent<Image>();
bg.color = new Color(0.03f, 0.02f, 0.04f, 0.97f);
Stretch((RectTransform)canvasGO.transform);
var title = CreateMenuText("Title", "MI MEMORIAL", 36, new Color(0.9f, 0.82f, 0.55f), font);
title.transform.SetParent(canvasGO.transform, false);
Anchor(title.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -80), new Vector2(1100, 60));
string subtitleText = kings.Count == 1
? "1 alma recordada"
: $"{kings.Count} almas recordadas";
if (kings.Count == 0)
subtitleText = "El Memorial aún guarda silencio.";
var subtitle = CreateMenuText("Subtitle", subtitleText, 22, new Color(0.75f, 0.72f, 0.65f), font);
subtitle.fontStyle = FontStyles.Italic;
subtitle.transform.SetParent(canvasGO.transform, false);
Anchor(subtitle.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -130), new Vector2(800, 40));
// Lista scrolleable centrada
var scrollGO = new GameObject("KingList");
scrollGO.transform.SetParent(canvasGO.transform, false);
var scrollRt = scrollGO.AddComponent<RectTransform>();
Anchor(scrollRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -20), new Vector2(940, 660));
var viewportGo = new GameObject("Viewport");
viewportGo.transform.SetParent(scrollGO.transform, false);
var viewportRt = viewportGo.AddComponent<RectTransform>();
Stretch(viewportRt);
viewportGo.AddComponent<Image>().color = new Color(0f, 0f, 0f, 0f);
viewportGo.AddComponent<RectMask2D>();
var contentGo = new GameObject("Content");
contentGo.transform.SetParent(viewportGo.transform, false);
var contentRt = contentGo.AddComponent<RectTransform>();
contentRt.anchorMin = new Vector2(0f, 1f);
contentRt.anchorMax = new Vector2(1f, 1f);
contentRt.pivot = new Vector2(0.5f, 1f);
contentRt.anchoredPosition = Vector2.zero;
contentRt.sizeDelta = new Vector2(0f, 0f);
var layout = contentGo.AddComponent<VerticalLayoutGroup>();
layout.spacing = 16f;
layout.padding = new RectOffset(24, 24, 12, 24);
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
var fitter = contentGo.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
for (int i = 0; i < kings.Count; i++)
{
DeadKingData king = kings[kings.Count - 1 - i];
string message = string.IsNullOrWhiteSpace(king.message) ? "Sin últimas palabras." : king.message;
string entry =
$"<color=#E6D28A>«{message}»</color>\n<color=#BDB8AC>— {king.playerName} · Capítulo {king.chapterReached + 1}</color>";
var row = CreateMenuText($"King_{i}", entry, 22, Color.white, font);
row.alignment = TextAlignmentOptions.TopLeft;
row.transform.SetParent(contentGo.transform, false);
var le = row.gameObject.AddComponent<LayoutElement>();
le.minHeight = 30f;
le.flexibleHeight = 0f;
}
if (kings.Count == 0)
{
var emptyNote = CreateMenuText("EmptyNote", "Cuando caigas en batalla, tu nombre y tus palabras descansarán aquí.", 22, new Color(0.65f, 0.62f, 0.55f), font);
emptyNote.fontStyle = FontStyles.Italic;
emptyNote.alignment = TextAlignmentOptions.Center;
emptyNote.transform.SetParent(contentGo.transform, false);
}
var scroll = scrollGO.AddComponent<ScrollRect>();
scroll.content = contentRt;
scroll.viewport = viewportRt;
scroll.horizontal = false;
scroll.vertical = true;
scroll.movementType = ScrollRect.MovementType.Clamped;
scroll.scrollSensitivity = 30f;
var closeButton = CreateMenuButton("CloseButton", "Volver", font);
closeButton.transform.SetParent(canvasGO.transform, false);
Anchor((RectTransform)closeButton.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -420), new Vector2(320, 60));
closeButton.onClick.AddListener(() =>
{
Destroy(canvasGO);
if (_memorialPanel == canvasGO)
_memorialPanel = null;
});
return canvasGO;
}
private static TextMeshProUGUI CreateMenuText(string name, string content, int size, Color color, TMP_FontAsset font)
{
var go = new GameObject(name);
var text = go.AddComponent<TextMeshProUGUI>();
text.text = content;
text.fontSize = size;
text.color = color;
text.raycastTarget = false;
text.textWrappingMode = TextWrappingModes.Normal;
if (font != null)
text.font = font;
return text;
}
private static Button CreateMenuButton(string name, string label, TMP_FontAsset font)
{
var go = new GameObject(name);
var image = go.AddComponent<Image>();
image.color = new Color(0.45f, 0.08f, 0.08f, 1f);
var button = go.AddComponent<Button>();
button.targetGraphic = image;
var labelTmp = CreateMenuText("Label", label, 26, Color.white, font);
labelTmp.alignment = TextAlignmentOptions.Center;
labelTmp.raycastTarget = false;
labelTmp.transform.SetParent(go.transform, false);
Stretch(labelTmp.rectTransform);
return button;
}
private static void Anchor(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offset, Vector2 size)
{
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
rt.offsetMin = new Vector2(offset.x - size.x * rt.pivot.x, offset.y - size.y * rt.pivot.y);
rt.offsetMax = new Vector2(offset.x + size.x * (1f - rt.pivot.x), offset.y + size.y * (1f - rt.pivot.y));
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
#endregion
}
}
@@ -1,11 +1,13 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;
namespace AjedrezPurgatorio.UI
{
/// <summary>
/// Pause menu controller. Allows player to continue, retry chapter, or return to main menu.
/// Pause menu controller. Allows player to continue, retry chapter, open options, or return to main menu.
/// El panel se construye en runtime para no depender del cableado de la escena.
/// </summary>
public class PauseMenuController : MonoBehaviour
{
@@ -29,6 +31,8 @@ namespace AjedrezPurgatorio.UI
#region Private Fields
private bool _isPaused = false;
private GameObject _runtimePanel;
private Button _hudPauseButton;
#endregion
@@ -49,6 +53,18 @@ namespace AjedrezPurgatorio.UI
// Hide panel initially
if (_panel != null)
_panel.SetActive(false);
// El botón HUD de pausa de la escena viene con onClick vacío: cablearlo aquí.
var hudGO = GameObject.Find("PauseButton");
if (hudGO != null)
{
_hudPauseButton = hudGO.GetComponent<Button>();
if (_hudPauseButton != null)
{
_hudPauseButton.onClick.AddListener(Show);
Debug.Log("[PauseMenu] Botón HUD de pausa cableado en runtime.");
}
}
}
private void Update()
@@ -74,6 +90,9 @@ namespace AjedrezPurgatorio.UI
if (_mainMenuButton != null)
_mainMenuButton.onClick.RemoveListener(OnMainMenuClick);
if (_hudPauseButton != null)
_hudPauseButton.onClick.RemoveListener(Show);
}
#endregion
@@ -90,18 +109,17 @@ namespace AjedrezPurgatorio.UI
_isPaused = true;
Debug.Log("[PauseMenu] Pausa mostrada (Escape).");
// Freeze time
Time.timeScale = 0f;
// Show panel
// El panel de la escena puede estar duplicado o hueco como en otros
// paneles: usar SIEMPRE el panel runtime garantizado.
if (_panel != null)
_panel.SetActive(true);
_panel.SetActive(false);
if (_canvasGroup != null)
_canvasGroup.alpha = 1f;
if (_enableDebugLogging)
Debug.Log("[PauseMenu] Pause menu shown.");
BuildRuntimePanel();
}
/// <summary>
@@ -114,6 +132,8 @@ namespace AjedrezPurgatorio.UI
_isPaused = false;
Debug.Log("[PauseMenu] Pausa oculta.");
// Resume time
Time.timeScale = 1f;
@@ -121,8 +141,11 @@ namespace AjedrezPurgatorio.UI
if (_panel != null)
_panel.SetActive(false);
if (_enableDebugLogging)
Debug.Log("[PauseMenu] Pause menu hidden.");
if (_runtimePanel != null)
{
Destroy(_runtimePanel);
_runtimePanel = null;
}
}
/// <summary>
@@ -132,6 +155,109 @@ namespace AjedrezPurgatorio.UI
#endregion
#region Runtime Panel
/// <summary>
/// Panel de pausa runtime: Continuar / Reiniciar capítulo / Opciones / Menú principal.
/// </summary>
private void BuildRuntimePanel()
{
if (_runtimePanel != null)
return;
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
var canvasGO = new GameObject("PauseRuntimeCanvas");
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 420;
var scaler = canvasGO.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0f;
canvasGO.AddComponent<GraphicRaycaster>();
_runtimePanel = canvasGO;
// Si un ciclo anterior falló a mitad de construcción pueden quedar
// canvases huérfanos acumulados: limpiarlos aquí.
foreach (var orphan in GameObject.FindObjectsOfType<GameObject>())
{
if (orphan.name == "PauseRuntimeCanvas" && orphan != canvasGO)
Destroy(orphan);
}
var bg = canvasGO.AddComponent<Image>();
bg.color = new Color(0.03f, 0.02f, 0.04f, 0.96f);
var title = CreateText("Title", "PAUSA", 40, new Color(0.9f, 0.82f, 0.55f), font);
title.alignment = TextAlignmentOptions.Center;
title.transform.SetParent(canvasGO.transform, false);
Anchor(title.rectTransform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, 210), new Vector2(600, 70));
CreateMenuButton("ContinueButton", "Continuar", font, new Color(0.18f, 0.36f, 0.18f), 120f, Hide);
CreateMenuButton("RetryButton", "Reiniciar capítulo", font, new Color(0.45f, 0.08f, 0.08f), 30f, OnRetryClick);
CreateMenuButton("OptionsButton", "Opciones", font, new Color(0.16f, 0.16f, 0.20f), -60f, OnOptionsClick);
CreateMenuButton("MainMenuButton", "Salir al menú principal", font, new Color(0.15f, 0.15f, 0.17f), -150f, OnMainMenuClick);
}
private void OnOptionsClick()
{
if (_enableDebugLogging)
Debug.Log("[PauseMenu] Opciones clicked.");
OptionsMenuController.Toggle();
}
private static TextMeshProUGUI CreateText(string name, string content, int size, Color color, TMP_FontAsset font)
{
var go = new GameObject(name);
var text = go.AddComponent<TextMeshProUGUI>();
text.text = content;
text.fontSize = size;
text.color = color;
text.raycastTarget = false;
if (font != null)
text.font = font;
return text;
}
private void CreateMenuButton(string name, string label, TMP_FontAsset font, Color color, float yOffset, UnityEngine.Events.UnityAction onClick)
{
var go = new GameObject(name);
go.transform.SetParent(_runtimePanel.transform, false);
var image = go.AddComponent<Image>();
image.color = color;
var button = go.AddComponent<Button>();
button.targetGraphic = image;
button.onClick.AddListener(onClick);
Anchor(go.GetComponent<RectTransform>(), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, yOffset), new Vector2(420, 64));
var labelTmp = CreateText("Label", label, 26, Color.white, font);
labelTmp.alignment = TextAlignmentOptions.Center;
labelTmp.raycastTarget = false;
labelTmp.transform.SetParent(go.transform, false);
Stretch(labelTmp.rectTransform);
}
private static void Anchor(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offset, Vector2 size)
{
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
rt.offsetMin = new Vector2(offset.x - size.x * rt.pivot.x, offset.y - size.y * rt.pivot.y);
rt.offsetMax = new Vector2(offset.x + size.x * (1f - rt.pivot.x), offset.y + size.y * (1f - rt.pivot.y));
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
#endregion
#region Button Handlers
/// <summary>
@@ -155,8 +281,8 @@ namespace AjedrezPurgatorio.UI
if (_enableDebugLogging)
Debug.Log("[PauseMenu] Retry clicked.");
// Resume time before loading scene
Time.timeScale = 1f;
// Restaura el tiempo y destruye el panel antes de recargar
Hide();
if (CampaignManager.Instance != null)
{
@@ -178,8 +304,8 @@ namespace AjedrezPurgatorio.UI
if (_enableDebugLogging)
Debug.Log("[PauseMenu] Main Menu clicked.");
// Resume time before loading scene
Time.timeScale = 1f;
// Restaura el tiempo y destruye el panel antes de cambiar de escena
Hide();
if (GameStateManager.Instance != null)
GameStateManager.Instance.TransitionTo(GameState.Menu);