Struggling With Twine
Saturday, 29 March 2014
Composite images: Superhero
This is a "proof of concent"; there is no game, just the character creation steps for creating your own superhero. In one step you get to choose your outfit, and then see an image of it.
Try it out
It has a limited number of images that it puts on top of each other to give the final effect (so six images of capes for guys, six for girls, etc.).
It uses CSS to superimpose the images. Each image goes into its own <div>, which has an absolute position set. Sounds complicated, but the CSS code is trivial:
div.abs-box {
position: absolute;
top: 200px;
right: 0px;
}
The basic Twine code looks like this - just a normal img tag inside a <div>, with the class set to correspond to the CSS already set. You can have as many of these on the page as you like; they will all get draw on top of each other.
<div class="abs-box">[img[m_red_cape]]</div>
However, what I did was set up a macro to handle it.
macros.pic = {
handler: function(place, macroName, params, parser) {
var s = '';
if (sex) {
if (cape != "") {
s+= '<div class="abs-box">[img[f_' + cape + '_cape]]</div>';
}
s+= '<div class="abs-box">[img[f_' + suit + '_suit]]</div>';
s+= '<div class="abs-box">[img[f_' + swimsuit + '_swimsuit]]</div>';
s+= '<div class="abs-box">[img[f_' + belt + '_belt]]</div>';
s+= '<div class="abs-box">[img[f_' + col + ']]</div>';
} else {
if (state.active.variables.cape != "") {
s+= '<div class="abs-box">[img[m_' + cape + '_cape]]</div>';
}
s+= '<div class="abs-box">[img[m_' + suit + '_suit]]</div>';
s+= '<div class="abs-box">[img[m_' + underwear + '_uw]]</div>';
s+= '<div class="abs-box">[img[m_' + belt + '_belt]]</div>';
s+= '<div class="abs-box">[img[m_' + col + ']]</div>';
}
new Wikifier(place, s);
},
};
It looks complicated, but all it is doing is building up a string, s, with a series of those img tags, then adding the string to the passage.
Then you need the images. I used PNG format, as it supports transparency, and edited it with GIMP. I started with two images, one for a guy, one for a girl. For each, I changed the cape colour using the colourise feature, saving each colour, then I erased the cape, and did the bodysuit, and so on.
Friday, 28 March 2014
The "tale" variable
This is following on from the post on the "state" variable. My start page looks like this, and there are two other pages.
<<set $x = 4>>
<<set alert(JSON.stringify(tale))>>
<<set $y = 7>>
[[Two]]
In Sugarcube I get this (after a little formatting):
{
"passages":{
"Three":{"title":"Three","id":0,"domId":"passage-three","text":"[[Start]]","textExcerpt":"Start…","tags":[],"classes":[],"className":""},
"Two":{"title":"Two","id":1,"domId":"passage-two","text":"A second page.\n<<set $z = 10>>\n[[Three]]","textExcerpt":"A second page. Three…","tags":[],"classes":[],"className":""},
"Start":{"title":"Start","id":2,"domId":"passage-start","text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(tale))>>\n<<set $y = 7>>\n[[Two]]","textExcerpt":"A first page. Two…","tags":[],"classes":[],"className":""},
"StoryTitle":{"title":"StoryTitle","id":3,"domId":"passage-storytitle","text":"Untitled Story","textExcerpt":"Untitled Story…","tags":[],"classes":[],"className":""},
"StoryAuthor":{"title":"StoryAuthor","id":5,"domId":"passage-storyauthor","text":"Anonymous","textExcerpt":"Anonymous…","tags":[],"classes":[],"className":""}
},
"title":"Untitled Story",
"domId":"untitled-story"
}
So tale has three properties, passages (a hash table of all the passages), title and domId (a computer friendly - slugified - form of the title). Each member of the passages hash table looks like this, after formating:
"Three":{
"title":"Three",
"id":0,
"domId":"passage-three",
"text":"[[Start]]",
"textExcerpt":"Start…",
"tags":[],
"classes":[],
"className":""
},
Some of this obvious; text is the text, tags is an array of tags, title is the name of the passage (and this is also used as the key for the hash table). The id property looks to be a unique number for each passage in the game. After that, I do not know.
In Sugarcane it looks like this:
{
"storysettings":{},
"passages":{
"Three":{"title":"Three","id":0,"tags":[],"text":"[[Start]]"},
"Two":{"title":"Two","id":1,"tags":[],"text":"A second page.\n<<set $z = 10>>\n[[Three]]"},
"Start":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(tale))>>\n<<set $y = 7>>\n[[Two]]"},
"StoryTitle":{"title":"StoryTitle","id":3,"tags":[],"text":"Untitled Story"},
"StoryAuthor":{"title":"StoryAuthor","id":5,"tags":[],"text":"Anonymous"}
}
}
So we just have two properties; storysettings and passages. Passages is again a hash table, and each entry has a title string, a tags array, a text string and an id number. All the esoteric properties are missing.
Tale has a number of methods. This is what I found in Sugarcube.
setTitle: Sets the title (and domId).
has: Returns true if the story has a passage with the given name or id.
get: Returns the passage from the given name or id.
lookup: Returns an array of passages. As far as I can see, this can only be used for looking up tags, though it is coded to handle any array property of a passage. The first parameter should be "tags", the second the tag to look for. An optional third parameter will have the array sorted by the given property (defaults to title).
reset: Resets the passage.
Here is an example of using lookup. It gets an array of passages with the tag "testtag", sorted by title, and pops up a message box with the name of the first.
ary = tale.lookup("tags", "testtag");
alert(ary[0].title);
<<set $x = 4>>
<<set alert(JSON.stringify(tale))>>
<<set $y = 7>>
[[Two]]
In Sugarcube I get this (after a little formatting):
{
"passages":{
"Three":{"title":"Three","id":0,"domId":"passage-three","text":"[[Start]]","textExcerpt":"Start…","tags":[],"classes":[],"className":""},
"Two":{"title":"Two","id":1,"domId":"passage-two","text":"A second page.\n<<set $z = 10>>\n[[Three]]","textExcerpt":"A second page. Three…","tags":[],"classes":[],"className":""},
"Start":{"title":"Start","id":2,"domId":"passage-start","text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(tale))>>\n<<set $y = 7>>\n[[Two]]","textExcerpt":"A first page. Two…","tags":[],"classes":[],"className":""},
"StoryTitle":{"title":"StoryTitle","id":3,"domId":"passage-storytitle","text":"Untitled Story","textExcerpt":"Untitled Story…","tags":[],"classes":[],"className":""},
"StoryAuthor":{"title":"StoryAuthor","id":5,"domId":"passage-storyauthor","text":"Anonymous","textExcerpt":"Anonymous…","tags":[],"classes":[],"className":""}
},
"title":"Untitled Story",
"domId":"untitled-story"
}
So tale has three properties, passages (a hash table of all the passages), title and domId (a computer friendly - slugified - form of the title). Each member of the passages hash table looks like this, after formating:
"Three":{
"title":"Three",
"id":0,
"domId":"passage-three",
"text":"[[Start]]",
"textExcerpt":"Start…",
"tags":[],
"classes":[],
"className":""
},
Some of this obvious; text is the text, tags is an array of tags, title is the name of the passage (and this is also used as the key for the hash table). The id property looks to be a unique number for each passage in the game. After that, I do not know.
In Sugarcane it looks like this:
{
"storysettings":{},
"passages":{
"Three":{"title":"Three","id":0,"tags":[],"text":"[[Start]]"},
"Two":{"title":"Two","id":1,"tags":[],"text":"A second page.\n<<set $z = 10>>\n[[Three]]"},
"Start":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(tale))>>\n<<set $y = 7>>\n[[Two]]"},
"StoryTitle":{"title":"StoryTitle","id":3,"tags":[],"text":"Untitled Story"},
"StoryAuthor":{"title":"StoryAuthor","id":5,"tags":[],"text":"Anonymous"}
}
}
So we just have two properties; storysettings and passages. Passages is again a hash table, and each entry has a title string, a tags array, a text string and an id number. All the esoteric properties are missing.
Tale has a number of methods. This is what I found in Sugarcube.
setTitle: Sets the title (and domId).
has: Returns true if the story has a passage with the given name or id.
get: Returns the passage from the given name or id.
lookup: Returns an array of passages. As far as I can see, this can only be used for looking up tags, though it is coded to handle any array property of a passage. The first parameter should be "tags", the second the tag to look for. An optional third parameter will have the array sorted by the given property (defaults to title).
reset: Resets the passage.
Here is an example of using lookup. It gets an array of passages with the tag "testtag", sorted by title, and pops up a message box with the name of the first.
ary = tale.lookup("tags", "testtag");
alert(ary[0].title);
Wednesday, 26 March 2014
The "state" variable
There is a useful JavaScript trick that will print all the properties of a variable. It might not work on all Browsers, but is good for experimenting with. In this example, it will give an alert with all the properties of the state.active variable. This is using Sugarcube.
By the way, it is safer to do this as an alert rather than putting the result in text as the text may be part of the variable we are looking at, and you can potentially get your browser stuck in a loop.
Also, I am going to refer to these things as variables; in strict object-orientated language they are properties.
So let us start by looking at state.active, like is:
<<set alert(JSON.stringify(state.active))>>
I set up a basic game, with this is the Start passage:
<<set $x = 4>>
<<set alert(JSON.stringify(state.active))>>
<<set $y = 7>>
And I got this:
{"title":"Start","variables":{"x":4},"sidx":0}
So I can see that state.active.title contains the name of the current passage, and state.active.variables contains all the variables currently in use (in this case just $x had been set).
We can take that back a step and look at state. I am going to add a link to a second passage, that links to a third passage, which links back here, and the second page also sets $z to 10:
<<set $x = 4>><<print JSON.stringify(state)>>
<<set $y = 7>>
[[Main]]
Now we see this:
{"active":{"title":"Start","variables":{"x":4,"y":7,"z":10},"sidx":3},"history":[{"title":"Start","variables":{},"sidx":0},{"title":"Two","variables":{"x":4,"y":7},"sidx":1},{"title":"Three","variables":{"x":4,"y":7,"z":10},"sidx":2},{"title":"Start","variables":{"x":4,"y":7,"z":10},"sidx":3}],"suid":"3bb64cef-573d-43d3-ad06-517b0d046bc5"}
So we can see that state.active holds the current values, while state.history contains an array of historic values of each page visited in order.
Let us see what Sugarcane does.:
{"history":[{"passage":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(state))>>\n<<set $y = 7>>\n[[Two]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Three","id":0,"tags":[],"text":"[[Start]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Two","id":1,"tags":[],"text":"A second page.\n<<set $z = 10>>\n[[Three]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(state))>>\n<<set $y = 7>>\n[[Two]]"},"variables":{"x":4,"y":7}},{"passage":null,"variables":{}}],"id":"1395737963066"}
Sugarcane is rather different. It has no active variable. Its history variable holds an array just like Sugarcube, but in reverse order, so for Sugarcane, state.history[0] is always the current page, for Sugarcube it is the first page.
Sugarcane also has a passage variable, and the passage title is in there, as well as the tags and the whole passage text. As you can see this means a lot more data being stored.
By the way, it is safer to do this as an alert rather than putting the result in text as the text may be part of the variable we are looking at, and you can potentially get your browser stuck in a loop.
Also, I am going to refer to these things as variables; in strict object-orientated language they are properties.
So let us start by looking at state.active, like is:
<<set alert(JSON.stringify(state.active))>>
I set up a basic game, with this is the Start passage:
<<set $x = 4>>
<<set alert(JSON.stringify(state.active))>>
<<set $y = 7>>
And I got this:
{"title":"Start","variables":{"x":4},"sidx":0}
So I can see that state.active.title contains the name of the current passage, and state.active.variables contains all the variables currently in use (in this case just $x had been set).
We can take that back a step and look at state. I am going to add a link to a second passage, that links to a third passage, which links back here, and the second page also sets $z to 10:
<<set $x = 4>><<print JSON.stringify(state)>>
<<set $y = 7>>
[[Main]]
Now we see this:
{"active":{"title":"Start","variables":{"x":4,"y":7,"z":10},"sidx":3},"history":[{"title":"Start","variables":{},"sidx":0},{"title":"Two","variables":{"x":4,"y":7},"sidx":1},{"title":"Three","variables":{"x":4,"y":7,"z":10},"sidx":2},{"title":"Start","variables":{"x":4,"y":7,"z":10},"sidx":3}],"suid":"3bb64cef-573d-43d3-ad06-517b0d046bc5"}
So we can see that state.active holds the current values, while state.history contains an array of historic values of each page visited in order.
Let us see what Sugarcane does.:
{"history":[{"passage":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(state))>>\n<<set $y = 7>>\n[[Two]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Three","id":0,"tags":[],"text":"[[Start]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Two","id":1,"tags":[],"text":"A second page.\n<<set $z = 10>>\n[[Three]]"},"variables":{"x":4,"y":7,"z":10}},{"passage":{"title":"Start","id":2,"tags":[],"text":"A first page.\n\n<<set $x = 4>>\n<<set alert(JSON.stringify(state))>>\n<<set $y = 7>>\n[[Two]]"},"variables":{"x":4,"y":7}},{"passage":null,"variables":{}}],"id":"1395737963066"}
Sugarcane is rather different. It has no active variable. Its history variable holds an array just like Sugarcube, but in reverse order, so for Sugarcane, state.history[0] is always the current page, for Sugarcube it is the first page.
Sugarcane also has a passage variable, and the passage title is in there, as well as the tags and the whole passage text. As you can see this means a lot more data being stored.
Tuesday, 25 March 2014
Macros with clauses
So what if I want some text to be displayed only when the player first sees a passage and optionally other text displayed on subsequent visits? We could use if/else/endif with the visited function, but let us see if we can create a macro to do. The passage might look like this:
Hi there. <<first>>
First time here, eh?
<<notfirst>>
Back again already?
<<endfirst>> What are you doing?
The principle is not unlike if/else/endif, and so I created a macro based on that code. I have heavily commented it, so if you have at least a little familiarity with code you could adapt it to other uses (for example, to handle male and female characters).
Note that this is for Sugarcube. For Sugarcane (and I guess other formats), you need to comment out one line, and uncomment another, towards the end - it says exactly which in the code.
macros['first'] = {
handler: function (place, macroName, params, parser) {
//var condition = parser.fullArgs();
// This is the place in the text that the first clause starts
var srcOffset = parser.source.indexOf('>>', parser.matchStart) + 2;
// This is the source text, with everything up to the start
// of the first clause chopped off
var src = parser.source.slice(srcOffset);
var endPos = -1;
var firstClause = '';
var notClause = '';
// First we need to scan through the text to extract the first and second clauses.
for (var i = 0, currentClause = true; i < src.length; i++) {
// Have we reached the end of the second clause?
// If so, set endPos (the start of the rest of the text)
// and terminate the loop
// The number 12 is the the length of <<endfirst>>
if (src.substr(i, 12) == '<<endfirst>>') {
endPos = srcOffset + i + 12;
break;
}
// Have we reached the end of the first clause?
// If so, flip to doing the second clause
// and jump ahead to skip the <<notfirst>> characters.
// The number 12 is the the length of <<notfirst>>
if (src.substr(i, 12) == '<<notfirst>>') {
currentClause = false;
i += 12;
}
// We are currently in the middle of one of the clauses, so this character
// should be added to whichever clause we are on.
if (currentClause) {
firstClause += src.charAt(i);
} else {
notClause += src.charAt(i);
}
};
// this could go wrong, so get ready to catch errors!
try {
// Now we want to display the correct clause
// Note that text instead the clause is trimmed to remove
// line breaks and spaces at either end
// This is for Sugarcube, if you are using sugarcane, comment the next line, and uncomment the one after
if (visited(state.active.title) == 1) {
// if (visited(state.history[0].passage.title) == 1) {
new Wikifier(place, firstClause.trim());
} else {
new Wikifier(place, notClause.trim());
}
// Finally push the parser past the entire expression
if (endPos != -1) {
parser.nextMatch = endPos;
} else {
throwError(place, "can't find matching endfirst");
}
// Oh, and just in case there was a problem, catch those errors
} catch (e) {
throwError(place, 'bad condition: ' + e.message);
};
}
};
// Also need to register <<notfirst>> and <<endfirst>> so Twine does
// not throw an error when it hits them.
macros['notfirst'] = macros['endfirst'] = { handler: function() {} };
Hi there. <<first>>
First time here, eh?
<<notfirst>>
Back again already?
<<endfirst>> What are you doing?
The principle is not unlike if/else/endif, and so I created a macro based on that code. I have heavily commented it, so if you have at least a little familiarity with code you could adapt it to other uses (for example, to handle male and female characters).
Note that this is for Sugarcube. For Sugarcane (and I guess other formats), you need to comment out one line, and uncomment another, towards the end - it says exactly which in the code.
macros['first'] = {
handler: function (place, macroName, params, parser) {
//var condition = parser.fullArgs();
// This is the place in the text that the first clause starts
var srcOffset = parser.source.indexOf('>>', parser.matchStart) + 2;
// This is the source text, with everything up to the start
// of the first clause chopped off
var src = parser.source.slice(srcOffset);
var endPos = -1;
var firstClause = '';
var notClause = '';
// First we need to scan through the text to extract the first and second clauses.
for (var i = 0, currentClause = true; i < src.length; i++) {
// Have we reached the end of the second clause?
// If so, set endPos (the start of the rest of the text)
// and terminate the loop
// The number 12 is the the length of <<endfirst>>
if (src.substr(i, 12) == '<<endfirst>>') {
endPos = srcOffset + i + 12;
break;
}
// Have we reached the end of the first clause?
// If so, flip to doing the second clause
// and jump ahead to skip the <<notfirst>> characters.
// The number 12 is the the length of <<notfirst>>
if (src.substr(i, 12) == '<<notfirst>>') {
currentClause = false;
i += 12;
}
// We are currently in the middle of one of the clauses, so this character
// should be added to whichever clause we are on.
if (currentClause) {
firstClause += src.charAt(i);
} else {
notClause += src.charAt(i);
}
};
// this could go wrong, so get ready to catch errors!
try {
// Now we want to display the correct clause
// Note that text instead the clause is trimmed to remove
// line breaks and spaces at either end
// This is for Sugarcube, if you are using sugarcane, comment the next line, and uncomment the one after
if (visited(state.active.title) == 1) {
// if (visited(state.history[0].passage.title) == 1) {
new Wikifier(place, firstClause.trim());
} else {
new Wikifier(place, notClause.trim());
}
// Finally push the parser past the entire expression
if (endPos != -1) {
parser.nextMatch = endPos;
} else {
throwError(place, "can't find matching endfirst");
}
// Oh, and just in case there was a problem, catch those errors
} catch (e) {
throwError(place, 'bad condition: ' + e.message);
};
}
};
// Also need to register <<notfirst>> and <<endfirst>> so Twine does
// not throw an error when it hits them.
macros['notfirst'] = macros['endfirst'] = { handler: function() {} };
Monday, 24 March 2014
Strategy games
Twine has the potential for creating strategy games; this is a quick guide to getting started. Strategy games can be as complicated as you like, we will build a very basic one involving running a small kingdom.
This is written for SugarCube, but should work in other formats too.
<<set $gold = 100>>
<<print $gold>>
If you want to change the value in a function, you might do this (in Sugarcube):
state.active.variables.gold += 50;
We are going to be doing a lot of coding, and I would really rather not have to type "state.active.variables." every time, so for this game we will be using variables that are local to the script passage. Setting the value and modifying might look like this:
var gold = 100;
gold += 50;
Note that you do need to declare it before it is used, so the "var" is necessary (that said, Sugarcane seems not to mind if it is missed sometimes).
To display the value, you then need a macro or a function, which is not so convenient, but only has to be set up once.
macros.gold = {
handler: function(place, macroName, params, parser) {
new Wikifier(place, "" + gold);
},
};
Note that Wikifier expects a string; if I sent it just a number, nothing would get added. By adding the gold to an empty string first, the number is converted to a string, and gets displayed properly.
In fact, a better way to handle this is a macro that displays all the important statistics:
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
new Wikifier(place, s);
},
};
Here is the "Start" passage:
<<set init()>>Manage the Kingdom of Gathnia, lead it to properity.
[[Main]]
Besides the introductory text, there is a macro that invokes a function called "init", and a link to the "Main" passage. We will get back to the "init" function in a bit.
The "Main" passage looks like this:
<<summary>>
[[Manage Knights]]
[[Next Turn|Main][nextTurn()]]
So far all it does is give a summary report, then give a link to the knight management section, and a link to this page. Linking back to the same page is something we will do a lot. Note that the link calls a function, "nextTurn".
Here is the passage for managing knights; the same sort of thing.
[[Recruit|Manage Knights][recruitKnight()]]
[[Main]]
Strategy games are great for competitions that limit the word and passage count!
var turn, knights;
window.init = function() {
turn = 0;
knights = 0;
}
window.nextTurn = function() {
turn++;
}
window.recruitKnight = function() {
knights++;
}
The "init" function sets up the initial values (it is best to do this in a function and call the function so they get re-initialised on a restart). The other two functions increase the value of a certain variable by one.
You should now have a working game where you can increase the turn and recruit knights. And nothing else.
var turn, knights, peasants, gold;
window.init = function() {
turn = 0;
knights = 0;
gold = 100;
peasants = 50;
}
window.nextTurn = function() {
turn++;
gold += (state.active.variables.peasants * 2);
gold -= (state.active.variables.knights * 20);
}
window.recruitKnight = function() {
knights++;
gold -= 50;
}
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
s += "\nPeasants: " + peasants;
s += "\nGold: " + gold;
new Wikifier(place, s);
},
};
<<summary>>
<<if banditFail() >>
GAME OVER
<<else>>
[[Manage Knights]]
[[Next Turn|Main][nextTurn()]]
<<endif>>
Now we can modify the summary macro so the player will get a report on the bandit situation, and if banditFail is true, that is game over. Clearly we need to modify the scripts. I am going to break that up somewhat to explain the changes. First we need a variable to track the situation, banditRisk, and that must be initialised. We also need some phrases to inform the player.
var turn, knights, peasants, gold, banditRisk;
window.init = function() {
turn = 0;
knights = 0;
gold = 100;
peasants = 50;
banditRisk = 50;
}
var banditRisks = [
"-",
"Some concern among the peasants about bandits in the hills",
"Some peasants are claiming there are bandits in the hills",
"Bandits have been seen in the area",
"The bandits are becoming bolder",
"The bandits have killed a couple of peasants; your people are demanding action",
"The bandits have killed a large number of peasants, and are threatening the castle",
"The bandits have overrun the castle",
];
The nextTurn function needs to be modified to update banditRisk. The more peasants you have, the more income and so the greater the risk of bandits. Having knights makes it go down. The value of banditRisk can never be below zero.
window.nextTurn = function() {
turn++;
gold += (state.active.variables.peasants * 2);
gold -= (state.active.variables.knights * 20);
// Knights quit if there is no gold for them
if (gold < 0) {
knights += (gold / 20);
if (knights < 0) {
knights = 0;
}
}
// handle bandits
banditRisk += (peasants * 2);
banditRisk -= (knights * 5);
if (banditRisk < 0) banditRisk = 0;
}
The next function returns true when banditRisk is high. How high depends on the number of risk phrases available. Note that I use banditRisks.length; we can add more phrases to the list without having to change anything here.
window.banditFail = function() {
return (Math.round(banditRisk / 60) >= (banditRisks.length - 1));
}
Then I need to update the summary macro. This just adds one of the phrases defined earlier to the text, depending on how high banditRisk is. Just in the case the risk is sky-high, the last phrase is used whenever banditRisk is high, and we can use the banditFail function to test that.
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
s += "\nPeasants: " + peasants;
s += "\nGold: " + gold;
new Wikifier(place, s);
if (banditFail()) {
new Wikifier(place, banditRisks[banditRisks.length - 1]);
} else {
new Wikifier(place, banditRisks[Math.round(state.active.variables.banditRisk / 60)]);
}
},
};
1 Test the game often. At least do Build - Test play, even if you do not play through, as this will quickly check the JavaScript code can be understood. If you write three lines of code and Twine throws an error, you only have thee lines to check through. If you have written 20 lines since the last Build - Test play, you have a lot more lines to search through.
2 If there is a problem, scatter alert instructions though the code, each giving a different message. Play the game, and see what messages are displayed when to track down where the code is faulty.
This is written for SugarCube, but should work in other formats too.
Variables
A strategy game is going to involve a lot of variables, tracking the game state. There are 2 ways to handle a variable in twine. Say we have a variable for tracking monery called $gold, we could set it and print it like this:<<set $gold = 100>>
<<print $gold>>
If you want to change the value in a function, you might do this (in Sugarcube):
state.active.variables.gold += 50;
We are going to be doing a lot of coding, and I would really rather not have to type "state.active.variables." every time, so for this game we will be using variables that are local to the script passage. Setting the value and modifying might look like this:
var gold = 100;
gold += 50;
Note that you do need to declare it before it is used, so the "var" is necessary (that said, Sugarcane seems not to mind if it is missed sometimes).
To display the value, you then need a macro or a function, which is not so convenient, but only has to be set up once.
macros.gold = {
handler: function(place, macroName, params, parser) {
new Wikifier(place, "" + gold);
},
};
Note that Wikifier expects a string; if I sent it just a number, nothing would get added. By adding the gold to an empty string first, the number is converted to a string, and gets displayed properly.
In fact, a better way to handle this is a macro that displays all the important statistics:
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
new Wikifier(place, s);
},
};
Passages
We will have relatively few passages. A "Start" to introduce the game will lead to the "Main" passage, and this will have links to a number of passages where the player can control different aspects of the game. A link to this very page for moving on time, and a link to a help page will be about it. All the real work will be done in JavaScript.Here is the "Start" passage:
<<set init()>>Manage the Kingdom of Gathnia, lead it to properity.
[[Main]]
Besides the introductory text, there is a macro that invokes a function called "init", and a link to the "Main" passage. We will get back to the "init" function in a bit.
The "Main" passage looks like this:
<<summary>>
[[Manage Knights]]
[[Next Turn|Main][nextTurn()]]
So far all it does is give a summary report, then give a link to the knight management section, and a link to this page. Linking back to the same page is something we will do a lot. Note that the link calls a function, "nextTurn".
Here is the passage for managing knights; the same sort of thing.
[[Recruit|Manage Knights][recruitKnight()]]
[[Main]]
Strategy games are great for competitions that limit the word and passage count!
The Script
Here is the script passage, where the variables are declared and the three functions are defined:var turn, knights;
window.init = function() {
turn = 0;
knights = 0;
}
window.nextTurn = function() {
turn++;
}
window.recruitKnight = function() {
knights++;
}
The "init" function sets up the initial values (it is best to do this in a function and call the function so they get re-initialised on a restart). The other two functions increase the value of a certain variable by one.
You should now have a working game where you can increase the turn and recruit knights. And nothing else.
Peasants and gold
Let us introduce peasants and gold. Pretty much any strategy game will use some form of money as it is a way of limiting what the player can do and measuring success. Let us say it costs 50 gold to recruit a knight, and you have to pay them 20 gold each turn. However, peasants make you 2 gold each turn. Here are some updated functions to reflect that.var turn, knights, peasants, gold;
window.init = function() {
turn = 0;
knights = 0;
gold = 100;
peasants = 50;
}
window.nextTurn = function() {
turn++;
gold += (state.active.variables.peasants * 2);
gold -= (state.active.variables.knights * 20);
}
window.recruitKnight = function() {
knights++;
gold -= 50;
}
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
s += "\nPeasants: " + peasants;
s += "\nGold: " + gold;
new Wikifier(place, s);
},
};
Bandits!
Wait... What is the point of recruiting knights? They cost money, so they need to have value. Let us say that knights protect the land from bandits and also give prestige, which in turn could give further bonuses. We can modify the Main passage like this:<<summary>>
<<if banditFail() >>
GAME OVER
<<else>>
[[Manage Knights]]
[[Next Turn|Main][nextTurn()]]
<<endif>>
Now we can modify the summary macro so the player will get a report on the bandit situation, and if banditFail is true, that is game over. Clearly we need to modify the scripts. I am going to break that up somewhat to explain the changes. First we need a variable to track the situation, banditRisk, and that must be initialised. We also need some phrases to inform the player.
var turn, knights, peasants, gold, banditRisk;
window.init = function() {
turn = 0;
knights = 0;
gold = 100;
peasants = 50;
banditRisk = 50;
}
var banditRisks = [
"-",
"Some concern among the peasants about bandits in the hills",
"Some peasants are claiming there are bandits in the hills",
"Bandits have been seen in the area",
"The bandits are becoming bolder",
"The bandits have killed a couple of peasants; your people are demanding action",
"The bandits have killed a large number of peasants, and are threatening the castle",
"The bandits have overrun the castle",
];
The nextTurn function needs to be modified to update banditRisk. The more peasants you have, the more income and so the greater the risk of bandits. Having knights makes it go down. The value of banditRisk can never be below zero.
window.nextTurn = function() {
turn++;
gold += (state.active.variables.peasants * 2);
gold -= (state.active.variables.knights * 20);
// Knights quit if there is no gold for them
if (gold < 0) {
knights += (gold / 20);
if (knights < 0) {
knights = 0;
}
}
// handle bandits
banditRisk += (peasants * 2);
banditRisk -= (knights * 5);
if (banditRisk < 0) banditRisk = 0;
}
The next function returns true when banditRisk is high. How high depends on the number of risk phrases available. Note that I use banditRisks.length; we can add more phrases to the list without having to change anything here.
window.banditFail = function() {
return (Math.round(banditRisk / 60) >= (banditRisks.length - 1));
}
Then I need to update the summary macro. This just adds one of the phrases defined earlier to the text, depending on how high banditRisk is. Just in the case the risk is sky-high, the last phrase is used whenever banditRisk is high, and we can use the banditFail function to test that.
macros.summary = {
handler: function(place, macroName, params, parser) {
var s = '';
s += "Turn: " + turn;
s += "\nKnights: " + knights;
s += "\nPeasants: " + peasants;
s += "\nGold: " + gold;
new Wikifier(place, s);
if (banditFail()) {
new Wikifier(place, banditRisks[banditRisks.length - 1]);
} else {
new Wikifier(place, banditRisks[Math.round(state.active.variables.banditRisk / 60)]);
}
},
};
What next?
From here on you can make the game as complicated as you like. You probably need at least three variables that the player has to juggle and can only indirectly control. So far we have gold and banditRisk, but happiness would be a good addition. We can add more demographics to add more options, allow the player to set tax rates, etc..Using JavaScript
Some general advice when coding in Twine:1 Test the game often. At least do Build - Test play, even if you do not play through, as this will quickly check the JavaScript code can be understood. If you write three lines of code and Twine throws an error, you only have thee lines to check through. If you have written 20 lines since the last Build - Test play, you have a lot more lines to search through.
2 If there is a problem, scatter alert instructions though the code, each giving a different message. Play the game, and see what messages are displayed when to track down where the code is faulty.
Monday, 17 March 2014
RegExp in Twine
A RegExp is a regular expression (also called a RegEx), a special kind of string for matching against other strings.
if ($input == 'dragon')
What if the player types "DRAGON" or "dragons" of " dragon"? None of these will match, as they are not exactly the same. Using a RegExp allows as to match the input against a pattern:
if ($input.match(/dragon/i))
Here we use the match method to compare the string against a pattern. the pattern itself has slashes at each, and a modifier, i, after the second slash. The i indicates that the pattern matching will be case insensitive, so this will match "Dragon". As long as the sequence "dragon" appears in the input, a match will be found.
s.replace(/story/, 'tale')
However, it will only replace the first occurance; it is usually better to add a modifier, g, to make is global. in this version, every occurance of story will be replaced.
s.replace(/story/g, 'tale')
/the \w+ story/
the great story
the 1st story
You can also match against alternatives:
/stor(y|ies)/
story
stories
s.replace(//the (\w+) story//g, 'the $1 tale')
the great story -> the great tale
/^The great story$/
http://www.cheatography.com/davechild/cheat-sheets/regular-expressions/
https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
Match
A simple example of their use in a game might be to ask the player to type in an answer to a question, and to test if the answer is correct. you might try this:if ($input == 'dragon')
What if the player types "DRAGON" or "dragons" of " dragon"? None of these will match, as they are not exactly the same. Using a RegExp allows as to match the input against a pattern:
if ($input.match(/dragon/i))
Here we use the match method to compare the string against a pattern. the pattern itself has slashes at each, and a modifier, i, after the second slash. The i indicates that the pattern matching will be case insensitive, so this will match "Dragon". As long as the sequence "dragon" appears in the input, a match will be found.
Replace
The string class also support the replace method, which you can use to replace one piece of text with another. This also supports RegExps. Here is an example that will replace "story" with "tale":s.replace(/story/, 'tale')
However, it will only replace the first occurance; it is usually better to add a modifier, g, to make is global. in this version, every occurance of story will be replaced.
s.replace(/story/g, 'tale')
Wild cards
The power of RegExp is that you can use wildcards to match against a string that is somewhat like what you expect. In this example, the \w will match any alphanumeric character (including underscore!). The thus sign indicates there can be any number of them, but at least one (use * to match against none or more)./the \w+ story/
the great story
the 1st story
You can also match against alternatives:
/stor(y|ies)/
story
stories
Capture groups
You can also grab some text from the pattern and use it. You surround the text you want to capture in brackets in the RegExp, then use it in the replacement as a dollar followed by a number.s.replace(//the (\w+) story//g, 'the $1 tale')
the great story -> the great tale
Start and End
Often you will want to match against the start of the text and the end. The start can be matched against \A or ^, the end against \Z or $. This example will match only if this is the entire text - no more and no less./^The great story$/
Finally
RegExp is a big topi and this is really only scratching the surface, but is hopefully enough to give a feel for what it is. A couple of links with lists of codes you can use:http://www.cheatography.com/davechild/cheat-sheets/regular-expressions/
https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
Monday, 10 March 2014
The click macro (Sugarcube only)
If you are using Sugarcube, you get a whole new set of macros to play with.This a way of updating a page the player is on. In this example, the player finds a sword, and if she clicks on the link, the sword is picked up.
You are in a room, with <<id "link">><<click "a sword">><<set $weapon = 'fire'>><<append "#fire">>You pick up the sword, and brandish it fiercely.<</append>><<replace "#link">>a sword<</replace>><</click>><</id>> on the floor.
<<id "fire">><</id>>
There is a lot going on there!
The <<click>> macro is the key. It takes a single argument with is the link text. When the player clicks the link, the macros inside <<click>> will be run.
The <<set>> macro sets a variable, so we can later test if the sword was taken.
The <<append>> macro adds some text to he end of the HTML element named, in this case the span element with an id "fire".
The <<replace>> is similar, but removes the existing text (we need to do that to stop the player picking up the sword multiple times).
The <<id>> macro simply sets up a span with the given ID.
Here is a something similar, but this time clicking the link displays a box with text; you can click the box to dismiss it. This can be used to allow the player to examine objects in the current location without sending her to another page.
You are in a room, with <<click "a button">><<set document.getElementById("hidden1").style.display="block">><</click>> on the floor.
<<id "hidden1">><<click "The button is big and red">><<set document.getElementById("hidden1").style.display="none">><</click>><</id>>
The stylesheet entry might be like this (this allows up to 5 links, named hidden1, hidden2...):
/* This is the panel that will appear */
#hidden1, #hidden2, #hidden3, #hidden4, #hidden5 {
display:none;
position:fixed;
top:300px;
left:200px;
background-color:#eef;
padding:20px;
border-radius: 15px;
border-color: #004;
}
/* This is the text in the panel */
#hidden1 a, #hidden2 a, #hidden3 a, #hidden4 a, #hidden5 a {
font-style:italic;
color:#004;
}
/* links in the main passage */
.link-click {
color:red;
}
Again the <<click>> macro adds the functionality to the text. In this case, the <<set>> macro is used to change whether the descruiption is displayed, by changing the "display" CSS property.
You are in a room, with <<id "link">><<click "a sword">><<set $weapon = 'fire'>><<append "#fire">>You pick up the sword, and brandish it fiercely.<</append>><<replace "#link">>a sword<</replace>><</click>><</id>> on the floor.
<<id "fire">><</id>>
There is a lot going on there!
The <<click>> macro is the key. It takes a single argument with is the link text. When the player clicks the link, the macros inside <<click>> will be run.
The <<set>> macro sets a variable, so we can later test if the sword was taken.
The <<append>> macro adds some text to he end of the HTML element named, in this case the span element with an id "fire".
The <<replace>> is similar, but removes the existing text (we need to do that to stop the player picking up the sword multiple times).
The <<id>> macro simply sets up a span with the given ID.
Here is a something similar, but this time clicking the link displays a box with text; you can click the box to dismiss it. This can be used to allow the player to examine objects in the current location without sending her to another page.
You are in a room, with <<click "a button">><<set document.getElementById("hidden1").style.display="block">><</click>> on the floor.
<<id "hidden1">><<click "The button is big and red">><<set document.getElementById("hidden1").style.display="none">><</click>><</id>>
The stylesheet entry might be like this (this allows up to 5 links, named hidden1, hidden2...):
/* This is the panel that will appear */
#hidden1, #hidden2, #hidden3, #hidden4, #hidden5 {
display:none;
position:fixed;
top:300px;
left:200px;
background-color:#eef;
padding:20px;
border-radius: 15px;
border-color: #004;
}
/* This is the text in the panel */
#hidden1 a, #hidden2 a, #hidden3 a, #hidden4 a, #hidden5 a {
font-style:italic;
color:#004;
}
/* links in the main passage */
.link-click {
color:red;
}
Again the <<click>> macro adds the functionality to the text. In this case, the <<set>> macro is used to change whether the descruiption is displayed, by changing the "display" CSS property.
Subscribe to:
Posts (Atom)