Showing posts with label coding. Show all posts
Showing posts with label coding. Show all posts

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() {} };

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.

Saturday, 8 March 2014

Handling an inventory

Here is a set of macros that will make an inventory easy. Just dump this into a script passage, and be sure to call <<initInv>> right at the start.

window.getInv = function() {
  return state.history[0].variables.inventory;
}

macros.initInv = {
  handler: function(place, macroName, params, parser) {
    state.history[0].variables.inventory = [];
  },
};

macros.addToInv = {
  handler: function(place, macroName, params, parser) {
    if (params.length == 0) {
      throwError(place, "<<" + macroName + ">>: no parameters given");
      return;
    }
    if (state.history[0].variables.inventory.indexOf(params[0]) == -1) {
      state.history[0].variables.inventory.push(params[0]);
    }
  },
};

macros.removeFromInv = {
  handler: function(place, macroName, params, parser) {
    if (params.length == 0) {
      throwError(place, "<<" + macroName + ">>: no parameters given");
      return;
    }
    var index = state.history[0].variables.inventory.indexOf(params[0]);
    if (index != -1) {
      state.history[0].variables.inventory.splice(index, 1);
    }
  },
};

macros.inv = {
  handler: function(place, macroName, params, parser) {
    if (state.history[0].variables.inventory.length == 0) {
      new Wikifier(place, 'nothing');
    } else {
      new Wikifier(place, state.history[0].variables.inventory.join(', '));
    }
  },
};

macros.emptyInv = {
  handler: function(place, macroName, params, parser) {
    state.history[0].variables.inventory = []
  },
};


Here is a single page that shows it in action.

First we set up the inventory.<<initInv>>

Then we can use it:
You have <<inv>>.
You pick up a rake.<<addToInv rake>>
You have <<inv>>.
You pick up a spoon.<<addToInv spoon>>
<<if isInInv('spoon')>>You have a spoon<<else>>You don't have a spoon<<endif>>
You have <<inv>>.
You drop the rake.<<removeFromInv rake>>
You have <<inv>>.
You drop the rake again.<<removeFromInv rake>>
You have <<inv>>.
You pick up the spoon again.<<addToInv spoon>>
You have <<inv>>.
You drop nothing (error).<<removeFromInv>>
You have <<inv>>.
You pick up a hat.<<addToInv hat>>
You have <<inv>>.
You drop everything.<<emptyInv>>
<<if isInInv('spoon')>>You have a spoon<<else>>You don't have a spoon<<endif>>
You have <<inv>>.


Note that you can only add and remove one item from the inventory at a time, and you can only have one of anything in the inventory at a time. Also note that this stores strings not objects (if you do not know what that means, it probably does not matter).

Use the getInv function to get a list of objects in the inventory, and isInInv to test if an object is in the inventory.

If you use Sugarcube, you will need to modify the code so that every state.history[0].variables becomes state.active.variables.

Friday, 7 March 2014

Prerender and postrender in Twine

Twine supports things called  prerender and postrender. I was hoping to use postrender to blank lines between paragraphs. It will not do that (still not found a way for that), but I learn a few things a long the way.

This may well depend on which format you are using; this is based on Sugarcane.
Sugarcane has a content variable that is set like this when a page is being displayed:

var content = insertElement(passage, "div", null, "body content");

I guess content is an XML node.

It then goes through all the prerenders defined, calling each one in turn, with the content variable. I do not know what that order is, but I guess it is the order they are defined in, though what that means in Twine is not clear.

What prerender does is add nodes to content. To me, prerendering would be some kind of manipulation; what this is doing is prepending. This is a way to add a title to the top of every page, not a way to modify the text.

Next, the passage text is added to content:

new Wikifier(content, this.processText());

I would guess that this.processText() gets the text for the current passage and does some kind of processing. This is then "wikified", which presumably converts the Wiki conventions (eg //) to HTML (eg <i>), etc. This all gets added as new nodes for content.

Then any postrenders are called. Again, this is really just appending nodes to content, not postrendering in its normal sense.



Postrender and prerender are just functions that are defined as normal, something like this (in passages tagged as "script"):

prerender.myname = function(content) {
  new Wikifier(content, "This is my title\n");
}

postrender.myname = function(content) {
  new Wikifier(content, "This is my footer");
}

Note that the functions have to be attached to the postrender and prerender objects. This ensures Twee can find them.

You can call them anything you like, I have chosen "myname"; you can also call content anything you like, as long as you are consistent within a function. Note that the text in the prerenderer ends "\n", this ensures the title goes on its own line. This is not necessary for the postrenderer. You can put all the normal formating codes into the text, and use JavaScript to build up complex strings to be added.


Here is a neat postrenderer that adds footers to pages. A page tagged as "footer" contains the text to be added. Any page that shares a tag with a footer passage gets that footer added.
http://twinery.org/forum/index.php?topic=672.0

postrender.tagFooter = function(content) {
    var i,j, feet = tale.lookup("tags", "footer");
    for (i = 0; i < feet.length; i++) {
        for (j=0;j<this.tags.length;j++) {
            if (!feet[i].tags.indexOf(this.tags[j])) {
                new Wikifier(content, feet[i].processText());
            }
        }
    }
}

Addendum

It turns out that you can use postrender to manipulate the content of the page, as I found from this forum thread. This only seems to work in Sugarcube by the way.


function processTextNodes(node) {
  if (node.nodeType == 3) { // text node
    node.nodeValue = node.nodeValue
      .replace(/story/g, 'tale');
  } else {
    for (var i in node.childNodes) {
      processTextNodes(node.childNodes[i]);
    }
  }
}

postrender.subst = function(content) {
  processTextNodes(content);
}


All the postrender function, subst, does is call another function processTextNodes. This is a recursive function that works through the XML structure. The bit that does the work, and that you will want to modify, is in blue. In this case, all it does is swap the "story" for "tale". The important value you will be using is node.nodeValue which is just a string contenting a single paragraph of your passage.



I still could not use this to add extra lines; searching for both /\n/ and /<br>/ failed to find any matches, and this is when I realised that each line (in the JavaScript sense; each paragraph in Twine) is stored as a node. Adding to the end of a line is easy enough:

node.nodeValue = node.nodeValue + "<br>";

Unfortunately, this just shows  "<br>" at the end of a line, rather than giving an extra line. Adding "\n" had no effect.




Thursday, 6 March 2014

How to write macros

I wrote this as part of a learn exercise about macros. I am no expert; if you spot errors please do tell me and I will correct them. You should have some understanding of objects in JavaScript before reading this!

Introduction

Macros are used in passages like this:

<<macroname param1 param2 param3 ... >>

When Twine/Twee hits a section of text like this, it takes a look at the macros object. This object has a whole bunch of properties, and Twine/Twee tries to match macroname to one of those properties. if successful, it then calls the handler function. That may sound incredible obscure, but if we have a go at making our own macro, it may become more clear.

This is going to go in a script passage, because it is done in JavaScript. Here is our first macro, called first:

  macros.first = {
    handler: function(place, macroName, params, parser) {
    },
  };


It does not actually do anything if you have <<first>> in the passage, but it does exist in Twee as a macro.

It is just an object

Let us look at that in detail. If you read the [url=http://twinery.org/forum/index.php/topic,1516.0.html]Objects are Your Friends[/url] thread at Twinery.org, you will have seen this in the OP (I have removed the <<set>> for clarity):

$foo= {
  property1: "bar",
  property2: 894,
  property3: $baz,
}


It is doing the same thing! It is creating an object, and setting properties on the object. In one case the object is $foo, in the other it is macros.first. The $%foo object has three properties, called property1, property2 and property3; while macros.first just has one, called handler.

Why macros.first? Well, that attaches our new object first to the existing macros object. That means that Twee will find it when it looks for a macro.

Why handler? Because that is what Twee will try to use when it runs your macro.

The properties assigned to $foo are simple values; for a macro, you need to assign a function, and not just any function. Twee is going to call your function with four parameters. You can call those functions anything you like, but it is easier to stick with those names so you know what Twee will be sending.

Alternative format

You can also define your macro like this:

  macros['second!'] = {
    handler: function(place, macroName, params, parser) {
    },
  };


I do not think that that is as neat, but it does allow you more flexibility in what you name your macro - you can include various punctuation marks in the name. Personally, I would stick to only using letters, numbers and underscores, and use the first form.

Doing stuff

Here is a third macro, and this one actually does stuff.

  macros.third = {
    handler: function(place, macroName, params, parser) {
      new Wikifier(place, " macroName=" + macroName + " params=" + params + "");
    },
  };


This lets us see what some of those parameters are. The first is a representation of your passage (actually a HTMLDivElement object), and you can use that to add content to the page. Do that through the Wikifier. In brief, what that does is take the existing text (place) and append your new text. The basic use is like this:

  new Wikifier(place, mystring);
 
And you can change mystring however you like. You can have several lines like that in one macro, each one adding its own bit to the page.

In the example above, I am adding macroName and params. As you might guess, macroName is just the name of the macro. Params is an array of what comes after the name. For example:

<<third>>
<<third one two three>>


In the first instance, params would be an array of length zero, as there is no further text after the name. In the second instance, the array is:

['one', 'two', 'three']

Here is what is actually output:

macroName=third params=
macroName=third params=one,two,three


Macros in macros

You can use a macro inside a macro. The Wikifier will sort out running the inner macro. In this example, macro third is put in the text.

  macros.fourth = {
    handler: function(place, macroName, params, parser) {
      new Wikifier(place, "Using third: <<third " + params[0] + ">>");
    },
  };


Now we are assuming that there is a parameter. Never wise! Let us modify that macro to check that the parameter exists.

  macros.fourth = {
    handler: function(place, macroName, params, parser) {
      if (params.length === 0) {

        throwError(place, "<<" + macroName + ">>: no parameter given");
        return;
      }
      new Wikifier(place, "Using third: <<third " + params[0] + ">>");
    },
  };


You will see an extra line of code. First it checks if the number of parameters is zero. If it is, take evasive action! It throws an error with the given message, which immediately stops the rest of the macro doing anything.

Accessing variables

You cannot access variables in the normal way, but there is a way to do it. All variables you use in your passages are actually just properties of an object, state.history[0].variables (or
state.active.variables in Sugarcube). This macro puts the value of the variable $myname into the text.

  macros.fifth = {
    handler: function(place, macroName, params, parser) {
      new Wikifier(place, state.active.variables.myname);
    },
  };


The passage might look like this:

<<set $myname = 'Bob'>>
<<fifth>>


Wednesday, 5 March 2014

Initialising Variables

In a passage


Setting variables in a passage is easy. It is probably a good idea to have a passage dedicated to that purpose, and it might look like this:

<<nobr>>
<<set //initalising

$name = 'Boris';
$age = 43;
>>
<<endnobr>>

Note that all the work is being done inside a <<set>> macro. You can pack as much JavaScript in there as you like, and you do not need to actually set anything. What you do need is for it to start with two angle brackets, the word "set", a single space, some non-whitespace character. This will fail:

<<set
$name = 'Boris';

By the way, the double slashes, //, indicate tht what follows on that line is a comment.


In a script


Personally, I would prefer to keep my code and my text separate as far as possible. All that JavaScript should go into a script passage. Sounds easy; you might think this would work:

$name = 'Boris';
$age = 43;

It does not (probably due to scoping rules).

In Sugarcube, you have to do this:

state.active.variables.name = 'Boris';
state.active.variables.age = 43;


In Sugarcane (and I guess the others):

state.history[0].variables.name = 'Boris';
state.history[0].variables.age = 43;


There is an added complication in Sugarcane that the state object  does not exist when a script passage is first run (in Sugarcube it does, so this does not apply). What you have to do then is to set the variable inside a function, and call the function. Your script will look like this;

window.myinit = function() {
  state.history[0].variables.name = 'Boris';
  state.history[0].variables.age = 43;
}

And you invoke it on the Start page like this:

<<set myinit() >>

Tuesday, 4 March 2014

Twine Coding

I am going to use this page to post interesting and useful code snippets for Twine. These are all tested in Sugarcane, by the way. There is some difference between the formats, in particular Sugarcube is different.

If Twine cannot find a macro with a given name, it will instead look for a passage and insert that (not with sugarcube, though). This is an easy way to include variables. One passage could then include this text:

<<playername>> attacks the gruesome fiend with <<playerweapon>>.

You can then set up two passages, playername and playerweapon, that will automatically get inserted. Note that these passage names cannot contain spaces (and obviously cannot be the same as an existing macro).

The playername passage might look like this:

<<print $name>> the <<print $rank>>

The playerweapon like this:

<<if $weapon = 1 >>
Sword of Fire
<<elseif $weapon = 2 >>
Spear of Might
<<elseif >>
his fists
<<endif>>


In Sugarcube, you have to do this (it does not support the display shortcut):

<<display "playername">> attacks the gruesome fiend with <<display "playerweapon">>.



You can define functions in a script passage, but you have to attach the function to the window object to make it accessible. Here is how to do that:

window.myFunction = function() {
  alert("Here I am");
};


I am still working on how to access variables and objects defned in a script passage



The <<set>> macro is effectively an eval function; if just runs the text though a JavaScript parser. This means you can call function, concatenate commands, etc.

<<set alert("Here I am"); count += 1 >>

Even:

<<set
n = $fullname.indexOf(' ');
if (n == -1) {
  $name = $fullname;
} else {
  $name = $fullname.slice(0, n);
}
>>


Sunday, 2 March 2014

Handling variables that have not been initialised

What happens if you forget to initialise a variable that you are using to track state? If you are using Sugarcube, it is going to generate an error when the player gets to a passage that uses it, right?

Not if you test to see if it is defined first.

Here is an example. The variable $weapon is used to hold the player's current weapon (as an integer).

You confront the goblin with
<<if $weapon === undefined>>
your bare hands
<<elseif $weapon = 1 >>
the Sword of Fire
<<elseif $weapon = 2 >>
the Spear of Might
<<elseif >>
yours fists
<<endif>>
and it flees in terror.


The first if tests whether the variable is undefined, and if not acts accordingly. If it is defined, the rest of the code handles it based on its value.

This is great for use in a display passage.

You confront the goblin with <<display "playerweapon">> and it flees in terror.

::playerweapon
<<if $weapon === undefined>>
your bare hands
<<elseif $weapon = 1 >>
the Sword of Fire
<<elseif $weapon = 2 >>
the Spear of Might
<<elseif >>
yours fists
<<endif>>


In other formats (Sugarcane, etc.) variables default to zero, so this is not relevant.

Saturday, 1 March 2014

Using visited and visitedTag

These two functions, visited and visitedTag, are very useful for maintaining state with a minimum of effort. Twine remembers every page the player has visited, and so you can check where she has been previously. Let us suppose there was an option to go to a page "Get the big sword", and some time later, the player is encountering a monster. We might do this:


<<if visited("Get the big sword") >>
You stand your ground, branishing the big sword you found earlier. Intimidated, the goblin runs away, allowing you to [[explore more]]
<<else>>
The goblin leaps at you, tearing you apart with his dirty claws. As you llie dying, you wonder if there was any way you could have found a weapon.

<<endif>>

Note that the link appears inside the conditional text.

It is important to type this accurately. This will fail:

<<if visited("get the big sword") >>

You need to get the page name exactly right. Also, there must be no spaces before "if", or either side of "else" and "endif".

The visitedTag function is similar, but checks if the player has visited pages with the given tag. This is useful if the game should check if the player has visited any one of a set of pages. Perhaps there are three places the player could get that big sword, and you want to check if she hasd been to any of them. there might be an number of passages in which the player gets injured, and you want to check if she has been to them. here is an example (note that tags cannot have spaces):

<<if visitedTag("bigsword") >>

Unfortunately, Sugarcube does not support visitedTag.

ETA: The day after I say  Sugarcube does not support visitedTag, it gets added to Sugarcube. Hurray!