Working with Storylets in Harlowe (3.2 and later)

Update (April 15, 2021):

The Twine Cookbook now has a “Storylets” example for Harlowe 3.2 as well.

Original Post:

The macros (storylet:) and (open-storylets:) were added in version 3.2 of Harlowe. These add support for using the concept of storylets in the story format.

Note: Harlowe 3.2 (published Jan. 4, 2021) became generally available in Twine 2 starting with version 2.3.11 (published Jan. 17, 2021)

Understanding Storylets

The Harlowe documentation explains storylets as “mini-stories within a story – disconnected sequences of passages that can be visited non-linearly when certain conditions are fulfilled.” More generally, a storylet contains two things: content and prerequisites.

A storylet is available when its prerequisites are met. These are expressed conditionally. In Harlowe, for example, the condition under which a storylet might be available are written using lambdas such as “when $fuel > 8″ or, in combination with the (history:) macro, “when “forest” is in (history: )“. When these conditions are true, Harlowe considers the storylet “open”.

Creating Storylets

In Harlowe a passage becomes a storylet when its first line includes the (storylet:) macro detailing the conditions under which the passage is open.

Note: This post uses Twee to express passages. Twee is a plain-text format used with tools like Tweego and Extwee to compile the format into Twine 2 HTML files.

:: Start
This is the start of a story using storylets.
(set: $collection to 0)

:: Forest
(storylet: when $collection < 2)
This is the forest.

:: Castle
(storylet: when $collection < 2)
This is the castle.

In the above example, the passages Forest and Castle become storylets through the use of the (storylet:) macro. For both, their conditions are “when $collection < 2”. (The story variable $collection is created in the Start passage.)

As passages, they can be accessed through using link markup. However, as storylets, a macro like (open-storylets:) is needed.

Listing Open Storylets

The macro (open-storylets:) returns an array of datamaps. To access every entry in an array, the macro (for:) can be used.

:: Start
(set: $collection to 0)
(for: each _p, ...(open-storylets:) )[]

In the above example, the macro (for:) is used with the spread syntax (…) to separate out every entry in the returned array from the (open-storylets:) macro. Each loop, the current entry is set to the temporary variable _p.

The datamap of each entry in the array returned by (open-storylet:) is the same structure as returned by the (passage:) macro. As storylets are passages, this includes the passage’s name, tags, and source as part of its datamap.

To access the value of a name in a datamap in Harlowe, the apostrophe (‘s) syntax is used. For example, to access the name of a passage returned by the (passage:) macro, it would be “(passage: “Start”)’s name”. Based on the use of the temporary variable created by the datamap of each passage in the array returned by (open-storylets:), the code to produce a listing of open passages could be the following using the (link-goto:) passage to create a link based on the passage’s name.

:: Start
This is the start of a story using storylets.
(set: $collection to 0)
(for: each _p, ...(open-storylets:) )[* (link-goto: _p's name)]

In the above code, the bullet list syntax (single asterisk) is also used to generate a list.

Closing Storylets

A storylet is considered closed when its condition is no longer true. For example, if the condition is “when $collection < 2” and the story variable $collection ever becomes 2 or greater, this would no longer be true.

Building off the previous example code shared in this post, a storylet can be considered closed when the value it is testing changes during the viewing of the passage.

:: Start
This is the start of a story using storylets.
(set: $collection to 0)
(for: each _p, ...(open-storylets:) )[* (link-goto: _p's name)]

:: Forest
(storylet: when $collection < 2)
This is the forest.
(set: $collection to it + 1)
(for: each _p, ...(open-storylets:) )[* (link-goto: _p's name)]

:: Castle
(storylet: when $collection < 2)
This is the castle.
(set: $collection to it + 1)
(for: each _p, ...(open-storylets:) )[* (link-goto: _p's name)]

In the above code, the Start passage displays a list of open storylets: Forest and Castle. Upon visiting either passage, the story variable $collection is increased by 1 (using the it special keyword). From this point, the player can either visit the same passage they are currently viewing or the other. Once they have made this choice and visited, the story variable $collection becomes 2 and the storylets (Forest and Castle) become no longer available.

The use of the same line of code across multiple passages marks it as a good use of the (display:) macro. This allows one macro to be displayed in another. Improving the previous code through using the (display:)macro would look like the following:

:: Start
This is the start of a story using storylets.
(set: $collection to 0)
(display: "Storylets")

:: Forest
(storylet: when $collection < 2)
This is the forest.
(set: $collection to it + 1)
(display: "Storylets")

:: Castle
(storylet: when $collection < 2)
This is the castle.
(set: $collection to it + 1)
(display: "Storylets")

:: Storylets
(for: each _p, ...(open-storylets:) )[* (link-goto: _p's name)]

Showing the player a link to a passage they are current viewing could be confusing. Now that the code to list the passages is in a single place, this make changing it must easier. Adding an (if:) macro to test is the current passage’s name could help with this problem.

:: Storylets
(for: each _p, ...(open-storylets:) )[\
	(if: (passage:)'s name is not _p's name)[\
		* (link-goto: _p's name)
	]
]

Now, in the above code, the (link-goto:) macro will only be used if the current name of the temporary variable _p does not match the name of the current passage.

Setting Storylet Urgency

The (urgency:) macro works with the (storylet:) macro. It sets the “urgency” of the storylet, which affects how it is sorted by the (open-storylets:) macro. A passage with a higher urgency will appear higher position in the array than a passage with a lower one.

:: Quest1
(storylet: when $adventuring is true)
(urgency: 1)

:: Quest2
(storylet: when $adventuring is true)
(urgency: 3)

:: Quest3
(storylet: when $adventuring is true)
(urgency: 2)

Given the above passages, the ordering produced by (open-storylets:) would be Quest2, Quest3, and then Quest1 based on their urgency.

Random Storylet Selection

When working with array values, the (either:) macro can returns a random entry within a spread array.

:: Start
(set: $adventuring to true)
Your next quest is:
(set: _questName to (either: ...(open-storylets:))'s name )
(link-goto: "Quest", _questName)

:: Quest 1
(storylet: when $adventuring is true)
This is quest 1!

:: Quest 2
(storylet: when $adventuring is true)
This is quest 2!

:: Quest 3
(storylet: when $adventuring is true)
This is quest 3!

In the above code, one of the three storylet passages (Quest 1, Quest 2, or Quest 3) will be selected by the (either:) macro. The name of the datamap representing the passage is saved and then used with the (link-goto:) passage to create a new link to the passage based on its name.

Storylet Exclusivity

The (exclusivity:) macro sets an exclusivity number for all other open storylets. If a passage has an exclusivity set to a number greater than the current one, it is open. Otherwise, it is closed.

:: Start
(set: $adventuring to true)
(set: $quests to 0)
[[Start Quests]]

:: Start Quests
Your next quest is:
(set: _questName to (either: ...(open-storylets:))'s name )
(link-goto: "Quest", _questName)

:: Quest 1
(storylet: when $adventuring is true and (history:) does not contain "Quest 1")
(set: $quests to it + 1)
This is quest 1!
[[Start Quests]]

:: Quest 2
(storylet: when $adventuring is true and (history:) does not contain "Quest 2")
(set: $quests to it + 1)
This is quest 2!
[[Start Quests]]

:: Quest 3
(storylet: when $adventuring is true and (history:) does not contain "Quest 3")
(set: $quests to it + 1)
This is quest 3!
[[Start Quests]]

:: Confront Enemy
(storylet: when $quests is > 1)
(exclusivity: 1)
After two quests, you confront the enemy!

In the above example, the player can take on a random selection of one of three initially open storylets (Quest 1, Quest 2, and Quest 3). Visiting any increases the story variable $quests by 1.

Upon visiting any of the initial storylets, however, they become part of the array returned by the (history:) macro. This prevents a player from returning to the same storylet a second time.

After two storylets are visited, the value of the story variable $quests becomes 2 and the storylet Confront Enemy becomes open. Because it has an exclusivity of 1 (which is higher than the default of 0), it becomes the only storylet available and the player, upon clicking the link generated by the (link-goto:) macro, views the Confront Enemy passage and is unable to return to the others.