// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System; using System.Collections.Generic; namespace PixelCrushers.QuestMachine { /// /// A quest node is a task or stage in a quest. /// [Serializable] public class QuestNode { #region Serialized Fields [Tooltip("Internal ID of the quest node, used to reference quest nodes in scripts.")] [SerializeField] private StringField m_id = new StringField(); [Tooltip("Internal name of the quest node for the designer's reference. Not seen by the player.")] [SerializeField] private StringField m_internalName = new StringField(); [Tooltip("Type of node, which determines some of its behavior.")] [SerializeField] private QuestNodeType m_nodeType; [Tooltip("Completion of this quest node is optional.")] [SerializeField] private bool m_isOptional; [Tooltip("The current state of the quest node.")] [SerializeField] private QuestNodeState m_state = QuestNodeState.Inactive; [Tooltip("Speaker of this node's dialogue content. If unassigned, the quest giver is the speaker.")] [SerializeField] private StringField m_speaker = new StringField(); [Tooltip("Info (actions & UI content) for a specific quest state, indexed by the int value of the QuestState enum.")] [SerializeField] private List m_stateInfoList = new List(); [Tooltip("Conditions required for the node's state to become true.")] [SerializeField] private QuestConditionSet m_conditionSet = new QuestConditionSet(); [HideInInspector] [SerializeField] private List m_childIndexList = new List(); // Indices into Quest.nodes. [HideInInspector] [SerializeField] private Rect m_canvasRect; // Position in editor canvas. #endregion #region Accessor Properties for Serialized Fields /// /// Internal ID of the quest node, used to reference quest nodes in scripts. /// public StringField id { get { return m_id; } set { m_id = value; } } /// /// Name for your internal reference; shown only in the editor. /// public StringField internalName { get { return m_internalName; } set { m_internalName = value; } } /// /// Type of node, which determines some of its behavior. /// public QuestNodeType nodeType { get { return m_nodeType; } set { m_nodeType = value; } } /// /// Specifies that the node is a type that terminates the quest tree. /// public bool isEndNodeType { get { return nodeType == QuestNodeType.Success || nodeType == QuestNodeType.Failure; } } /// /// Specifies that the node is a type that can connect to other nodes. /// public bool isConnectionNodeType { get { return !isEndNodeType; } } /// /// Completion of this quest node is optional. /// public bool isOptional { get { return m_isOptional; } set { m_isOptional = value; } } /// /// Speaker of this node's dialogue content. If unassigned, the quest giver is the speaker. /// public StringField speaker { get { return m_speaker; } set { m_speaker = value; } } /// /// Info (actions & UI content) for a specific quest state. This list is indexed by the /// int value of the QuestState enum, such as stateInfoList[(int)QuestState.Active]. /// public List stateInfoList { get { return m_stateInfoList; } set { m_stateInfoList = value; } } /// /// Conditions required for the node's state to become true. /// public QuestConditionSet conditionSet { get { return m_conditionSet; } set { m_conditionSet = value; } } /// /// Indices into the quest's node list. Since Unity can't serialize nested types, such as /// a QuestNode reference inside the QuestNode class, we use a list of indices to record /// connections between nodes for serialization. At runtime, we use this list of indices /// to construct a list of QuestNode references. /// public List childIndexList { get { return m_childIndexList; } set { m_childIndexList = value; } } #endregion #region Runtime References [NonSerialized] private TagDictionary m_tagDictionary = new TagDictionary(); [NonSerialized] private Quest m_quest; /// /// The Quest that this node belongs to. /// public Quest quest { get { return m_quest; } set { m_quest = value; } } [NonSerialized] public List m_childList; /// /// References to the child nodes linked from this node. /// public List childList { get { return m_childList; } set { m_childList = value; } } [NonSerialized] private List m_parentList; /// /// References to the parent nodes that link to this node. /// public List parentList { get { return m_parentList; } set { m_parentList = value; } } [NonSerialized] private List m_optionalParentList; /// /// The subset of parents that are marked optional. /// public List optionalParentList { get { return m_optionalParentList; } set { m_optionalParentList = value; } } [NonSerialized] private List m_nonoptionalParentList; /// /// The subset of parents that are not marked optional. /// public List nonoptionalParentList { get { return m_nonoptionalParentList; } set { m_nonoptionalParentList = value; } } /// /// Dictionary of tags defined in this quest node and their values. /// public TagDictionary tagDictionary { get { return m_tagDictionary; } set { m_tagDictionary = value; } } /// /// Invoked when the node changes state. /// public event QuestNodeParameterDelegate stateChanged = delegate { }; private bool m_isCheckingConditions = false; #endregion #region Editor // Node sizes for editor: public const float DefaultNodeWidth = 120; public const float DefaultNodeHeight = 48; public const float ShortNodeHeight = 35; public const float DefaultStartNodeX = 200; public const float DefaultStartNodeY = 20; /// /// Position of the quest node in the Quest Editor window. /// public Rect canvasRect { get { return m_canvasRect; } set { m_canvasRect = value; } } public string GetEditorName() { if (!StringField.IsNullOrEmpty(internalName)) return internalName.value; if (!StringField.IsNullOrEmpty(id)) return id.value; return "Node"; } #endregion #region Initialization public QuestNode() { } public QuestNode(StringField id, StringField internalName, QuestNodeType nodeType, bool isOptional = false) { m_id = id; m_internalName = internalName; m_nodeType = nodeType; m_isOptional = isOptional; } public void InitializeAsStartNode(string questID) { id = new StringField(questID + ".start"); internalName = new StringField("Start"); nodeType = QuestNodeType.Start; m_state = QuestNodeState.Inactive; stateInfoList = new List(); canvasRect = new Rect(DefaultStartNodeX, DefaultStartNodeY, DefaultNodeWidth, DefaultNodeHeight); } public void CloneSubassetsInto(QuestNode copy) { // Assumes lists are identical except subassets haven't been copied. if (copy == null) return; conditionSet.CloneSubassetsInto(copy.conditionSet); QuestStateInfo.CloneSubassets(stateInfoList, copy.stateInfoList); tagDictionary.CopyInto(copy.tagDictionary); } public static void CloneSubassets(List original, List copy) { // Assumes lists are identical except subassets haven't been copied. if (original == null || copy == null || copy.Count != original.Count) { if (Debug.isDebugBuild) Debug.LogWarning("Quest Machine: QuestNode.CloneSubassets() failed because copy or original is invalid."); return; } for (int i = 0; i < original.Count; i++) { if (original[i] != null) original[i].CloneSubassetsInto(copy[i]); } } public void DestroySubassets() { if (conditionSet != null) conditionSet.DestroySubassets(); QuestStateInfo.DestroyListSubassets(stateInfoList); } public static void DestroyListSubassets(List nodes) { if (nodes == null) return; for (int i = 0; i < nodes.Count; i++) { if (nodes[i] != null) nodes[i].DestroySubassets(); } } public void InitializeRuntimeReferences(Quest quest) { this.quest = quest; // Set references in condition set: if (conditionSet != null) conditionSet.SetRuntimeReferences(quest, this); // Build children list: if (quest.nodeList != null) { childList = new List(); for (var i = 0; i < childIndexList.Count; i++) { var index = childIndexList[i]; if (0 <= index && index < quest.nodeList.Count) childList.Add(quest.nodeList[index]); } } parentList = new List(); optionalParentList = new List(); nonoptionalParentList = new List(); } public void ConnectRuntimeNodeReferences() { if (childList == null) return; for (int i = 0; i < childList.Count; i++) { if (childList[i] != null) childList[i].SetParent(this); } } private void SetParent(QuestNode parent) { if (parent == null) return; if (parentList == null) parentList = new List(); parentList.Add(parent); if (parent.isOptional) { if (optionalParentList == null) optionalParentList = new List(); optionalParentList.Add(parent); } else { if (nonoptionalParentList == null) nonoptionalParentList = new List(); nonoptionalParentList.Add(parent); } parent.stateChanged -= OnParentStateChange; parent.stateChanged += OnParentStateChange; } public void SetRuntimeNodeReferences() { var stateCount = Enum.GetNames(typeof(QuestNodeState)).Length; for (int i = 0; i < stateCount; i++) { var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, (QuestNodeState)i); if (stateInfo != null) stateInfo.SetRuntimeReferences(quest, this); } } #endregion #region Quest Node State /// /// Returns the current state of the quest node. /// public QuestNodeState GetState() { return m_state; } /// /// Sets the quest node to a quest state and performs all related activities /// such as enabling connections and executing actions. This may cause other /// nodes to advance their states, too. /// /// New state. public void SetState(QuestNodeState newState, bool informListeners = true) { if (QuestMachine.debug) Debug.Log("Quest Machine: " + ((quest != null) ? quest.GetEditorName() : "Quest") + "." + GetEditorName() + ".SetState(" + newState + ")", quest); m_state = newState; SetConditionChecking(newState == QuestNodeState.Active); if (!informListeners) return; // Execute state actions: var stateInfo = GetStateInfo(m_state); if (stateInfo != null && stateInfo.actionList != null) { for (int i = 0; i < stateInfo.actionList.Count; i++) { if (stateInfo.actionList[i] == null) continue; stateInfo.actionList[i].Execute(); } } // Notify that state changed: QuestMachineMessages.QuestNodeStateChanged(this, quest.id, 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); } // Handle special node types: switch (m_state) { case QuestNodeState.Active: if (nodeType != QuestNodeType.Condition) { // Automatically switch non-Condition nodes to True state: SetState(QuestNodeState.True); } break; case QuestNodeState.True: // If it's an endpoint, set the overall quest state: switch (nodeType) { case QuestNodeType.Success: if (quest != null) quest.SetState(QuestState.Successful); break; case QuestNodeType.Failure: if (quest != null) quest.SetState(QuestState.Failed); break; } break; } } /// /// Sets the internal state value without performing any state change processing. /// public void SetStateRaw(QuestNodeState state) { m_state = state; } /// /// Returns the state info associated with a quest node state. /// public QuestStateInfo GetStateInfo(QuestNodeState state) { return (stateInfoList != null) ? QuestStateInfo.GetStateInfo(stateInfoList, state) : null; } /// /// Starts or stops condition checking on condition nodes. /// /// Specifies whether to start (enable) or stop. public void SetConditionChecking(bool enable) { if (!Application.isPlaying) return; if ((enable && m_isCheckingConditions) || (!enable && !m_isCheckingConditions)) return; if (!isConnectionNodeType || conditionSet == null) return; if (enable == true) { conditionSet.StartChecking(OnConditionsTrue); } else { conditionSet.StopChecking(); } m_isCheckingConditions = enable; } private void OnConditionsTrue() { SetState(QuestNodeState.True); } /// /// Invoked by parent when parent's state changes. /// /// Parent node whose state changed. private void OnParentStateChange(QuestNode parent) { if (parent != null && parent.GetState() == QuestNodeState.True && quest != null && quest.GetState() == QuestState.Active && GetState() == QuestNodeState.Inactive) { SetState(QuestNodeState.Active); } } #endregion #region UI Content /// /// Checks if there is any UI content for a specific category. /// /// The content category (Dialogue, Journal, etc.). /// True if GetContentList would return anything. public bool HasContent(QuestContentCategory category) { if (!IsContentValidForCurrentSpeaker(category)) return false; var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, m_state); if (stateInfo == null) return false; var contentList = stateInfo.GetContentList(category); return contentList != null && contentList.Count > 0; } /// /// Gets the UI content for a specific category. /// /// The content category (Dialogue, Journal, etc.). /// A list of UI content items based on the current state of the quest and all of its nodes. public List GetContentList(QuestContentCategory category) { if (!IsContentValidForCurrentSpeaker(category)) return null; var stateInfo = QuestStateInfo.GetStateInfo(stateInfoList, m_state); return (stateInfo != null) ? stateInfo.GetContentList(category) : null; } private bool IsContentValidForCurrentSpeaker(QuestContentCategory category) { // Non-dialogue content is always valid: if (category != QuestContentCategory.Dialogue) return true; if (quest == null) return true; // Are quest's current speaker and this node's speaker both the quest giver? if (quest.currentSpeaker == null) { return StringField.IsNullOrEmpty(speaker) || StringField.Equals(speaker, quest.questGiverID); } // Otherwise is quest's current speaker same as this node's speaker? return StringField.Equals(speaker, quest.currentSpeaker.id); } #endregion } }