// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System; using System.Collections; using System.Collections.Generic; namespace PixelCrushers.DialogueSystem { /// /// This is the abstract base class for quest log windows. You can implement a quest log /// window in any GUI system by creating a subclass. /// /// When open, the window displays active and completed quests. It gets the titles, /// descriptions, and states of the quests from the QuestLog class. /// /// The window allows the player to abandon quests (if the quest's Abandonable field is /// true) and toggle tracking (if the quest's Trackable field is true). /// /// /// If pauseWhileOpen is set to true, the quest log window pauses the game by setting /// Time.timeScale to 0. When closed, it restores the previous time scale. /// public abstract class QuestLogWindow : MonoBehaviour { [Tooltip("Optional localized text table to use to localize no active/completed quests.")] public TextTable textTable = null; // v2: changed from LocalizedTextTable. [Tooltip("Text to show (or localize) when there are no active quests.")] public string noActiveQuestsText = "No Active Quests"; [Tooltip("Text to show (or localize) when there are no completed quests.")] public string noCompletedQuestsText = "No Completed Quests"; [Tooltip("Check if quest has a field named 'Visible'. If field is false, don't show quest.")] public bool checkVisibleField = false; public enum QuestHeadingSource { /// /// Use the name of the item for the quest heading. /// Name, /// /// Use the item's Description field for the quest heading. /// Description }; /// /// The quest title source. /// public QuestHeadingSource questHeadingSource = QuestHeadingSource.Name; /// /// The state to assign abandoned quests. /// [Tooltip("State to assign to quests when player abandons then.")] [QuestState] public QuestState abandonQuestState = QuestState.Unassigned; /// /// If true, the window sets Time.timeScale = 0 to pause the game while /// displaying the quest log window. /// public bool pauseWhileOpen = true; /// /// If true, the cursor is unlocked while the quest log window is open. /// public bool unlockCursorWhileOpen = true; /// /// If true, organize the quests by group. /// [Tooltip("Organize quests by the values of their Group fields.")] public bool useGroups = false; [Tooltip("If not blank, show this text next to quest titles that haven't been viewed yet. Will be localized if text has entry in Dialogue Manager's Text Table.")] public string newQuestText = string.Empty; [Tooltip("Allow only one quest to be tracked at a time.")] public bool trackOneQuestAtATime = false; [Tooltip("Clicking again on selected quest title deselects quest.")] public bool deselectQuestOnSecondClick = true; [Serializable] public class QuestInfo { public string Group { get; set; } public string GroupDisplayName { get; set; } public string Title { get; set; } public FormattedText Heading { get; set; } public FormattedText Description { get; set; } public FormattedText[] Entries { get; set; } public QuestState[] EntryStates { get; set; } public bool Trackable { get; set; } public bool Track { get; set; } public bool Abandonable { get; set; } public QuestInfo(string group, string groupDisplayName, string title, FormattedText heading, FormattedText description, FormattedText[] entries, QuestState[] entryStates, bool trackable, bool track, bool abandonable) { this.Group = group; this.GroupDisplayName = groupDisplayName; this.Title = title; this.Heading = heading; this.Description = description; this.Entries = entries; this.EntryStates = entryStates; this.Trackable = trackable; this.Track = track; this.Abandonable = abandonable; } public QuestInfo(string group, string title, FormattedText heading, FormattedText description, FormattedText[] entries, QuestState[] entryStates, bool trackable, bool track, bool abandonable) { this.Group = group; this.GroupDisplayName = string.Empty; this.Title = title; this.Heading = heading; this.Description = description; this.Entries = entries; this.EntryStates = entryStates; this.Trackable = trackable; this.Track = track; this.Abandonable = abandonable; } public QuestInfo(string title, FormattedText heading, FormattedText description, FormattedText[] entries, QuestState[] entryStates, bool trackable, bool track, bool abandonable) { this.Group = string.Empty; this.GroupDisplayName = string.Empty; this.Title = title; this.Heading = heading; this.Description = description; this.Entries = entries; this.EntryStates = entryStates; this.Trackable = trackable; this.Track = track; this.Abandonable = abandonable; } } /// /// Indicates whether the quest log window is currently open. /// /// /// true if open; otherwise, false. /// public bool isOpen { get; protected set; } /// /// The current list of quests. This will change based on whether the player is /// viewing active or completed quests. /// /// The quests. public QuestInfo[] quests { get; protected set; } /// /// The current list of quest groups. /// /// The quest group names. public string[] groups { get; protected set; } /// /// The title of the currently-selected quest. /// /// The selected quest. public string selectedQuest { get; protected set; } /// /// The message to show if Quests[] is empty. /// /// The no quests message. public string noQuestsMessage { get; protected set; } /// /// Indicates whether the window is showing active quests or completed quests. /// /// true if showing active quests; otherwise, false. public virtual bool isShowingActiveQuests { get { return currentQuestStateMask == ActiveQuestStateMask; } } /// @cond FOR_V1_COMPATIBILITY public bool IsOpen { get { return isOpen; } protected set { isOpen = value; } } public QuestInfo[] Quests { get { return quests; } protected set { quests = value; } } public string[] Groups { get { return groups; } protected set { groups = value; } } public string SelectedQuest { get { return selectedQuest; } protected set { selectedQuest = value; } } public string NoQuestsMessage { get { return noQuestsMessage; } protected set { noQuestsMessage = value; } } public bool IsShowingActiveQuests { get { return isShowingActiveQuests; } } /// @endcond protected const QuestState ActiveQuestStateMask = QuestState.Active | QuestState.ReturnToNPC; /// /// The current quest state mask. /// protected QuestState currentQuestStateMask = ActiveQuestStateMask; /// /// The previous time scale prior to opening the window. /// protected float previousTimeScale = 1; protected Coroutine refreshCoroutine = null; protected bool started = false; public virtual void Awake() { isOpen = false; quests = new QuestInfo[0]; groups = new string[0]; selectedQuest = string.Empty; noQuestsMessage = string.Empty; } protected virtual void Start() { started = true; RegisterForUpdateTrackerEvents(); } protected virtual void OnEnable() { if (started) RegisterForUpdateTrackerEvents(); } protected virtual void OnDisable() { refreshCoroutine = null; UnregisterFromUpdateTrackerEvents(); } protected void RegisterForUpdateTrackerEvents() { if (!started || DialogueManager.instance == null) return; if (GetComponentInParent() != null) return; // Children of Dialogue Manager automatically receive UpdateTracker; no need to register. DialogueManager.instance.receivedUpdateTracker -= UpdateTracker; DialogueManager.instance.receivedUpdateTracker += UpdateTracker; } protected void UnregisterFromUpdateTrackerEvents() { if (!started || DialogueManager.instance == null) return; DialogueManager.instance.receivedUpdateTracker -= UpdateTracker; } /// /// Opens the window. Your implementation should override this to handle any /// window-opening activity, then call openedWindowHandler at the end. /// /// Opened window handler. public virtual void OpenWindow(Action openedWindowHandler) { openedWindowHandler(); } /// /// Closes the window. Your implementation should override this to handle any /// window-closing activity, then call closedWindowHandler at the end. /// /// Closed window handler. public virtual void CloseWindow(Action closedWindowHandler) { closedWindowHandler(); } /// /// Called when the quest list has been updated -- for example, when switching between /// active and completed quests. Your implementation may override this to do processing. /// public virtual void OnQuestListUpdated() { } /// /// Asks the player to confirm abandonment of a quest. Your implementation should override /// this to show a modal dialogue box or something similar. If confirmed, it should call /// confirmedAbandonQuestHandler. /// /// Title. /// Confirmed abandon quest handler. public virtual void ConfirmAbandonQuest(string title, Action confirmedAbandonQuestHandler) { } /// /// Opens the quest window. /// public virtual void Open() { QuestLog.trackOneQuestAtATime = trackOneQuestAtATime; PauseGameplay(); OpenWindow(OnOpenedWindow); } protected virtual void OnOpenedWindow() { isOpen = true; ShowQuests(currentQuestStateMask); } /// /// Closes the quest log window. While you can call this manually in your own script, this /// method is normally called internally when the player clicks the close button. You can /// call it manually to support alternate methods of closing the window. /// /// /// if (Input.GetKeyDown(KeyCode.L) && myQuestLogWindow.IsOpen) { /// myQuestLogWindow.Close(); /// } /// public virtual void Close() { //--- No need to clear it: selectedQuest = string.Empty; CloseWindow(OnClosedWindow); } protected virtual void OnClosedWindow() { isOpen = false; ResumeGameplay(); } private bool wasCursorActive = false; protected virtual void PauseGameplay() { if (pauseWhileOpen) { previousTimeScale = Time.timeScale; Time.timeScale = 0; } if (unlockCursorWhileOpen) { wasCursorActive = Tools.IsCursorActive(); Tools.SetCursorActive(true); } } protected virtual void ResumeGameplay() { if (pauseWhileOpen) Time.timeScale = previousTimeScale; if (unlockCursorWhileOpen && !wasCursorActive) Tools.SetCursorActive(false); } public virtual bool IsQuestVisible(string questTitle) { return !checkVisibleField || Lua.IsTrue("Quest[\"" + DialogueLua.StringToTableIndex(questTitle) + "\"].Visible ~= false"); } protected virtual void ShowQuests(QuestState questStateMask) { currentQuestStateMask = questStateMask; noQuestsMessage = GetNoQuestsMessage(questStateMask); List questList = new List(); if (useGroups) { var records = QuestLog.GetAllGroupsAndQuests(questStateMask, true); foreach (var record in records) { if (!IsQuestVisible(record.questTitle)) continue; questList.Add(GetQuestInfo(record.groupName, record.questTitle)); } } else { string[] titles = QuestLog.GetAllQuests(questStateMask, true, null); foreach (var title in titles) { if (!IsQuestVisible(title)) continue; questList.Add(GetQuestInfo(string.Empty, title)); } } quests = questList.ToArray(); OnQuestListUpdated(); } protected virtual QuestInfo GetQuestInfo(string group, string title) { FormattedText description = FormattedText.Parse(QuestLog.GetQuestDescription(title), DialogueManager.masterDatabase.emphasisSettings); FormattedText localizedTitle = FormattedText.Parse(QuestLog.GetQuestTitle(title), DialogueManager.masterDatabase.emphasisSettings); FormattedText heading = (questHeadingSource == QuestHeadingSource.Description) ? description : localizedTitle; string localizedGroup = string.IsNullOrEmpty(group) ? string.Empty : QuestLog.GetQuestGroup(title); string localizedGroupDisplayName = string.IsNullOrEmpty(group) ? string.Empty : QuestLog.GetQuestGroupDisplayName(title); bool abandonable = QuestLog.IsQuestAbandonable(title) && isShowingActiveQuests; bool trackable = QuestLog.IsQuestTrackingAvailable(title) && isShowingActiveQuests; bool track = QuestLog.IsQuestTrackingEnabled(title); int entryCount = QuestLog.GetQuestEntryCount(title); FormattedText[] entries = new FormattedText[entryCount]; QuestState[] entryStates = new QuestState[entryCount]; for (int i = 0; i < entryCount; i++) { entries[i] = FormattedText.Parse(QuestLog.GetQuestEntry(title, i + 1), DialogueManager.masterDatabase.emphasisSettings); entryStates[i] = QuestLog.GetQuestEntryState(title, i + 1); } // Check if need to show [new]: if (!string.IsNullOrEmpty(newQuestText)) { if (!QuestLog.WasQuestViewed(title)) { heading.text += " " + FormattedText.Parse(DialogueManager.GetLocalizedText(newQuestText)).text; } } return new QuestInfo(localizedGroup, localizedGroupDisplayName, title, heading, description, entries, entryStates, trackable, track, abandonable); } /// /// Gets the "no quests" message for a quest state (active or success|failure). This /// method uses the strings "No Active Quests" and "No Completed Quests" or their /// localized equivalents if you've set the localized text table. /// /// The "no quests" message. /// Quest state mask. protected virtual string GetNoQuestsMessage(QuestState questStateMask) { return (questStateMask == ActiveQuestStateMask) ? GetLocalizedText(noActiveQuestsText) : GetLocalizedText(noCompletedQuestsText); } /// /// Gets the localized text for a field name. /// /// The localized text. /// Field name. public virtual string GetLocalizedText(string fieldName) { if ((textTable != null) && textTable.HasFieldTextForLanguage(fieldName, Localization.GetCurrentLanguageID(textTable))) { return textTable.GetFieldTextForLanguage(fieldName, Localization.GetCurrentLanguageID(textTable)); } else { return DialogueManager.GetLocalizedText(fieldName); } } /// /// Determines whether the specified questInfo is for the currently-selected quest. /// /// true if this is the selected quest; otherwise, false. /// Quest info. public virtual bool IsSelectedQuest(QuestInfo questInfo) { return string.Equals(questInfo.Title, selectedQuest); } /// /// Your GUI close button should call this. /// /// Ignored. public void ClickClose(object data) { Close(); } /// /// Your GUI "show active quests" button should call this. /// /// Ignored. public virtual void ClickShowActiveQuests(object data) { ShowQuests(ActiveQuestStateMask); } /// /// Your GUI "show completed quests" button should call this. /// /// Ignored. public virtual void ClickShowCompletedQuests(object data) { ShowQuests(QuestState.Success | QuestState.Failure); } /// /// Your GUI should call this when the player clicks on a quest to expand /// or close it. /// /// The quest title. public virtual void ClickQuest(object data) { if (!IsString(data)) return; string clickedQuest = (string)data; selectedQuest = (deselectQuestOnSecondClick && string.Equals(selectedQuest, clickedQuest)) ? string.Empty : clickedQuest; // Mark viewed: if (!string.IsNullOrEmpty(newQuestText) && !string.IsNullOrEmpty(selectedQuest)) { QuestLog.MarkQuestViewed(selectedQuest); foreach (var quest in quests) { if (IsSelectedQuest(quest)) { var newQuestInfo = GetQuestInfo(quest.Group, quest.Title); quest.Heading = newQuestInfo.Heading; break; } } } OnQuestListUpdated(); } /// /// Your GUI should call this when the player clicks to abandon a quest. /// /// Ignored. public virtual void ClickAbandonQuest(object data) { if (string.IsNullOrEmpty(selectedQuest)) return; ConfirmAbandonQuest(selectedQuest, OnConfirmAbandonQuest); } /// /// Your GUI should call this when the player confirms abandonment of a quest. /// protected virtual void OnConfirmAbandonQuest() { QuestLog.SetQuestState(selectedQuest, abandonQuestState); selectedQuest = string.Empty; ShowQuests(currentQuestStateMask); DialogueManager.instance.BroadcastMessage(DialogueSystemMessages.OnQuestTrackingDisabled, selectedQuest, SendMessageOptions.DontRequireReceiver); string sequence = QuestLog.GetQuestAbandonSequence(selectedQuest); if (!string.IsNullOrEmpty(sequence)) DialogueManager.PlaySequence(sequence); } /// /// Your GUI should call this when the player clicks to toggle quest tracking. /// /// Ignored. public virtual void ClickTrackQuest(object data) { if (string.IsNullOrEmpty(selectedQuest)) return; bool track = !QuestLog.IsQuestTrackingEnabled(selectedQuest); QuestLog.SetQuestTracking(selectedQuest, track); } private bool IsString(object data) { return (data != null) && (data.GetType() == typeof(string)); } // Parameter-less versions of methods for GUI systems that require them for button hookups: public virtual void ClickShowActiveQuestsButton() { ClickShowActiveQuests(null); } public void ClickShowCompletedQuestsButton() { ClickShowCompletedQuests(null); } public void ClickCloseButton() { ClickClose(null); } public void ClickAbandonQuestButton() { ClickAbandonQuest(null); } public void ClickTrackQuestButton() { ClickTrackQuest(null); } public void UpdateTracker() { if (isOpen) { if (refreshCoroutine == null) { refreshCoroutine = StartCoroutine(UpdateQuestDisplayAtEndOfFrame()); } } } protected IEnumerator UpdateQuestDisplayAtEndOfFrame() { yield return CoroutineUtility.endOfFrame; refreshCoroutine = null; ShowQuests(currentQuestStateMask); } } }