I know Harlowe is not the generally first choice of Twine developers, but I was inspired by @Anvilandthechipmunks’s Big Aspirations to tinker a bit myself with it more.
Figured it could be useful to have a support thread to collect some arcane knowledge not pointed out in the docs.
For instance, my current struggles:
I’m trying to figure out if I can use an array like(a: "strawberry", "apple", "pear", "hourglass")
as a(p-either:)-type
restriction to create an easily re-usable data type, but I don’t think it is?Similarly, how to copy an array, everything seems by reference? or at least also(shuffled:)
despite docs saying takesAny
seems to only care about Strings and ignores anArray
?
Known Tips & Tricks / Resources
I’ll try and edit and summarize these as they come up below in the discussion?
Arrays
Of course, as soon as I post, I think I figure it out. Various ways to copy an array:
(set: $array to (a: 1, 2, 3, 4)) <!-- Initialize an array as an example -->
<!-- Method 1, use slicer and just pass into (a: or (shuffled: -->
(shuffled: ...$array)
(a: ...$array)
<!-- Method 2, use altered to iterate over array and copy into a new one (could be useful if you want to modify at the same time -->
(set: $array2 to (altered: _a via it, ...$array))(print: $array2)
Thanks to this post which helped demystify the spread ‘…’ operator sprinkled throughout the docs…
If you’re looking to do dynamic lists this post helped me out, you can even combine with choices and other hooks and such for some complex interactions. For instance, applying to my choice/subprocedure style setup:
|choices>[{
(set: _options to (array: "|small>[Salad]", "|medium>[Burger]", "|large>[Pizza]"))
(if: $hunger > 5)[(set: _options to it + (array: "|xlarge>[All of the above]"))]
(print: "* " + (joined: "\n* ", ..._options))
}]\
(click: ?small)[(replace: ?choices)[{
(set: $food to 1)
}\
You decide to have the //Salad//. It's pretty good, but not that filling.
]]\
...
In the example above, you can see we set a temp variable to store our default choices, and then can modify and add to the array based on conditions. We can then print out that entire array as a bulleted list complete with hooks that’ll be processed by our code further below it.
Datamaps
If you have a datamap that represents a bunch of different stats for instance and want to find the maximal stat:
(set: const-type $max_dm to (macro: dm-type _dm, [
(set: num-type _max_value to -1)
(set: str-type _max_key to "")
(for: each _item, ...(dm-entries: _dm))[
(if: _item's value > _max_value)[
(set: _max_value to _item's value)
(set: _max_key to _item's name)
]
]
(output-data: _max_key)
]))
Similarly, if you want to retrieve a sorted list of the stats from largest to smallest:
(set: const-type $sorted_dm to (macro: dm-type _dm, [
(set: _values to (sorted: ...(dm-values: _dm)))
(set: _values to (reversed: ..._values))
(set: _entries to (dm-entries: _dm))
(set: _names to (a:))
(for: each _v, ..._values)[
(set: _find to 1st of (find: _attr where _attr's value is _v, ..._entries))
(set: _names to it + (a: _find's name))
(set: _entries to it - (a: _find))
]
(output-data: _names)
]))
If you want smallest to largest, just remove the 2nd set to _values
with the call to reversed
.
Datatypes
This also solved my other problem, where I can setup a global array like:
(set: $body_shapes to (a: "strawberry", "apple", "pear", "hourglass"))
<!-- and use elsewhere to setup/seed a typed value -->
(set: (p-either: ...$body_shapes)-type $asset to (shuffled: ...$body_shapes)'s 1st)
You can also save the datatype directly to be even more re-usable:
(set: $body_shapes to (a: "strawberry", "apple", "pear", "hourglass"))
(set: $t_body_shape to (p-either: ...$body_shapes))
<!-- and use like: -->
(set: $t_body_shape-type $asset to "strawberry")
Saving
- @hamadana’s post Big Aspirations (0.6.2) A weight gain interactive story - #422 by hamadana
- GitHub - Kajot-dev/Twine-Harlowe-Save-To_File: Harlowe utility for saving and loading game progress to encrypted file
Scope
Temporary Scope for Passage
It can be a bit frustrating to deal with temporary scoped variables as they can only exist within a hook and its descendants, this can make it a bit hard when you’re doing formatting or blocking code and trying to have a temporary variable remain in scope for your entire passage.
You can use a Unclosed Hook at the top of your passage in order to keep a temporary variable in scope for your entire passage easily:
:: IntroPassage
(set: _body_choices to (shuffled: "strawberry", "apple", "pear", "hourglass"))[==\
Subprocedures
I found this little gem pattern. If you have a piece of logic you want to run for multiple choices in a passage:
|choices>[\
* |choice1>[First Choice]
* |choice2>[Another Choice]
]|process_both)[{
<!-- Do Common logic in this hidden hook-->
(set: $choices_made to it+1)
<!-- Remove the hidden hook completely if you want -->
(replace: ?process_both)[]
}]\
(click: ?choice1)[(replace: ?choices)[{
<!-- Execute the Subprocedure -->
(show: ?process_both)
<!-- do branch specific logic -->
(set: $mood to it+1)
}
You picked the first choice...
]]\
(click: ?choice2)[(replace: ?choices)[{
(show: ?process_both)
(set: $mood to it-2)
}
You picked the second choice...
]]\
Basically just put your code in a Hidden Hook and show it when you want to run it (the one time). I imagine you may be able to just hide it again to re-hide to re-run later, but I haven’t tried that yet…
Variables
Variable names are case-sensitive, can’t contain -
, nor start with a numeral (so you can write $1.50
in story text for instance).
If you need to translate/map one value that you may be displaying in your story, for instance with a (cycling-link: 2bind _story_value, "one", "two", "three")
, then you can use a datamap to help set the value elsewhere:
(set: $number to (_story_value of (dm:
"one", 1,
"two", 2,
"three", 3
)))
Bit of a contrived example above, but can be useful for datamaps themselves to index into different keys/‘names’ as well.