A Conversation Engine for A Video Game in JavaScript

The JavaScript code given below implements a simple conversation “tree” such as might be used in a video game. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.  Or, for an online version, visit https://thiscouldbebetter.neocities.org/conversation.html.

While you can hold an intelligible (if unrewarding) conversation with the current code, it could still use some tweaking. Notably, I had to add the nodes for some of the Options before the actual statement that precedes them. It might also be nice to be able to parse the conversation tree from a text “script” rather than building it as JavaScript objects.

ProfessorSurly

<html>
<body>
<script type='text/javascript'>

// main

function TalkTest()
{
	this.main = function()
	{
		var defns = TalkNodeDefn.Instances;

		var talkNodes =
		[
			new TalkNode("Greet", 	defns.Display, 	"I'm Professor Surly."),
			new TalkNode(null, 	defns.Option, 	["Math", 	"Let's talk about math." ]),
			new TalkNode(null, 	defns.Option, 	["Science", 	"Let's talk about science."]), 
			new TalkNode(null, 	defns.Option, 	["History", 	"Let's talk about history." ]),
			new TalkNode(null, 	defns.Option, 	["Quit", 	"Never mind. I hate you." ]),
			new TalkNode("Subject",	defns.Display, 	"What do you want to talk about?"),
			new TalkNode("SubjectPrompt", defns.Prompt),

			new TalkNode("History", defns.Push),
			new TalkNode(null, 	defns.Display, "Okay, but the past is pretty pointless."),
			new TalkNode(null, 	defns.Display, 	"What kind of history interests you?"),
			new TalkNode(null, 	defns.Option, 	["History.Ancient", 	"Tell me about ancient times."] ),
			new TalkNode(null, 	defns.Option, 	["History.Recent", 	"Tell me about recent events."] ),
			new TalkNode(null, 	defns.Option, 	["History.NeverMind", 	"Never mind."] ), 
			new TalkNode(null,	defns.Prompt),

			new TalkNode("History.Ancient", defns.Display, "That all happened a long time ago."),
			new TalkNode(null, 	defns.Prompt),

			new TalkNode("History.Recent", defns.Display, "If it's recent, is it really history?"),
			new TalkNode(null, 	defns.Prompt),

			new TalkNode("History.NeverMind", defns.Display, "Fine, you're the one who brought it up."),
			new TalkNode(null, 	defns.Pop,	"Subject"),

			new TalkNode("Math", 	defns.Display, 	"Math is too complicated..."),
			new TalkNode(null, 	defns.Display, 	"...what with all those numbers."),
			new TalkNode(null, 	defns.Goto,	"SubjectPrompt" ), 

			new TalkNode("Science", defns.Display, 	"Science is way too broad a subject."),
			new TalkNode(null, 	defns.Display, 	"I mean, what ISN'T science, really?"),
			new TalkNode(null, 	defns.Goto,	"SubjectPrompt" ), 		

			new TalkNode("Quit", 	defns.Display, 	"Same to you, buddy."),
			new TalkNode(null, 	defns.Display, 	"[This conversation is over.]"),
			new TalkNode(null,	defns.Quit),
		];

		var conversationDefn0 = new ConversationDefn
		(
			"ConversationDefn0",
			talkNodes
		);

		var conversationRun0 = new ConversationRun
		(
			conversationDefn0
		);

		conversationRun0.start();
	}
}

// classes

function ConversationDefn(name, talkNodes)
{
	this.name = name;
	this.talkNodes = talkNodes;

	for (var i = 0; i < this.talkNodes.length; i++)
	{
		var talkNode = this.talkNodes[i];

		this.talkNodes[TalkNode.Underscore + talkNode.name] = talkNode;
	}
}
{
	ConversationDefn.prototype.talkNodeByName = function(nameOfTalkNodeToGet)
	{
		return this.talkNodes[TalkNode.Underscore + nameOfTalkNodeToGet];
	}

	ConversationDefn.prototype.talkNodesByNames = function(namesOfTalkNodesToGet)
	{
		var returnNodes = [];

		for (var i = 0; i < namesOfTalkNodesToGet.length; i++)
		{
			var nameOfTalkNodeToGet = namesOfTalkNodesToGet[i];
			var talkNode = this.talkNodeByName(nameOfTalkNodeToGet);
			returnNodes.push(talkNode);
		}

		return returnNodes;
	}
}

function ConversationRun(defn)
{
	this.defn = defn;

	var talkNodeStart = this.defn.talkNodes[0];

	this.scopeCurrent = new ConversationScope
	(
		null, // parent
		talkNodeStart,
		// talkNodesForOptions
		[]
	);
}
{
	// static methods

	ConversationRun.handleClickEvent = function(event)
	{
		var htmlElement = event.target;
		while (htmlElement.conversationRun == null)
		{
			htmlElement = htmlElement.parentElement;	
		}

		var conversationRun = htmlElement.conversationRun;

		conversationRun.update();
	}

	// instance methods

	ConversationRun.prototype.start = function()
	{
		document.body.appendChild(this.htmlElementBuild());

		this.update();		
	}	

	ConversationRun.prototype.update = function()
	{
		this.scopeCurrent.update(this);

		this.htmlElementUpdate();
	}

	// html

	ConversationRun.prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("div");
		returnValue.id = "divConversation";

		returnValue.conversationRun = this;

		var htmlElementForDisplay = document.createElement("div");
		htmlElementForDisplay.id = "divConversationDisplay";
		htmlElementForDisplay.style.width = "300px";
		htmlElementForDisplay.style.height = "100px";
		htmlElementForDisplay.style.border = "1px solid";

		returnValue.appendChild(htmlElementForDisplay);

		var htmlElementForOptions = document.createElement("div");
		htmlElementForOptions.id = "divConversationOptions";
		htmlElementForOptions.style.width = "300px";
		htmlElementForOptions.style.height = "100px";
		htmlElementForOptions.style.border = "1px solid";
		returnValue.appendChild(htmlElementForOptions);

		htmlElementForDisplay.onclick = ConversationRun.handleClickEvent;

		this.htmlElementForDisplay = htmlElementForDisplay;
		this.htmlElementForOptions = htmlElementForOptions;

		this.htmlElement = returnValue;

		return returnValue;
	}

	ConversationRun.prototype.htmlElementUpdate = function()
	{
		this.htmlElementForDisplay.innerHTML = this.scopeCurrent.displayTextCurrent;

		this.htmlElementForOptions.innerHTML = ""; 

		if (this.scopeCurrent.areOptionsVisible == true)
		{
			for (var i = 0; i < this.scopeCurrent.talkNodesForOptions.length; i++)
			{
				var talkNodeForOption = this.scopeCurrent.talkNodesForOptions[i];
				var htmlElementForOption = talkNodeForOption.htmlElementBuild(this);
				htmlElementForOption.innerHTML = talkNodeForOption.parameters[1];
				this.htmlElementForOptions.appendChild
				(
					htmlElementForOption
				);	
			}
		}
		else
		{
			var htmlElementForContinue = document.createElement("div");
			htmlElementForContinue.innerHTML = "[Continue]";
			htmlElementForContinue.onclick = ConversationRun.handleClickEvent;
			this.htmlElementForOptions.appendChild
			(
				htmlElementForContinue
			);	
		}
	}
}

function ConversationScope(parent, talkNodeCurrent, talkNodesForOptions)
{
	this.parent = parent;
	this.talkNodeCurrent = talkNodeCurrent;
	this.areOptionsVisible = false;
	this.talkNodesForOptions = talkNodesForOptions;

	this.displayTextCurrent = "[text]";
}
{
	ConversationScope.prototype.talkNodeAdvance = function(conversationRun)
	{
		var talkNodeIndex = conversationRun.defn.talkNodes.indexOf(this.talkNodeCurrent);
		var talkNodeNext = conversationRun.defn.talkNodes[talkNodeIndex + 1];
		this.talkNodeCurrent = talkNodeNext;
	}

	ConversationScope.prototype.update = function(conversationRun)
	{
		this.talkNodeCurrent.execute(conversationRun, this);	
	}
}

function TalkNode(name, defn, parameters)
{
	this.name = name;
	this.defn = defn;
	this.parameters = parameters;
}
{
	// constant

	TalkNode.Underscore = "_"; // Prepended for array lookup in case name is numeric

	// static methods

	TalkNode.handleClickEvent = function(event)
	{
		var htmlElementClicked = event.target;
		var conversationRun = htmlElementClicked.conversationRun; // hack
		var talkNodeClicked = htmlElementClicked.talkNode;

		talkNodeClicked.click(conversationRun, conversationRun.scopeCurrent);
	}

	// instance methods

	TalkNode.prototype.click = function(conversationRun, scope)
	{
		if (this.defn.click != null)
		{
			this.defn.click(conversationRun, scope, this);
		}
	}

	TalkNode.prototype.execute = function(conversationRun, scope)
	{
		this.defn.execute(conversationRun, scope, this);
	}

	TalkNode.prototype.htmlElementBuild = function(conversationRun)
	{
		var returnValue = document.createElement("div");
		returnValue.innerHTML = this.parameters;
		returnValue.conversationRun = conversationRun;
		returnValue.onclick = TalkNode.handleClickEvent;

		returnValue.talkNode = this;
		this.htmlElement = returnValue;

		return returnValue;
	}
}

function TalkNodeDefn(name, execute, click)
{
	this.name = name;
	this.execute = execute;
	this.click = click;
}
{
	// instances

	TalkNodeDefn.Instances = new TalkNodeDefn_Instances();

	function TalkNodeDefn_Instances()
	{
		this.Display = new TalkNodeDefn
		(
			"Display",
			// execute
			function(conversationRun, scope, talkNode) 
			{ 
				scope.displayTextCurrent = talkNode.parameters;
				scope.talkNodeAdvance(conversationRun);
			}
		);

		this.Goto = new TalkNodeDefn
		(
			"Goto", 
			// execute
			function(conversationRun, scope, talkNode) 
			{ 
				scope.talkNodeCurrent = conversationRun.defn.talkNodeByName
				(
					talkNode.parameters
				);

				conversationRun.update();
			}
		);

		this.Option = new TalkNodeDefn
		(
			"Option", 
			// execute
			function(conversationRun, scope, talkNode) 
			{ 
				scope.talkNodesForOptions.push
				(
					talkNode
				);

				scope.talkNodeAdvance(conversationRun);

				conversationRun.update();
			},
			// click
			function(conversationRun, scope, talkNode)
			{
				scope.areOptionsVisible = false;

				var nameOfTalkNodeNext = talkNode.parameters[0];
				var talkNodeNext = conversationRun.defn.talkNodeByName(nameOfTalkNodeNext);
				scope.talkNodeCurrent = talkNodeNext;
				conversationRun.update();
			}
		);

		this.Push = new TalkNodeDefn
		(
			"Push", 
			function(conversationRun, scope, talkNode) 
			{ 
				var runDefn = conversationRun.defn;
				var talkNodeIndex = runDefn.talkNodes.indexOf(talkNode);
				var talkNodeNext = runDefn.talkNodes[talkNodeIndex + 1];

				conversationRun.scopeCurrent = new ConversationScope
				(
					scope, // parent
					talkNodeNext,
					[] // options
				);

				conversationRun.update();
			}
		);

		this.Pop = new TalkNodeDefn
		(
			"Pop", 	
			function(conversationRun, scope, talkNode) 
			{ 	
				var scope = scope.parent;
				conversationRun.scopeCurrent = scope;

				scope.talkNodeCurrent = conversationRun.defn.talkNodeByName
				(
					talkNode.parameters
				);

				conversationRun.update();
			}
		);

		this.Prompt = new TalkNodeDefn
		(
			"Prompt", 	
			function(conversationRun, scope, talkNode) 
			{ 
				scope.areOptionsVisible = true;
			}
		);

		this.Quit = new TalkNodeDefn
		(
			"Quit", 	
			function(conversationRun, scope, talkNode) 
			{ 
				// do nothing
			}	
		);

	}
}

// run

new TalkTest().main();

</script>
</body>
</html>
Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s