Harlowe Support Group Thread + Tips & Tricks

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:

  1. 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?
  2. Similarly, how to copy an array, everything seems by reference? or at least also (shuffled:) despite docs saying takes Any seems to only care about Strings and ignores an Array?

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…

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

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.

3 Likes

:duck:

Alright, writing this post and a bit more tinkering helped me rubber-duck debug my problems, yay!

Thanks for the help community! :blush:

Hope these notes are helpful to others now. Happy to chat more and share any other tips and tricks.

Figured out the magic for datatypes finally! Feels great! :slight_smile: Added above, this is going to be so useful. Need to explore some magic with datamaps in the future next.

Edit: Figured out apparently variables can’t contain - in their name, though that’s not documented, only call out is case-sensitivity and not starting with numeral, aggregated that above now too.

Been trying to think of more tips for what I’ve been working on. Added a small one I’ve used for mapping text choices in the story to other values I’ve needed for logic in the code.

Would love to know of any other techniques folks are using so we can add to this library of knowledge. :slight_smile:

Added a couple of macros for Datamaps to retrieve the maximum value and sort. Useful if you’re using them as a map of string key stat names to stat values, for instance.

Hey, cool stuff here! I have been looking for an answer to a question you might be able to help me with, but no worries if not. I’m trying to create a macro that can access the global variable’s data by reference rather than by value, do you know if there is a way to accomplish this? For context here is the code I’m working on, a macro for adding a quest to an NPC’s quest list:



 (set: $INIT_QUEST to (macro: any-type _NPC, str-type _QuestName,[
 (set: _NPC's Quests to it + (a: (dm:"QuestName", _QuestName, "Completed", false)))
 (output-data:(print: (str:"QuestName " + _QuestName + " added to " + _NPC's FirstName + "'s Quest List :" + _NPC's Quests's last's QuestName)))
]
 ))

The error I’m running into is that the _NPC value I’m accessing and then altering is a copy and not the actual value of the global $NPC variable I’m passing in. Do you know if there is a way to pass data by reference in harlowe 3.8?

Thanks in advance, also, cool tips and tricks you’ve got posted here, I definitely found value in them :slight_smile:

After further research it looks like there is not a way to pass a value by reference, but I might have a work-around. Cheers!

After a day of learning I decided to go with a much simpler set up for my quest system. I am used to having custom classes available in the toolbox and am finding that I don’t quite have that control with harlowe, so instead decided to just keep the quest log as a separate array, and also track finished quests with another array that the storylet conditions and other lambdas can then check for:

(set: $PlayerQuests to (a:))
(set: $FinishedPlayerQuests to (a:))


(set: $INIT_QUEST to (macro: str-type _QuestName,[
(if: $FinishedPlayerQuests does not contain _QuestName)[
(set: $PlayerQuests to it + (a: _QuestName))
(output-data:(print: (str:"QuestName " + _QuestName + " added to $PlayerQuests")))
]
(output:)[]
]
))

(set: $COMPLETE_QUEST to (macro: str-type _QuestName,[
(set: $PlayerQuests to it - (a: _QuestName))
(set: $FinishedPlayerQuests to it + (a: _QuestName))
(output-data:(print: (str:"QuestName " + _QuestName + " added to $FinishedPlayerQuests")))
]
))
1 Like

I thought things passed by reference, arrays are a bit finicky sometimes in general vs. datamaps. Though looking at the functions I’ve written they’re all just outputting stuff vs. modifying anything in place (having side-effects like that can be dangerous anyway). So, I guess this could be something I’ll hit in my future too… :stuck_out_tongue:

Your approach here seems good though. I haven’t gotten back into my adventure yet. Best of luck on yours!

1 Like

Thanks! I am enjoying reading through your code and will definitely be taking some inspiration from a few of your methods. Cheers!

1 Like