Advanced Chapbook Authoring

I don’t know of any way to run code within the footer in Chapbook. It doesn’t have a dedicated header and footer passage, that I know of, just a pre-built <div> structure that you can send stuff to display. That was an important missed opportunity, I feel. However…

You could replace the Stats link in the footer template with a disabled version from within in the Stats passage code. That would be quite straight forward. And if you had more menu-type passages you could disable all the those links upon visiting any one of them to avoid navigating amongst them and hindering the {back link} insert going to the proper story passage. That would work too.

For coming back to a story passage (that shouldn’t trigger variable manipulation again), I suppose you could check the third last passage in the trail for a match to the current one, since all other navigation was disabled in the Stats passage. It would look like [..., "Passage 24", "Passage 25", "Stats", "Passage 25"] at that point. You would also have to embed a passage that reinstates the menu-type links for every story passage. (Unfortunately, this draws dashed connection lines in the Twine editor from practically every story passage so it makes the layout of it all messy, but it works – and this is why proper header and footer passages would be a godsend.)

I don’t know. My flux capacitor, time travel solution might be cleaner. Just saying. :wink:

Sometimes, when you’re so used to swingin’ a hammer though, every problem looks like a nail. I’ll be the first to admit that I over engineer things.


I was able to make my own embedded (JavaScript-only, unfortunately) passage code that doesn’t draw those dashed lines all over the Twine editor passage map grid. And I have a very nice solution to check tags that are assigned to a passage. Chapbook ignores the tags that Twine provides, but the Twine raw data still exists hidden in the HTML thankfully.

I’m liking Chapbook quite a bit, but it really requires JavaScript knowledge to make it comparable to other story formats. There’s so much to Chapbook that I still don’t understand though. That said, I’m going to keep pushing hard with Chapbook, but I was doing a little reading on Snowman 2 and it looks like another potential candidate.

By the time I’m done, I’ll know everything except SugarCube. I think I’m a story format hipster. I just can’t go with the flow. Marching to beat of my own drum… right off the edge of the cliff. :wink:

Right. Normally in Twine that functionality is best employed in Sugarcube in a header or footer passage that runs consistently. That’s where you’d basically check if a passage should be restricted from tangent linking either by passage name, or some whimsical tagging mechanism #noinventory or whatever.

So that might be the one thing Chapbook could potentially benefit from: an Every Turn passage to do these sorts of checks.

The Header and Footer in Chapbook are basically status lines that can change whenever you want them to and you wouldn’t want to run code in them.

Chapbook does a lot of stuff right and I appreciate that once I figure out what I’m doing, coding in it will be fast and efficient. I wrote the first section of my WIP in Sugarcube and was just exhausted by it - mainly because the sound capabilities gave me way too many opportunities to DJ with fading and crossfading tracks and that’s sort of a distraction!! :musical_score:

1 Like

SugarCube actually scared me off a bit because it looked so complicated. You’re braver than I am. I checked out AXMA a bit and it’s very cool. Looks like it has a lot of options, yet stays manageable. I can see why you champion it. It looks solid.

I was reading up more on Snowman and it lead me to understand more about Chapbook, if you can believe it. Chapbook is so exposed under it’s thin shell of basic commands that when I looked at Snowman, which has no shell whatsoever, I learned more about the basics of what is really happening. Anyway, Snowman talks about rendering passages and such and it dawned on me that the browser’s console can expose objects quite nicely… so I outputted the engine object to the console and a whole bunch of undocumented functions are right there for the picking. I’ve also looked at the built-in modifers and inserts to see how they tick.

Note: You might even be able to turn off the autosave if you look closely at the screenshot above.

I have no idea what I’m doing (lots of trial and error), but damn if I wasn’t able to render a passage directly in the header because of this:

[javascript]
let _passage = engine.story.passageNamed("Render Me");
config.header.center = engine.render(_passage.source);
[continue]

…there is a pretty big bug in Chapbook it seems though. It’s surrounding the -- part of passages where you declare variables above it. When using Chapbook’s own example code for extending things, it causes story breaking errors if -- is used. If you don’t extend Chapbook, -- code works just fine. For example, the passage that I injected into the center of the header with the code above will crash if any -- is present in the “Render Me” passage. Of course, -- is not required to set variables because it can be done in a [javascript] modifier all the same, but it’s unfortunate. I’m going to take the time to shed more light on this bug in the GitHub pages.

If this bug gets squashed, Chapbook can be opened up to the community for custom code solutions to help extend it. As it sits right now, when extending Chapbook, you have to use [javascript] to manipulate variables and do things that typically happen above the --. That’s not ideal, but it’s not a game stopper either.

(and @HanonO )
The following TWEE Notation based example demonstrates one method for adding business logic to a footer.
note: The Passage named Story JavaScript in the following represents the Story > JavaScript area of your project.

:: Story JavaScript
config.footer.center = "{embed passage: 'StoryFooter'}";

:: StoryFooter
[unless passage.name === 'Final']
The footer's content

:: Start
Welcome to the Test, see the footer?
[[Second]]

:: Second
The Second passage, there should be a footer,
[[Final]]

:: Final
The Final passage, and no footer.

Because Chapbook automatically loads its auto-save each time you test or play a project there are times when the order you implement things is very important. The following explanation is example of this issue…

1: You create a new project and add a reference to the config object to the first Passage…

config.footer.center: "My Footer"
--
Hello world

…and when you Test or Play that project everything is fine.

2: You decide to extend your project by adding a Custom Insert that displays a smiley face emoji, and because you want to use that insert through out your project you define it in its Story > JavaScript area…

engine.extend('1.0.0', () => {
    config.template.inserts = [{
        match: /^smiley face$/i,
        render: () => '😀'
    }, ...config.template.inserts];
});

3: And you update the first Passage to use the new insert…

config.footer.center: "My Footer"
--
Hello world
{smiley face}

…however when you use the Test or Play option this time you see the text {smiley face} instead of the expected emoji, and there is a “Cannot read properties of undefined” related error message in your web-browser’s Console.

4: But if you create another new project and reverse the order of two enhancements (define & test the Custom Insert before adding the config.footer.center reference and testing that) everything works as expected! So what is happening?

The loading of the auto-save seems to be done early in the engine start-up process. And after testing/playing step 1 that auto-save contains a config.footer.center variable, and the parent config object part of that variable reference doesn’t have any of the other properties normally expected on engine’s built-in config object.

note: This can be seen in the web-browser’s Console by typing config in it.

So when the Custom Insert defined in step 2 tries to access config.template.inserts it fails because the ‘global like’ config object no longer has a template property.

And the only way I’ve found to resolve that above error is to delete the relevant entry in my web-browser’s Local Storage. The key will start with chapbook-state-<project-name>

3 Likes

WHERE HAS CHAPBOOK BEEN ALL OUR LIVES? :slight_smile:

1 Like

@Greyelf

This is fantastic! Thanks so much for digging into this. This reveals so many possibilities.

I read a lot of the Cookbook stuff, but couldn’t digest it all at once. I’m going to have to revisit it again. It’s hard because the story formats are broken up so much that trying to get a clear reference for one story format is hard.

I really appreciate you challenging me like this. I’m learning so much right now!

(@HanonO )
I updated my previous post with information about why some of the Chapbook examples, like Adding Custom Inserts, don’t always work.

2 Likes

So much to unpack there, but thank you so much for going that extra mile. I’m going look into this further with what you’ve explained. Wow. Thanks!

Hey, I’m new here and am very interested in using Chapbook as I like it’s simplicity, even as an experienced coder. I read through this entire thread and I’m glad you guys made lots of discoveries as to its inner workings.

But there’s one thing I’m struggling with:
Rather than simply auto-loading in the user’s progress, I’d like to always go to a title screen passage on startup, where the user can either start a new game, or continue existing progress (if present).

Is there a way to make this happen?

I have a rough idea, but it’s probably not the best way:

  • On startup, regardless of if the user is returning or not, go to a title passage (How?)
  • That passage has a “New Game” link (simply restarts)
  • Or, if there’s a “lastPassage” variable present, also display a “Continue” link, which goes to that named passage.

It seems very simple in theory, but may work? I’m just not sure how to “force” go to a passage on startup even if the user is being “loaded in.”

For context, I do not plan on publishing my story on a website, but rather packaging the html (and associated images/sounds) into a standalone application.

Any help would be appreciated! Thanks!

First off, welcome! :slight_smile:

More meat for the grinder! I love it! :wink:

There are some internal variables that you can change and one of them might pertain to saving and loading. I’m still digging into Chapbook. However, you can try this and see if it suits your needs. This assumes your starting passage is called “Start”.

Story JavaScript:

if (trail.slice(-1) != 'Start') {
  trail.push('Start');
}

The trail variable will always contain an array of your visited passages so when the story first loads, if the last passage in the array is not the starting one, add the starting passage to the trail array. The condition prevents adding additional “Start” passages if the last one is already “Start” so that the continue button will work (see below).

Starting Passage:

[if trail.length > 1]
{back link, label:'CONTINUE'}
[continue]

In your starting passage, if the trail length is greater than 1, then show the a {back link}. A back link will always take you to the previous passage. The current one is “Start” (because of the JavaScript code) and the previous one will be where the user previously exited the game.

Note: This will not work if you have prior screens leading up to the starting passage though, so you may have to change the number of the trail.length to be checked or compare the trail some other way. Also, the Story JavaScript will have to change to check against those screens leading up before adding the starting passage to the trail. For example, you might have a credits screen or some sort of introduction before the main menu / title screen. Let me know if you need help dealing with that. Also, it may be prudent to rest the trail and all other variables manually after upon starting a new game a new game because the {restart} link clears all state variables, but it also just takes you to the starting passage again. It’s not impossible, but it does require a bit of testing and planning, but I believe the solution will be quite succinct when it’s implemented and complete.

To really see this in action, you can output {trail} in your passages and see the chain of passages grow and then get altered when you close the browser and open the game up again.

There might be another way, but this one works. I’ve tested it and I think I might use it for my story. :wink:


Edit: There are so many ways to tackle this. It might be easier to set a boolean Chapbook variable in the passage after the user starts the story. Then in the Story JavaScript, you could check to see if it exists and then add the Start passage to trail. Then your condition for the CONTINUE button would be reliant on this boolean variable too. That might make more sense to do given the uncertainty of what you may have before the Title / Main passage. Let us know if you require any clarification for getting this new method to work for you.

1 Like

Have you tested what happens if the web-page is refreshed by the web-browser for whatever reason?

eg. a mobile device’s Operating System changes application focus back to the web-browser; the end-user uses the web-browser’s “page refresh” key; etc…

Thank you for the detailed insight, it’s super helpful! I think using a separate boolean makes a bit more sense to me too.

I’ll play around with it and report back :slight_smile: And thank you for being so welcoming. A lot of other game development (ish) forums are full of gatekeeping.

No, but I did think about it afterwards. In fact, I’ve made Harlowe fail on remembering variables by doing so in some cases. Baby steps, but thank you for bringing it up because it really does need to be fool-proof. I’m the fool in the scenario, by the way. :wink:

Seriously though. I really appreciate you keeping tabs on this topic because there is so little out there on the subject. It’s nice to have someone with so much Twine experience. I feel like we’re taming the wild Chapbook frontier. Hopefully we don’t die of scurvy.


You’re welcome… again. :wink:

Definitely try things out for yourself. We do the most learning that way. I also thought about another scenario where the {restart} insert can be used to actually begin a new game the way you want it to work. You’d have to set the Twine starting passage to the passage just after the main menu (when you click to start a new game). Then, in the Story JavaScript, you change the last passage to that of where you really want the game to launch (credits, intro stuff, etc.) Then the main menu has “Begin” and “Continue” options being {restart} and {back link}'. {restart}` will reset all the variables and send you to the next passage properly then.


Regarding the refreshing of the browser window that Greyelf mentioned (and I have not had the time to check this out to see if it’s an issue, though I suspect it is), as long as we can determine when the Story JavaScript is loading from a new browser session versus a refresh/reload, we’ll be in business. Perhaps there is something that the browser itself can determine in this scenario? I’ll give it more thought, but it sounds like it might be possible… discussion with deprecated solution and link to modern methods.

Maybe there is a way with Chapbook variables somehow. Seems unlikely though. Greyelf is probably sitting on the solution until our caveman minds connect the dots. :wink:

Edit: Oh, maybe we can use a specific type of “flag” cookie… session cookies vs persistent cookies. Hmmm…

1 Like

I would basically just put a note on the title screen: This game will use your browser to save your progress. If you’d like to start from the beginning, please click the “Restart” link in the bottom right. And leave the default restart link in the footer (or move it where you like.) I think the normal expectation is players would like to start from where they left off, but they can manually restart as they like.

Screen Shot 2023-06-06 at 2.32.01 PM

It’s a little disconcerting if you’re compiling and testing that it will remember and start you mid-story, but that’s your own browser. Any new player is always going to start from the beginning since there’s nothing remembered in their cache.

Since there is no built in save system, restarting the player when they come back without their expectation might be a less-appealing option! I believe this is justified and possibly explained in the documentation - if a player is on mobile and switches away from the browser or tab for a while, some mobile browsers might clear history as a space/speed saving option and force them to restart even if they meant to continue.

I think the previous responses have explained a way to do this - it sounds like they’re testing the transcript length to push a “continue or restart?” passage to the top of the player’s history. That might actually be a decent option without having to somehow invent a separate save system.

2 Likes

Where others see sage words of caution… I see, “Oh, you wanna be like that, do ya?! I’ll show you! I’ll make it work if it kills me!” :wink:

There is a save system on the testing side. I do wonder if it can’t be “turned on” for the regular playing side of things. One day I might look into it, but for me personally… I’m like you, I like the way it works already. I’m trying to embrace the simplicity of Chapbook as much as possible. Less is more.

My desire for extending Chapbook is more about incorporating standalone widgets of sorts. Like I want to roll some dice on the passage that look like dice and just have that insert be one line of code and let the JavaScript do the rest. Another one I want to finish testing (seems like it’s working fine) is exposing the tags assigned in Twine to the passages in Chapbook. Things of that nature. I’ll definitely share whatever I figure out.

1 Like

Okay, I’ve either discovered something or found a potential bug?

I thought ambient sounds and sound effects were different since they’re set up differently. However, today I accidentally called one of my ambient sound names as a sound effect - and it started playing over the existing ambient sound simultaneously! And then it stopped when it finished like effects do instead of repeating like an ambient sound.

Does this mean any setup sound can be either? A repeating ambient sound or a one-off sound effect depending how it’s called? That’s very useful to know if any existing clip can be used either way.

The one potential thing - I called the second clip at .5 volume as a sound effect with the ambient sound at full. Did that make a difference?

1 Like

What am I doing wrong here? I can’t seem to wrap my head around conditional display. I’m not sure if my boolean is weird or I’m not grokking the way if/else if formatted… can you not add extra carriage returns?

I’m trying to vary the display based on a string the user types in from the previous passage.

correct: skipguess === 'candy show' || skipguess === 'CANDY SHOW' || skipguess === 'Candy Show'
--

[correct]

Correct!

> [[Continue|test]]

[else]

That is incorrect.

[continue]

{restart link, label: 'Back'}

I can see in the vars section that my text input is correctly recorded into the skipguess variable.

Removing || (or statements) limiting to just one string doesn’t help either.

EDIT: It works if I directly check the variable AND INCLUDE THE WORD ‘IF’, but I can’t seem to set a true/false new variable if “this || this || this” is true…

1 Like

Could you post what you’re trying to write? This works for me:

color: 'crimson'
colorIsRed: color === 'pink' || color === 'crimson' || color === 'rose'
--
Is {color} red? {colorIsRed}

And yes, a conditional modifier always has to start with if. You can’t write just the variable or condition.

2 Likes

Thanks, I think I got it sorted and working well enough. I’ve just experimented with so many systems and different flavors of Twine that I’m confusing formatting conventions between them.

I am curious though about my previous question: By accident I discovered I can call an audio clip set up as an ambient sound as a sound effect and have it behave like a sound effect: it plays simultaneously with the already-playing ambient sound and stops after one repetition.

This is actually quite useful to know because I can set up everything as ambient sound and then basically call it as ambient sound to repeat or a sound effect to play once. Is this an undocumented feature, a bug, or just the way web sound works? (When I used AXMA, ambient sound/“music” was just played from the file in the folder, but sound effects actually got written into the code (in a media passage as a giant block of gibberish) so they could play without pre-loading and work simultaneously with a playing clip.)

Is there any reason to set things up as a sound effect specifically? I remember the guide saying it needs to preload somehow, and how an author might need to preload(?) the same sound twice with two different names to play in rapid succession. (Sorry if this is fuzzy and I’m not explaining well…)

I know there’s that section where you can manipulate music on and off with truth-statements for better control, like multiple ambient sounds at once.

It’s not hurting anything if it’s consistent and is definitely an awesome feature.

1 Like

The only real difference in how sound effects and ambient sound are loaded is that sound effects get loaded with a preload="auto" attribute on their <audio> element. I’ll point to MDN docs for details, but the effect should be that sound effects get loaded when stories begin, vs. ambients begin loading when they first begin playing. But the preload attribute is just a hint to browsers, so it might disregard this.

The goal is that if you have a sound effect play when a passage appears, the preload makes it so that the effect plays right when the passage appears, instead of having a slight delay while the browser loads the asset. I imagined most ambient sounds will have a bigger file size, so preloading them wouldn’t be ideal.

The fact that you can play ambient sound as a sound effect wasn’t intentional, so I guess it could go away in a future version, but I don’t see any harm in it.

Hope this helps clarify things!

2 Likes