// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System; using System.Collections.Generic; namespace PixelCrushers.QuestMachine { /// /// Quest object. May be saved as an asset file in the project or may be a /// runtime instance in the scene. /// //--- No [CreateAssetMenu]. Hide from asset menu; use wrapper class instead. public class Quest : ScriptableObject { #region Serialized Fields [Tooltip("Quest is a runtime instance, not an asset file.")] [SerializeField] private bool m_isInstance; [Tooltip("If a runtime instance, this is the original asset from which the instance was created.")] [SerializeField] private Quest m_originalAsset; [SerializeField] private int m_fileVersion = 0; [Tooltip("Unique identifier for this quest.")] [SerializeField] private StringField m_id; [Tooltip("Title shown in UIs.")] [SerializeField] private StringField m_title; [Tooltip("Optional icon shown in UIs.")] [SerializeField] private Sprite m_icon; [Tooltip("Optional group under which to categorize this quest.")] [SerializeField] private StringField m_group; [Tooltip("Optional labels to assign to this quest for sorting and filtering.")] [SerializeField] private List m_labels; [Tooltip("ID of the quest giver that offered this quest. Typically set on the quester's runtime instance of the quest when the quester accepts a quest.")] [SerializeField] private StringField m_questGiverID; [Tooltip("Allow the player to toggle tracking on and off.")] [SerializeField] private bool m_isTrackable; [Tooltip("Show in the quest HUD.")] [SerializeField] private bool m_showInTrackHUD; [Tooltip("Allow the player to abandon the quest.")] [SerializeField] private bool m_isAbandonable; [Tooltip("Keep in quest journal if abandoned.")] [SerializeField] private bool m_rememberIfAbandoned; [Tooltip("Delete when completed even if Quest Journal specifies Remember Completed Quests.")] [SerializeField] private bool m_deleteWhenComplete; [Tooltip("If specified, conditions that autostart the quest when true.")] [SerializeField] private QuestConditionSet m_autostartConditionSet; [Tooltip("Conditions that must be true before the quest can be offered.")] [SerializeField] private QuestConditionSet m_offerConditionSet; [Tooltip("Show this dialogue content when the offer conditions are unmet.")] [SerializeField] private List m_offerConditionsUnmetContentList; [Tooltip("Show this dialogue content to offer the quest.")] [SerializeField] private List m_offerContentList; [Tooltip("Max number of times this quest can be accepted.")] [SerializeField] private int m_maxTimes; [Tooltip("Number of times the quest has been accepted.")] [SerializeField] private int m_timesAccepted; [Tooltip("Minimum duration in seconds that must pass after quest acceptance to offer it again.")] [SerializeField] private float m_cooldownSeconds; [Tooltip("Seconds remaining until cooldown period is over.")] [SerializeField] private float m_cooldownSecondsRemaining; [Tooltip("Do not offer again if quester already has successful completion in its journal.")] [SerializeField] private bool m_noRepeatIfSuccessful = false; [Tooltip("Save counters, node states, & indicator states if waiting to start. Normally omitted to reduce save file size/time.")] [SerializeField] private bool m_saveAllIfWaitingToStart = false; [Tooltip("The current state of the quest.")] [SerializeField] private QuestState m_state; [Tooltip("State info, indexed by the int value of the QuestState enum.")] [SerializeField] private List m_stateInfoList; [Tooltip("Counters defined for this quest.")] [SerializeField] private List m_counterList; [Tooltip("All quest nodes in this quest.")] [SerializeField] private List m_nodeList; [HideInInspector] [SerializeField] private string m_goalEntityTypeName = null; [HideInInspector] [SerializeField] private int m_nextContentID = 0; #endregion #region Property Accessors to Serialized Fields /// /// Quest is a runtime instance, not an asset file. /// public bool isInstance { get { return m_isInstance; } set { m_isInstance = value; } } /// /// Quest is an asset, not a runtime instance. /// public bool isAsset { get { return !isInstance; } set { isInstance = !value; } } /// /// If a runtime instance, this is the original asset from which the instance was created. /// public Quest originalAsset { get { return isInstance ? m_originalAsset : this; } set { m_originalAsset = value; } } public int fileVersion { get { return m_fileVersion; } set { m_fileVersion = value; } } /// /// Quest was procedurally generated. /// public bool isProcedurallyGenerated { get { return isInstance && originalAsset == null; } } /// /// Unique identifier for this quest. /// public StringField id { get { return m_id; } set { m_id = value; } } /// /// Title shown in UIs. /// public StringField title { get { return m_title; } set { m_title = value; } } /// /// Optional quest icon shown in UIs. /// public Sprite icon { get { return m_icon; } set { m_icon = value; } } /// /// Optional group under which to categorize this quest. /// public StringField group { get { return m_group; } set { m_group = value; } } /// /// Optional labels to assign to this quest for sorting and filtering. /// public List labels { get { return m_labels; } set { m_labels = value; } } /// /// ID of the quest giver that offered this quest. Typically set on the quester's /// runtime instance of the quest when the quester accepts a quest. If this is an /// asset or it hasn't been accepted yet, questGiverID will be empty. /// public StringField questGiverID { get { return m_questGiverID; } set { m_questGiverID = value; } } /// /// ID of the quester assigned to this quest. If this is an asset or it hasn't been /// accepted yet, questerID will be empty; /// public string questerID { get { return tagDictionary.GetTagValue(QuestMachineTags.QUESTERID, string.Empty); } } /// /// ID of the quester who is greeting the quest giver. For quests that can be accepted /// by quester A and turned in by quester B, this is the ID of quester B. /// public string greeterID { get { return tagDictionary.GetTagValue(QuestMachineTags.GREETERID, string.Empty); } set { tagDictionary.SetTag(QuestMachineTags.GREETERID, value); } } /// /// Display name of the quester who is greeting the quest giver. For quests that can be accepted /// by quester A and turned in by quester B, this is the name of quester B. /// public string greeter { get { return tagDictionary.GetTagValue(QuestMachineTags.GREETER, string.Empty); } set { tagDictionary.SetTag(QuestMachineTags.GREETER, value); } } /// /// Specifies whether the player is allowed to toggle tracking on and off. /// public bool isTrackable { get { return m_isTrackable; } set { m_isTrackable = value; } } /// /// Specifies whether to show in the quest tracking HUD. /// public bool showInTrackHUD { get { return m_showInTrackHUD; } set { m_showInTrackHUD = value; QuestMachineMessages.QuestTrackToggleChanged(this, id, value); } } /// /// Specifies whether the player is allowed to abandon the quest. /// public bool isAbandonable { get { return m_isAbandonable; } set { m_isAbandonable = value; } } /// /// Specifies whether to keep in quest journal if abandoned. /// public bool rememberIfAbandoned { get { return m_rememberIfAbandoned; } set { m_rememberIfAbandoned = value; } } /// /// Delete when completed even if Quest Journal specifies Remember Completed Quests. /// public bool deleteWhenComplete { get { return m_deleteWhenComplete; } set { m_deleteWhenComplete = value; } } /// /// If specified, conditions that autostart the quest when true. /// public QuestConditionSet autostartConditionSet { get { return m_autostartConditionSet; } set { m_autostartConditionSet = value; } } /// /// If true, the quest has autostart conditions. The quest will start /// automatically when the conditions are met. /// public bool hasAutostartConditions { get { return QuestConditionSet.ConditionCount(autostartConditionSet) > 0; } } /// /// Conditions that must be true before the quest can be offered. /// public QuestConditionSet offerConditionSet { get { return m_offerConditionSet; } set { m_offerConditionSet = value; } } /// /// If true, the quest has offer conditions. The giver should not offer the /// quest until the conditions are met. /// public bool hasOfferConditions { get { return QuestConditionSet.ConditionCount(offerConditionSet) > 0; } } /// /// If true, the giver can offer the quest. /// public bool canOffer { get { return (!hasOfferConditions || offerConditionSet.areConditionsMet) && (timesAccepted < maxTimes) && (cooldownSecondsRemaining <= 0); } } /// /// Dialogue text to show when the offer conditions are unmet. /// public List offerConditionsUnmetContentList { get { return m_offerConditionsUnmetContentList; } set { m_offerConditionsUnmetContentList = value; } } /// /// Dialogue text to show when offering the quest. /// public List offerContentList { get { return m_offerContentList; } set { m_offerContentList = value; } } /// /// Max number of times this quest can be accepted. /// public int maxTimes { get { return m_maxTimes; } set { m_maxTimes = value; } } /// /// The number of times the quest has been accepted. /// public int timesAccepted { get { return m_timesAccepted; } set { m_timesAccepted = value; } } /// /// Minimum duration in seconds that must pass after quest acceptance to /// offer it again. /// public float cooldownSeconds { get { return m_cooldownSeconds; } set { m_cooldownSeconds = value; } } /// /// Seconds remaining until cooldown period is over. /// public float cooldownSecondsRemaining { get { return m_cooldownSecondsRemaining; } set { m_cooldownSecondsRemaining = value; } } /// /// Do not offer again if quester already has successful completion in its journal. /// public bool noRepeatIfSuccessful { get { return m_noRepeatIfSuccessful; } set { m_noRepeatIfSuccessful = value; } } /// /// Save counters, node states, & indicator states if waiting to start. Normally omitted to reduce save file size/time. /// public bool saveAllIfWaitingToStart { get { return m_saveAllIfWaitingToStart; } set { m_saveAllIfWaitingToStart = value; } } /// /// Info for each state, indexed by the int value of the QuestState enum. /// public List stateInfoList { get { return m_stateInfoList; } set { m_stateInfoList = value; } } /// /// Counters defined for this quest. /// public List counterList { get { return m_counterList; } set { m_counterList = value; } } /// /// All nodes in this quest. /// public List nodeList { get { return m_nodeList; } set { m_nodeList = value; } } /// /// The quest's start node. /// public QuestNode startNode { get { return (m_nodeList != null && m_nodeList.Count > 0) ? m_nodeList[0] : null; } } /// /// If this quest was procedurally generated, the goal EntityType's name. /// public string goalEntityTypeName { get { return m_goalEntityTypeName; } set { m_goalEntityTypeName = value; } } /// /// Used by QuestContent to assign a unique ID number usable by LinkQuestContent. /// public int nextContentID { get { return m_nextContentID; } set { m_nextContentID = value; } } #endregion #region Runtime References [NonSerialized] private float m_timeCooldownLastChecked; [NonSerialized] private TagDictionary m_tagDictionary = new TagDictionary(); [NonSerialized] private Dictionary m_questIndicatorStates = new Dictionary(); [NonSerialized] private HashSet m_speakers = new HashSet(); [NonSerialized] private Dictionary m_questContentByID = new Dictionary(); /// /// Dictionary of tags defined in this quest and their values. /// public TagDictionary tagDictionary { get { return m_tagDictionary; } set { m_tagDictionary = value; } } /// /// Current quest state indicator states by entity ID. /// public Dictionary indicatorStates { get { return m_questIndicatorStates; } set { m_questIndicatorStates = value; } } /// /// List of all quest node speakers. /// public HashSet speakers { get { return m_speakers; } set { m_speakers = value; } } private QuestParticipantTextInfo m_currentSpeaker = null; /// /// The current speaker's info, if the speaker is different /// from the quest giver. If the quest giver is speaking, this /// property will be null. /// public QuestParticipantTextInfo currentSpeaker { get { return m_currentSpeaker; } private set { m_currentSpeaker = value; } } /// /// Raised when the quest has become offerable. /// public event QuestParameterDelegate questOfferable = delegate { }; /// /// Raised when the quest's state has changed. /// public event QuestParameterDelegate stateChanged = delegate { }; #endregion #region Editor public string GetEditorName() { if (!StringField.IsNullOrEmpty(title)) return title.value; if (!StringField.IsNullOrEmpty(id)) return id.value; return "Unnamed Quest"; } #endregion #region Initialization & Destruction /// /// Initializes a quest to empty starting values. Invoked when object is /// created by ScriptableObjectUtility.CreateInstance. /// public void Initialize() { // (isInstance & originalAsset are not set here.) var instanceID = GetInstanceID(); id = new StringField("Quest" + instanceID); title = new StringField("Quest " + instanceID); icon = null; group = new StringField(); labels = new List(); questGiverID = new StringField(); isTrackable = true; showInTrackHUD = true; isAbandonable = false; rememberIfAbandoned = false; autostartConditionSet = new QuestConditionSet(); offerConditionSet = new QuestConditionSet(); offerConditionsUnmetContentList = new List(); offerContentList = new List(); maxTimes = 1; timesAccepted = 0; cooldownSeconds = 3600; cooldownSecondsRemaining = 0; m_state = QuestState.WaitingToStart; var numStates = Enum.GetNames(typeof(QuestState)).Length; stateInfoList = new List(); for (int i = 0; i < numStates; i++) { stateInfoList.Add(new QuestStateInfo()); } counterList = new List(); var startNode = new QuestNode(); startNode.InitializeAsStartNode(id.value); nodeList = new List(); nodeList.Add(startNode); } /// /// Returns a new instance of the quest, including new instances of all subassets /// such as QuestAction, QuestCondition, and QuestContent subassets. /// public Quest Clone() { var clone = Instantiate(this); SetRuntimeReferences(); // Fix original's references since Instantiate calls OnEnable > SetRuntimeReferences while clone's fields still point to original. clone.isInstance = true; clone.originalAsset = originalAsset; clone.cooldownSecondsRemaining = 0; autostartConditionSet.CloneSubassetsInto(clone.autostartConditionSet); offerConditionSet.CloneSubassetsInto(clone.offerConditionSet); clone.offerConditionsUnmetContentList = QuestSubasset.CloneList(offerConditionsUnmetContentList); clone.offerContentList = QuestSubasset.CloneList(offerContentList); QuestStateInfo.CloneSubassets(stateInfoList, clone.stateInfoList); QuestNode.CloneSubassets(nodeList, clone.nodeList); tagDictionary.CopyInto(clone.tagDictionary); clone.SetRuntimeReferences(); return clone; } private void OnDestroy() { if (isInstance && Application.isPlaying) { QuestMachine.UnregisterQuestInstance(this); SetState(QuestState.Disabled); if (autostartConditionSet != null) autostartConditionSet.DestroySubassets(); if (offerConditionSet != null) offerConditionSet.DestroySubassets(); QuestSubasset.DestroyList(offerConditionsUnmetContentList); QuestSubasset.DestroyList(offerContentList); QuestStateInfo.DestroyListSubassets(stateInfoList); QuestNode.DestroyListSubassets(nodeList); } } public static void DestroyInstance(Quest quest) { if (quest != null && quest.isInstance) { if (quest.GetState() != QuestState.Disabled) quest.SetState(QuestState.Disabled); Destroy(quest); } } private void OnEnable() { SetRuntimeReferences(); } /// /// Sets sub-objects' runtime references to this quest. /// public void SetRuntimeReferences() { // Set references in start info: if (Application.isPlaying) m_timeCooldownLastChecked = GameTime.time; if (autostartConditionSet != null) autostartConditionSet.SetRuntimeReferences(this, null); if (offerConditionSet != null) offerConditionSet.SetRuntimeReferences(this, null); QuestContent.SetRuntimeReferences(offerConditionsUnmetContentList, this, null); QuestContent.SetRuntimeReferences(offerContentList, this, null); // Set references in counters: if (counterList != null) { for (int i = 0; i < counterList.Count; i++) { counterList[i].SetRuntimeReferences(this); } } // Set references in state info: if (stateInfoList != null) { for (int i = 0; i < stateInfoList.Count; i++) { var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, (QuestState)i); if (stateInfo != null) stateInfo.SetRuntimeReferences(this, null); } } // Set references in nodes: if (nodeList != null) { for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i] != null) nodeList[i].InitializeRuntimeReferences(this); } for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i] != null) nodeList[i].ConnectRuntimeNodeReferences(); } for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i] != null) nodeList[i].SetRuntimeNodeReferences(); } } // Record list of any nodes' speakers who aren't the quest giver: RecordSpeakersUsedInQuestAndAnyNodes(); // Add tags to dictionary: QuestMachineTags.AddTagsToDictionary(tagDictionary, title); QuestMachineTags.AddTagsToDictionary(tagDictionary, group); if (!StringField.IsNullOrEmpty(questGiverID)) tagDictionary.SetTag(QuestMachineTags.QUESTGIVERID, questGiverID); } /// /// Populates the speakers list with the speakers used in the quest and /// any of its nodes. /// private void RecordSpeakersUsedInQuestAndAnyNodes() { if (speakers == null) speakers = new HashSet(); speakers.Clear(); if (!StringField.IsNullOrEmpty(questGiverID)) speakers.Add(StringField.GetStringValue(questGiverID)); if (nodeList == null) return; for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i] == null) continue; var speaker = StringField.GetStringValue(nodeList[i].speaker); if (string.IsNullOrEmpty(speaker) || speakers.Contains(speaker)) continue; speakers.Add(speaker); } } /// /// Assigns a quest giver to the quest. /// /// Identifying information about the quest giver. public void AssignQuestGiver(QuestParticipantTextInfo questGiverTextInfo) { if (questGiverTextInfo == null) return; questGiverID = questGiverTextInfo.id; if (!StringField.IsNullOrEmpty(questGiverTextInfo.id)) speakers.Add(StringField.GetStringValue(questGiverTextInfo.id)); QuestMachineTags.AddTagValuesToDictionary(tagDictionary, questGiverTextInfo.textTable); tagDictionary.SetTag(QuestMachineTags.QUESTGIVERID, questGiverTextInfo.id); tagDictionary.SetTag(QuestMachineTags.QUESTGIVER, questGiverTextInfo.displayName); } /// /// Assigns a quester (e.g., player) to the quest. /// /// Idenntifying information about the quester. public void AssignQuester(QuestParticipantTextInfo questerTextInfo) { if (questerTextInfo == null || StringField.IsNullOrEmpty(questerTextInfo.id)) return; tagDictionary.SetTag(QuestMachineTags.QUESTERID, questerTextInfo.id); tagDictionary.SetTag(QuestMachineTags.QUESTER, questerTextInfo.displayName); } #endregion #region Startup /// /// Invoke to tell the quest to perform its runtime startup actions. /// public void RuntimeStartup() { if (Application.isPlaying) SetState(m_state, !QuestMachine.isLoadingGame); } /// /// Begins checking autostart and offer conditions. /// public void SetStartChecking(bool enable) { if (!Application.isPlaying) return; if (enable) { SetRandomCounterValues(); if (hasAutostartConditions) autostartConditionSet.StartChecking(Autostart); if (GetState() == QuestState.WaitingToStart) // Autostart check above might set quest active. { if (hasOfferConditions) { offerConditionSet.StartChecking(BecomeOfferable); } else { BecomeOfferable(); } } } else { if (hasAutostartConditions) autostartConditionSet.StopChecking(); if (hasOfferConditions) offerConditionSet.StopChecking(); } } private void Autostart() { SetState(QuestState.Active); } public void BecomeOfferable() { try { if (GetState() != QuestState.WaitingToStart) SetState(QuestState.WaitingToStart); questOfferable(this); SetQuestIndicatorState(questGiverID, QuestIndicatorState.Offer); } catch (Exception e) // Don't let exceptions in user-added events break our code. { if (Debug.isDebugBuild) Debug.LogException(e); } } public void BecomeUnofferable() { try { if (GetState() != QuestState.Disabled) SetState(QuestState.Disabled); SetQuestIndicatorState(questGiverID, QuestIndicatorState.None); } catch (Exception e) // Don't let exceptions in user-added events break our code. { if (Debug.isDebugBuild) Debug.LogException(e); } } /// /// Starts the cooldown period for this quest. /// public void StartCooldown() { if (cooldownSeconds <= 0) return; cooldownSecondsRemaining = cooldownSeconds; m_timeCooldownLastChecked = GameTime.time; } /// /// Checks the current game time and updates the cooldown period. /// public void UpdateCooldown() { if (cooldownSecondsRemaining <= 0) return; var elapsed = GameTime.time - m_timeCooldownLastChecked; m_timeCooldownLastChecked = GameTime.time; cooldownSecondsRemaining = Mathf.Max(0, cooldownSecondsRemaining - elapsed); if (cooldownSecondsRemaining <= 0) BecomeOfferable(); } #endregion #region Quest State /// /// Gets the quest state. /// /// The current quest state. Each quest node also has its own state. public QuestState GetState() { return m_state; } /// /// Sets the quest state. This may also affect the states of the quest's nodes. /// /// The new quest state. public void SetState(QuestState newState, bool informListeners = true) { if (QuestMachine.debug) Debug.Log("Quest Machine: " + GetEditorName() + ".SetState(" + newState + ", informListeners=" + informListeners + ")", this); m_state = newState; SetStartChecking(m_state == QuestState.WaitingToStart); SetCounterListeners(m_state == QuestState.Active || (m_state == QuestState.WaitingToStart && (hasAutostartConditions || hasOfferConditions))); if (m_state != QuestState.Active) StopNodeListeners(); if (!informListeners) return; // Execute state actions: ExecuteStateActions(m_state); // Notify that state changed: QuestMachineMessages.QuestStateChanged(this, id, m_state); try { stateChanged(this); } catch (Exception e) // Don't let exceptions in user-added events break our code. { if (Debug.isDebugBuild) Debug.LogException(e); } // If going active, activate the start node: if (m_state == QuestState.Active && startNode != null) startNode.SetState(QuestNodeState.Active); // If inactive, clear the indicators: if (m_state != QuestState.Active) ClearQuestIndicatorStates(); // If done, set all active nodes to inactive: if (m_state == QuestState.Successful || m_state == QuestState.Failed || m_state == QuestState.Abandoned) { for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i].GetState() == QuestNodeState.Active) nodeList[i].SetState(QuestNodeState.Inactive); } if (QuestMachineConfiguration.instance != null && QuestMachineConfiguration.instance.untrackCompletedQuests) { showInTrackHUD = false; } } } /// /// Sets the internal state value without performing any state change processing. /// public void SetStateRaw(QuestState state) { m_state = state; } /// /// Returns the state info associated with a quest state. /// public QuestStateInfo GetStateInfo(QuestState state) { return (stateInfoList != null) ? QuestStateInfo.GetStateInfo(stateInfoList, state) : null; } private void StopNodeListeners() { if (nodeList == null) return; for (int i = 0; i < nodeList.Count; i++) { if (nodeList[i] != null) nodeList[i].SetConditionChecking(false); } } public void ExecuteStateActions(QuestState state) { var stateInfo = GetStateInfo(state); if (stateInfo != null && stateInfo.actionList != null) { for (int i = 0; i < stateInfo.actionList.Count; i++) { if (stateInfo.actionList[i] != null) stateInfo.actionList[i].Execute(); } } } #endregion #region Counters private void SetRandomCounterValues() { if (counterList == null) return; for (int i = 0; i < counterList.Count; i++) { var counter = counterList[i]; if (counter != null && counter.randomizeInitialValue) counter.InitializeToRandomValue(); } } private void SetCounterListeners(bool enable) { if (counterList == null) return; for (int i = 0; i < counterList.Count; i++) { if (counterList[i] != null) counterList[i].SetListeners(enable); } } /// /// Gets a counter defined in this quest. /// /// The index of the counter defined in the quest. /// The counter, or null if there is no counter with the specified name. public QuestCounter GetCounter(int index) { return (counterList != null && 0 <= index && index < counterList.Count) ? counterList[index] : null; } /// /// Gets a counter defined in this quest. /// /// The name of the counter defined in the quest. /// The counter, or null if there is no counter with the specified name. public QuestCounter GetCounter(string counterName) { if (counterList == null) return null; for (int i = 0; i < counterList.Count; i++) { var counter = counterList[i]; if (counter != null && StringField.Equals(counter.name, counterName)) return counter; } return null; } /// /// Gets a counter defined in this quest. /// /// The name of the counter defined in the quest. /// The counter, or null if there is no counter with the specified name. public QuestCounter GetCounter(StringField counterName) { return GetCounter(StringField.GetStringValue(counterName)); } public int GetCounterIndex(string counterName) { if (counterList == null) return -1; for (int i = 0; i < counterList.Count; i++) { var counter = counterList[i]; if (counter != null && StringField.Equals(counter.name, counterName)) return i; } return -1; } public int GetCounterIndex(StringField counterName) { return GetCounterIndex(StringField.GetStringValue(counterName)); } #endregion #region Nodes /// /// Looks up a node by its ID. /// public QuestNode GetNode(string questNodeID) { if (string.IsNullOrEmpty(questNodeID) || nodeList == null) return null; return nodeList.Find(x => StringField.Equals(StringField.GetStringValue(x.id), questNodeID)); } /// /// Looks up a node by its ID. /// public QuestNode GetNode(StringField questNodeID) { return GetNode(StringField.GetStringValue(questNodeID)); } #endregion #region UI Content public bool IsSpeakerQuestGiver(QuestParticipantTextInfo speaker) { return (speaker == null) || StringField.Equals(speaker.id, questGiverID); } /// /// Checks if there is any UI content for a specific category. /// /// The content category (Dialogue, Journal, etc.). /// The speaker whose content to check, or blank for the quest giver. /// True if GetContentList would return anything. public bool HasContent(QuestContentCategory category, QuestParticipantTextInfo speaker = null) { currentSpeaker = IsSpeakerQuestGiver(speaker) ? null : speaker; var stateInfo = GetStateInfo(GetState()); if (stateInfo.HasContent(category)) return true; if (nodeList == null) return false; for (int i = 0; i < nodeList.Count; i++) { var node = nodeList[i]; if (node != null && node.HasContent(category)) return true; } return false; } /// /// Gets the UI content for a specific category. /// /// The content category (Dialogue, Journal, etc.). /// The speaker whose content to get, or blank for the quest giver. /// A list of content items based on the current state of the quest and all of its nodes. public List GetContentList(QuestContentCategory category, QuestParticipantTextInfo speaker = null) { var contentList = new List(); currentSpeaker = IsSpeakerQuestGiver(speaker) ? null : speaker; var stateInfo = GetStateInfo(GetState()); if (stateInfo != null) AddToContentList(contentList, stateInfo.GetContentList(category)); if (nodeList != null) { for (int i = 0; i < nodeList.Count; i++) { var node = nodeList[i]; var nodeContentList = (node != null) ? node.GetContentList(category) : null; if (nodeContentList != null) AddToContentList(contentList, nodeContentList); } } return contentList; } // Adds to contentList, replacing links with the linked content. private void AddToContentList(List contentList, List add) { if (add == null) return; for (int i = 0; i < add.Count; i++) { var content = add[i]; if (content == null) continue; if (content is LinkQuestContent) { var linkedContent = GetContentByID((content as LinkQuestContent).linkedContentID); if (linkedContent != null) contentList.Add(linkedContent); } else { contentList.Add(content); } } } public void AssignContentID(QuestContent content) { if (content == null) return; content.contentID = m_nextContentID++; } public QuestContent GetContentByID(int contentID) { QuestContent content; if (!m_questContentByID.TryGetValue(contentID, out content)) { content = FindContentByID(contentID); if (content != null) m_questContentByID[contentID] = content; } return content; } protected QuestContent FindContentByID(int contentID) { return FindContentByID(contentID, offerContentList) ?? FindContentByID(contentID, offerConditionsUnmetContentList) ?? FindContentByIDInMainQuest(contentID) ?? FindContentByIDInNodes(contentID); } protected QuestContent FindContentByID(int contentID, List contentList) { if (contentList == null) return null; for (int i = 0; i < contentList.Count; i++) { var content = contentList[i]; if (content != null && content.contentID == contentID) return content; } return null; } protected QuestContent FindContentByIDInStateInfo(int contentID, QuestStateInfo stateInfo) { if (stateInfo == null) return null; return FindContentByID(contentID, stateInfo.GetContentList(QuestContentCategory.Dialogue)) ?? FindContentByID(contentID, stateInfo.GetContentList(QuestContentCategory.Journal)) ?? FindContentByID(contentID, stateInfo.GetContentList(QuestContentCategory.HUD)); } protected QuestContent FindContentByIDInMainQuest(int contentID) { for (int i = 0; i <= (int)QuestState.Abandoned; i++) { var content = FindContentByIDInStateInfo(contentID, GetStateInfo((QuestState)i)); if (content != null) return content; } return null; } protected QuestContent FindContentByIDInNodes(int contentID) { for (int i = 0; i < nodeList.Count; i++) { var node = nodeList[i]; if (node == null) continue; var content = FindContentByIDInStateInfo(contentID, node.GetStateInfo(QuestNodeState.Inactive)) ?? FindContentByIDInStateInfo(contentID, node.GetStateInfo(QuestNodeState.Active)) ?? FindContentByIDInStateInfo(contentID, node.GetStateInfo(QuestNodeState.True)); if (content != null) return content; } return null; } #endregion #region Quest Indicator States public void SetQuestIndicatorState(string entityID, QuestIndicatorState questIndicatorState) { if (string.IsNullOrEmpty(entityID)) return; if (!indicatorStates.ContainsKey(entityID)) indicatorStates.Add(entityID, QuestIndicatorState.None); indicatorStates[entityID] = questIndicatorState; MessageSystem.SendMessageWithTarget(this, entityID, QuestMachineMessages.SetIndicatorStateMessage, id, questIndicatorState); } public void SetQuestIndicatorState(StringField entityID, QuestIndicatorState questIndicatorState) { SetQuestIndicatorState(StringField.GetStringValue(entityID), questIndicatorState); } public QuestIndicatorState GetQuestIndicatorState(string entityID) { return (string.IsNullOrEmpty(entityID) || !indicatorStates.ContainsKey(entityID)) ? QuestIndicatorState.None : indicatorStates[entityID]; } public QuestIndicatorState GetQuestIndicatorState(StringField entityID) { return GetQuestIndicatorState(StringField.GetStringValue(entityID)); } public void ClearQuestIndicatorStates() { if (indicatorStates == null) return; foreach (var kvp in indicatorStates) { MessageSystem.SendMessageWithTarget(this, kvp.Key, QuestMachineMessages.SetIndicatorStateMessage, id, QuestIndicatorState.None); } indicatorStates.Clear(); QuestMachineMessages.RefreshIndicators(questGiverID); } #endregion #region Compress Generated Content /// /// If quest is procedurally generated and completed, destroys all content and /// conditions not needed to show in UIs. /// public void CompressGeneratedContent() { var canCompress = isProcedurallyGenerated && (GetState() == QuestState.Successful || GetState() == QuestState.Failed); if (!canCompress) return; CompressGeneratedContent(autostartConditionSet); CompressGeneratedContent(offerConditionSet); CompressGeneratedContent(offerConditionsUnmetContentList); CompressGeneratedContent(offerContentList); var stateCount = Enum.GetNames(typeof(QuestState)).Length; for (int j = 0; j < stateCount; j++) { if (j == (int)QuestState.Successful || j == (int)QuestState.Failed) continue; var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, (QuestNodeState)j); if (stateInfo != null) { stateInfo.DestroySubassets(); stateInfo.actionList.Clear(); for (int k = 0; k < stateInfo.categorizedContentList.Count; k++) { stateInfo.categorizedContentList[k].contentList.Clear(); } } } for (int i = 0; i < nodeList.Count; i++) { var node = nodeList[i]; if (node == null) continue; CompressGeneratedContent(node.conditionSet); var nodeStateCount = Enum.GetNames(typeof(QuestNodeState)).Length; for (int j = 0; j < nodeStateCount; j++) { if (j == (int)QuestNodeState.True) continue; var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, (QuestNodeState)j); if (stateInfo != null) { stateInfo.DestroySubassets(); stateInfo.actionList.Clear(); for (int k = 0; k < stateInfo.categorizedContentList.Count; k++) { stateInfo.categorizedContentList[k].contentList.Clear(); } } } } } private void CompressGeneratedContent(QuestConditionSet conditionSet) { conditionSet.DestroySubassets(); conditionSet.conditionList.Clear(); } private void CompressGeneratedContent(List contentList) { QuestContent.DestroyList(contentList); contentList.Clear(); } #endregion } }