fix: defeat-to-Memorial chain, first-run dialogues, memorial typing, dialogue layout

This commit is contained in:
2026-08-25 17:41:03 -03:00
parent dc45653be4
commit 97677fbfff
5 changed files with 231 additions and 41 deletions
@@ -7,6 +7,7 @@ using UnityEngine;
/// Sistema central de diálogos.
/// Maneja la carga de nodos, navegación, typewriter effect y branching condicional.
/// </summary>
[DefaultExecutionOrder(-100)]
public class DialogueSystem : MonoBehaviour
{
public static DialogueSystem Instance { get; private set; }
@@ -56,10 +56,15 @@ public class CampaignConfig : ScriptableObject
/// <summary>
/// Carga los datos desde el JSON al habilitar el ScriptableObject.
/// Silencioso si aún no hay fuente: CreateInstance dispara OnEnable antes
/// de que CampaignManager pueda asignar el JSON en runtime.
/// </summary>
void OnEnable()
{
LoadFromJson();
if (_campaignDataJson != null || _runtimeJsonSource != null)
LoadFromJson();
else
_isLoaded = false;
}
/// <summary>
+106 -23
View File
@@ -51,6 +51,10 @@ namespace AjedrezPurgatorio.UI
private int _currentChapterIndex;
private bool _isSubmitting = false;
private UnityEngine.EventSystems.BaseInputModule _disabledModule;
private UnityEngine.EventSystems.StandaloneInputModule _enabledLegacyModule;
private bool _legacyModuleWasAdded;
#endregion
#region Unity Lifecycle
@@ -92,6 +96,66 @@ namespace AjedrezPurgatorio.UI
{
_submitButton.onClick.RemoveListener(OnSubmitClick);
}
RestoreInputModule();
}
/// <summary>
/// TMP_InputField edita texto por el pipeline clásico (Input.inputString).
/// El EventSystem de las escenas usa InputSystemUIInputModule, que procesa
/// clics pero no alimenta la escritura legacy: mientras el Memorial esté
/// abierto se usa StandaloneInputModule y al cerrarse se restaura el módulo.
/// </summary>
private void EnsureTypableEventSystem()
{
var es = FindObjectOfType<UnityEngine.EventSystems.EventSystem>();
if (es == null)
{
var go = new GameObject("EventSystem");
es = go.AddComponent<UnityEngine.EventSystems.EventSystem>();
_enabledLegacyModule = go.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
_legacyModuleWasAdded = true;
return;
}
var newModule = es.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
if (newModule == null || !newModule.enabled)
return;
newModule.enabled = false;
_disabledModule = newModule;
var legacy = es.GetComponent<UnityEngine.EventSystems.StandaloneInputModule>();
if (legacy != null)
{
legacy.enabled = true;
}
else
{
_enabledLegacyModule = es.gameObject.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
_legacyModuleWasAdded = true;
}
Debug.Log("[DeadKingInputUI] Módulo de UI cambiado a StandaloneInputModule para permitir escritura.");
}
private void RestoreInputModule()
{
if (_disabledModule != null)
{
_disabledModule.enabled = true;
_disabledModule = null;
}
if (_enabledLegacyModule != null)
{
if (_legacyModuleWasAdded && _enabledLegacyModule != null)
Destroy(_enabledLegacyModule);
else
_enabledLegacyModule.enabled = false;
_enabledLegacyModule = null;
_legacyModuleWasAdded = false;
}
}
#endregion
@@ -104,7 +168,16 @@ namespace AjedrezPurgatorio.UI
/// </summary>
public bool IsProperlyWired =>
_panel != null && _nameInputField != null &&
_messageInputField != null && _submitButton != null;
_messageInputField != null && _submitButton != null &&
InputFieldUsable(_nameInputField) && InputFieldUsable(_messageInputField);
/// <summary>
/// Un InputField sin viewport o sin textComponent (cáscara vacía de la
/// escena) no acepta escritura aunque exista: se considera sin cablear.
/// </summary>
private static bool InputFieldUsable(TMP_InputField field) =>
field != null && field.textViewport != null &&
field.textComponent != null && !field.readOnly;
/// <summary>
/// Shows the Dead King input panel with campaign context.
@@ -113,6 +186,8 @@ namespace AjedrezPurgatorio.UI
/// <param name="chapterIndex">Chapter where defeat occurred</param>
public void Show(CampaignState campaignState, int chapterIndex)
{
Debug.Log($"[DeadKingInputUI] Show() llamado (capítulo {chapterIndex}).");
if (campaignState == null)
{
Debug.LogError("[DeadKingInputUI] Cannot show with null campaign state.");
@@ -120,12 +195,20 @@ namespace AjedrezPurgatorio.UI
}
// Escenas con copias duplicadas o sin cablear: si faltan referencias
// esenciales se construye un panel funcional en runtime para que el
// jugador SIEMPRE pueda inscribir su nombre y mensaje.
// esenciales (o los InputFields son cáscaras sin viewport/texto)
// se oculta el panel roto y se construye uno funcional en runtime
// para que el jugador SIEMPRE pueda inscribir su nombre y mensaje.
if (!IsProperlyWired)
{
Debug.LogWarning($"[DeadKingInputUI] Referencias sin cablear (panel={(_panel != null)} nombre={(_nameInputField != null)} mensaje={(_messageInputField != null)} submit={(_submitButton != null)}). Construyendo panel en runtime...");
Debug.LogWarning($"[DeadKingInputUI] Referencias inservibles (panel={(_panel != null)} nombre={(_nameInputField != null)} mensaje={(_messageInputField != null)} submit={(_submitButton != null)}). Construyendo panel en runtime...");
if (_panel != null && _panel != gameObject)
_panel.SetActive(false);
BuildRuntimeFallback();
// El botón del panel runtime invoca handlers de ESTE componente:
// su GameObject debe estar activo o StartCoroutine fallará.
if (!gameObject.activeInHierarchy)
gameObject.SetActive(true);
}
_campaignState = campaignState;
@@ -188,6 +271,8 @@ namespace AjedrezPurgatorio.UI
/// </summary>
private void OnSubmitClick()
{
Debug.Log("[DeadKingInputUI] Inscribir clickeado.");
if (_isSubmitting)
{
if (_enableDebugLogging)
@@ -248,8 +333,8 @@ namespace AjedrezPurgatorio.UI
_feedbackText.gameObject.SetActive(true);
}
// Wait for feedback display time
yield return new WaitForSeconds(_feedbackDisplayTime);
// Wait for feedback display time (unscaled: el juego puede estar pausado)
yield return new WaitForSecondsRealtime(_feedbackDisplayTime);
// Hide feedback and input panel
if (_feedbackText != null)
@@ -472,7 +557,7 @@ namespace AjedrezPurgatorio.UI
/// </summary>
private void BuildRuntimeFallback()
{
EnsureEventSystem();
EnsureTypableEventSystem();
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
@@ -480,7 +565,10 @@ namespace AjedrezPurgatorio.UI
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 460;
canvasGO.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
var scaler = canvasGO.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0f;
canvasGO.AddComponent<GraphicRaycaster>();
_panel = canvasGO;
@@ -492,29 +580,34 @@ namespace AjedrezPurgatorio.UI
var title = CreateText("Title", "INSCRIBE TU NOMBRE EN EL MEMORIAL", 32, 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, -110), new Vector2(1200, 55));
Anchor(title.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -95), new Vector2(1200, 56));
var subtitle = CreateText("Subtitle", "El Purgatorio recuerda a sus reyes caídos.", 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, -160), new Vector2(900, 40));
Anchor(subtitle.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -145), new Vector2(900, 40));
_nameInputField = CreateInputField(
"NameInput", "Tu nombre...", font, canvasGO.transform,
new Vector2(0, -235), new Vector2(620, 58), multiline: false);
new Vector2(0, 112), new Vector2(620, 58), multiline: false);
_messageInputField = CreateInputField(
"MessageInput", "Un mensaje para los vivos (opcional)...", font, canvasGO.transform,
new Vector2(0, -345), new Vector2(620, 110), multiline: true);
new Vector2(0, 0), new Vector2(620, 110), multiline: true);
_submitButton = CreateButton("SubmitButton", "Inscribir en el Memorial", font, new Color(0.45f, 0.08f, 0.08f));
_submitButton.transform.SetParent(canvasGO.transform, false);
Anchor((RectTransform)_submitButton.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -250), new Vector2(420, 64));
Anchor((RectTransform)_submitButton.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -105), new Vector2(420, 64));
// Awake corrió con referencias nulas: cablear límites y listener aquí.
_nameInputField.characterLimit = _maxNameLength;
_messageInputField.characterLimit = _maxMessageLength;
_submitButton.onClick.AddListener(OnSubmitClick);
var module = UnityEngine.EventSystems.EventSystem.current != null
? UnityEngine.EventSystems.EventSystem.current.currentInputModule
: null;
Debug.Log($"[DeadKingInputUI] Panel runtime construido. Módulo UI activo: {(module != null ? module.GetType().Name : "NINGUNO")}.");
}
/// <summary>
@@ -577,16 +670,6 @@ namespace AjedrezPurgatorio.UI
return inputField;
}
private static void EnsureEventSystem()
{
if (FindObjectOfType<UnityEngine.EventSystems.EventSystem>() != null)
return;
var es = new GameObject("EventSystem");
es.AddComponent<UnityEngine.EventSystems.EventSystem>();
es.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
}
#endregion
#region Fade Effects
+25 -8
View File
@@ -62,6 +62,21 @@ namespace AjedrezPurgatorio.UI
{
_onContinueCallback = onContinue;
Canvas parentCanvas = GetComponentInParent<Canvas>();
if (parentCanvas != null)
{
parentCanvas.sortingOrder = 300;
Debug.Log($"[DefeatUI] Mostrando derrota '{chapterName}' ({reason}). Canvas '{parentCanvas.name}' orden forzado a {parentCanvas.sortingOrder}.");
}
else
Debug.LogWarning("[DefeatUI] Mostrando derrota sin Canvas padre.");
if (_canvasGroup != null)
{
_canvasGroup.blocksRaycasts = true;
_canvasGroup.interactable = true;
}
if (_titleText != null)
_titleText.text = "DERROTA";
@@ -84,6 +99,11 @@ namespace AjedrezPurgatorio.UI
else
gameObject.SetActive(true);
if (_continueButton != null)
Debug.Log($"[DefeatUI] Botón Continuar: interactable={_continueButton.interactable} activoEnJerarquia={_continueButton.gameObject.activeInHierarchy}.");
else
Debug.LogWarning("[DefeatUI] Botón Continuar NO asignado.");
StartCoroutine(FadeIn());
if (_enableDebugLogging)
@@ -97,17 +117,14 @@ namespace AjedrezPurgatorio.UI
private void OnContinueClick()
{
if (_enableDebugLogging)
Debug.Log("[DefeatUI] Continue clicked.");
Debug.Log("[DefeatUI] Continuar clickeado.");
System.Action callback = _onContinueCallback;
_onContinueCallback = null;
Hide();
StartCoroutine(InvokeContinueAfterFade());
}
private IEnumerator InvokeContinueAfterFade()
{
yield return new WaitForSecondsRealtime(_fadeOutDuration);
_onContinueCallback?.Invoke();
callback?.Invoke();
}
private IEnumerator FadeIn()
+93 -9
View File
@@ -24,9 +24,11 @@ public class DialogueUI : MonoBehaviour
[Header("Input System")]
[SerializeField] private InputActionReference _advanceDialogueAction;
// Estado
private Coroutine _blinkCoroutine;
private bool _isInputEnabled = false;
// Estado
private Coroutine _blinkCoroutine;
private bool _isInputEnabled = false;
private bool _subscribed = false;
private Coroutine _subscribeCoroutine;
void Update()
{
@@ -54,14 +56,16 @@ public class DialogueUI : MonoBehaviour
void OnEnable()
{
// Suscribirse a eventos de DialogueSystem
// Suscribirse a eventos de DialogueSystem.
// En la primera carga puede no existir aún (orden de Awake/OnEnable):
// diferir la suscripción en vez de perderla para siempre.
if (DialogueSystem.Instance != null)
{
DialogueSystem.Instance.OnNodeDisplayed += OnNodeDisplayed;
DialogueSystem.Instance.OnNodeComplete += OnNodeComplete;
DialogueSystem.Instance.OnDialogueComplete += OnDialogueComplete;
DialogueSystem.Instance.OnSpeakerChanged += OnSpeakerChanged;
DialogueSystem.Instance.OnTextUpdated += OnTextUpdated;
SubscribeToDialogueSystem();
}
else if (_subscribeCoroutine == null)
{
_subscribeCoroutine = StartCoroutine(SubscribeWhenReady());
}
// Habilitar input action
@@ -76,11 +80,90 @@ public class DialogueUI : MonoBehaviour
{
_dialoguePanel.SetActive(false);
}
RelayoutToAvoidPortraitOverlap();
}
/// <summary>
/// Reubica retrato (izquierda), nombre y texto para que el texto nunca
/// quede encima de la imagen del personaje. Idempotente.
/// </summary>
private void RelayoutToAvoidPortraitOverlap()
{
if (_speakerPortrait != null)
{
RectTransform portraitRt = _speakerPortrait.rectTransform;
portraitRt.anchorMin = new Vector2(0f, 0.5f);
portraitRt.anchorMax = new Vector2(0f, 0.5f);
portraitRt.pivot = new Vector2(0.5f, 0.5f);
portraitRt.sizeDelta = new Vector2(240f, 300f);
portraitRt.anchoredPosition = new Vector2(150f, 0f);
}
if (_dialogueText != null)
{
RectTransform textRt = _dialogueText.rectTransform;
textRt.anchorMin = new Vector2(0f, 0f);
textRt.anchorMax = new Vector2(1f, 1f);
textRt.pivot = new Vector2(0.5f, 0.5f);
textRt.offsetMin = new Vector2(315f, 24f);
textRt.offsetMax = new Vector2(-28f, -102f);
_dialogueText.alignment = TextAlignmentOptions.TopLeft;
}
if (_speakerNameText != null)
{
RectTransform nameRt = _speakerNameText.rectTransform;
nameRt.anchorMin = new Vector2(0f, 1f);
nameRt.anchorMax = new Vector2(0f, 1f);
nameRt.pivot = new Vector2(0f, 1f);
nameRt.sizeDelta = new Vector2(520f, 46f);
nameRt.anchoredPosition = new Vector2(315f, -16f);
}
}
private void SubscribeToDialogueSystem()
{
if (_subscribed || DialogueSystem.Instance == null)
return;
DialogueSystem.Instance.OnNodeDisplayed += OnNodeDisplayed;
DialogueSystem.Instance.OnNodeComplete += OnNodeComplete;
DialogueSystem.Instance.OnDialogueComplete += OnDialogueComplete;
DialogueSystem.Instance.OnSpeakerChanged += OnSpeakerChanged;
DialogueSystem.Instance.OnTextUpdated += OnTextUpdated;
_subscribed = true;
}
private IEnumerator SubscribeWhenReady()
{
yield return new WaitUntil(() => DialogueSystem.Instance != null);
_subscribeCoroutine = null;
SubscribeToDialogueSystem();
// Si un diálogo ya está activo al momento de suscribirse (arrancó
// antes de que esta UI existiera), sincronizar su estado actual.
if (_subscribed && DialogueSystem.Instance.IsActive && DialogueSystem.Instance.CurrentNode != null)
{
DialogueNode currentNode = DialogueSystem.Instance.CurrentNode;
OnSpeakerChanged(currentNode.speaker);
OnNodeDisplayed(currentNode);
OnTextUpdated(currentNode.text);
if (!DialogueSystem.Instance.IsTyping)
OnNodeComplete(currentNode);
}
}
void OnDisable()
{
// Desuscribirse para evitar memory leaks
if (_subscribeCoroutine != null)
{
StopCoroutine(_subscribeCoroutine);
_subscribeCoroutine = null;
}
if (DialogueSystem.Instance != null)
{
DialogueSystem.Instance.OnNodeDisplayed -= OnNodeDisplayed;
@@ -89,6 +172,7 @@ public class DialogueUI : MonoBehaviour
DialogueSystem.Instance.OnSpeakerChanged -= OnSpeakerChanged;
DialogueSystem.Instance.OnTextUpdated -= OnTextUpdated;
}
_subscribed = false;
// Deshabilitar input action
if (_advanceDialogueAction != null && _advanceDialogueAction.action != null)