Snowman Dev Update (2.2: 3 July 2022)

Snowman 2.2 Development Updates:


No more Markdown

I have finally removed all traces of Markdown parsing from the story format. The only changes to code the new version makes is in using the Twine story link format of double square brackets, [[ and ]].

This change was not made easily, as I had re-wrote the Markdown code nearly a month ago to be much simpler. However, as I have been progressing through testing examples from the Twine Cookbook, I have noticed very few of them use Markdown. In fact, nearly all of them use HTML elements for formatting. In seeing this, I decided to remove the remaining formatting and will be considering ways to better surface to new authors how they should be using HTML in the future.

Re-adding History (and more advanced state management)

One of my first changes when working on Snowman 2.2 was to remove the HTML 5 History API code. (I mentioned it a month ago as of this writing, in fact!) It was frustrating for authors to use and hard to test.

Up to this week, I had added the use of localStorage to the new State class to handle reading and writing storage values. However, this was not optimal, and I knew I had to change it once I returned to working on the undo and redo code, which was a major TODO item for this week from last week’s work.

Splitting off the code into a new Storage class meant also breaking off management of history into its own class, History as well; the Storage class should handle any related to storage and History should only touch functionality related to story navigation.

I knew I wanted the use of the undo and redo icons and associated events to affect the use of the global s shortcut (what is window.story.state within Story for legacy reasons and a proxy for State.store internally). The new History class needed to not only track which passages a reader was progressing through, but also rewind the current values of the properties of s each time the undo and redo icons and associated events were used. This required some major changes to how State worked. The new History class now constantly tracks the current passage and state values. Each time a user visits a new passage (first when a story starts and then as they click story links), a copy of the state store is saved along with the passage name. A reader can then “rewind” the story’s state back to the beginning or to a previously navigated to passage and start from there to progress the story again.

Using the new event-based architectural, I was also able to connect the browser events of clicking on the “undo” and “redo” icons with events of the same name. Internally, these call History.undo() and History.redo() methods. This also meant it was easy to add new pseudo-global functions undo() and redo(). They, like the icons, trigger the History methods of the same name within template code.

Screen locking and unlocking

One of the example sets in the Twine Cookbook, “Loading Screen”, has only a single entry for SugarCube. In considering this code, I realized adding screen locking as a loading screen would not be difficult.

Within about 10 minutes, I had added the screenLock() and screenUnlock() pseudo-global functions. Like the use of the undo() and redo() pseudo-global functions, these trigger State events. When the ‘screen-lock’ event is trigger, a new element, <tw-screenlock>, is appended to the <body>. Because of its associated CSS, it covers all other visible elements until the corresponding event, ‘screen-unlock’, is trigger, removing it from the <body>. Together, these form a “loading screen” where an author can trigger the screen lock, load or otherwise process some data or media, and then unlock the screen when they are ready for a reader to continue the story.

Rethinking pid and name passage attribute processing

Most people are probably not aware, but each <tw-passagedata> element has two attributes guaranteed to be unique in a story. The first is pid, which is the “passage identification” attribute used by Twine 2. Every <tw-passagedata> in a story has a unique numerical pid based on its creation order during editing starting with 1 and increasing as new passages are added to the story. The second attribute is name. As the Twine 2 editor prevents passages from sharing the same name, every passage in a story will have a different name value.

Previously, when parsing <tw-passagedata> elements to create Passage objects inside of the Story class in Snowman, every pid and name was saved. This was not as efficient as it could be. The pid is only used in a story format in reference to the startnode attribute of the <tw-storydata> element. This defines what the starting passage’s pid is, and in order to start a story, a story format must look for the <tw-passagedata> element with the matching pid attribute.

In summary, the pid attribute is incredibly important for starting a story. After the story starts, however, this attribute is not used again. This means keeping around as part of a Passage object does not make much sense, as an author would not know what the pid values of passages were without looking at the generated HTML from the Twine 2 editor, which authors generally are not doing.

Limiting the use of the pid attribute led to another thought: why are stories limited to the HTML elements within the document? In other words, a story could be generated dynamically based on code for a reader to progress. There is no reason for story navigation to be limited to only the <tw-passagedata> elements. In working through this reasoning, I decided to add two new methods: Story.addPassage() and Story.removePassage(). An author can create or remove as many passages as they want. Starting with a single passage and using JavaScript to dynamically build out a story is completely possible now without trying to hack or otherwise modify a running story in strange ways.

Storylets support

The last functionality I am working on is support for non-linear story navigation based on requirements. This was something I had last worked on over a year ago as a more generic library for Twine 2 and other projects. Like the “Loading Screen” set for the Twine Cookbook, there is only one official entry for “Storylets”: Harlowe 3.2 and newer. I want Snowman to have the next entry in the set.

Currently, any passage with the tag ‘storylet’ and using the element <requirements> is considered to be a storylet within the new code. Borrowing from how Harlowe 3.2 processes potential storylets, Snowman 2.2 looks through the source of any passages using the ‘storylet’ tag. If any have the <requirements> element, its content is parsed as if it was JSON and the passage name and requirements are added to an internal collection. If, as part of the <requirements> content, an author also included the priority property, this is also processed. Otherwise, every new added storylet is set with the same default priority value of 0.

An example might look like the following:

:: StoryTitle
Storylets in Snowman 2.2

:: UserScript[script]
s.testing = true;

:: Start
<%
  // Retrieve only 1 passage object from 
  //  any available based on their requirements.
  const availablePassages = Storylets.getAvailablePassages(1);
  // Use the show() pseudo-global shorthand 
  //  for window.story.show() and show passage by name.
  show(availablePassages[0].passage.name);
%>

:: First[storylet]
<requirements>
{
  "testing": true
}
</requirements>
<p>First</p>

:: Second[storylet]
<requirements>
{
  "testing": true
}
</requirements>
<p>Second</p>

As the above example hints toward, a storylet is considered available if its requirements match the current properties and values in the global store s. In the previous example, both the “First” and “Second” passages would be considered available based on their requirements. However, as the code limited the selection of those available, only the “First” passage would be shown, as both passages have the same priority values internally in the collection.

A separate example using a defined priority property would be slightly different:

:: StoryTitle
Storylets in Snowman 2.2

:: UserScript[script]
s.testing = true;

:: Start
<%
  // Retrieve only 1 passage object from 
  //  any available based on their requirements.
  const availablePassages = Storylets.getAvailablePassages(1);
  // Use the show() pseudo-global shorthand 
  //  for window.story.show() and show passage by name.
  show(availablePassages[0].passage.name);
%>

:: First[storylet]
<requirements>
{
  "testing": true
}
</requirements>
<p>First</p>

:: Second[storylet]
<requirements>
{
  "testing": true,
  "priority": 1
}
</requirements>
<p>Second</p>

In the updated code, the passage “Second” has a defined priority property with a value greater than the default of 0. This would mean it was sorted to the top of the returned collection based on its priority value.

As I write this, the last part of this functionality I’m working on is testing the use of the methods Storylets.addPasssge() and Storylets.removePassage(). Like the Story equivalents of Story.addPassage() and Story.removePassage(), these allow authors to dynamically associate existing passages as storylets or to remove them from consideration. There are a few edge cases I’m still working through as part of code coverage for the project.

Twine Cookbook and documentation coverage

There are 49 example sets in the Twine Cookbook. Of those, only two, “Using Addons” and “Templates”, do not now have Snowman equivalents (accounting for the new functionality in Snowman 2.2). However, only 31 of the 49 (around 63%) work as-is. The other 18 will require new Twine Cookbook entries to explain, for the most part, using State.events instead of the older event system using jQuery.

Added to those changes is also a major re-writing of the Snowman documentation. Originally, I used GitBook to create the documentation because of my work on the Twine Cookbook, which was also using the same library two years ago. Since then, I moved the Twine Cookbook to MkDocs over a year ago as some of my final edits on the project. In looking for my original files, I have also learned I did not make backups of the source files for the current Snowman documentation.

I am currently considering using HonKit as a replacement for the GitBook code. However, if I can find a better replacement, I’ll go with another package.