L's blog

Twine 1: Bugfix update of my Combined Replace Macros

I've recently updated my <<Replace>> Macro Set to version 1.1.5. Normally I don't make auxiliary posts explaining changes to my macros, but I think this deserves one due to the number of nuanced fixes that this attends to.

<<endinsertion>> fixed
This particular end tag had a nasty and embarrassing bug - the text "n>>" was left behind after it, unless you instead substituted <<endinsert>> for it (which was also a bug, as it shouldn't work like that). That's fixed, and <<insertion>> spans may be used with their proper end tags again.

<<revise>>: "end" option fixed
Consider this code:

<<revision "B">>The balcony is vacant.<<becomes>>The balcony is occupied by a party of ghosts.<<endrevision>>
Maybe you should <<revise B "look closely" end>>.

The "end" in the <<revise>> macro is supposed to make it so that the text "look around" remains after you click to the end of the revision... but until now, it's been broken. Now that's fixed. While making some Twine games of my own, I discovered (or rediscovered) that this particular idiom is actually a valuable usage case, so I deeply regret that it's been unavailable for so long.

<<hoverreplace>> <<gains>> behaviour fixed

<<gains>> hasn't quite worked with <<hoverreplace>> since its inception. Consider this code:

<<hoverreplace>>First bit<<gains>> and next bit<<endhoverreplace>>
If you moused over the first bit (causing the next bit to appear), but did not ever touch the next bit, it wouldn't disappear when you moused off it. This is now fixed.

<<hoverreplace>> rapid mouse behaviour fixed
For awhile, <<hoverreplace>> has been more than a little flaky when rapidly moving the mouse on and off the element. I've tried a few fixes in the past to alleviate this, but this one looks to be much better, and testing shows it to be robust.

A note about <<hoverreplace>>
One thing that is still not yet fixed is the problem caused when the initial state is larger than the end state. Consider this code:

<<hoverreplace>>[img[large image]]<<gains>>Small text<<endhoverreplace>>

This code is still buggy because as soon as you mouseover the large image, it disappears, leaving the small text behind, and thus your pointer is no longer "over" the hoverreplace structure. I may still be able to fix this in the future, but generally, in this particular situation you should resort to <<mousereplace>>.

Q: Why did it take you 9 months to write the fixes for these bugs?
A: At the time, my attention had been single-mindedly focused on both my day job, preparing Twine 1.4.2, and developing Twine 2. I apologise sincerely for this unsightly delay in basic maintenance, and will try to be less badly negligent in the future.

Twine: A script to convert parser IF transcripts into linear Twines

I've devised a way to convert game transcripts exported from Gargoyle (using the "script" command) directly into basic Twine games, programmatically. Here is a two-step process:

0) Open a new Twine 1 story.

1) Include this script:

var currentPassage=new Passage("Start");currentPassage.text="";var id=currentPassage.id=1;tale.get("Transcript").text.split(/\n/g).forEach(function(b){var a=b.indexOf(">");if(a===-1){currentPassage.text+=b+"\n"}else{id+=1;var c="Turn "+id;currentPassage.text+=b.slice(0,a)+"\n ''>'' [["+b.slice(a+1)+"|"+c+"]]\n";tale.passages[currentPassage.title]=currentPassage;currentPassage=new Passage(c);currentPassage.id=id;currentPassage.text=""}});currentPassage.text=currentPassage.text.trim()||"<<set window.close()>>";tale.passages[currentPassage.title]=currentPassage;Wikifier.formatters.forEach(function(a){if(a.name=="list"){a.match="^(?!.)(?=.)"}});

2) Paste the entire transcript, from beginning to end, into a passage named "Transcript".

When you run the game, the Start passage and all other passages will be replaced with passages derived from the transcript. Each passage will only have 1 command.

* This assumes that the ">" character is only used for player commands throughout the entire story - and it also assumes that all player commands end in a line break. If these aren't the case, there'll be trouble.
* If the transcript contains other text structures which correspond to Twine syntax, those may unwittingly activate. This script removes one fairly common occurrence - HTML bullet points denoted by * - but leaves the others present.

* Play in the Jonah format (and optionally remove the title using CSS).
* Use basic CSS to make the links larger (similar to how MS Paint Adventures displays its page links).

Q: Is this Lighan ses Lion compliant???
A: Naturally!

Twine: either(), a random picker function

Update: This script is now built into Twine 1.4! It is no longer necessary to install it.

This is a very short script that allows you to use a function called "either" in Twine's <<set>>, <<print>> and <<if>> macros. Give it several string or number values, separated by commas, and it will pick one of them randomly to use in the macro.

Obsolete script removed: use Twine 1.4

This functions largely identically to the Game Maker choose() function. You can use it like this:

<<print either("a dusty glade", "a sinister vale", "a murky gully", "a desolate gulch") >> - Prints one of the strings, chosen randomly.

<<set $value = either(0,1,2,3,4)>> - Sets $value to one of the numbers.

<<if either(true, false)>> - This is true 50% of the time. Equivalent to <<if Math.random() lt 0.5>>

In the first case, you can see it's functionally similar to the <<rnd>> macro - but, as demonstrated, it can also be used inside <<set>> as well as <<print>>.

If you patch the <<display>> macro, you can also use it to display a random passage, serving as a more primitive kind of <<randomp>>:

<<display either("Cellar", "Garden", "Observatory")>>

Feel free to report any bugs to @webbedspace.

Twine: <<if>> and whitespace

Update: This behaviour has been changed in Twine 1.4! It no longer applies.

This is just a summary of how <<if>>, by default, handles whitespace characters (that is, line breaks, spaces, tabs, and such.)

The behaviour

The rule is: the <<if>> macro removes all whitespace contained between the <<if>>, <<endif>> or <<else>> tags and any non-whitespace contained text.

Consider this code sample, in which the line breaks are marked:

The magenta line breaks (those contained between the <<if>> and <<endif>> and the actual text) will be removed by the macro. Thus, this will render as follows:

A slice of marmalade toast on a plate,
a bit of quiet,
and a spot of tea.

A slice of marmalade toast 
on a plate,
a bit of quiet,
and a spot of tea.

Resisting this behaviour

The <<if>> macro was primarily designed for inserting whole paragraphs. This behaviour is, however, less useful for, say, inserting sentence fragments into paragraphs, which often have leading spaces. There are a few ways to get around this. Such as:

  • Empty comment syntax

    If you pad the interiors of <<if>> macros with an empty comment tag "/%%/", then the behaviour will be overridden, because the comment tags will be treated as text despite not appearing in the final story.

    First line. <<if $a gt 0>>/%%/
    Second line.
    This might be the most basic method of all listed here.

  • Using <<print>>
    The <<print>> macro can also be used.

    A complete sentence<<if $a gt 0>>
    <<print " with an extra amendment">>

    However, it isn't obvious how to add line breaks using this method. There is a way, obscure though it is:

    <<if $a gt 0>><<print "First line." + String.fromCharCode(13) + "Second line.">>

  • Using a HTML <br>

    If you want to preserve a line break inside <<if>>, you can do so by the forceful method of using inline HTML.

    First line.<<if $a gt 0>>
    <br>Second line.

    [*]Patching <<if>> to not remove whitespace

    Much like most undesirable Twine behaviour, this can be patched out on a story-by-story basis. The script code for doing so is as follows... however, using this will conflict with my <<else if>> script, so you can't have one and the other.

    var conditions=[],clauses=[],srcOffset=parser.source.indexOf(">>",parser.matchStart)+2,src=parser.source.slice(srcOffset),endPos=-1,currentCond=parser.fullArgs(),currentClause="",t=0,nesting=0;
    for(var i=0;i<src.length;i++){if(src.substr(i,9)=="<<endif>>"){nesting--;if(nesting<0){endPos=srcOffset+i+9;
    clauses.push(currentClause);currentClause="";t=src.indexOf(">>",i+6);if(src.substr(i+6,4)==" if "){currentCond=Wikifier.parse(src.slice(i+10,t));
    }else{currentCond="true";}i=t+2;}if(src.substr(i,5)=="<<if "){nesting++;}currentClause+=src.charAt(i);
    }try{if(endPos!=-1){parser.nextMatch=endPos;for(i=0;i<clauses.length;i++){if(eval(conditions.shift())){new Wikifier(place,clauses[i ]);
    break;}}}else{throwError(place,"can't find matching endif");}}catch(e){throwError(place,"bad condition: "+e.message);

    The future
    The next version of Twine 1 may change the behaviour of <<if>> to not remove whitespace. I'm still debating how or whether this should be enabled, and how to enable current story code to continue to function correctly.

  • Twine: <<autolink>>, a macro to automatically make links.

    This macro, when surrounding a span of text, will search through the text, finds words that are also names of passages in the story, and converts them to internal links.

    * Since Twine passage names are case-sensitive, this search is also case-sensitive.
    * It currently only matches single ASCII words (letters and numbers) and hyphens contained within, but currently not apostrophe-containing words.

    macros.autolink={handler:function(g,e,f,b){function tagcontents(starttag,endtag,k){var a=b.source.slice(k),l=0,c="";
    for(var i=0;i<a.length;i++){var w=endtag.length;if(a.substr(i,w)==endtag){if(l==0){b.nextMatch=k+i+w;
    return c}else{l--;c+=a.charAt(i)}}else{if(a.substr(i,starttag.length)==starttag){l++
    }c+=a.charAt(i)}}return""}var k=b.source.indexOf(">>",b.matchStart)+2,d=tagcontents("<<"+e,"<<end"+e+">>",k),bs=String.fromCharCode(92),re=new RegExp("(["+bs+"w-]+)","g"),words,len;
    re.lastIndex+=(len)}}while(words);new Wikifier(g,d)}};macros.endautolink={handler:function(){}}

    Usage example:
    * <<autolink>>The most popular pets are dogs and cats<<endautolink>> - Assuming the passage "dogs" exists, that becomes "dogs".

    Not too sure of the name - might change it later.

    Feel free to report any bugs to @webbedspace.

    Twine: improved YouTube background audio macros

    This code is largely based on some original macros posted to the Google Group by, er, somebody. These take a YouTube video ID and plays the audio of that video invisibly in the background of your Twine game. The player will require Flash installed, of course.

    http://www.glorioustrainwrecks.com/files/TwineMacros-YouTubeAudio-2.1.0.txt (Last updated 16/3/14)

    What I've done is upgrade it in the following ways:
    * The volume slider is removed, on the basis that most authors will probably not have much call for it. Also, it required jQuery and jQuery UI, which not only inflated the footprint of the script, but may have interfered with other jQuery-using Twine scripts.
    * It supports multiple sound tracks per story, and lets you play them simultaneous with each other if you so wish.
    * The script will attempt to preload all of the videos, ensuring that they play as soon as their macros are called. (Note: currently it only preloads the tracks from the start, so this performance gain may not be seen when starting playback from the middle of the track.)
    * A few more macros are provided, roughly in line with my HTML5 audio macros: <<unloopbgm>> and <<pausebgm>>.
    * The code has less implicit globals. (This is a meaningless distinction.)
    * There should be more informative error messages for specific error codes.

    The macro names themselves are similar, but I made the following changes: <<playbgm>> now plays the audio once, and <<loopbgm>> causes it to constantly loop. Otherwise, they function identically.

    Note: For browser security reasons, local HTML files are prohibited from accessing network files through a Flash embed. So, you have to test these macros by running the game from a web server.

    Usage examples
    *<<playbgm iHBWZn9re0U 32>> plays the YouTube video starting from 32 seconds in.
    *<<playbgm iHBWZn9re0U>> plays the YouTube video from the beginning, or resumes it if it was paused.
    *<<stopbgm iHBWZn9re0U>> stops the sound track.
    *<<loopbgm iHBWZn9re0U>> makes the sound track constantly loop (although it may not quite be seamless).
    *<<unloopbgm iHBWZn9re0U>> stops it from looping.
    *<<pausebgm iHBWZn9re0U>> pauses the sound track.
    *<<stopbgm iHBWZn9re0U>> stops playback and resets its position to the start.

    Feel free to report any bugs to @webbedspace.

    Twine: Combined <<Replace>> Macro Set

    Last updated: 6-Jun-15
    I've worked hard to combine my <<replace>>, <<timedreplace>>, <<revision>>, <<hoverreplace>> and <<once>> macros, and all their variations, into a single codebase, and included some new variations as well. Here's my combined "Replace Macro Set":

    http://twine1.neocities.org/ReplaceMacros.min.js (version 1.1.7)

    The first good news is that you only need this much default CSS for all these macros:

    .revision-span-in {
    	opacity: 0;
    .revision-span:not(.revision-span-out) {
    	transition: 1s; -webkit-transition: 1s;
    .revision-span-out {
    	opacity: 0;
    The reason I require this CSS is to enable people to modify it to make their own effects. You can, for instance, change this to an instant transition by removing the middle CSS block (with the "transition" property line.)

    List of macros
    This set includes the following "revision macros":
    * <<insert>>, <<replace>>, <<continue>> — triggered by clicking their contents.
    * <<timedreplace>>, <<timedinsert>>, <<timedremove>>, <<timedcontinue>>, <<timedcycle>> — triggered by time.
    * <<hoverreplace>>, <<hoverremove>> — triggered by the mouse touching their contents, only temporarily.
    * <<mousereplace>>, <<mouseremove>>, <<mousecontinue>>, <<mousecycle>> — triggered by the mouse touching their contents, permanently.
    * <<once>>, <<later>> — triggered by visiting the passage a certain number of times in the game session.
    * <<insertion>>, <<revision>>, <<removal>>, <<cycle>> — triggered by a separate "triggering macro".

    It also includes these "triggering macros":
    * <<revise>>, <<revert>>, <<randomise>> — hyperlinks.
    * <<mouserevise>>, <<hoverrevise>> — sections that the mouse can touch.

    Changed syntax
    * <<replacewith>>, used to separate sections in <<timedreplace>>, has been replaced with <<becomes>>.
    * You can now have multiple sections in most of the revision macros, by separating them with <<becomes>> or <<gains>> macros:

    These function just like they do within <<revision>> macros - <<becomes>> replaces the previous section with the next section, and <<gains>> merely appends the next section.
    * To reconcile differences between <<replace>> and <<timedreplace>>'s syntax, some macros (currently just the <<replace>>, <<mousereplace>> and <<hoverreplace>> varieties, as well as <<mousecontinue>>) have multiple syntax for specifying sections - the "normal" syntax, where each section is separated by <<becomes>>/<<gains>>, and a "shorthand" syntax, where the first several sections are strings within the opening tag:

    The final section must be between the opening tag and the <<endreplace>> macro tag, though.
    For most of these, the shorthand strings behave as if the <<becomes>> macro separates them. The exception to this is the "insert" named macros, wherein the strings will behave as if the <<gains>> macro separates them.

    New macros
    * <<later>> is the antonym of <<once>> - it displays its contained text on the second and subsequent visits.
    * <<mousecycle>> creates a section of text that cycles between different versions whenever the mouse touches it. It's similar to <<cyclinglink>> in some respects, though you can't yet easily bind a variable to change when it changes.
    * <<timedcycle>> constantly rotates through its sections without stopping. It is similar to <<timedloop>> and may well replace it in future.


    In any of these examples, <<becomes>> can be substituted with <<gains>>.

    Feel free to report any bugs to @webbedspace.
    Don't use the attached file. It's out of date.

    Twine: mix HTML tags and Twine markup syntax

    Update: This script is now built into Twine 1.4! It is no longer necessary to install it.

    Just now I was thinking to myself, "Why is it that inline HTML must be stuck between the <html> tags in Twine, anyway? Would it be possible to make HTML usable intersperced with Twine syntax?" So I decided to give it a try. The resulting Javascript code is as follows:

    Obsolete script removed: use Twine 1.4

    This code will allow you to put HTML tags in Twine passage text, and have them function in the compiled game. Note: it requires all HTML tags to be properly closed, with the exception of the so-called "void tags" that never have a closing pair (br, hr, area, img, input, embed, param, source, track). Unmatched end tags will be ignored!

    Feel free to report any bugs to @webbedspace.

    Twine: apply CSS to individual characters

    Update: The Javascript on this page is now built into Twine 1.4! It is no longer necessary to install it.

    This script causes every single character in passages to be wrapped in a <span class="char"> element. This enables a number of mildly interesting CSS effects.

    Obsolete script removed: use Twine 1.4

    These characters have the class of "char", and also a class equal to themselves ("a" for the letter "a", "2" for "2", etc.) It's recommended that you use the :nth-child pseudo-class to select them. Some potential CSS effects that can be performed include the following (examples only):

    Horizontally spin characters on mouseover:
    (works best with large text)

    .char:not(.space):hover {
      transform: rotateY(1440deg);
      -webkit-transform: rotateY(1440deg);
    .char:not(.space) {
      display: inline-block;
      transition: transform 2s ease-out;
      -webkit-transition: -webkit-transform 2s ease-out;

    Wavy text:

     .char{ position:relative; }
    .char:nth-child(8n) { top:0px; }
    .char:nth-child(8n+1) { top:-1px; }
    .char:nth-child(8n+2) { top:-1.5px; }
    .char:nth-child(8n+3) { top:-1px; }
    .char:nth-child(8n+4) { top:-0px; }
    .char:nth-child(8n+5) { top: 1px; }
    .char:nth-child(8n+6) { top: 1.5px; }
    .char:nth-child(8n+7) { top: 1px; }

    Animated wavy text:

    .passage {
      font-size: 3em;
    .char { 
    .char:nth-child(8n) { 
      animation: wavetext 4s 0s infinite;
      -webkit-animation: wavetext 4s 0s infinite;
    .char:nth-child(8n+1) { 
      animation: wavetext 4s -0.5s infinite;
      -webkit-animation: wavetext 4s -0.5s infinite;
    .char:nth-child(8n+2) { 
      animation: wavetext 4s -1s infinite;
      -webkit-animation: wavetext 4s -1s infinite;
    .char:nth-child(8n+3) { 
      animation: wavetext 4s -1.5s infinite;
      -webkit-animation: wavetext 4s -1.5s infinite;
    .char:nth-child(8n+4) { 
      animation: wavetext 4s -2s infinite;
      -webkit-animation: wavetext 4s -2s infinite;
    .char:nth-child(8n+5) { 
      animation: wavetext 4s -2.5s infinite;
      -webkit-animation: wavetext 4s -2.5s infinite;
    .char:nth-child(8n+6) { 
      animation: wavetext 4s -3s infinite;
      -webkit-animation: wavetext 4s -3s infinite;
    .char:nth-child(8n+7) { 
      animation: wavetext 4s -3.5s infinite;
      -webkit-animation: wavetext 4s -3.5s infinite;
    @keyframes wavetext {
      0%, 100% { top: 0em; } 50% { top: 0.5em; }
    @-webkit-keyframes wavetext {
      0%, 100% { top: 0em; } 50% { top: 0.5em; }

    Rapid rainbow text:

    .char:nth-child(8n) { color:hsl(45,100%,75%); }
    .char:nth-child(8n+1) {color:hsl(90,100%,75%); }
    .char:nth-child(8n+2) {color:hsl(135,100%,75%); }
    .char:nth-child(8n+3) {color:hsl(180,100%,75%); }
    .char:nth-child(8n+4) {color:hsl(225,100%,75%); }
    .char:nth-child(8n+5) {color:hsl(270,100%,75%); }
    .char:nth-child(8n+6) {color:hsl(315,100%,75%); }
    .char:nth-child(8n+7) {color:hsl(0,100%,75%); }

    Illuminate letters on mouseover

    .char { 
      transition: all 5s; -webkit-transition: all 5s;
      opacity: 0.4;
    .char:hover {
      transition: all 0.1s; -webkit-transition: all 0.1s;
      text-shadow: 0 0 1em white;

    Erase text on mouseover

    .char { 
      transition: opacity 999s step-end; -webkit-transition: opacity 999s step-end;
    .char:hover {
      transition: opacity 1ms; -webkit-transition: opacity 1ms;

    Remove all the T's in the passage text:
    .char.t {

    Change "u" to "U":

    .char.u {
    .char.u::before {
      content: "U";

    These are to be considered basic examples - prompts for more practical uses.

    This code also enables some particularly interesting Javascript visual effects to be performed, which I shall explore in a future blog post.

    Feel free to report any bugs to @webbedspace.

    Twine: Improved back and forward buttons in Sugarcane

    Update: this feature is now built into Twine 1.4! This behaviour is now the default and this page is no longer necessary.

    This script allows Sugarcane to use HTML5 history management instead of URL hash strings to alter the browser history. This means that various non-deterministic game state changes (random numbers, player data input, state changes inside <<replace>> macros, etc.) will be properly remembered when you use the browser's Back button. This code also updates <<back>> and <<return>> and the Rewind menu.

    Obsolete script removed: use Twine 1.4

    Feel free to report any bugs to @webbedspace.

    Syndicate content