Now to the good stuff. Redirecting from one piece of dialogue to the next is the bread and butter of most stories in games, and at the end of the day much of what you write will end up will likely be linear in nature; however, what makes games unique in terms of story telling is the ability to add interactivity, to create an experience that changes based on user input.
So what will we need to implement choice in our game? We already defined our MakeChoice function, but we have yet to implement it. Our function will need to:
- Take in a choice object, passed out to listeners by our NextNode function
- Perform all of the operations on variables described in the ‘set_conditions’ array of the choice passed in
- Get the next valid redirect from the choice and call NextNode with it
We’ll be building a basic affection system where the character changes their action in the end based on how much they like / dislike you based on your actions. Here’s an example:
Here’s the json we’ll be using for this project, and here’s a starter respository with some tests if you choose to use them. As before, I highly reccomend that you try and work it out yourself first! If you get stuck or frustrated you can always come back to look for hints or the solution. If you want to try it yourself, stop here!
So, the first order of business is taking the ‘set_conditions’ inside of the choice passed into the argument and operate on our variables. Similar to how we defined CheckCondition and CheckConditionSet in the previous section, I’m going to define a SetVariable function which takes one variable setter and applies it, and a SetVariableSet function (silly name, but it works) which simply calls that function for each object in the ‘set_condition’ array.
SetVariable(setter) {
switch (setter.operator) {
case '=':
this.variables[setter.variable] = setter.value;
break;
case '*=':
this.variables[setter.variable] *= setter.value;
break;
case '/=':
this.variables[setter.variable] /= setter.value;
break;
case '+=':
this.variables[setter.variable] += setter.value;
break;
case '-=':
this.variables[setter.variable] -= setter.value;
break;
case 'toggle':
this.variables[setter.variable] != storydata.variables[setter.var];
break;
case 'random':
this.variables[setter.variable] = Math.floor(Math.random() * setter.value);
break;
default:
console.log('Invalid operator');
}
}
SetVariableSet(setters) {
setters.forEach(setter => {
this.SetVariable(setter);
})
}
Finally, if you recall, when we wrote the implementation for NextNode in the previous section, we created a function GetValidRedirect which takes an array of redirects and spits out the first valid one. Since choices also store a list of redirects just like nodes without choices, we can completely reuse this function! MakeChoice ends up looking like this:
MakeChoice(choice) {
if(choice.set_conditions) {
this.SetVariables(choice.set_conditions);
}
let redirect = this.GetValidRedirect(choice.redirects);
this.NextNode(redirect.node_name);
}
If you’re newer to Javascript or programming, hopefully this case shows how valuable it can be to take the time to break your functionality into smaller functions which you glue together. If we had written NextNode as one monolithic function instead of breaking it up into a few pieces, we would have had to go digging back through the code for NextNode to copy paste into MakeChoice. We might have spent a decent amount of time rewriting the solution for a problem we had already solved. Try and identify as you’re writing when it seems like a good idea to take the time to break a piece of code into its own function, or even its own class / object. It may be more work upfront, but it makes your code much more readable, organized, reusable, and it will make refactoring it much easier.
In the next section, we’ll be finalizing things by using our newly created class with some DOM manipulation to make our simple GUI.