How to Create Custom Macros in Twine

2,591 views
Skip to first unread message

Stormrose

unread,
Dec 13, 2012, 6:38:22 AM12/13/12
to twee...@googlegroups.com
I wrote a tutorial on creating javascript macros for twine. This uses a very simple "Hello World!" example but it introduces all the lovely boilerplate that makes macro coding easier to debug. All this is informed by my experience with writing macros since I tend to like "gamey" style stories.

My motivation in writing this is to show the Javascript wizzes out there that Twine is incredibly flexible to work with and that they can use their existing JS knowledge right away. This is one of the reasons why I choose Twine over it's competitors.

Be warned that this is a technical area: if you're not comfortable with javascript then this tutorial might not be for you. For those less JS inclined, I'll take suggestions on other Twine tutorials that need writing.


I have two more Custom Macro tutorials in the pipeline: One in draft, one in outline. I'll add them to this thread when they're written.

Stormrose

unread,
Dec 15, 2012, 1:20:16 AM12/15/12
to twee...@googlegroups.com
This next tutorial is on how to work with JavaScript variables in Twine macros is here:

http://eturnerx.blogspot.co.nz/2012/12/how-to-use-parameters-and-variables-in.html

It's not too hard, but there a few things to do to make things easier.

Stormrose

unread,
Dec 18, 2012, 1:59:27 PM12/18/12
to twee...@googlegroups.com
This next tutorial is on how to have JavaScript custom macros work back and forth with Tweecode variables and macros.
http://eturnerx.blogspot.com/2012/12/howto-twine-vars-and-macros.html

This makes some cool stuff possible. Just use your powers for good!
Enjoy.

Remy Porter

unread,
Jan 20, 2013, 7:59:50 PM1/20/13
to twee...@googlegroups.com
Out of curiosity, what sort of errors occur with macros? I ask, because I built a pair of macros according to these instructions, tagging the block with "script", and so far as I can tell, they're completely ignored. The only error I get is "no macro found".

I am using Jonah, right now, but this isn't the Start passage, 

Details: I'm trying to build a pair of macros that makes it easy to increment and decrement a few variables, but _also_ audits these changes so that I can have a delta. Currently, the code looks like this:

::Macros [script]
try {
version.extensions["customOperators"] = {
major: 1, minor: 0, revision: 0
};
macros["up"] = {
handler: function(place, macroName, params, parser) {
var attr = params[0];
var amt = params[1] || 5;
state.history[0].variables[attr] += amt;
state.history[0].variables["act" + attr] += amt;
}
};
macros["down"] = {
handler: function(place, macroName, params, parser) {
var attr = params[0];
var amt = params[1] || 5;
state.history[0].variables[attr] -= amt;
state.history[0].variables["act" + attr] += amt;
}
}
} catch (e) {
throwError(place,"macro setup failure: " + e.message);

Stormrose

unread,
Jan 21, 2013, 4:38:52 PM1/21/13
to twee...@googlegroups.com
If you're not getting an alert box saying there's an error and if the console is clean then....

1) Some versions (older) of Jonah / Sugarcane don't run macros from until after the Start passage has been displayed. This means that if you use <<display>> in the Start passage (and even if that later nests into more <<display>> calls), then the macro will simply not run at all.
There are fixes about: namely looking at the Sugarcane / Jonah main() function and just shifting the "find and make the macro" code up above the "display the Start passage" code. This is what I do in mine Twine stories... I expect I'll roll out the changes in a new alpha.
Responsive storyformat already has this fix.

2) It's possible that those macros might give some errors. Use the init member of the macro object to set:
state.history[0].variables[attr]
state.history[0].variables["act" + .... various values]
Otherwise you might get some null errors. 

--Et

Remy Porter

unread,
Jan 21, 2013, 6:54:35 PM1/21/13
to twee...@googlegroups.com
This isn't in the Start macro or displayed in it. And I don't think the init method does what I want to do- I want a macro that works like this:

<<down "foo">> translates to <<set $foo = $foo - 5>><<set $actfoo = $actfoo + 5>>

The purpose here, and I admit this is a little pedantic, is to allow me to a) break a story up into acts, and b) audit my operations on certain variables, so that I can balance how I set them to guarantee an even experience regardless of the path the player chooses, based on which act they're in. This is for my use as a designer, not so much something that directly impacts the player experience.

Right now, I get no errors aside from "macro not found". I assume I've screwed something up, but I'm curious how I can find out what.

Stormrose

unread,
Jan 21, 2013, 7:30:11 PM1/21/13
to twee...@googlegroups.com
So it turns out that all current versions of SugarCane & Jonah have this fault. I took a bit of time and fixed it, though I'm not guaranteeing that I didn't cause another regression someplace else. WARNING backup your StoryFormat files just in case.

gl,
--Et

On Tuesday, January 22, 2013 10:38:52 AM UTC+13, Stormrose wrote:
1) Some versions (older) of Jonah / Sugarcane don't run macros from until after the Start passage has been displayed. This means that if you use <<display>> in the Start passage (and even if that later nests into more <<display>> calls), then the macro will simply not run at all.
There are fixes about: ... [snip]
StoryFormats_macrofix20130122.zip

Stormrose

unread,
Jan 21, 2013, 8:06:53 PM1/21/13
to twee...@googlegroups.com
The reasons for the init() is that state.history[0].variables ends up with "NaN" if you try to do anything to the values it contains. It was easy enough to work around the error (and not using an init()) but doing a <<set>> macro before each of the <<up>> or <<down>>...  <<set $foo = 0>><<set $actfoo = 0>><<up "foo">>
But, given your usage... doing that will most likely be too much bother (esp for the $act... vars). Here's a little rewrite:

::Macros [script]
try {
 version
.extensions["customOperators"] = {
   major
: 1, minor: 0, revision: 0
 
};
macros
["up"] = {
 handler
: function(place, macroName, params, parser) {
 
var attr = params[0];

 
var amt = 5;
 
if(params.length >= 2) amt = parseInt(params[1]);
 
if(isNaN(state.history[0].variables[attr])) state.history[0].variables[attr] = 0;
 
if(isNaN(state.history[0].variables["act" + attr])) state.history[0].variables["act" + attr] = 0;

 state
.history[0].variables[attr] += amt;
 state
.history[0].variables["act" + attr] += amt;
 
}
};
macros
["down"] = {
 handler
: function(place, macroName, params, parser) {
 
var attr = params[0];

 
var amt = 5;
 
if(params.length >= 2) amt = parseInt(params[1]);
 
if(isNaN(state.history[0].variables[attr])) state.history[0].variables[attr] = 0;
 
if(isNaN(state.history[0].variables["act" + attr])) state.history[0].variables["act" + attr] = 0;

 state
.history[0].variables[attr] -= amt;
 state
.history[0].variables["act" + attr] += amt;
 
}
}
} catch (e) {
 throwError
(place,"macro setup failure: " + e.message);
}

And here's a tweecode snippet to test it. Expected values are in the round brackets.
!!Up
foo
: <<print $foo>> (blank)
<<up "foo">>foo: <<print $foo>> (5)
actfoo
: <<print $actfoo>> (5)


!!7Up
<<up "foo" 7>>foo: <<print $foo>> (12)
actfoo
: <<print $actfoo>> (12)


!!Down
<<down "foo">>foo: <<print $foo>> (7)
actfoo
: <<print $actfoo>> (17)

The formatting went wobbly - I'll let you fix that.
--Et

Stormrose

unread,
Jan 21, 2013, 8:13:37 PM1/21/13
to twee...@googlegroups.com
Not easily.
1) Macro not found without a popup alert = Macro called from Start (or <<display>>'d from Start or in a Menu). OR code doesn't make a macro.
2) Popup alert then Macro not found = Error in the macro code. Check what the popup said

It's possible to use the javascript console to peak around what Twine is doing, but only after it's done.
Use lots of alerts, new Wikifier(place, "debug message!"); and console.write()
It can be hard finding the DOM objects for the code in order to set breakpoints / watches. Do this once and you have to find the sucker the next time the page is refreshed. We could make this easier .... if there's enough desire... TBH I use alerts and debug text myself.

--Et

HarmlessTrouble

unread,
Feb 1, 2013, 11:59:55 AM2/1/13
to twee...@googlegroups.com
This was just what I needed to warm up to Twine again.  It's funny how quickly these skills leave you when you're not using them.  Thanks Stormrose

-Henry.

Richard Sharpe

unread,
Dec 1, 2013, 9:04:20 PM12/1/13
to twee...@googlegroups.com
We're approaching the first anniversary of this post!

Any progress? :-)

You talked about making a tutorial for Twine variables in macros. I know I'd like to assign a Twine variable a random number each time it is accessed. So, if in a passage, a person had, "Hi, Joe. You can have <<print $dice>> oranges and <<print $dice>> apples today!" It would show a different random number for both every time the passage is opened even though they are the same variable.

Stormrose

unread,
Dec 2, 2013, 7:23:26 AM12/2/13
to twee...@googlegroups.com
Twine variables can be accessed from inside macros using:
state.history[0].variables['variable_name']

But no code is called when variables are accessed or written to from passages. One way to accomplish what you're asking is to make a custom macro to set the random variables. Then call that macro at the start of your passage. This would have the added advantage of being able to repeat your use of the variable with the same value (until it is changed).

HarmlessTrouble

unread,
Dec 8, 2013, 9:00:45 AM12/8/13
to twee...@googlegroups.com
While it's probably not what twee variables where intended to do.  It's technically possible to assign a function to a twee variable. 

Where ever you initialize variables or before you use dice for the first time.

<<set $dieRoll = function(d){ return Math.floor(Math.random()*d)+1;}>>

You can have <
<print $dieRoll(6)>> oranges and <<print $dieRoll(10)>> apples today!"



Like the set macro, You can use assignments in print. If need to save the results for later;

You have <<print $oranges = $dieRoll(6);>> oranges.


That's right <<print $oranges>>.



Reply all
Reply to author
Forward
0 new messages