// Copyright (c) Pixel Crushers. All rights reserved. using System.Collections.Generic; using UnityEngine; namespace PixelCrushers.DialogueSystem { /// /// Manages response menus for StandardDialogueUI. /// [System.Serializable] public class StandardUIResponseMenuControls : AbstractUIResponseMenuControls { #region Public Fields /// /// Assign this delegate if you want it to replace the default timeout handler. /// public System.Action timeoutHandler = null; public override AbstractUISubtitleControls subtitleReminderControls { get { return null; } } // Not used. #endregion #region Private Fields protected List m_builtinPanels = new List(); protected StandardUIMenuPanel m_defaultPanel = null; protected Dictionary m_actorPanelCache = new Dictionary(); protected Dictionary m_actorIdPanelCache = new Dictionary(); protected StandardUIMenuPanel m_currentPanel = null; protected StandardUIMenuPanel m_forcedOverridePanel = null; protected Sprite m_pcPortraitSprite = null; protected string m_pcPortraitName = null; protected bool useFirstResponseForPortrait = false; public StandardUIMenuPanel defaultPanel { get { return m_defaultPanel; } set { m_defaultPanel = value; } } public virtual bool allowDialogueActorCustomPanels { get; set; } = true; #endregion #region Initialization & Lookup public void Initialize(StandardUIMenuPanel[] menuPanels, StandardUIMenuPanel defaultMenuPanel, bool useFirstResponseForMenuPortrait) { m_builtinPanels.Clear(); m_builtinPanels.AddRange(menuPanels); m_defaultPanel = (defaultMenuPanel != null) ? defaultMenuPanel : (m_builtinPanels.Count > 0) ? m_builtinPanels[0] : null; ClearCache(); if (timeoutHandler == null) timeoutHandler = DefaultTimeoutHandler; useFirstResponseForPortrait = useFirstResponseForMenuPortrait; } public void ClearCache() { m_actorPanelCache.Clear(); m_actorIdPanelCache.Clear(); } /// /// Changes a dialogue actor's menu panel for the current conversation. /// public virtual void SetActorMenuPanelNumber(DialogueActor dialogueActor, MenuPanelNumber menuPanelNumber) { if (dialogueActor == null) return; OverrideActorMenuPanel(dialogueActor.transform, menuPanelNumber, dialogueActor.standardDialogueUISettings.customMenuPanel); } /// /// Forces menus to use a specific panel regardless of any other default or override settings. /// public void ForceOverrideMenuPanel(StandardUIMenuPanel panel) { m_forcedOverridePanel = panel; } /// /// For speakers who do not have DialogueActor components, this method overrides the /// actor's default panel. /// public void OverrideActorMenuPanel(Transform actorTransform, MenuPanelNumber menuPanelNumber, StandardUIMenuPanel customPanel) { if (actorTransform == null) return; m_actorPanelCache[actorTransform] = GetPanelFromNumber(menuPanelNumber, customPanel); } /// /// For speakers who do not have a GameObject, this method overrides the actor's default panel. /// public void OverrideActorMenuPanel(Actor actor, MenuPanelNumber menuPanelNumber, StandardUIMenuPanel customPanel) { if (actor == null) return; m_actorIdPanelCache[actor.id] = GetPanelFromNumber(menuPanelNumber, customPanel); } protected Transform GetActorTransformFromID(int actorID) { var actor = DialogueManager.masterDatabase.GetActor(actorID); if (actor != null) { var actorTransform = CharacterInfo.GetRegisteredActorTransform(actor.Name); if (actorTransform == null) { var actorGO = GameObject.Find(actor.Name); if (actorGO != null) actorTransform = actorGO.transform; } if (actorTransform != null) return actorTransform; } return DialogueManager.currentActor; } public virtual StandardUIMenuPanel GetPanel(Subtitle lastSubtitle, Response[] responses) { // Check if we have a forced override panel: if (m_forcedOverridePanel != null) return m_forcedOverridePanel; // Find player's transform & DialogueActor: // [2021-04-20]: Prioritize responses[0] panel only if useFirstResponseForPortrait is true: var playerTransform = (lastSubtitle != null && lastSubtitle.speakerInfo.isPlayer) ? lastSubtitle.speakerInfo.transform : null; if (playerTransform == null) { if (useFirstResponseForPortrait) { playerTransform = (responses != null && responses.Length > 0) ? GetActorTransformFromID(responses[0].destinationEntry.ActorID) : (lastSubtitle != null && lastSubtitle.listenerInfo.isPlayer) ? lastSubtitle.listenerInfo.transform : DialogueManager.currentActor; } else { playerTransform = (lastSubtitle != null && lastSubtitle.listenerInfo.isPlayer) ? lastSubtitle.listenerInfo.transform : (responses != null && responses.Length > 0) ? GetActorTransformFromID(responses[0].destinationEntry.ActorID) : DialogueManager.currentActor; } } if (playerTransform == null) playerTransform = DialogueManager.currentActor; var playerDialogueActor = DialogueActor.GetDialogueActorComponent(playerTransform); // Check NPC for non-default menu panel: var playerUsesDefaultMenuPanel = playerDialogueActor != null && playerDialogueActor.standardDialogueUISettings.menuPanelNumber == MenuPanelNumber.Default; var npcTransform = (lastSubtitle != null && lastSubtitle.speakerInfo.isNPC) ? lastSubtitle.speakerInfo.transform : (lastSubtitle != null) ? lastSubtitle.listenerInfo.transform : DialogueManager.currentConversant; if (npcTransform == null) npcTransform = DialogueManager.currentConversant; if (playerUsesDefaultMenuPanel && npcTransform != null && m_actorPanelCache.ContainsKey(npcTransform)) { // We've already cached a menu panel to use when responding to this NPC, so return it: return m_actorPanelCache[npcTransform]; } var npcDialogueActor = DialogueActor.GetDialogueActorComponent(npcTransform); if (npcDialogueActor != null && (npcDialogueActor.standardDialogueUISettings.useMenuPanelFor == DialogueActor.UseMenuPanelFor.MeAndResponsesToMe || (npcDialogueActor.standardDialogueUISettings.menuPanelNumber != MenuPanelNumber.Default && playerUsesDefaultMenuPanel))) { // NPC's DialogueActor specifies a menu panel to use when responding to it, so cache and return it: var npcMenuPanel = GetDialogueActorPanel(npcDialogueActor); if (npcMenuPanel != null) { m_actorPanelCache[npcTransform] = npcMenuPanel; return npcMenuPanel; } } if (playerTransform != null) { // If NPC doesn't specify a menu panel, check for an override by player's transform: if (m_actorPanelCache.ContainsKey(playerTransform)) { var actorTransformPanel = m_actorPanelCache[playerTransform]; if (actorTransformPanel != m_defaultPanel) return actorTransformPanel; } } // Check for an override by player actor ID: var playerID = (lastSubtitle != null && lastSubtitle.speakerInfo.isPlayer) ? lastSubtitle.speakerInfo.id : (responses != null && responses.Length > 0) ? responses[0].destinationEntry.ActorID : -1; if (m_actorIdPanelCache.ContainsKey(playerID)) return m_actorIdPanelCache[playerID]; // Otherwise use player's menu panel: var panel = GetDialogueActorPanel(playerDialogueActor); if (panel == null) panel = m_defaultPanel; if (playerTransform != null) m_actorPanelCache[playerTransform] = panel; return panel; } protected StandardUIMenuPanel GetDialogueActorPanel(DialogueActor dialogueActor) { if (dialogueActor == null) return null; return GetPanelFromNumber(dialogueActor.standardDialogueUISettings.menuPanelNumber, dialogueActor.standardDialogueUISettings.customMenuPanel); } protected StandardUIMenuPanel GetPanelFromNumber(MenuPanelNumber menuPanelNumber, StandardUIMenuPanel customPanel) { switch (menuPanelNumber) { case MenuPanelNumber.Default: return m_defaultPanel; case MenuPanelNumber.Custom: if (!allowDialogueActorCustomPanels) return null; return customPanel; default: var index = PanelNumberUtility.GetMenuPanelIndex(menuPanelNumber); return (0 <= index && index < m_builtinPanels.Count) ? m_builtinPanels[index] : null; } } #endregion #region Portraits /// /// Sets the PC portrait name and sprite to use in the response menu. /// /// Portrait sprite. /// Portrait name. public override void SetPCPortrait(Sprite portraitSprite, string portraitName) { m_pcPortraitSprite = portraitSprite; m_pcPortraitName = portraitName; } /// /// Sets the portrait sprite to use in the response menu if the named actor is the player. /// This is used to immediately update the GUI control if the SetPortrait() sequencer /// command changes the portrait sprite. /// /// Actor name in database. /// Portrait sprite. public override void SetActorPortraitSprite(string actorName, Sprite portraitSprite) { if (string.Equals(actorName, m_pcPortraitName)) { var actorPortraitSprite = AbstractDialogueUI.GetValidPortraitSprite(actorName, portraitSprite); m_pcPortraitSprite = portraitSprite; if (m_currentPanel != null && m_currentPanel.pcImage != null && DialogueManager.masterDatabase.IsPlayer(actorName)) { m_currentPanel.pcImage.sprite = actorPortraitSprite; } } } #endregion #region Show & Hide Responses protected override void ClearResponseButtons() { } // Unused. Handled by StandardUIMenuPanel. protected override void SetResponseButtons(Response[] responses, Transform target) { } // Unused. Handled by StandardUIMenuPanel. public override void SetActive(bool value) { // Only hide. Show is handled by StandardUIMenuPanel. if (value == false && m_currentPanel != null) m_currentPanel.HideResponses(); } /// /// Shows a response menu. /// /// The last subtitle shown. Used to determine which menu panel to use. /// Responses to show in menu panel. /// Send OnClick events to this GameObject (the dialogue UI). public override void ShowResponses(Subtitle lastSubtitle, Response[] responses, Transform target) { var panel = GetPanel(lastSubtitle, responses); if (panel == null) { if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: Can't find menu panel."); } else { m_currentPanel = panel; if (useFirstResponseForPortrait && responses.Length > 0) { var menuCharacterInfo = DialogueManager.conversationModel.GetCharacterInfo(responses[0].destinationEntry.ActorID); if (menuCharacterInfo != null) { m_pcPortraitName = menuCharacterInfo.Name; m_pcPortraitSprite = menuCharacterInfo.portrait; } } panel.SetPCPortrait(m_pcPortraitSprite, m_pcPortraitName); panel.ShowResponses(lastSubtitle, responses, target); } } /// /// Makes the current menu panel's buttons non-clickable. /// Typically called by the dialogue UI as soon as a button has been /// clicked to make sure the player can't click another one while the /// menu is playing its hide animation. /// public virtual void MakeButtonsNonclickable() { if (m_currentPanel != null) { m_currentPanel.MakeButtonsNonclickable(); } } /// /// Close all panels. /// public void Close() { for (int i = 0; i < m_builtinPanels.Count; i++) { if (m_builtinPanels[i] != null) m_builtinPanels[i].Close(); } if (m_defaultPanel != null && !m_builtinPanels.Contains(m_defaultPanel)) m_defaultPanel.Close(); foreach (var kvp in m_actorPanelCache) { var panel = kvp.Value; if (panel != null && !m_builtinPanels.Contains(panel)) panel.Close(); } if (m_actorIdPanelCache.Count > 0) { var cachedPanels = new List(m_actorIdPanelCache.Values); foreach (var kvp in m_actorIdPanelCache) { var panel = kvp.Value; if (panel != null && !m_builtinPanels.Contains(panel) && !cachedPanels.Contains(panel)) panel.Close(); } } //--- No longer close cache when closing menus because SetDialoguePanel may close them: ClearCache(); } public bool AreAnyPanelsClosing() { for (int i = 0; i < m_builtinPanels.Count; i++) { if (m_builtinPanels[i] != null && m_builtinPanels[i].panelState == UIPanel.PanelState.Closing) return true; } if (m_defaultPanel != null && !m_builtinPanels.Contains(m_defaultPanel) && m_defaultPanel.panelState == UIPanel.PanelState.Closing) return true; foreach (var kvp in m_actorPanelCache) { var panel = kvp.Value; if (panel != null && !m_builtinPanels.Contains(panel) && panel.panelState == UIPanel.PanelState.Closing) return true; } if (m_actorIdPanelCache.Count > 0) { var cachedPanels = new List(m_actorIdPanelCache.Values); foreach (var kvp in m_actorIdPanelCache) { var panel = kvp.Value; if (panel != null && !m_builtinPanels.Contains(panel) && !cachedPanels.Contains(panel) && panel.panelState == UIPanel.PanelState.Closing) return true; } } return false; } /// /// Starts the timer. /// /// Timeout duration in seconds. public override void StartTimer(float timeout) { if (m_currentPanel != null) m_currentPanel.StartTimer(timeout, timeoutHandler); } public void DefaultTimeoutHandler() { DialogueManager.instance.SendMessage(DialogueSystemMessages.OnConversationTimeout); } #endregion } }