Files
2024-11-20 15:21:28 +01:00

569 lines
19 KiB
C#

// Copyright (c) Pixel Crushers. All rights reserved.
using UnityEngine;
using System;
using System.Collections.Generic;
namespace PixelCrushers.QuestMachine
{
/// <summary>
/// A quest node is a task or stage in a quest.
/// </summary>
[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<QuestStateInfo> m_stateInfoList = new List<QuestStateInfo>();
[Tooltip("Conditions required for the node's state to become true.")]
[SerializeField]
private QuestConditionSet m_conditionSet = new QuestConditionSet();
[HideInInspector]
[SerializeField]
private List<int> m_childIndexList = new List<int>(); // Indices into Quest.nodes.
[HideInInspector]
[SerializeField]
private Rect m_canvasRect; // Position in editor canvas.
#endregion
#region Accessor Properties for Serialized Fields
/// <summary>
/// Internal ID of the quest node, used to reference quest nodes in scripts.
/// </summary>
public StringField id
{
get { return m_id; }
set { m_id = value; }
}
/// <summary>
/// Name for your internal reference; shown only in the editor.
/// </summary>
public StringField internalName
{
get { return m_internalName; }
set { m_internalName = value; }
}
/// <summary>
/// Type of node, which determines some of its behavior.
/// </summary>
public QuestNodeType nodeType
{
get { return m_nodeType; }
set { m_nodeType = value; }
}
/// <summary>
/// Specifies that the node is a type that terminates the quest tree.
/// </summary>
public bool isEndNodeType
{
get { return nodeType == QuestNodeType.Success || nodeType == QuestNodeType.Failure; }
}
/// <summary>
/// Specifies that the node is a type that can connect to other nodes.
/// </summary>
public bool isConnectionNodeType
{
get { return !isEndNodeType; }
}
/// <summary>
/// Completion of this quest node is optional.
/// </summary>
public bool isOptional
{
get { return m_isOptional; }
set { m_isOptional = value; }
}
/// <summary>
/// Speaker of this node's dialogue content. If unassigned, the quest giver is the speaker.
/// </summary>
public StringField speaker
{
get { return m_speaker; }
set { m_speaker = value; }
}
/// <summary>
/// Info (actions & UI content) for a specific quest state. This list is indexed by the
/// int value of the QuestState enum, such as <c>stateInfoList[(int)QuestState.Active]</c>.
/// </summary>
public List<QuestStateInfo> stateInfoList
{
get { return m_stateInfoList; }
set { m_stateInfoList = value; }
}
/// <summary>
/// Conditions required for the node's state to become true.
/// </summary>
public QuestConditionSet conditionSet
{
get { return m_conditionSet; }
set { m_conditionSet = value; }
}
/// <summary>
/// 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.
/// </summary>
public List<int> 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;
/// <summary>
/// The Quest that this node belongs to.
/// </summary>
public Quest quest
{
get { return m_quest; }
set { m_quest = value; }
}
[NonSerialized]
public List<QuestNode> m_childList;
/// <summary>
/// References to the child nodes linked from this node.
/// </summary>
public List<QuestNode> childList
{
get { return m_childList; }
set { m_childList = value; }
}
[NonSerialized]
private List<QuestNode> m_parentList;
/// <summary>
/// References to the parent nodes that link to this node.
/// </summary>
public List<QuestNode> parentList
{
get { return m_parentList; }
set { m_parentList = value; }
}
[NonSerialized]
private List<QuestNode> m_optionalParentList;
/// <summary>
/// The subset of parents that are marked optional.
/// </summary>
public List<QuestNode> optionalParentList
{
get { return m_optionalParentList; }
set { m_optionalParentList = value; }
}
[NonSerialized]
private List<QuestNode> m_nonoptionalParentList;
/// <summary>
/// The subset of parents that are not marked optional.
/// </summary>
public List<QuestNode> nonoptionalParentList
{
get { return m_nonoptionalParentList; }
set { m_nonoptionalParentList = value; }
}
/// <summary>
/// Dictionary of tags defined in this quest node and their values.
/// </summary>
public TagDictionary tagDictionary
{
get { return m_tagDictionary; }
set { m_tagDictionary = value; }
}
/// <summary>
/// Invoked when the node changes state.
/// </summary>
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;
/// <summary>
/// Position of the quest node in the Quest Editor window.
/// </summary>
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<QuestStateInfo>();
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<QuestNode> original, List<QuestNode> 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<QuestNode> 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<QuestNode>();
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<QuestNode>();
optionalParentList = new List<QuestNode>();
nonoptionalParentList = new List<QuestNode>();
}
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<QuestNode>();
parentList.Add(parent);
if (parent.isOptional)
{
if (optionalParentList == null) optionalParentList = new List<QuestNode>();
optionalParentList.Add(parent);
}
else
{
if (nonoptionalParentList == null) nonoptionalParentList = new List<QuestNode>();
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
/// <summary>
/// Returns the current state of the quest node.
/// </summary>
public QuestNodeState GetState()
{
return m_state;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="newState">New state.</param>
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;
}
}
/// <summary>
/// Sets the internal state value without performing any state change processing.
/// </summary>
public void SetStateRaw(QuestNodeState state)
{
m_state = state;
}
/// <summary>
/// Returns the state info associated with a quest node state.
/// </summary>
public QuestStateInfo GetStateInfo(QuestNodeState state)
{
return (stateInfoList != null) ? QuestStateInfo.GetStateInfo(stateInfoList, state) : null;
}
/// <summary>
/// Starts or stops condition checking on condition nodes.
/// </summary>
/// <param name="enable">Specifies whether to start (enable) or stop.</param>
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);
}
/// <summary>
/// Invoked by parent when parent's state changes.
/// </summary>
/// <param name="parent">Parent node whose state changed.</param>
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
/// <summary>
/// Checks if there is any UI content for a specific category.
/// </summary>
/// <param name="category">The content category (Dialogue, Journal, etc.).</param>
/// <returns>True if GetContentList would return anything.</returns>
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;
}
/// <summary>
/// Gets the UI content for a specific category.
/// </summary>
/// <param name="category">The content category (Dialogue, Journal, etc.).</param>
/// <returns>A list of UI content items based on the current state of the quest and all of its nodes.</returns>
public List<QuestContent> 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
}
}