Dialogue Sim: New Unexplored Subtree
-
- Posts: 222
- Joined: Wed Jan 22, 2020 10:48 pm
Dialogue Sim: New Unexplored Subtree
I was just wondering if there's any built in facility for un-graying out a dialogue option that was previously completely explored, but that now has new dialogue options in sub-trees based on changes in game state?
I guess it would require you to walk the tree before starting the conversation? Or is there maybe a better way?
If it's not written, do you have any advice on what NOT to do, and what might make sense?
I guess it would require you to walk the tree before starting the conversation? Or is there maybe a better way?
If it's not written, do you have any advice on what NOT to do, and what might make sense?
Re: Dialogue Sim: New Unexplored Subtree
Hi,
I think you'll need to walk the tree. There isn't any built-in utility method for that.
You could handle it semi-manually by keeping a list of IDs. When some event happens, set the SimStatus of all of those IDs back to Untouched.
However, it's probably better in the long run to manually walk the tree and look for available options. If there are any available options, set the parents' SimStatuses back to Untouched. As you're walking the tree, remember that you links can loop back. So maintain a list of nodes that you've already visited so you don't end up in an infinite loop.
I think you'll need to walk the tree. There isn't any built-in utility method for that.
You could handle it semi-manually by keeping a list of IDs. When some event happens, set the SimStatus of all of those IDs back to Untouched.
However, it's probably better in the long run to manually walk the tree and look for available options. If there are any available options, set the parents' SimStatuses back to Untouched. As you're walking the tree, remember that you links can loop back. So maintain a list of nodes that you've already visited so you don't end up in an infinite loop.
-
- Posts: 222
- Joined: Wed Jan 22, 2020 10:48 pm
Re: Dialogue Sim: New Unexplored Subtree
Alrighty, thanks for the information. I'll have to decide what makes the most sense for us. We may only have a few of these, and evaluating all the lua repeatedly is probably expensive. It might be easiest just to walk to tree for children that haven't been visited without evaluating the lua. The parent dialogue choice would be de-greyed until all children were visited. That'd probably be fine.
Thanks again for the help!
Thanks again for the help!
Re: Dialogue Sim: New Unexplored Subtree
You could add a custom Boolean dialogue entry field to indicate that a subtree has nodes that could potentially de-grey the subtree. Then you could quickly loop through the dialogue entries to identify any nodes whose Boolean field is true. If the conversation has any, you can evaluate the Lua of those subtrees.
Code: Select all
var entries = new List<DialogueEntry>();
foreach (DialogueEntry entry in currentConversation.dialogueEntries)
{
if (Field.LookupBool(entry.fields, "Your Field Name") == true)
{
entries.Add(entry);
}
}
// Now you only need to evaluate the subtrees (if any) starting from the elements in entries.
-
- Posts: 222
- Joined: Wed Jan 22, 2020 10:48 pm
Re: Dialogue Sim: New Unexplored Subtree
I decided to try to hack something together this weekend. This code traverses the conversations breadth first, and at each decision node descends to the bottom of the tree to get a list of all the responses children.
My definition of "decision node" is very much imperfect because I'm not evaluating the lua conditions and even if I were, I don't have to chops to do the static analysis to decide if two nodes are actually mutually exclusive. So there are a few instances where I'm manually having to correct things.
With this list of responses and all response children, I can, see if all the children of a response have been visited to decide if the node is greyed out.
My definition of "decision node" is very much imperfect because I'm not evaluating the lua conditions and even if I were, I don't have to chops to do the static analysis to decide if two nodes are actually mutually exclusive. So there are a few instances where I'm manually having to correct things.
With this list of responses and all response children, I can, see if all the children of a response have been visited to decide if the node is greyed out.
Code: Select all
using System;
using System.Collections.Generic;
using PixelCrushers.DialogueSystem;
using UnityEngine;
namespace Dialogue
{
public class ResponseTreeCache : MonoBehaviour
{
// Constants
private const int DefaultListSize = 512;
// Singleton
private static ResponseTreeCache instance;
// Properties
private DialogueDatabase database;
// Temporary Data Structures
private List<DialogueEntry> unvisited;
private HashSet<DialogueEntry> visited;
private Dictionary<int, List<DecisionNode>> conversationIdToDecisions;
// private Dictionary<int, HashSet<int>> globalResponseIdCache;
// Cache
private Dictionary<int, ConversationCache> conversationIdToCache;
void Awake()
{
instance = this;
// Initialize Temporary Data Structures
visited = new HashSet<DialogueEntry>();
unvisited = new List<DialogueEntry>(DefaultListSize);
conversationIdToDecisions = new Dictionary<int, List<DecisionNode>>();
// globalResponseIdCache = new Dictionary<int, HashSet<int>>();
// Initialize Cache
conversationIdToCache = new Dictionary<int, ConversationCache>();
// Gather References
database = DialogueManager.databaseManager.masterDatabase;
// Initialize Cache
Action handler = null;
DialogueManager.instance.initializationComplete += handler = () =>
{
DialogueManager.instance.initializationComplete -= handler;
// Build Pre-Cache
BuildPreCache();
// CacheConversation(database.GetConversation(120), unvisited, visited); // Helpful for testing
// Build Cache
BuildCache();
// Clear Temporary Data
unvisited = null;
visited = null;
conversationIdToDecisions = null;
// globalResponseIdCache = null;
// Helpful for testing
// string tmp = "";
// foreach (KeyValuePair<int, ConversationCache> entry in conversationIdToCache)
// {
// ConversationCache cache = entry.Value;
// foreach (KeyValuePair<int, List<int>> cacheEntry in cache.responseIdToChildResponses)
// {
// tmp += $"Response ID: {cacheEntry.Key} - ({ string.Join(",", cacheEntry.Value)})\n";
// }
// }
// Debug.Log(tmp);
};
}
public static bool IsResponseExhausted(int conversationId, int responseId)
{
ConversationCache cache = instance.conversationIdToCache[conversationId];
return cache.IsResponseExhausted(responseId);
}
private void BuildCache()
{
// Iterate over all conversations
foreach (KeyValuePair<int, List<DecisionNode>> entry in conversationIdToDecisions)
{
// Create Conversation Cache
ConversationCache cache = new ConversationCache(entry.Key); // Key is conversation id
conversationIdToCache[entry.Key] = cache;
// Conversation Global Response ID Cache
// HashSet<int> responseIdCache = globalResponseIdCache[entry.Key];
// Store All Responses
foreach (DecisionNode decisionNode in entry.Value)
{
foreach (ResponseNode responseNode in decisionNode.responses)
{
cache.AddResponseChildren(responseNode.entry.id, responseNode.childEntryIds);
// foreach (int responseChildId in responseNode.childEntryIds)
// {
// // Response Child Node is a Response
// // Note: I no longer only list the response children. There are cases where
// // we have multiple paths beneath a response without another decision.
// // We'd end up marking those as grey inadvertantly.
// // if (responseIdCache.Contains(responseChildId))
// // {
// cache.AddResponseChild(responseNode.entry.id, responseChildId);
// // }
// }
}
}
}
}
private void BuildPreCache()
{
// Build Dialogue Choice Trees
foreach (Conversation conversation in database.conversations)
{
visited.Clear();
unvisited.Clear();
CacheConversation(conversation, unvisited, visited);
}
}
private void CacheConversation(Conversation conversation, List<DialogueEntry> localUnvisited, HashSet<DialogueEntry> localVisited)
{
// Grab Conversation Response ID Cache
// HashSet<int> globalResponseIdSet;
// globalResponseIdCache.TryGetValue(conversation.id, out globalResponseIdSet);
// if (globalResponseIdSet == null)
// {
// globalResponseIdSet = new HashSet<int>();
// globalResponseIdCache[conversation.id] = globalResponseIdSet;
// }
// Initialize for Traversal
localUnvisited.Add(conversation.GetFirstDialogueEntry());
while (localUnvisited.Count > 0)
{
// Get Current Node
int unvisitedIndex = localUnvisited.Count - 1;
DialogueEntry currentNode = unvisited[unvisitedIndex];
localUnvisited.RemoveAt(unvisitedIndex);
// Set Current Node Visited
localVisited.Add(currentNode);
// No Links, Continue
if (currentNode.outgoingLinks.Count == 0) continue;
// Gather the Frontier
int pcNodes = 0;
int npcNodes = 0;
int frontierStartIndex = localUnvisited.Count;
GatherFrontier(conversation, currentNode, localUnvisited, localVisited);
for (int i = frontierStartIndex; i < localUnvisited.Count; i++)
{
DialogueEntry frontierNode = localUnvisited[i];
if (IsPCNode(frontierNode)) pcNodes++;
else npcNodes++;
}
// This is a decision
bool isDecisionNode = pcNodes > 1 && npcNodes == 0;
if (isDecisionNode) CacheDecisionNode(currentNode, conversation, frontierStartIndex, localUnvisited.Count);//, globalResponseIdSet);
}
}
private void GatherFrontier(Conversation conversation, DialogueEntry node, List<DialogueEntry> localUnvisited, HashSet<DialogueEntry> localVisited)
{
for (int i = 0; i < node.outgoingLinks.Count; i++)
{
Link link = node.outgoingLinks[i];
// Skip links to other conversations
if (link.destinationConversationID != conversation.id) continue;
DialogueEntry childNode = conversation.GetDialogueEntry(link.destinationDialogueID);
if (!localVisited.Contains(childNode))
{
if (childNode.isGroup)
{
// Recurse if group or NPC node
localVisited.Add(childNode);
GatherFrontier(conversation, childNode, localUnvisited, localVisited);
}
else localUnvisited.Add(childNode);
}
}
}
private void CacheDecisionNode(DialogueEntry decisionNode, Conversation conversation, int responsesStart, int responsesEnd)//, HashSet<int> globalResponseIdSet)
{
// Add Node to Cache
List<DecisionNode> conversationDecisions;
conversationIdToDecisions.TryGetValue(conversation.id, out conversationDecisions);
if (conversationDecisions == null)
{
conversationDecisions = new List<DecisionNode>(DefaultListSize);
conversationIdToDecisions[conversation.id] = conversationDecisions;
}
// Create Decision Node
HashSet<DialogueEntry> localVisited = new HashSet<DialogueEntry>(); // All visited and unvisited for master BFS
foreach (DialogueEntry entry in visited) localVisited.Add(entry);
foreach (DialogueEntry entry in unvisited) localVisited.Add(entry);
DecisionNode newNode = new DecisionNode(decisionNode);
for (int i = responsesStart; i < responsesEnd; i++)
{
// Create Response
DialogueEntry frontierNode = unvisited[i];
ResponseNode response = new ResponseNode(frontierNode, conversation);
PopulateResponseChildren(response, frontierNode, localVisited);
newNode.responses.Add(response);
// Cache Response
// globalResponseIdSet.Add(frontierNode.id);
}
// Add Node
conversationDecisions.Add(newNode);
}
/// <summary>
/// Recursively find all children of this response.
/// </summary>
private void PopulateResponseChildren(ResponseNode response, DialogueEntry currentNode, HashSet<DialogueEntry> localVisited)
{
for (int i = 0; i < currentNode.outgoingLinks.Count; i++)
{
// Grab Child
Link link = currentNode.outgoingLinks[i];
// Skip links to other conversations
if (link.destinationConversationID != currentNode.conversationID) continue;
DialogueEntry childNode = response.conversation.GetDialogueEntry(link.destinationDialogueID);
if (!localVisited.Contains(childNode))
{
// Add Child (if it's not a group and has a sequence or dialogue)
if (!childNode.isGroup && (!string.IsNullOrEmpty(childNode.DialogueText) || !string.IsNullOrEmpty(childNode.Sequence)))
{
response.childEntryIds.Add(childNode.id);
}
// Set Visited
localVisited.Add(childNode);
// Recurse
PopulateResponseChildren(response, childNode, localVisited);
}
}
}
private bool IsPCNode(DialogueEntry entry) => database.GetCharacterType(entry.ActorID) == CharacterType.PC;
private class DecisionNode
{
public DialogueEntry entry;
public List<ResponseNode> responses;
public DecisionNode(DialogueEntry entry)
{
this.entry = entry;
this.responses = new List<ResponseNode>(DefaultListSize);
}
}
private class ResponseNode
{
public DialogueEntry entry;
public Conversation conversation;
public HashSet<int> childEntryIds;
public ResponseNode(DialogueEntry entry, Conversation conversation)
{
this.entry = entry;
this.conversation = conversation;
this.childEntryIds = new HashSet<int>();
}
}
private class ConversationCache
{
public int conversationId;
public Dictionary<int, List<int>> responseIdToChildren;
public ConversationCache(int conversationId)
{
this.conversationId = conversationId;
this.responseIdToChildren = new Dictionary<int, List<int>>();
}
public void AddResponseChildren(int responseId, ICollection<int> responseChildren)
{
List<int> responseChildrenList = null;
responseIdToChildren.TryGetValue(responseId, out responseChildrenList);
if (responseChildrenList == null)
{
responseChildrenList = new List<int>(responseChildren.Count);
responseIdToChildren[responseId] = responseChildrenList;
}
responseChildrenList.AddRange(responseChildren);
}
// public void AddResponseChild(int responseId, int responseChildId)
// {
// // Ensure Initialized
// List<int> responseChildren = null;
// responseIdToChildren.TryGetValue(responseId, out responseChildren);
// if (responseChildren == null)
// {
// responseChildren = new List<int>(DefaultListSize);
// responseIdToChildren[responseId] = responseChildren;
// }
// responseChildren.Add(responseChildId);
// }
public bool IsResponseExhausted(int responseEntryId)
{
// Check the response itself first
if (!DialogueChoiceMemory.ContainsEntry(conversationId, responseEntryId)) return false;
// Check children if the response itself is exhausted
bool childrenExhausted = true;
List<int> responseChildren;
responseIdToChildren.TryGetValue(responseEntryId, out responseChildren);
if (responseChildren != null)
{
foreach (int responseChildId in responseChildren)
{
if (!DialogueChoiceMemory.ContainsEntry(conversationId, responseChildId))
{
childrenExhausted = false;
break;
}
}
}
return childrenExhausted;
}
}
}
}
Re: Dialogue Sim: New Unexplored Subtree
Neat!
One note: The Dialogue Manager's DialogueSystemController sets up the masterDatabase property in Awake(). DialogueSystemController has a script execution order of -7, which means its Awake will run before other scripts (such as yours) that don't specify an execution order, so it should be all good. Just FYI in case you decide to manually set your script's execution order in the future.
One note: The Dialogue Manager's DialogueSystemController sets up the masterDatabase property in Awake(). DialogueSystemController has a script execution order of -7, which means its Awake will run before other scripts (such as yours) that don't specify an execution order, so it should be all good. Just FYI in case you decide to manually set your script's execution order in the future.
-
- Posts: 222
- Joined: Wed Jan 22, 2020 10:48 pm
Re: Dialogue Sim: New Unexplored Subtree
I rewrote this so that it evaluates the condition statements as it's deciding which responses are exhausted. I just figured I'd share the updated version in case that's useful to anyone.
Code: Select all
using System;
using System.Collections.Generic;
using PixelCrushers.DialogueSystem;
using UnityEngine;
namespace Dialogue
{
public class ResponseTreeCache : MonoBehaviour
{
// Singleton
private static ResponseTreeCache instance;
// Properties
private DialogueDatabase database;
// Temporary Data Structures
private Dictionary<int, List<Node>> conversationIdToDecisions;
private Dictionary<int, ConversationTree> conversationIdToConversationTree;
void Awake()
{
instance = this;
// Initialize Temporary Data Structures
conversationIdToDecisions = new Dictionary<int, List<Node>>();
conversationIdToConversationTree = new Dictionary<int, ConversationTree>();
// Gather References
database = DialogueManager.databaseManager.masterDatabase;
// Initialize Cache
Action handler = null;
DialogueManager.instance.initializationComplete += handler = () =>
{
DialogueManager.instance.initializationComplete -= handler;
// Build List of Decisions and Their Response Children
BuildDecisionsAndResponseTree();
// Clear Temporary Data
conversationIdToDecisions = null;
};
}
public static bool IsResponseExhausted(int conversationId, int responseId)
{
ConversationTree conversationTree = instance.conversationIdToConversationTree[conversationId];
bool exhausted = conversationTree.IsResponseExhausted(responseId);
return exhausted;
}
private void BuildDecisionsAndResponseTree()
{
// Build Dialogue Choice Trees
foreach (Conversation conversation in database.conversations)
{
CacheConversation(conversation);
// Now that we have a list of all decisions and their responses,
// we can go back and find all the children of all the responses
// that are decisions
ConversationTree conversationTree = new ConversationTree();
PopulateResponseNodes(conversation, conversationTree);
conversationTree.CleanTemporaryStructures();
// Cache Tree
conversationIdToConversationTree[conversation.id] = conversationTree;
}
}
private void CacheConversation(Conversation conversation)
{
// Initialize for Traversal
// Dictionary<DialogueEntry, HashSet<DialogueEntry>> visitedSetCache = new Dictionary<DialogueEntry, HashSet<DialogueEntry>>();
Queue<HashSet<DialogueEntry>> visitedSetQueue = new Queue<HashSet<DialogueEntry>>();
Queue<DialogueEntry> unvisited = new Queue<DialogueEntry>();
HashSet<DialogueEntry> visited = new HashSet<DialogueEntry>();
unvisited.Enqueue(conversation.GetFirstDialogueEntry());
visitedSetQueue.Enqueue(new HashSet<DialogueEntry>());
while (unvisited.Count > 0)
{
// Get Current Node
DialogueEntry currentNode = unvisited.Dequeue();
HashSet<DialogueEntry> previousVisitedSet = visitedSetQueue.Dequeue();
// Set Current Node Visited
visited.Add(currentNode);
// No Links, Continue
if (currentNode.outgoingLinks.Count == 0) continue;
// Add Previous Frontier Visited Set
foreach (DialogueEntry visitedEntry in visited) previousVisitedSet.Add(visitedEntry);
// Create New Frontier List
List<DialogueEntry> frontier = new List<DialogueEntry>(currentNode.outgoingLinks.Count);
// Gather the Frontier
GatherFrontier(conversation, currentNode, unvisited, previousVisitedSet, frontier, visitedSetQueue);//, visitedSetCache);
// Is Decision
if (frontier.Count > 1)
{
CacheDecisionNode(conversation, currentNode, frontier, unvisited, visited);
}
}
}
private void GatherFrontier(Conversation conversation, DialogueEntry node, Queue<DialogueEntry> unvisited, HashSet<DialogueEntry> visited, List<DialogueEntry> frontier, Queue<HashSet<DialogueEntry>> visitedSetQueue)
{
for (int i = 0; i < node.outgoingLinks.Count; i++)
{
Link link = node.outgoingLinks[i];
// Skip links to other conversations
if (link.destinationConversationID != conversation.id) continue;
DialogueEntry childNode = conversation.GetDialogueEntry(link.destinationDialogueID);
// Skip Groups
if (childNode.isGroup)
{
if (!visited.Contains(childNode))
{
// Recurse if group
visited.Add(childNode);
GatherFrontier(conversation, childNode, unvisited, visited, frontier, visitedSetQueue);
}
continue;
}
// Not Group, Add to Frontier (we don't check visited because we want to see all children regardless to get all decision nodes)
frontier.Add(childNode);
// Add to Unvisited
if (!visited.Contains(childNode))
{
// visitedSetCache[childNode] = new HashSet<DialogueEntry>(visited);
visitedSetQueue.Enqueue(new HashSet<DialogueEntry>(visited));
unvisited.Enqueue(childNode);
}
}
}
private void CacheDecisionNode(Conversation conversation, DialogueEntry decisionEntry, List<DialogueEntry> responses, Queue<DialogueEntry> unvisited, HashSet<DialogueEntry> visited)//, HashSet<int> globalResponseIdSet)
{
// Add Node to Cache
List<Node> conversationDecisions;
conversationIdToDecisions.TryGetValue(conversation.id, out conversationDecisions);
if (conversationDecisions == null)
{
conversationDecisions = new List<Node>();
conversationIdToDecisions[conversation.id] = conversationDecisions;
}
// Create Response Local Visited Set
HashSet<DialogueEntry> localVisitedForResponse = new HashSet<DialogueEntry>(); // All visited and unvisited for master BFS
foreach (DialogueEntry entry in visited) localVisitedForResponse.Add(entry);
foreach (DialogueEntry entry in unvisited) localVisitedForResponse.Add(entry);
// Create Decision Local Visited Set
HashSet<DialogueEntry> localVisitedForDecision = new HashSet<DialogueEntry>(localVisitedForResponse);
localVisitedForDecision.Remove(decisionEntry);
// Create Decision Node
Node decisionNode = new Node(conversation, decisionEntry, localVisitedForDecision);
foreach (DialogueEntry responseEntry in responses)
{
// Create Response
Node responseNode = new Node(conversation, responseEntry, localVisitedForResponse);
decisionNode.immediateChildren.Add(responseNode);
}
// Add Node
conversationDecisions.Add(decisionNode);
}
private void PopulateResponseNodes(Conversation conversation, ConversationTree conversationTree)
{
// Get Decisions
List<Node> conversationDecisions;
conversationIdToDecisions.TryGetValue(conversation.id, out conversationDecisions);
if (conversationDecisions == null) return; // Some conversations don't have decisions
// Conversation Decision Map
foreach (Node decision in conversationDecisions)
{
conversationTree.AddDecision(decision);
}
// Iterate Over Responses
foreach (Node decision in conversationDecisions)
{
for (int i = 0; i < decision.immediateChildren.Count; i++)
{
// Deduplicate Response Nodes That Are Also Decisions
Node hydratedResponse;
Node response = decision.immediateChildren[i];
conversationTree.decisionNodeMap.TryGetValue(response.entry.id, out hydratedResponse);
if (hydratedResponse != null)
{
// Response's Children Already Hydrated
decision.immediateChildren[i] = hydratedResponse;
response = hydratedResponse;
}
else
{
// Populate Response's Children
HydrateResponseChildren(response, response.entry, response.localVisited, conversationTree);
}
conversationTree.AddResponse(response);
}
}
}
private void HydrateResponseChildren(Node response, DialogueEntry currentNode, HashSet<DialogueEntry> localVisited, ConversationTree conversationTree)
{
for (int i = 0; i < currentNode.outgoingLinks.Count; i++)
{
// Grab Child
Link link = currentNode.outgoingLinks[i];
if (link.destinationConversationID != currentNode.conversationID) continue; // Skip links to other conversations
DialogueEntry childNode = response.conversation.GetDialogueEntry(link.destinationDialogueID);
// Ensure No Looping and Stop at Decision Nodes
if (!localVisited.Contains(childNode))
{
// Is Decision
Node childDecision;
conversationTree.decisionNodeMap.TryGetValue(childNode.id, out childDecision);
if (childDecision != null)
{
// Add All Decision Nodes
response.immediateChildren.Add(childDecision);
continue;
}
// Is Response
Node childResponse;
conversationTree.responseNodeMap.TryGetValue(childNode.id, out childResponse);
if (childResponse != null)
{
response.immediateChildren.Add(childResponse);
continue;
}
// Is Normal Node, Recurse
HydrateResponseChildren(response, childNode, localVisited, conversationTree);
}
}
}
private bool IsPCNode(DialogueEntry entry) => database.GetCharacterType(entry.ActorID) == CharacterType.PC;
private class ConversationTree
{
public Dictionary<int, Node> responseNodeMap;
public Dictionary<int, Node> decisionNodeMap;
public ConversationTree()
{
this.decisionNodeMap = new Dictionary<int, Node>();
this.responseNodeMap = new Dictionary<int, Node>();
}
public void AddDecision(Node decision) => this.decisionNodeMap[decision.entry.id] = decision;
public void AddResponse(Node response) => this.responseNodeMap[response.entry.id] = response;
public void CleanTemporaryStructures()
{
this.decisionNodeMap = null;
foreach (KeyValuePair<int, Node> entry in this.responseNodeMap)
{
entry.Value.localVisited = null;
}
}
public bool IsResponseExhausted(int responseId)
{
// Grab Response Node
Node responseNode = responseNodeMap[responseId];
// Recursively Check
return IsResponseExhausted(responseNode);
}
private bool IsResponseExhausted(Node responseNode)
{
// Check the response itself first
if (!IsConditionMet(responseNode.entry)) return true;
if (!DialogueChoiceMemory.ContainsEntry(responseNode.conversation.id, responseNode.entry.id)) return false;
// Check Decisions
foreach (Node child in responseNode.immediateChildren)
{
if (!IsResponseExhausted(child))
{
return false;
}
}
return true;
}
private bool IsConditionMet(DialogueEntry entry)
{
bool isConditionMet = true;
if (!string.IsNullOrEmpty(entry.conditionsString))
{
isConditionMet = Lua.IsTrue(entry.conditionsString);
}
return isConditionMet;
}
}
private class Node
{
public Conversation conversation;
public DialogueEntry entry;
public List<Node> immediateChildren;
public HashSet<DialogueEntry> localVisited; // parents
public Node(Conversation conversation, DialogueEntry entry, HashSet<DialogueEntry> localVisited)
{
this.conversation = conversation;
this.entry = entry;
this.immediateChildren = new List<Node>();
this.localVisited = localVisited;
}
}
}
}