<center><h1>Twine 2 / SugarCube 2 Sample Code</h1></center> - ''by HiEv''<span style="float:right;">(<span title="Click link to see changelog">''[[Last update:|Last Update]]'' Sept. 4th, '26</span>)</span>
Here's an ever-expanding collection of sample code, tips, and tricks for use with <a href="http://twinery.org/" target="_blank">Twine 2</a> and the <a href="http://www.motoslave.net/sugarcube/2/" target="_blank">SugarCube 2</a> story format.
Please note that some of these are quick code dumps from things I've written for myself or others, so you may need to download this HTML file and import it into Twine 2 to see the full code. (See the bottom of this page for details on how to do that.)
<center>Click a category below to see links to various information on that topic.</center>
<<nobr>>
<<set _noUnread = true>>
<div class="accordion" style="fill: white">
<h3 class="text">Text Display: (<<= setup.passages.text.length>> items)<span class="right"><<catInfo "text">></span></h3>
<div><ul>
<<showLinks "text">>
</ul></div>
<h3 class="link">Link Elements: (<<= setup.passages.link.length>> items)<span class="right"><<catInfo "link">></span></h3>
<div><ul>
<<showLinks "link">>
</ul></div>
<h3 class="image">Image Display: (<<= setup.passages.image.length>> items)<span class="right"><<catInfo "image">></span></h3>
<div><ul>
<<showLinks "image">>
</ul></div>
<h3 class="audio">Audio and Video: (<<= setup.passages.audio.length>> items)<span class="right"><<catInfo "audio">></span></h3>
<div><ul>
<<showLinks "audio">>
</ul></div>
<h3 class="interactive">Other Interactive Elements: (<<= setup.passages.interactive.length>> items)<span class="right"><<catInfo "interactive">></span></h3>
<div><ul>
<<showLinks "interactive">>
</ul></div>
<h3 class="data">Data Handling: (<<= setup.passages.data.length>> items)<span class="right"><<catInfo "data">></span></h3>
<div><ul>
<<showLinks "data">>
</ul></div>
<h3 class="coding">Coding Tips and Tricks: (<<= setup.passages.coding.length>> items)<span class="right"><<catInfo "coding">></span></h3>
<div><ul>
<<showLinks "coding">>
</ul></div>
</div>
<<if setup.passages && !_noUnread>>
<<button "Clear all 'Unread', 'Updated', and 'New' indicators" "Main Menu">>
<<for _cat, _dummy range setup.passages>>
<<for _obj range setup.passages[_cat]>>
<<run delete _obj.unread>>
<<run delete _obj.updated>>
<<run delete _obj.new>>
<<run memorize("passageStatus", setup.passages)>>
<</for>>
<</for>>
<</button>><br>
<</if>>
<</nobr>>
You can download this HTML and all of the files you need from here: <a href="https://hiev-heavy-ind.com/Sample_Code/Twine_Sample_Code.zip" target="_blank">Twine_Sample_Code.zip</a> (alternately <a href="https://drive.google.com/file/d/1bK6-QZOf2i8cdV710iiaDvPwooIQxV88/view?usp=sharing" target="_blank">get it from here</a> by clicking the "download" icon in the upper-right corner of that page). You should also make sure you have <a href="http://www.motoslave.net/sugarcube/2/#downloads" target="_blank">the latest version of SugarCube v2</a> installed.
Once you do that, you can import the HTML file into Twine 2 to take a look at the code. If you do that and want to be able to launch it from the Twine editor, you'll also need to change "{{{D:/Games/Twine_Sample_Code/}}}" in the JavaScript section to the directory where your extracted the files on your machine (and be sure to use forward slashes "/", instead of the backslashes "\" that Windows prefers).
You're free to use or modify any of this code for use in your own projects. If you'd like to include a shout out to me, HiEv, in the credits of your project, that would be nice, but it's not required.
<span class="dumb_terminal"> </span><<run forget("countdown")>>\
<<done>>
<<set _expand = setup.accordion ?? false>>
<<run $(".accordion").accordion({ activate: function (event, ui) { setup.accordion = $(".accordion").accordion("option", "active"); } })>>
<<if _expand !== false>>
<<run $(".accordion").accordion("option", "active", _expand)>>
<</if>>
<</done>>!Repeating background image
[[Single background image|Background 2]]
[[Linear gradient background|Background 3]]
[[Radial gradient background|Background 4]]
This passage has a custom background by using the "{{{repeating-background}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags) along with the following CSS in the Stylesheet section:
{{{
/* Make the body >= 100% the height of the page, so that backgrounds which fill the whole screen look as expected. */
html, body {
position: absolute;
min-width: 100%;
min-height: 100vh;
}
/* A repeating background image (e.g. wallpaper) applied to any passage with a tag of "repeating-background". */
body.repeating-background {
background-image: url("images/Pattern_SwirlPattern.png"), url("file:///D:/Games/Twine_Sample_Code/images/Pattern_SwirlPattern.png");
background-attachment: fixed;
background-repeat: repeat;
}
}}}
This tries to use {{{images/Pattern_SwirlPattern.png}}} as the background image, but if that fails, then it tries to use the file in {{{D:/Games/Twine_Sample_Code/images/Pattern_SwirlPattern.png}}}. You can change that second path to be the one on your device so that the will be displayed properly when launched from the Twine editor.
If you'd like to have this background applied to //all// passages, simply remove {{{.repeating-background}}} from the above CSS.
For more information on backgrounds, see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background" target="_blank">the MDN's documentation on the CSS "background" property</a>.
Additionally, to make sure that the text in your passages are legible with the background, you'll probably need to add something like this:
{{{
/* Text area styling */
#passages {
/* A white outline around the text area */
border: 5px solid white;
/* A semi-transparent black background color */
background-color: rgba(0 0 0 / 0.85);
/* Space between the edges of the text area and the text */
padding: 20px 50px 40px 50px;
/* Round off the edges of the text area */
border-radius: 5px;
}
}}}
The first three value in the {{{rgba()}}} function are the red, green, and blue color values for the text area background, and the last is the opacity, where {{{1}}} is totally opaque and {{{0}}} is totally transparent.
You can change the above CSS however you'd like to best suit your game.
To see what the passage would look like without the text area styling: <<button "Toggle Styling">><<toggleclass "#passages" "default">><</button>>
The text below is just filler to show how the background stays in place when scrolling due to the {{{background-attachment: fixed;}}} setting.
<<include "Lorem ipsum">>
!Single background image
[[Repeating background image|Backgrounds]]
[[Linear gradient background|Background 3]]
[[Radial gradient background|Background 4]]
This passage has a custom background by using the "{{{single-background}}}" tag along with the following CSS in the Stylesheet section:
{{{
/* Make the body >= 100% the height of the page, so that backgrounds which fill the whole screen look as expected. */
html, body {
position: absolute;
min-width: 100%;
min-height: 100vh;
}
/* A single image, centered and stretched to cover the page, applied to any passage with a tag of "single-background". */
body.single-background {
background-image: url("images/space-1.jpg"), url("file:///D:/Games/Twine_Sample_Code/images/space-1.jpg");
background-attachment: fixed;
background-position-x: center;
background-position-y: center;
background-repeat: no-repeat;
background-size: cover;
}
}}}
This tries to use {{{images/space-1.jpg}}} as the background image, but if that fails, then it tries to use the file in {{{D:/Games/Twine_Sample_Code/images/space-1.jpg}}}. You can change that second path to be the one on your device so that the will be displayed properly when launched from the Twine editor.
If you'd like to have this background applied to //all// passages, simply remove {{{.single-background}}} from the above CSS.
Additionally, to make sure that the text in your passages are legible with the background, you'll probably need to add something like this:
{{{
/* Text area styling */
#passages {
/* A white outline around the text area */
border: 5px solid white;
/* A semi-transparent black background color */
background-color: rgba(0 0 0 / 0.85);
/* Space between the edges of the text area and the text */
padding: 20px 50px 40px 50px;
/* Round off the edges of the text area */
border-radius: 5px;
}
}}}
The first three value in the {{{rgba()}}} function are the red, green, and blue color values for the text area background, and the last is the opacity, where {{{1}}} is totally opaque and {{{0}}} is totally transparent.
You can change the above CSS however you'd like to best suit your game.
To see what the passage would look like without the text area styling: <<button "Toggle Styling">><<toggleclass "#passages" "default">><</button>>
The text below is just filler to show how the background stays in place when scrolling due to the {{{background-attachment: fixed;}}} setting.
<<include "Lorem ipsum">>
!Linear gradient background
[[Repeating background image|Backgrounds]]
[[Single background image|Background 2]]
[[Radial gradient background|Background 4]]
This passage has a custom background by using the "{{{repeating-background}}}" tag along with the following CSS in the Stylesheet section:
{{{
/* A linear gradient of color, applied to any passage with a tag of "linear-gradient". */
body.linear-gradient {
background: fixed linear-gradient(135deg, red, blue);
}
}}}
If you'd like a different color range, you can replace the two colors given there. You can also change the degrees to modify the angle of the gradient. There are also sites which can help you generate more complicated gradients, such as the <a href="https://www.colorzilla.com/gradient-editor/" target="_blank">Colorzilla Gradient Generator</a>.
If you'd like to have this background applied to //all// passages, simply remove {{{.linear-gradient}}} from the above CSS.
For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/gradient/linear-gradient" target="_blank">the MDN's documentation on linear-gradient()</a>.
Additionally, to make sure that the text in your passages are legible with the background, you'll probably need to add something like this:
{{{
/* Text area styling */
#passages {
/* A white outline around the text area */
border: 5px solid white;
/* A semi-transparent black background color */
background-color: rgba(0 0 0 / 0.85);
/* Space between the edges of the text area and the text */
padding: 20px 50px 40px 50px;
/* Round off the edges of the text area */
border-radius: 5px;
}
}}}
The first three value in the {{{rgba()}}} function are the red, green, and blue color values for the text area background, and the last is the opacity, where {{{1}}} is totally opaque and {{{0}}} is totally transparent.
You can change the above CSS however you'd like to best suit your game.
To see what the passage would look like without the text area styling: <<button "Toggle Styling">><<toggleclass "#passages" "default">><</button>>
The text below is just filler to show how the background stays in place when scrolling due to the {{{background-attachment: fixed;}}} setting.
<<include "Lorem ipsum">>
!Radial gradient background
[[Repeating background image|Backgrounds]]
[[Single background image|Background 2]]
[[Linear gradient background|Background 3]]
This passage has a custom background by using the "{{{radial-background}}}" tag along with the following CSS in the Stylesheet section:
{{{
/* Make the body >= 100% the height of the page, so that backgrounds which fill the whole screen look as expected. */
html, body {
position: absolute;
min-width: 100%;
min-height: 100vh;
}
/* A radial gradient of color, applied to any passage with a tag of "radial-gradient". */
body.radial-gradient {
background: fixed radial-gradient(circle, #fcb900, #ff2b2b);
}
}}}
If you'd like a different color range, you can replace the two colors given there. There are also sites which can help you generate more complicated gradients, such as the <a href="https://www.colorzilla.com/gradient-editor/" target="_blank">Colorzilla Gradient Generator</a>.
If you'd like to have this background applied to //all// passages, simply remove {{{.radial-gradient}}} from the above CSS.
For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/gradient/radial-gradient" target="_blank">the MDN's documentation on radial-gradient()</a>.
Additionally, to make sure that the text in your passages are legible with the background, you'll probably need to add something like this:
{{{
/* Text area styling */
#passages {
/* A white outline around the text area */
border: 5px solid white;
/* A semi-transparent black background color */
background-color: rgba(0 0 0 / 0.85);
/* Space between the edges of the text area and the text */
padding: 20px 50px 40px 50px;
/* Round off the edges of the text area */
border-radius: 5px;
}
}}}
The first three value in the {{{rgba()}}} function are the red, green, and blue color values for the text area background, and the last is the opacity, where {{{1}}} is totally opaque and {{{0}}} is totally transparent.
You can change the above CSS however you'd like to best suit your game.
To see what the passage would look like without the text area styling: <<button "Toggle Styling">><<toggleclass "#passages" "default">><</button>>
The text below is just filler to show how the background stays in place when scrolling due to the {{{background-attachment: fixed;}}} setting.
<<include "Lorem ipsum">>
<<set $MaxHP = 50>>
<<set $CurHP = 35>>
<<set setup.HideHP = true>> /* This is just used to hide the HP bar by default here. You don't normally need this. */
<<set _bgm = setup.SoundPath + "Peach Girl OST - 15 Oyasumi Romantic Night.mp3">>
<<cacheaudio "night_bgm" _bgm>>
<<createaudiogroup ":ui">>
<<track "night_bgm">>
<</createaudiogroup>>
<<set setup.levels = [Number.NEGATIVE_INFINITY, 100, 200, 300, 500, 800, Infinity]>>
<<set $name = "Anne">>
<<set $text = "some text">>
<<set $pie = "cherry">><<if passage() != "Main Menu">><div class="mmlink">[[🌟 Go to Main Menu 🌟|Main Menu]]</div><</if>>
Volume: <<volume>><div id="verticalhealthbarbkg" class="vertbarbkg"><div id="verticalhealthbar" class="vertbar"></div></div><input type="checkbox" id="fullscreen" tabindex="0"><label for="fullscreen" class="gofullscreen"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m82 324v87c0 11 8 19 19 19h88c15 0 22-17 11-29l-27-28 83-83 83 83-27 28c-11 11-4 29 11 29h88c11 0 19-8 19-19v-87c0-15-17-23-29-12l-28 27-83-83 83-83 28 27c11 11 29 3 29-12v-87c0-11-8-19-19-19h-88c-15 0-22 17-11 29l27 28-83 83-83-83 27-28c11-11 4-29-11-29h-88c-11 0-19 8-19 19v87c0 15 17 23 29 12l28-27 83 83-83 83-28-27c-12-11-29-4-29 12zm374 188h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z' fill='%23fff' /%3E%3C/svg%3E" alt="Go full screen" title="Go full screen" class="fullscreenImg nohide"></label><label for="fullscreen" class="exitfullscreen"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m65 99 83 83-27 28c-11 11-4 29 11 29h88c11 0 19-8 19-19v-87c0-15-17-23-29-12l-28 27-83-83zm117 265 28 27c11 11 29 3 29-12v-87c0-11-8-19-19-19h-88c-15 0-22 17-11 29l27 28-83 83 34 34zm265 49-83-83 27-28c11-11 4-29-11-29h-88c-11 0-19 8-19 19v87c0 15 17 23 29 12l28-27 83 83zm-117-265-28-27c-12-11-29-4-29 12v87c0 11 8 19 19 19h88c15 0 22-17 11-29l-27-28 83-83-34-34zm126 364h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-17 0-20 10-20 20v390c0 16 10 20 20 20h390c8 0 20-4 20-20v-390c0-18-12-20-20-20z' fill='%23fff' /%3E%3C/svg%3E" alt="Exit full screen" title="Exit full screen" class="fullscreenImg nohide"></label><img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" opacity="0.5" fill="white" /></svg>' alt="Larger font" title="Larger font" class="fontSizeImg" style="top: 70px;" onclick="fontSize(1)" tabindex="0"> <img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" opacity="0.5" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" fill="white" /></svg>' alt="Smaller font" title="Smaller font" class="fontSizeImg" style="top: 100px;" onclick="fontSize(-1)" tabindex="0">
<<nobr>>''Images:'' <div @class="'toggle-wrapper' + (settings.images ? '' : ' pushed')">
<div class="rect_2"></div>
<div class="rect_1">
<div class="rect_1_inset"></div>
Off On
</div>
<div class="rect_3"></div>
<div class="toggle_handler">
<div class="toggle_ellipse"></div>
</div>
</div><</nobr>>
<<if passage() == "Main Menu">>If you'd like to support
or thank me:
<a href="https://www.patreon.com/HiEv" target="_blank"><img @src="setup.ImagePath+'Patreon-donate.png'" style="padding-top: 5px;" class="nohide"></a>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EA6ATKEY5463A&source=url" target="_blank"><img @src="setup.ImagePath+'PayPal-donate.png'" class="nohide"></a><</if>>\
<<done>>
<<if passage() != "Main Menu" && setup.JSLoaded && setup.mminit != true>>
<<run $(".mmlink").effect("highlight", 1000).effect("highlight", 1000).effect("highlight", 1000)>> /* Uses jQuery UI for the .effect(). */
<<set setup.mminit = true>>
<</if>>
<</done>><h1>Settings Example</h1>If you want to have settings persist across all games and saves, you can use the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api" target="_blank">Setting API</a> and the <a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api-object-settings" target="_blank">''settings'' object</a> to do that. This section shows two examples of how to create those settings and how to use the values of those settings in your code, as well as an example of how to fix a "broken" setting. (For additional examples of how to define settings, see the "[[Day and Night Mode Setting]]", "[[Image Toggle]]", and "[[Music]]" sections.)
''For example:'' <<timed settings.textDelay t8n>>The appearance of this text was delayed by <<= settings.textDelay>>, based on the settings.<</timed>>
The Twine passage code for the above line is this:
{{{
''For example:'' <<timed settings.textDelay t8n>>The appearance of this text was delayed by <<= settings.textDelay>>, based on the settings.<</timed>>
}}}
The player can change the settings which affect the delay in the {{{<<timed>>}}} macro by clicking the "<<scicon cog>> Settings" button in the UI bar. Try changing the text delay setting yourself and then [[reload this passage|Settings]] to see the new display delay. (''Note:'' The {{{<<=>>}}} macro in the above code is just a shorthand method for calling the {{{<<print>>}}} macro.)
To add a setting to the "Settings" window to change the delayed text display speed to your own game, you'll need code like the following in your game's JavaScript section:
{{{
Setting.addList("textDelay", {
label : "Delayed text speed in seconds:",
list : ["0.04s", "0.5s", "1s", "2s", "3s"],
default : "2s"
});
}}}
That code adds the "{{{textDelay}}}" setting with a "{{{default}}}" value of two seconds. The user can then choose any of the five options given in the "{{{list}}}" array from a dropdown list in the settings. (''Note:'' 40ms/0.04s is the shortest delay allowed for the <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-timed" target="_blank">{{{<<timed>>}}} macro</a>). This code is what sets the value of "{{{settings.textDelay}}}", which was used in the above Twine passage code. See the "<a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api-method-addlist" target="_blank">Setting.addList() method</a>" documentation for details on that method.
----
This next example shows you a possible way to have a setting which mutes/unmutes the audio of any videos. (''Note:'' If you're playing this using the offline Twine editor v2.2.1 or earlier, the video below will only work in the published HTML file, since the Twine editor can't play videos.)
Here's an example of how that would work (see below for the code used here):
Muted = <span id="mute"><<= settings.videoMute>></span>
<<set _vid = setup.ImagePath + "MoireTunnel15_CH3D_512kb_AAC.mp4">>Path = _vid
(video top)
<<if settings.videoMute>>
\<video @src="_vid" autoplay loop controls muted height="240px"></video>
\<<else>>
\<video @src="_vid" autoplay loop controls height="240px"></video>
\<</if>>
(video bottom)
The above video has sound, but by default it is muted. You can use the "<<scicon cog>> Settings" button in the UI bar to unmute it. The JavaScript section code for that setting looks like this:
{{{
function videoMuteHandler() { // (Un)Mute the videos.
$("video").prop("muted", settings.videoMute);
}
Setting.addToggle("videoMute", {
label : "Video mute:",
default : false,
onInit : videoMuteHandler,
onChange : videoMuteHandler
});
}}}
The "Video mute" toggle also triggers the {{{videoMuteHandler()}}} function (which mutes or unmutes videos that are currently visible) when the program first starts (due to the {{{onInit}}}) and also whenever the "Video Mute" toggle is changed (due to the {{{onChange}}}). If you want changing a setting to take effect immediately, then you'll need to create your own "handler" function of some sort to make whatever changes are necessary to have the new setting take effect immediately (such as how the {{{videoMuteHandler()}}} function does it). Otherwise the player will likely only see any changes to the settings after the next passage transition. See the "<a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api-method-addtoggle" target="_blank">Setting.addToggle() method</a>" documentation for details on that method.
Within this passage, the code checks the value of {{{settings.videoMute}}} to determine whether or not to play the video muted like this:
{{{
Muted = <span id="mute"><<= settings.videoMute>></span>
<<set _vid = setup.ImagePath + "MoireTunnel15_CH3D_512kb_AAC.mp4">>Path = _vid
(video top)
<<if settings.videoMute>>
\<video @src="_vid" autoplay loop controls muted height="240px"></video>
\<<else>>
\<video @src="_vid" autoplay loop controls height="240px"></video>
\<</if>>
(video bottom)
}}}
That code will display one of two possible {{{<video>}}} elements, depending on the "{{{videoMute}}}" setting's value, with the only difference between those elements being the existence of the "{{{muted}}}" attribute. (See the "<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video" target="_blank">{{{<video>}}} element</a>" documentation for more details on that.)
The {{{videoMuteHandler()}}} function, when triggered by changing the setting, changes whether or not any currently existing {{{<video>}}} elements have that "{{{muted}}}" attribute, based on the setting. That's how this setting affects things immediately.
----
Finally, if you have a "broken" setting, that is to say, a value which might be in a player's settings which was valid in an older version of your game, but no longer works in the newer version of your game, you can fix that broken setting using code in your JavaScript section which will look something like this:
{{{
$(document).one(':storyready', function (ev) { // Fix old broken settings.
if (settings.textDelay == "No delay") { // If an incorrect setting value is found...
Setting.setValue("textDelay", "0.04s"); //...update it to the valid equivalent value.
}
});
}}}
That code waits for the <a href="https://www.motoslave.net/sugarcube/2/docs/#events-system-event-storyready" target="_blank">"{{{:storyready}}}" event</a>, and then checks to see if the given setting has the old "broken" value, and if it does, then it updates that setting to a new valid value. The reason why you have to do it that way is that the setting will initially be undefined when the JavaScript code is first run, but it will be loaded by the time the "{{{:storyready}}}" event is triggered.
<<silently>>
<<repeat 0.5s>>
<<replace "#mute">><<= settings.videoMute>><</replace>>
<</repeat>>
<</silently>><h1>Music Example</h1><<audio "night_bgm" volume 0.05 play loop>>
<<button "Stop music">>
<<masteraudio stop>>
<</button>>
This passage:
{{{
<<audio "night_bgm" volume 0.05 play loop>>
}}}
StoryInit passage:
{{{
<<set _bgm = setup.SoundPath + "Peach Girl OST - 15 Oyasumi Romantic Night.mp3">>
<<cacheaudio "night_bgm" _bgm>>
<<createaudiogroup ":ui">>
<<track "night_bgm">>
<</createaudiogroup>>
}}}
StoryCaption passage:
{{{
Volume: <<volume>>
}}}
Stylesheet section:
{{{
/* Used to align label for volume slider */
input[type=range] {
vertical-align: middle;
}
}}}
JavaScript section: <span id="newcode">(for SugarCube >= v2.28.0) <<button "See <= v2.27.0 code">><<toggleclass "#newcode" "hidden">><<toggleclass "#oldcode" "hidden">><<run $("#oldcode button").focus()>><</button>>
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
// Volume Slider, by Chapel; for SugarCube 2
// version 1.2.1 (modified by HiEv)
// For custom CSS for slider use: http://danielstern.ca/range.css/#/
/*
Changelog:
v1.2.1:
- Changed "var" to "let".
v1.2.0:
- Fixed using/storing the current volume level in the settings.
v1.1.0:
- Fixed compatibility issues with SugarCube version 2.28 (still
compatible with older versions, too).
- Added settings API integration for SugarCube 2.26.
- Internal improvements and greater style consistency with my
other work.
- Added a pre-minified version.
- By default, the slider is now more granular than before
(101 possible positions vs 11). Change the 'current' and
'rangeMax' options to 10 to restore the old feel.
*/
(function () {
// Set initial values.
let options = {
current : 50, // Default volume level.
rangeMax : 100,
step : 1,
setting : true
};
Setting.load();
if (options.setting && settings.volume) {
options.current = parseInt(settings.volume);
}
let vol = {
last: options.current,
start: (options.current / options.rangeMax).toFixed(2)
};
// Function to update the volume level.
function setVolume (val) {
if (typeof val !== 'number') val = Number(val);
if (Number.isNaN(val) || val < 0) val = 0;
if (val > 1) val = 1;
options.current = Math.round(val * options.rangeMax);
if (options.setting) {
settings.volume = options.current;
Setting.save();
}
if ($('input[name=volume]').val() != options.current) {
$('input[name=volume]').val(options.current);
}
try {
if (SimpleAudio) {
if (typeof SimpleAudio.volume === 'function') {
SimpleAudio.volume(val);
} else {
SimpleAudio.volume = val;
}
return val;
} else {
throw new Error('Cannot access audio API.');
}
} catch (err) {
// Fall back to the wikifier if we have to.
console.error(err.message, err);
$.wiki('<<masteraudio volume ' + val + '>>');
return val;
}
}
// Fix the initial volume level display.
postdisplay['volume-task'] = function (taskName) {
delete postdisplay[taskName];
setVolume(vol.start);
};
// Grab volume level changes from the volume slider.
$(document).on('input', 'input[name=volume]', function() {
let change = parseInt($('input[name=volume]').val());
setVolume(change / options.rangeMax);
vol.last = change;
});
// Create the <<volume>> macro.
Macro.add('volume', {
handler : function () {
let wrapper = $(document.createElement('span'));
let slider = $(document.createElement('input'));
let className = 'macro-' + this.name;
slider.attr({
id : 'volume-control',
type : 'range',
name : 'volume',
min : '0',
max : options.rangeMax,
step : options.step,
value : options.current
});
// Class '.macro-volume' and ID '#volume-control' for styling the slider
wrapper.append(slider).addClass(className).appendTo(this.output);
}
});
// Add Setting API integration for SugarCube 2.26 and higher.
function updateVolume () {
setVolume(settings.volume / options.rangeMax);
}
if (options.setting) {
if (Setting && Setting.addRange && typeof Setting.addRange === 'function') {
Setting.addRange('volume', {
label : 'Volume: ',
min : 0,
max : options.rangeMax,
step : options.step,
default : options.current,
onInit : updateVolume,
onChange : updateVolume
});
} else {
console.error('This version of SugarCube does not include the `Settings.addRange()` method; please try updating to the latest version of SugarCube.');
}
}
}());
}}}
</span><span id="oldcode" class="hidden">(for SugarCube <= v2.27.0) <<button "See >= v2.28.0 code">><<toggleclass "#newcode" "hidden">><<toggleclass "#oldcode" "hidden">><<run $("#newcode button").focus()>><</button>>
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
// volume slider, by chapel; for SugarCube up to v2.27.0
// version 1.0
// For custom CSS for slider use: http://danielstern.ca/range.css/#/
// create namespace
setup.vol = {};
// options object
setup.vol.options = {
current : 5,
rangeMax : 10,
step : 1
};
setup.vol.last = setup.vol.options.current;
setup.vol.start = setup.vol.last / setup.vol.options.rangeMax;
postdisplay['volume-task'] = function (taskName) {
delete postdisplay[taskName];
SimpleAudio.volume = setup.vol.start;
};
!function () {
$(document).on('input', 'input[name=volume]', function() {
// grab new volume from input
let volRef = setup.vol.options;
let change = $('input[name=volume]').val();
let newVol = change / volRef.rangeMax;
volRef.current = newVol.toFixed(2);
// change volume; set slider position
SimpleAudio.volume = volRef.current;
setup.vol.last = change;
});
}();
Macro.add('volume', {
handler : function () {
// set up variables
let $wrapper = $(document.createElement('span'));
let $slider = $(document.createElement('input'));
let className = 'macro-' + this.name;
let volRef = setup.vol.options;
// create range input
$slider
.attr({
id : 'volume-control',
type : 'range',
name : 'volume',
min : '0',
max : volRef.rangeMax,
step : volRef.step,
value : setup.vol.last
}).css('max-width', '154px');
// class '.macro-volume' and id '#volume-control' for styling
// output
$wrapper
.append($slider)
.addClass(className)
.appendTo(this.output);
}
});
}}}
</span>
<<include PathNote>>
--
(Note to self: Add below text properly.)
Audio can be a bit tricky to get a handle on, since there are a couple of ways things can go wrong.
First, you want to make sure your code can find the audio files properly. Putting this bit of code at the beginning of your JavaScript section should help:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
}}}
<<include PathNote>>
This also assumes your audio is in a sub-directory called "''sounds''", so you'll need to change that if it's not.
Next, you'll want to cache any audio you plan to play, so you'll need something like this in your ''StoryInit'' passage:
{{{
<<set _bgm = setup.SoundPath + "StartMusic.mp3">>
<<cacheaudio "start_bgm" _bgm>>
}}}
Just repeat those two lines for each audio file, only changing the "''StartMusic.mp3''" to the appropriate filename and "''start_bgm''" to the ''trackID'' you want to use for that file.
Speaking of filenames, not all browsers support all audio formats. Generally speaking the <a href="https://caniuse.com/#search=audio%20format" target="_blank">MP3 format has the widest support</a>, so I'd recommend going with that.
Also, some browsers, mainly Chrome, do not allow you to play audio until the user has interacted with the page, so you may want to either have the user click something to "opt-in" to playing the music, or only start it in the next passage they go to.
Which brings us back to playing the audio. When doing that you probably should stop any other audio first, and then try to play the one you want using its trackID:
{{{
<<masteraudio stop>><<audio "start_bgm" volume 0.2 play loop>>
}}}
See <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-audio" target="_blank">the SugarCube Audio Macros documentation</a> for details on the parameters passed to that macro.
If you want to be able to tell if a track is already loaded or if it's currently playing, here are two other functions you can add to your JavaScript section:
<span id="newcode2">(for SugarCube >= v2.28.0) <<button "See <= v2.27.0 code">><<toggleclass "#newcode2" "hidden">><<toggleclass "#oldcode2" "hidden">><<run $("#oldcode2 button").focus()>><</button>>
{{{
// Check to see if trackID is currently loaded
window.TrackExists = function (trackID) {
return SimpleAudio.tracks.has(trackID);
};
// Check to see if trackID is the currently playing track
window.isPlaying = function (trackID) {
let track = SimpleAudio.tracks.get(trackID);
return track !== null && track.isPlaying();
};
}}}
</span><span id="oldcode2" class="hidden">(for SugarCube <= v2.27.0) <<button "See >= v2.28.0 code">><<toggleclass "#newcode2" "hidden">><<toggleclass "#oldcode2" "hidden">><<run $("#newcode2 button").focus()>><</button>>
{{{
// Check to see if BGM is currently loaded
window.TrackExists = function (BGM) {
let tracks = Macro.get("cacheaudio").tracks;
if (tracks.hasOwnProperty(BGM)) {
return true;
}
return false;
};
// Check to see if BGM is the currently playing background music
window.isPlaying = function (BGM) {
let tracks = Macro.get("cacheaudio").tracks;
if (tracks.hasOwnProperty(BGM)) {
let currentbgm = tracks[BGM];
if (!currentbgm.audio.paused && currentbgm.audio.duration) {
return true;
}
}
return false;
};
}}}
</span>
Those functions would allow you to create a widget like this:
{{{
/* PlayTrack: Safely plays audio files in track list. */
/* Usage: <<PlayTrack start_bgm>> */
<<widget "PlayTrack">><<nobr>>
<<set _trackList = { /* Add your track IDs and filenames below. */
"start_bgm" : "StartMusic.mp3",
"middle_bgm" : "DramaticMusic.mp3",
"end_bgm" : "EndMusic.mp3"
}>>
<<set _nextTrack = $args.raw>>
<<if def _trackList[_nextTrack]>>
<<if !TrackExists(_nextTrack)>> /* If track doesn't exist, load it. */
<<set _bgm = setup.SoundPath + _trackList[_nextTrack]>>
<<cacheaudio _nextTrack _bgm>>
<</if>>
<<if !TrackExists(_nextTrack)>>
<<run alert('Error: Cannot load file "' + setup.SoundPath + _trackList[_nextTrack] + '".')>>
<<else>>
<<if !isPlaying(_nextTrack)>> /* If track isn't already playing, play it. */
<<masteraudio stop>><<audio _nextTrack volume 0.2 play loop>>
<</if>>
<</if>>
<<else>>
<<run alert('Error: Unknown track "' + _nextTrack + '".')>>
<</if>>
<</nobr>><</widget>>
}}}
Put that code into a non-special non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags). (See <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-widget" target="_blank">the {{{<<widget>>}}} macro documentation</a> for details on how widgets work.)
Whenever you use it, the {{{<<PlayTrack>>}}} widget would load the audio track you gave it if it's not already loaded, and then play it if it's not already playing. Just modify the ''_trackList'' in the widget to have the trackIDs and filenames you want.
Hope that helps! :-)
--
(Note to self: More stuff to add and integrate with above code.)
Code for settings with a "master" volume/mute and a separate "background music" volume. (See the "[[Settings]]" section for more info on how to create settings.)
{{{
// Setting up a mute control for the settings property 'masterMute' w/ callback
function masterMuteHandler() {
SimpleAudio.mute(settings["masterMute"]);
}
Setting.addToggle("masterMute", {
label : "Master Mute.",
desc : "Mute control for all audio.",
onInit : masterMuteHandler,
onChange : masterMuteHandler
}); // default value not defined, so false is used
// Setting up a volume control for the settings property 'masterVolume' w/ callback
function masterVolumeHandler() {
SimpleAudio.volume(settings["masterVolume"] / 10);
}
Setting.addRange("masterVolume", {
label : "Master volume.",
desc : "Volume control for all audio.",
min : 0,
max : 10,
step : 1,
onInit : masterVolumeHandler,
onChange : masterVolumeHandler
}); // default value not defined, so max value (10) is used
// Setting up a volume control for the settings property 'bgmVolume' w/ callback
function bgmVolumeInitHandler() {
try {
bgmVolumeHandler();
}
catch (ex) {
setTimeout(bgmVolumeInitHandler, 50);
}
}
function bgmVolumeHandler() {
SimpleAudio.select(":bgm").volume(settings["bgmVolume"] / 10);
}
Setting.addRange("bgmVolume", {
label : "BGM volume.",
desc : "Volume control for background music.",
min : 0,
max : 10,
step : 1,
onInit : bgmVolumeInitHandler,
onChange : bgmVolumeHandler
}); // default value not defined, so max value (10) is used
}}}<h1>Time Code Example</h1>/*
Rewrite this.
Take a look at: https://www.reddit.com/r/twinegames/comments/1s4j8p7/comment/ocnxs1t/
*/
For more information on the JavaScript {{{Date}}} object, see the MDN documentation on the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date" target="_blank">Date object</a>.
Your local time:<<set $CurDate = new Date(Date.now())>>
<<= $CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
Some other specific time and date:
<<set $CurDate = new Date('August 19, 1975 23:15:30')>>$CurDate
add 15 min:
<<set $CurDate = new Date($CurDate.setMinutes($CurDate.getMinutes() + 15))>>$CurDate
Formatted:
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
add 1 day:<<set $CurDate.setDate($CurDate.getDate() + 1)>>
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
set specific time:<<set $CurDate.setHours(14)>><<set $CurDate.setMinutes(15)>><<set $CurDate.setSeconds(0)>>
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
That code looks like this:
{{{
Your local time:<<set $CurDate = new Date(Date.now())>>
<<= $CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
Some other specific time and date:
<<set $CurDate = new Date('August 19, 1975 23:15:30')>>$CurDate
add 15 min:
<<set $CurDate = new Date($CurDate.setMinutes($CurDate.getMinutes() + 15))>>$CurDate
Formatted:
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
add 1 day:<<set $CurDate.setDate($CurDate.getDate() + 1)>>
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
set specific time:<<set $CurDate.setHours(14)>><<set $CurDate.setMinutes(15)>><<set $CurDate.setSeconds(0)>>
<<=$CurDate.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )>>
}}}
----
<<nobr>>
<<set $CurDate = new Date('June 14, 2018 15:30')>>
Current date/time: <<print getDayName($CurDate)>>, $CurDate<br>
<<set $Appointment = clone($CurDate)>>
<<set $Appointment.setDate($Appointment.getDate() + 1)>>
<<set $Appointment.setHours(18, 0, 0, 0)>>
Raw Appointment date/time: <<print getDayName($Appointment)>>, $Appointment<br>
<<set _timeOptions = { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' }>>
Formatted Appointment date/time: <<print $Appointment.toLocaleString("en-US", _timeOptions)>><br>
Appointment within the next 30 min? -
<<if $Appointment - $CurDate >= 0>>
<<if $Appointment - $CurDate < 30*60*1000>>
True
<<else>>
False
<</if>><br>
<<set _Remaining = $Appointment - $CurDate>>
<<set _hours = Math.floor(_Remaining / 3.6e6)>>
<<set _minutes = Math.floor((_Remaining % 3.6e6) / 6e4)>>
<<set _seconds = Math.floor((_Remaining % 6e4) / 1000)>>
Time remaining: _hours hours, _minutes minutes, and _seconds seconds<br>
<<else>>
False - You missed your appointment.<br>
<</if>>
<<set $CurDate = clone($Appointment)>>
<<set $CurDate = new Date($CurDate.setMinutes($CurDate.getMinutes() - 15))>>
New date/time: <<print $CurDate.toLocaleString("en-US", _timeOptions)>><br>
Appointment within the next 30 min? -
<<if $Appointment - $CurDate >= 0>>
<<if $Appointment - $CurDate < 30*60*1000>>
True
<<else>>
False
<</if>><br>
<<set _Remaining = $Appointment - $CurDate>>
<<set _hours = Math.floor(_Remaining / 3.6e6)>>
<<set _minutes = Math.floor((_Remaining % 3.6e6) / 6e4)>>
<<set _seconds = Math.floor((_Remaining % 6e4) / 1000)>>
Time remaining: _hours hours, _minutes minutes, and _seconds seconds<br>
<<else>>
False - You missed your appointment.<br>
<</if>>
<</nobr>>
And that code looks like this:
{{{
<<nobr>>
<<set $CurDate = new Date('June 14, 2018 15:30')>>
Current date/time: <<print getDayName($CurDate)>>, $CurDate<br>
<<set $Appointment = clone($CurDate)>>
<<set $Appointment.setDate($Appointment.getDate() + 1)>>
<<set $Appointment.setHours(18, 0, 0, 0)>>
Raw Appointment date/time: <<print getDayName($Appointment)>>, $Appointment<br>
<<set _timeOptions = { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' }>>
Formatted Appointment date/time: <<print $Appointment.toLocaleString("en-US", _timeOptions)>><br>
Appointment within the next 30 min? -
<<if $Appointment - $CurDate >= 0>>
<<if $Appointment - $CurDate < 30*60*1000>>
True
<<else>>
False
<</if>><br>
<<set _Remaining = $Appointment - $CurDate>>
<<set _hours = Math.floor(_Remaining / 3.6e6)>>
<<set _minutes = Math.floor((_Remaining % 3.6e6) / 6e4)>>
<<set _seconds = Math.floor((_Remaining % 6e4) / 1000)>>
Time remaining: _hours hours, _minutes minutes, and _seconds seconds<br>
<<else>>
False - You missed your appointment.<br>
<</if>>
<<set $CurDate = clone($Appointment)>>
<<set $CurDate = new Date($CurDate.setMinutes($CurDate.getMinutes() - 15))>>
New date/time: <<print $CurDate.toLocaleString("en-US", _timeOptions)>><br>
Appointment within the next 30 min? -
<<if $Appointment - $CurDate >= 0>>
<<if $Appointment - $CurDate < 30*60*1000>>
True
<<else>>
False
<</if>><br>
<<set _Remaining = $Appointment - $CurDate>>
<<set _hours = Math.floor(_Remaining / 3.6e6)>>
<<set _minutes = Math.floor((_Remaining % 3.6e6) / 6e4)>>
<<set _seconds = Math.floor((_Remaining % 6e4) / 1000)>>
Time remaining: _hours hours, _minutes minutes, and _seconds seconds<br>
<<else>>
False - You missed your appointment.<br>
<</if>>
<</nobr>>
}}}
JavaScript section:
{{{
/* Date code - Start */
window.getDayName = function(CurDate) {
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][CurDate.getDay()];
};
/* Date code - End */
}}}
----
If you want to break up the day into segments instead, say for example, morning, afternoon, evening, and night, then just make another variable to track that. For example, you would set things up in your code like this in your {{{StoryInit}}} passage:
{{{
<<set setup.dayPart = ["morning", "afternoon", "evening", "night"]>> /* your day segments */
<<set $gameTime = 0>> /* start in the first day segment, morning */
}}}
(Since the day segments will never change, we set this on the <a href="http://www.motoslave.net/sugarcube/2/docs/#special-variable-setup" target="_blank">setup object</a> so that it won't waste space in the game history.)
Now you can display the day segment using something like this:
{{{
<<=setup.dayPart[$gameTime]>>
}}}
({{{<<=>>}}} is the same thing as {{{<<print>>}}})
To advance the day segment, you would do something like this:
{{{
<<if ++$gameTime >= setup.dayPart.length>>
<<set $gameTime = 0>>
<<set $gameDate.setDate($gameDate.getDate() + 1)>>
<</if>>
}}}
The "++" advances the {{{$gameTime}}} by 1 and then, if it's the end of the day, it goes from the last segment back to the first segment and advances the date.
You could put that code inside of a <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-widget" target="_blank">widget</a> if you want to be able to easily use it to advance the day segment anywhere in your story. (If you //do// create a widget, make sure you tag the widget passage with a {{{widget}}} tag. Also, tagging it with {{{nobr}}} will help prevent the widget displaying unnecessary blank lines, though you'll then need to use {{{<br>}}} to create line breaks.)The listboxes should remember your selections of $$pie ("$pie"), $$class ("$class"), and $$FontName ("$FontName") when you return to the [[Listbox Tricks]] section now.
Also, the multi-select list had the following values selected within $$values: <<= $values.join(", ")>><h1>{{{<<capture>>}}} Example</h1>Honestly, in SugarCube the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-capture" target="_blank">{{{<<capture>>}}} macro</a> is probably one of the hardest macros for most people to fully grasp. Here's a bit of sample code to help explain it:
<<nobr>>
<<set _n = "Initial value">>
<<button "Button 1">>
<<run alert(_n)>>
<</button>><br><br>
<<capture _n>>
<<button "Button 2">>
<<run alert(_n)>>
<</button>><br>
<</capture>>
<<set _n = "Final value">>
<</nobr>>
And here's the code for those two buttons:
{{{
<<nobr>>
<<set _n = "Initial value">>
<<button "Button 1">>
<<run alert(_n)>>
<</button>><br><br>
<<capture _n>>
<<button "Button 2">>
<<run alert(_n)>>
<</button>><br>
<</capture>>
<<set _n = "Final value">>
<</nobr>>
}}}
"Button 1" //did NOT// capture the value of {{{_n}}} when that line of code was initially run, so if you click that button //now,// after all of that code has run, it will display "{{{Final value}}}", because that's what the value of {{{_n}}} is //currently,// due to its value being changed in the the second to last line.
"Button 2" //did// capture the value of {{{_n}}}, so if you click that button, it will display "{{{Initial value}}}", because that's what the value of {{{_n}}} was at the time the button was created, and that code was within the {{{<<capture _n>>}}} macro.
That example shows how the {{{<<capture>>}}} macro is needed in cases where you have a variable which may have a different value at the time the user interacts with that element.
Typically you will need to use {{{<<capture>>}}} if you're generating buttons or links within a {{{<<for>>}}} loop. Here's an example of that:
<<set _value = 0>><<for _i = 1; _i <= 3; _i++>>\
<<capture _i>>\
<<button "Set value to _i">>
<<set _value = _i>>
<<redo "showValue">>
<</button>>
<</capture>>\
<</for>>\
''Current value:'' <<do tag "showValue">>_value<</do>>
The code for the above example is:
{{{
<<set _value = 0>><<for _i = 1; _i <= 3; _i++>>\
<<capture _i>>\
<<button "Set value to _i">>
<<set _value = _i>>
<<redo "showValue">>
<</button>>
<</capture>>\
<</for>>\
''Current value:'' <<do tag "showValue">>_value<</do>>
}}}
As you can see, that example code works as expected because the {{{<<capture _i>>...<</capture>>}}} within the {{{<<for>>}}} loop captures the value of the loop variable, {{{_i}}}, that is used within in the {{{<<button>>}}} macro's code.
However, if we leave out the {{{<<capture>>}}} macro, then that code would work //incorrectly,// like this:
<<for _i = 1; _i <= 3; _i++>>
<<button "Set value to _i">>
<<set _value = _i>>
<<redo "test2">>
<</button>>
<</for>>
''Current value:'' <<do tag "test2">>_value<</do>>
Without the {{{<<capture>>}}}, all of the buttons set the value to "4", because that's what {{{_i}}} is set to after the {{{<<for>>}}} loop is complete, hence why it shows "4" when the user clicks those buttons which are using an "uncaptured" loop variable.
So, in cases where you have something like a loop variable, and the loop creates an element which the player interacts with later on, and that element needs to use the loop variable's value as it was at the time of the element's creation (not its final value), then that would be an example of a variable that you would need to {{{<<capture>>}}}.
Hope that helps!<h1>Table of Links Example</h1>If you'd like to make a test passage which helps you quickly skip to passages for testing, you can use the code here.
The code below produces a table of links to all passages in the story, except those with "Unlisted" or "non-story-passage" tags. The "Main Menu" link is placed first and shown in bold, and the rest are sorted alphabetically.
Here's how the table of links would look:
<<nobr>>
<<set $inv = []>>
/* Get an array of story passage objects. */
<<set _passages = Story.filter(function () { return true; })>>
/* Put the names of all of the passages which don't have "Unlisted" or "non-story-passage" tags into the $inv array. */
<<for _tmp range _passages>>
<<if _tmp.hasOwnProperty("title")>>
/* Skip passages with these tags. */
<<if !tags(_tmp.title).includesAny("Unlisted", "non-story-passage")>>
<<set $inv.push(_tmp.title)>>
<</if>>
<</if>>
<</for>>
/* Sort the array passage names. */
<<set $inv = $inv.sort()>>
/* Make "Main Menu" the first item in the $inv array. Change this to your own start passage name. */
<<if $inv.indexOf("Main Menu") > 0>>
/* Make "Main Menu" the first link */
<<set _tmp = $inv.deleteAt($inv.indexOf("Main Menu"))>>
<<set $inv.unshift(_tmp)>>
<</if>>
<<set _tmp = "">>
<table width="100%">
/* Make three columns in our table. */
<col width="33%"><col width="33%"><col width="33%">
/* Put each element of the array in its own cell in the table. */
<<for _i = 0; _i < $inv.length; _i++>>
<<if _i == 0>>
/* Bold first link */
<<set _tmp += "<td>''[" + "[" + $inv[_i] + "]]''</td>">>
<<else>>
/* Uses "[" + "[" to prevent Twine from creating a link. */
<<set _tmp += "<td>[" + "[" + $inv[_i] + "]]</td>">>
<</if>>
<<if _i % 3 == 2>>
/* Print out a table row. */
<<print "<tr>" + _tmp + "</tr>">>
<<set _tmp = "">>
<</if>>
<</for>>
/* Print any remaining cells. */
<<if _tmp != "">>
<<print "<tr>" + _tmp + "</tr>">>
<</if>>
</table>
<</nobr>>
(''Note:'' Debug mode messes up tables like this one.)
Here's the code you can use for your own "table of links" passage:
{{{
<<nobr>>
<<set $inv = []>>
/* Get an array of story passage objects. */
<<set _passages = Story.filter(function () { return true; })>>
/* Put the names of all of the passages which don't have "Unlisted" or "non-story-passage" tags into the $inv array. */
<<for _tmp range _passages>>
<<if _tmp.hasOwnProperty("title")>>
/* Skip passages with these tags. */
<<if !tags(_tmp.title).includesAny("Unlisted", "non-story-passage")>>
<<set $inv.push(_tmp.title)>>
<</if>>
<</if>>
<</for>>
/* Sort the array passage names. */
<<set $inv = $inv.sort()>>
/* Make "Main Menu" the first item in the $inv array. Change this to your own start passage name. */
<<if $inv.indexOf("Main Menu") > 0>>
<<set _tmp = $inv.deleteAt($inv.indexOf("Main Menu"))>>
<<set $inv.unshift(_tmp)>>
<</if>>
<<set _tmp = "">>
<table width="100%">
/* Make three columns in our table. */
<col width="33%"><col width="33%"><col width="33%">
/* Put each element of the array in its own cell in the table. */
<<for _i = 0; _i < $inv.length; _i++>>
<<if _i == 0>>
/* Bold first link */
<<set _tmp += "<td>''[" + "[" + $inv[_i] + "]]''</td>">>
<<else>>
/* Uses "[" + "[" to prevent Twine from creating a link. */
<<set _tmp += "<td>[" + "[" + $inv[_i] + "]]</td>">>
<</if>>
<<if _i % 3 == 2>>
/* Print out a table row. */
<<print "<tr>" + _tmp + "</tr>">>
<<set _tmp = "">>
<</if>>
<</for>>
/* Print any remaining cells. */
<<if _tmp != "">>
<<print "<tr>" + _tmp + "</tr>">>
<</if>>
</table>
<</nobr>>
}}}
You'll need to change "Main Menu" in the above code to the name of your own "start" passage name if you want that passage to be set as the first link.<h1>Health Bar Example</h1><<set setup.HideHP = false>><<button "Toggle Health Bar Visibility">>
<<set setup.HideHP = $("#verticalhealthbarbkg").css("display") != "none">>
<<if setup.HideHP>>
<<run $("#horizontalhealthbarbkg").hide()>>
<<run $("#verticalhealthbarbkg").hide()>>
<<else>>
<<run $("#horizontalhealthbarbkg").show()>>
<<run $("#verticalhealthbarbkg").show()>>
<</if>>
<</button>>
<div id="horizontalhealthbarbkg" class="hzbarbkg"><div id="horizontalhealthbar" class="hzbar"></div></div><<if setup.HideHP>><<run $("#horizontalhealthbarbkg").toggle()>><</if>><<done>>
<<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>>
<<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>>
<</done>>
Hover the mouse over the health bar to see the exact health amounts.
Click to Change Health: <<button "20%">><<set $CurHP = 10>><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>><<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>><</button>> <<button "50%">><<set $CurHP = 25>><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>><<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>><</button>> <<button "100%">><<set $CurHP = 50>><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>><<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>><</button>> <<button "Random">><<set $CurHP = random(0, $MaxHP)>><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>><<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>><</button>>
To change the current Hit Points ({{{$CurHP}}}) you would just do something like this for a ''vertical'' health bar:
{{{
<<set $CurHP = 10>><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>>
}}}
or this for a ''horizontal'' health bar:
{{{
<<set $CurHP = 10>><<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>>
}}}
''Note:'' The 4th parameter of the {{{Health()}}} function (see below) has to be {{{true}}} for ''horizontal'' health bars, and {{{false}}} for ''vertical'' health bars.
Set your initial values in the {{{StoryInit}}} passage:
{{{
<<set $MaxHP = 50>> /* Maximum Hit Points */
<<set $CurHP = 50>> /* Current Hit Points */
}}}
The code for the {{{Health()}}} function goes in the JavaScript section:
{{{
/* Health Bar code - Start */
window.Health = function (CurHP, MaxHP, BarID, Horizontal, Container) {
if (Container == undefined) {
Container = document;
}
let HP = Math.clamp(parseInt((CurHP / MaxHP) * 100), 0, 100);
let BarElement = $(Container).find("#" + BarID);
if (Horizontal) {
BarElement.css({ width: HP + "%" });
} else {
BarElement.css({ height: HP + "%" });
}
BarElement.attr("title", CurHP + "/" + MaxHP + " HP");
$(Container).find("#" + BarID + "bkg").attr("title", CurHP + "/" + MaxHP + " HP");
};
/* Health Bar code - End */
}}}
For a ''vertical'' health bar in the {{{StoryCaption}}} passage:
{{{
<div id="verticalhealthbarbkg" class="vertbarbkg"><div id="verticalhealthbar" class="vertbar"></div></div><<run Health($CurHP, $MaxHP, "verticalhealthbar", false)>>
}}}
In the UI bar you can simply call the {{{Health()}}} function to have the bar display the current value when you change passages.
For a ''horizontal'' health bar in a normal passage:
{{{
<div id="horizontalhealthbarbkg" class="hzbarbkg"><div id="horizontalhealthbar" class="hzbar"></div></div>
<<done>>
<<run Health($CurHP, $MaxHP, "horizontalhealthbar", true)>>
<</done>>
}}}
In a normal passage, unlike in the UI bar, you need to wait until the passage is going to render (using the above code) to get it to display the health correctly when the passage first appears.
Example Stylesheet section for a ''vertical'' health bar in the UI bar:
{{{
/* Vertical Health Bar - Start */
.vertbarbkg { /* default class for all vertical bar backgrounds */
position: relative;
width: 15px;
height: 100%;
background-color: #111;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to left, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px);
border-radius: 10px;
z-index: 10;
visibility: visible;
}
.vertbar { /* default class for all vertical bars */
position: absolute;
bottom: 0px;
left: 0px;
width: 15px;
border-radius: 10px;
z-index: 20;
transition: height ease 1s;
}
#verticalhealthbarbkg { /* custom positioning for specific vertical bar backgrounds */
--bkg-top: 133px; /* set top here */
position: absolute;
top: 133px; /* IE workaround */
top: var(--bkg-top);
left: 262px;
height: calc(100vh - 155px); /* IE workaround */
height: calc(100vh - 22px - var(--bkg-top));
}
#verticalhealthbar { /* colors for specific vertical bars */
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to right, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), radial-gradient( circle at bottom right, #c00, #c00 60% );
}
/* Vertical Health Bar - End */
}}}
You can just set {{{--bkg-top}}} to where you want the top position of the bar to be, and it will calculate the height automatically.
(''Note:'' Internet Explorer doesn't recognize {{{var()}}} in CSS, so if you plan on supporting IE you'll need to fill in the correct values manually.)
Example Stylesheet section for a ''horizontal'' health bar in a normal passage:
{{{
/* Horizontal Health Bar - Start */
.hzbarbkg { /* default class for all horizontal bar backgrounds */
position: relative;
height: 15px;
width: 100%;
background-color: #111;
background-image: linear-gradient(to right, rgba(68, 68, 68, 0.5), rgba(34, 34, 34, 0.5) 5px), linear-gradient(to bottom, rgba(68, 68, 68, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to left, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px);
border-radius: 10px;
z-index: 10;
visibility: visible;
}
.hzbar { /* default class for all horizontal bars */
position: absolute;
bottom: 0px;
left: 0px;
height: 15px;
background-color: transparent;
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to right, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px);
border-radius: 10px;
z-index: 20;
transition: width ease 1s, background-color ease 1s;
}
#horizontalhealthbar { /* colors for specific fixed-color horizontal bars */
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to right, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), radial-gradient( circle at bottom right, #0c0, #0c0 60% );
}
/* Horizontal Health Bar - End */
}}}
This just assumes that you want the health bar wherever you put the {{{<div>}}} in the passage.
You can modify the CSS for the stylesheets above if you want your health bars to display differently. For example, to change the color of the bars, just change the two "''#0c0''" values to the color value you want in the {{{radial-gradient( circle at bottom right, #0c0, #0c0 60% )}}} part.
If you would like the healthbars to change color based on their value, you can use a slightly modified version of the above code like this:
<div id="hzhealthbarbkg" class="hzbarbkg"><div id="hzhealthbar" class="hzbar"></div></div>
<<done>>
<<run Health2(100, 100, "hzhealthbar", true)>>
<</done>><<button "Test Color Shifting Health Bar">>
<<set $CurHP2 = random(0, 100)>>
<<run Health2($CurHP2, 100, "hzhealthbar", true)>>
<</button>>
The above code:
{{{
<div id="hzhealthbarbkg" class="hzbarbkg"><div id="hzhealthbar" class="hzbar"></div></div>
<<done>>
<<run Health2(100, 100, "hzhealthbar", true)>>
<</done>><<button "Test Color Shifting Health Bar">>
<<set $CurHP2 = random(0, 100)>>
<<run Health2($CurHP2, 100, "hzhealthbar", true)>>
<</button>>
}}}
The JavaScript section code:
{{{
window.Health2 = function (CurHP, MaxHP, BarID, Horizontal, Container) {
if (Container == undefined) {
Container = document;
}
let HP = Math.clamp(parseInt((CurHP / MaxHP) * 100), 0, 100);
let BarElement = $(Container).find("#" + BarID);
if (Horizontal) {
BarElement.css({ width: HP + "%" });
} else {
BarElement.css({ height: HP + "%" });
}
// Hue goes from -20 to 240 = red (hue = 0) -> green -> blue (hue = 240)
let col = "hsl(" + (Math.floor(HP * 2.6) - 20) + ", 100%, 50%)";
BarElement.css("background-color", col);
BarElement.attr("title", CurHP + "/" + MaxHP + " HP");
$(Container).find("#" + BarID + "bkg").attr("title", CurHP + "/" + MaxHP + " HP");
};
}}}
You can calculate your own color range using the <a href="https://www.w3schools.com/colors/colors_hsl.asp" target="_blank">W3Schools.com HSL Calculator</a>.
And the Stylesheet section code:
{{{
/* Horizontal Health Bar - Start */
.hzbarbkg { /* default class for all horizontal bar backgrounds */
position: relative;
height: 15px;
width: 100%;
background-color: #111;
background-image: linear-gradient(to right, rgba(68, 68, 68, 0.5), rgba(34, 34, 34, 0.5) 5px), linear-gradient(to bottom, rgba(68, 68, 68, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to left, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px);
border-radius: 10px;
z-index: 10;
visibility: visible;
}
.hzbar { /* default class for all horizontal bars */
position: absolute;
bottom: 0px;
left: 0px;
height: 15px;
background-color: transparent;
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 5px), linear-gradient(to right, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px), linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 5px);
border-radius: 10px;
z-index: 20;
transition: width ease 1s, background-color ease 1s;
}
/* Horizontal Health Bar - End */
}}}<h1>{{{<<replace>>}}} Macro Sample Code</h1>If, instead of displaying a whole new passage, you just want to update some text in the current passage, you can use the <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-replace" target="_blank">{{{<<replace>>}}} macro</a>. Here's some sample code showing how that can be done, followed by an explanation for how it works.
To illustrate this, there is going to be an HTML element with an ID of "ReplaceMe" which will have a <span style="background-color: #16324b">dark blue background</span> so you can see exactly where that section is:
<<nobr>>
<span id="ReplaceMe">
Do you want to
<<link "check if there's something hidden underneath the commode">>
<<replace "#ReplaceMe">>
You find something hidden!<br>
Oh, no, it's just... eww. Never mind.
<</replace>>
<</link>>
or do you
<<link "believe you've found all that there is">>
<<replace "#ReplaceMe">>
Yeah, probably a good idea to move on.
<</replace>>
<</link>>?
</span>
<</nobr>>
Here's the source code for the above:
{{{
<<nobr>>
<span id="ReplaceMe">
Do you want to
<<link "check if there's something hidden underneath the commode">>
<<replace "#ReplaceMe">>
You find something hidden!<br>
Oh, no, it's just... eww. Never mind.
<</replace>>
<</link>>
or do you
<<link "believe you've found all that there is">>
<<replace "#ReplaceMe">>
Yeah, probably a good idea to move on.
<</replace>>
<</link>>?
</span>
<</nobr>>
}}}
As you can see, the whole section, text, links, and other code, are all within an HTML {{{<span>}}} element with an ID of "ReplaceMe" (i.e. {{{<span id="ReplaceMe">...</span>}}}). When you refer to an element by its ID using a selector, you add a "{{{#}}}" in front of the ID name. When either of the above links are clicked, the {{{<<replace>>}}} macros above use that selector to replace everything within the span which has that ID with the text that's inside of the macro. The span itself is ''not'' removed, it's only that the contents within the span are replaced.
''NOTE:'' HTML element IDs should be unique within the document (the HTML page). If you have two elements with the same ID in the document at the same time, then that is technically invalid HTML. For information on what kinds of names are valid for IDs, see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/ident" target="_blank">the MDN's {{{<ident>}}} documentation</a>.
There are a few similar macros:
''<a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-append" target="_blank">{{{<<append>>}}} macro</a>:''
<<set _a = 0>><<button "Click to add more text">>
<<append "#appendTest">> More text (<<= ++_a>>).<</append>>
<</button>>
<span id="appendTest">Text goes here.</span>
Which uses this code:
{{{
<<set _a = 0>><<button "Click to add more text">>
<<append "#appendTest">> More text (<<= ++_a>>).<</append>>
<</button>>
<span id="appendTest">Text goes here.</span>
}}}
As you can see, instead of replacing the text within the span, it adds new text to the end.
''<a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-prepend" target="_blank">{{{<<prepend>>}}} macro</a>:''
<<set _p = 0>><<button "Click to add more text">>
<<prepend "#prependTest">>More text (<<= ++_p>>). <</prepend>>
<</button>>
<span id="prependTest">Text goes here.</span>
Which uses this code:
{{{
<<set _p = 0>><<button "Click to add more text">>
<<prepend "#prependTest">>More text (<<= ++_p>>). <</prepend>>
<</button>>
<span id="prependTest">Text goes here.</span>
}}}
This works the same as {{{<<append>>}}}, except it adds the new text to the front.
<h1>Alerts and Dialogs Example</h1>You can use JavaScript alerts and SugarCube dialog windows to quickly display information to your players, without leaving the current passage. This page shows how they look and how to code them.
----
This displays a standard JavaScript alert:
<<button "Show Alert">>
<<run alert("Alert text.")>>
<</button>>
{{{
<<button "Show Alert">>
<<run alert("Alert text.")>>
<</button>>
}}}
See the JavaScript "<a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/alert" target="_blank">alert() method</a>" documentation for details.
----
This displays a SugarCube dialog window:
<<button "Show Dialog">>
<<run Dialog.create("Dialog title")>>
<<run Dialog.wiki("Dialog text.")>>
<<run Dialog.open()>>
<</button>>
{{{
<<button "Show Dialog">>
<<run Dialog.create("Dialog title")>>
<<run Dialog.wiki("Dialog text.")>>
<<run Dialog.open()>>
<</button>>
}}}
See the SugarCube "<a href="https://www.motoslave.net/sugarcube/2/docs/#dialog-api" target="_blank">Dialog API</a>" documentation for details.
''NOTE:'' SugarCube versions prior to v2.37.0 used {{{Dialog.setup()}}} instead of {{{Dialog.create()}}}.
----
This displays a SugarCube dialog window, including code to show a textbox in it to enter a name, and displays the name that was entered upon closing:
<<button "Show Input Dialog">>
<<script>>
Dialog.create("Please enter your name:");
Dialog.wiki('<<textbox "$name" "Steve" autofocus>> <<button "Done">><<replace "#name">>Hello there, $name.<</replace>><<run Dialog.close()>><</button>>');
Dialog.open({top: "calc(50vh - 51px)"}, setup.done);
<</script>>
<</button>>
<span id="name"></span>
{{{
<<button "Show Input Dialog">>
<<script>>
Dialog.create("Please enter your name:");
Dialog.wiki('<<textbox "$name" "Steve" autofocus>> <<button "Done">><<replace "#name">>Hello there, $name.<</replace>><<run Dialog.close()>><</button>>');
Dialog.open({top: "calc(50vh - 51px)"}, setup.done);
<</script>>
<</button>>
<span id="name"></span>
}}}
The {{{<span>}}} at the end gives the code a place to display the name that was entered.
Note also that this example uses a {{{<<script>>}}} macro (instead of a {{{<<run>>}}} macro) to open the dialog, showing that this method works from JavaScript as well.
Finally, the {{{Dialog.open()}}} example here shows how you can style the dialog to appear in a different location on the screen. "{{{50vh}}}" means 50% of the view height and "{{{51px}}}" means 51 pixels, so this puts the top of the dialog window at 51 pixels higher on the screen than the center of the screen vertically.<h1>Using objects for a simple inventory</h1><<nobr>>
/* Create an empty inventory. */
<<set $inventory = {}>>
/* Add 5 pieces of iron ore worth 20 gold each to the inventory. */
<<set $inventory.IronOre = { name: "iron ore", quantity: 5, value: 20 }>>
/* Add 5 more. */
<<set $inventory.IronOre.quantity += 5>>
/* Check to see if you have copper ore. */
<<if def $inventory.CopperOre>>
You have $inventory.CopperOre.quantity $inventory.CopperOre.name.<br>
<<else>>
You have no copper ore.<br>
<</if>>
/* Add 1 piece of steel worth 50 gold each to the inventory. */
<<set $inventory.Steel = { name: "steel", quantity: 1, value: 50 }>>
/* Display the inventory. */
<<for _key range $inventory>>
_key.quantity _key.name worth _key.value gold each.<br>
<</for>>
/* Three different ways to show the same value. */
<<set _temp = "IronOre">>
$inventory.IronOre.quantity = $inventory["IronOre"].quantity = $inventory[_temp].quantity<br>
/* Get an array of all inventory keys and display it. */
<<set _keys = Object.keys($inventory)>>_keys
/* Get rid of the steel. */
<<run delete $inventory.Steel>>
<</nobr>>
Here is the Twine code for the above:
{{{
<<nobr>>
/* Create an empty inventory. */
<<set $inventory = {}>>
/* Add 5 pieces of iron ore worth 20 gold each to the inventory. */
<<set $inventory.IronOre = { name: "iron ore", quantity: 5, value: 20 }>>
/* Add 5 more. */
<<set $inventory.IronOre.quantity += 5>>
/* Check to see if you have copper ore. */
<<if def $inventory.CopperOre>>
You have $inventory.CopperOre.quantity $inventory.CopperOre.name.<br>
<<else>>
You have no copper ore.<br>
<</if>>
/* Add 1 piece of steel worth 50 gold each to the inventory. */
<<set $inventory.Steel = { name: "steel", quantity: 1, value: 50 }>>
/* Display the inventory. */
<<for _key range $inventory>>
_key.quantity _key.name worth _key.value gold each.<br>
<</for>>
/* Three different ways to show the same value. */
<<set _temp = "IronOre">>
$inventory.IronOre.quantity = $inventory["IronOre"].quantity = $inventory[_temp].quantity<br>
/* Get an array of all inventory keys and display it. */
<<set _keys = Object.keys($inventory)>>_keys
/* Get rid of the steel. */
<<run delete $inventory.Steel>>
<</nobr>>
}}}
...
Another example:
<<nobr>>
/* Creates an empty inventory. The following line should normally be in the StoryInit passage. */
<<set $inventory = {}>>
/* Add the pistol to the inventory. Do something like this whenever a player acquires an item. */
<<set $inventory.pistol = { quantity: 1, bullets: 5, description: "old fashioned six-shooter pistol" }>>
/* Add spare bullets to the inventory. */
<<set $inventory.bullets = { quantity: 10, description: "standard .44 caliber bullets for your pistol" }>>
/* Show the inventory. */
Your inventory:<br>
<<for _key range $inventory>>
- _key.quantity _key.description<<if def _key.bullets>> (with _key.bullets bullets in it)<</if>>.<br>
<</for>>
/* Show a link if the pistol has bullets. If the link is clicked, remove one bullet and go to the "passage name" passage. */
<<if $inventory.pistol.bullets > 0>>[[Fire the pistol|passage name][$inventory.pistol.bullets -= 1]]<br><</if>>
/* Show a link to allow reloading the pistol if you have bullets in your inventory and your pistol isn't fully loaded. */
<<if def $inventory.bullets>> /* determines if "bullets" is a defined property of $inventory */
<<if ($inventory.bullets.quantity > 0) && ($inventory.pistol.bullets < 6)>>
<<link "Reload your gun" "passage name">> /* shows the link */
<<if $inventory.bullets.quantity > (6 - $inventory.pistol.bullets)>>
<<set $inventory.bullets.quantity -= (6 - $inventory.pistol.bullets)>>
<<set $inventory.pistol.bullets = 6>>
<<else>>
<<set $inventory.pistol.bullets += $inventory.bullets.quantity>>
<<set delete $inventory.bullets>> /* you're out of bullets in your main inventory, so delete this property */
<</if>>
<</link>><br>
<</if>>
<</if>>
<</nobr>>
Here is the Twine code for the above:
{{{
<<nobr>>
/* Creates an empty inventory. The following line should normally be in the StoryInit passage. */
<<set $inventory = {}>>
/* Add the pistol to the inventory. Do something like this whenever a player acquires an item. */
<<set $inventory.pistol = { quantity: 1, bullets: 5, description: "old fashioned six-shooter pistol" }>>
/* Add spare bullets to the inventory. */
<<set $inventory.bullets = { quantity: 10, description: "standard .44 caliber bullets for your pistol" }>>
/* Show the inventory. */
Your inventory:<br>
<<for _key range $inventory>>
- _key.quantity _key.description<<if def _key.bullets>> (with _key.bullets bullets in it)<</if>>.<br>
<</for>>
/* Show a link if the pistol has bullets. If the link is clicked, remove one bullet and go to the "passage name" passage. */
<<if $inventory.pistol.bullets > 0>>[[Fire the pistol|passage name][$inventory.pistol.bullets -= 1]]<br><</if>>
/* Show a link to allow reloading the pistol if you have bullets in your inventory and your pistol isn't fully loaded. */
<<if def $inventory.bullets>> /* determines if "bullets" is a defined property of $inventory */
<<if ($inventory.bullets.quantity > 0) && ($inventory.pistol.bullets < 6)>>
<<link "Reload your gun" "passage name">> /* shows the link */
<<if $inventory.bullets.quantity > (6 - $inventory.pistol.bullets)>>
<<set $inventory.bullets.quantity -= (6 - $inventory.pistol.bullets)>>
<<set $inventory.pistol.bullets = 6>>
<<else>>
<<set $inventory.pistol.bullets += $inventory.bullets.quantity>>
<<set delete $inventory.bullets>> /* you're out of bullets in your main inventory, so delete this property */
<</if>>
<</link>><br>
<</if>>
<</if>>
<</nobr>>
}}}
<h1>Scroll to Top Example</h1>There are a couple of different ways you can make a passage scroll back up to the top. See the buttons at bottom of text for how this can be done and you can test them out as well.
This large chunk of "lorem ipsum" text is just so that the page will be long enough to have a scrollbar, to make it possible to test the scroll-to-top code.
<<include "Lorem ipsum">>
<<button "scroll1">><<set document.documentElement.scrollTop = 0>><</button>>
Code for "scroll1":
{{{
<<button "scroll1">><<set document.documentElement.scrollTop = 0>><</button>>
}}}
<<button "scroll2">><<run window.scrollTo(0, 0)>><</button>>
Code for "scroll2":
{{{
<<button "scroll2">><<run window.scrollTo(0, 0)>><</button>>
}}}
<<button "scroll3">><<run $("#story")[0].scrollIntoView()>><</button>>
Code for "scroll3":
{{{
<<button "scroll3">><<run $("#story")[0].scrollIntoView()>><</button>>
}}}
If you'd like to be able to control scrolling, not just to the top of the page, but to any particular element on the page by its ID, then take a look at the "[[<<ScrollTo>> macro|ScrollTo Macro]]" section, which makes it easy for you to do that.
<h1>"Glitchy" Text Example</h1>"Glitch" uses the depreciated "clip" attribute, which was replaced with the "clip-path" attribute used in "Glitchy". However, while "Glitchy" is technically correct to modern standards, the older "Glitch" CSS works in Internet Explorer and (as far as I'm aware) all other current browsers, while the newer "Glitchy" CSS doesn't work in IE.
Glitch (old):
<span class="glitch" data-text="This is some glitchy text here, let me tell you!">This is some glitchy text here, let me tell you!</span>
Just one <span class="glitch" data-text="glitchy">glitchy</span> word here.
Source for the above example:
{{{
<span class="glitch" data-text="This is some glitchy text here, let me tell you!">This is some glitchy text here, let me tell you!</span>
Just one <span class="glitch" data-text="glitchy">glitchy</span> word here.
}}}
Glitchy (updated):
<span class="glitchy" data-text="This is some glitchy text here, let me tell you!">This is some glitchy text here, let me tell you!</span>
Just one <span class="glitchy" data-text="glitchy">glitchy</span> word here.
Source for the above example:
{{{
<span class="glitchy" data-text="This is some glitchy text here, let me tell you!">This is some glitchy text here, let me tell you!</span>
Just one <span class="glitchy" data-text="glitchy">glitchy</span> word here.
}}}
Also, you can also use these as "<span class="glitchy" data-text="awesome">subliminal</span>" messages if you make the {{{data-text}}} different than the actual text.
To use "glitch" or "glitchy" text in your game, in addition to the above, you'll need to add the following code to your game's "Stylesheet" section:
{{{
/* Glitchy Text - Start */
/* The "clip" in ".glitch" is depreciated, so technically you should use the "clip-path" that's in ".glitchy" instead, however IE doesn't support "clip-path", and most browsers still support "clip". */
.glitch {
color: white;
position: relative;
}
.glitch::before {
content: attr(data-text);
width: calc(100% + 0.5em);
position: absolute;
left: -9px;
text-shadow: 1px 0 blue;
top: -5px;
color: #FFD;
background: #111;
overflow: hidden;
clip: rect(0, 900px, 0, 0);
animation: noise-anim-2 3s infinite linear alternate-reverse;
-webkit-animation: noise-anim-2 3s infinite linear alternate-reverse;
}
.glitch::after {
content: attr(data-text);
width: calc(100% + 0.5em);
position: absolute;
left: -5px;
text-shadow: -1px 0 red;
top: -5px;
color: #DFF;
background: #111;
overflow: hidden;
clip: rect(0, 900px, 0, 0);
animation: noise-anim 2s infinite linear alternate-reverse;
-webkit-animation: noise-anim 2s infinite linear alternate-reverse;
}
@keyframes noise-anim {
0% {
clip: rect(80px, 9999px, 16px, 0);
}
5% {
clip: rect(94px, 9999px, 17px, 0);
}
10% {
clip: rect(9px, 9999px, 92px, 0);
}
15.0% {
clip: rect(37px, 9999px, 4px, 0);
}
20% {
clip: rect(53px, 9999px, 47px, 0);
}
25% {
clip: rect(99px, 9999px, 69px, 0);
}
30.0% {
clip: rect(15px, 9999px, 49px, 0);
}
35% {
clip: rect(83px, 9999px, 81px, 0);
}
40% {
clip: rect(55px, 9999px, 43px, 0);
}
45% {
clip: rect(28px, 9999px, 27px, 0);
}
50% {
clip: rect(64px, 9999px, 28px, 0);
}
55.0% {
clip: rect(76px, 9999px, 91px, 0);
}
60.0% {
clip: rect(29px, 9999px, 72px, 0);
}
65% {
clip: rect(70px, 9999px, 10px, 0);
}
70% {
clip: rect(5px, 9999px, 84px, 0);
}
75% {
clip: rect(46px, 9999px, 81px, 0);
}
80% {
clip: rect(7px, 9999px, 85px, 0);
}
85.0% {
clip: rect(17px, 9999px, 89px, 0);
}
90% {
clip: rect(41px, 9999px, 59px, 0);
}
95% {
clip: rect(53px, 9999px, 90px, 0);
}
100% {
clip: rect(10px, 9999px, 30px, 0);
}
}
@keyframes noise-anim-2 {
0% {
clip: rect(84px, 9999px, 3px, 0);
}
5% {
clip: rect(31px, 9999px, 69px, 0);
}
10% {
clip: rect(37px, 9999px, 33px, 0);
}
15.0% {
clip: rect(18px, 9999px, 6px, 0);
}
20% {
clip: rect(25px, 9999px, 87px, 0);
}
25% {
clip: rect(100px, 9999px, 8px, 0);
}
30.0% {
clip: rect(24px, 9999px, 87px, 0);
}
35% {
clip: rect(39px, 9999px, 16px, 0);
}
40% {
clip: rect(96px, 9999px, 25px, 0);
}
45% {
clip: rect(16px, 9999px, 100px, 0);
}
50% {
clip: rect(92px, 9999px, 76px, 0);
}
55.0% {
clip: rect(29px, 9999px, 40px, 0);
}
60.0% {
clip: rect(40px, 9999px, 39px, 0);
}
65% {
clip: rect(94px, 9999px, 44px, 0);
}
70% {
clip: rect(2px, 9999px, 78px, 0);
}
75% {
clip: rect(86px, 9999px, 50px, 0);
}
80% {
clip: rect(2px, 9999px, 46px, 0);
}
85.0% {
clip: rect(41px, 9999px, 71px, 0);
}
90% {
clip: rect(75px, 9999px, 15px, 0);
}
95% {
clip: rect(41px, 9999px, 8px, 0);
}
100% {
clip: rect(23px, 9999px, 75px, 0);
}
}
.glitchy {
color: white;
position: relative;
}
.glitchy::before {
content: attr(data-text);
width: calc(100% + 0.5em);
position: absolute;
left: -9px;
text-shadow: 1px 0 blue;
top: -5px;
color: #FFD;
background: #111;
overflow: hidden;
clip-path: polygon(0 0);
animation: noise-animY-2 3s infinite linear alternate-reverse;
-webkit-animation: noise-animY-2 3s infinite linear alternate-reverse;
}
.glitchy::after {
content: attr(data-text);
width: calc(100% + 0.5em);
position: absolute;
left: -5px;
text-shadow: -1px 0 red;
top: -5px;
color: #DFF;
background: #111;
overflow: hidden;
clip-path: polygon(0 0);
animation: noise-animY 2s infinite linear alternate-reverse;
-webkit-animation: noise-animY 2s infinite linear alternate-reverse;
}
@keyframes noise-animY {
0% {
clip-path: polygon(0 80px, 100% 80px, 100% 16px, 0 16px);
}
5% {
clip-path: polygon(0 94px, 100% 94px, 100% 17px, 0 17px);
}
10% {
clip-path: polygon(0 9px, 100% 9px, 100% 92px, 0 92px);
}
15.0% {
clip-path: polygon(0 37px, 100% 37px, 100% 4px, 0 4px);
}
20% {
clip-path: polygon(0 53px, 100% 53px, 100% 47px, 0 47px);
}
25% {
clip-path: polygon(0 99px, 100% 99px, 100% 69px, 0 69px);
}
30.0% {
clip-path: polygon(0 15px, 100% 15px, 100% 49px, 0 49px);
}
35% {
clip-path: polygon(0 83px, 100% 83px, 100% 81px, 0 81px);
}
40% {
clip-path: polygon(0 55px, 100% 55px, 100% 43px, 0 43px);
}
45% {
clip-path: polygon(0 28px, 100% 28px, 100% 27px, 0 27px);
}
50% {
clip-path: polygon(0 64px, 100% 64px, 100% 28px, 0 28px);
}
55.0% {
clip-path: polygon(0 76px, 100% 76px, 100% 91px, 0 91px);
}
60.0% {
clip-path: polygon(0 29px, 100% 29px, 100% 72px, 0 72px);
}
65% {
clip-path: polygon(0 70px, 100% 70px, 100% 10px, 0 10px);
}
70% {
clip-path: polygon(0 5px, 100% 5px, 100% 84px, 0 84px);
}
75% {
clip-path: polygon(0 46px, 100% 46px, 100% 81px, 0 81px);
}
80% {
clip-path: polygon(0 7px, 100% 7px, 100% 85px, 0 85px);
}
85.0% {
clip-path: polygon(0 17px, 100% 17px, 100% 89px, 0 89px);
}
90% {
clip-path: polygon(0 41px, 100% 41px, 100% 59px, 0 59px);
}
95% {
clip-path: polygon(0 53px, 100% 53px, 100% 90px, 0 90px);
}
100% {
clip-path: polygon(0 10px, 100% 10px, 100% 30px, 0 30px);
}
}
@keyframes noise-animY-2 {
0% {
clip-path: polygon(0 84px, 100% 84px, 100% 3px, 0 3px);
}
5% {
clip-path: polygon(0 31px, 100% 31px, 100% 69px, 0 69px);
}
10% {
clip-path: polygon(0 37px, 100% 37px, 100% 33px, 0 33px);
}
15.0% {
clip-path: polygon(0 18px, 100% 18px, 100% 6px, 0 6px);
}
20% {
clip-path: polygon(0 25px, 100% 25px, 100% 87px, 0 87px);
}
25% {
clip-path: polygon(0 100px, 100% 100px, 100% 8px, 0 8px);
}
30.0% {
clip-path: polygon(0 24px, 100% 24px, 100% 87px, 0 87px);
}
35% {
clip-path: polygon(0 39px, 100% 39px, 100% 16px, 0 16px);
}
40% {
clip-path: polygon(0 96px, 100% 96px, 100% 25px, 0 25px);
}
45% {
clip-path: polygon(0 16px, 100% 16px, 100% 100px, 0 100px);
}
50% {
clip-path: polygon(0 92px, 100% 92px, 100% 76px, 0 76px);
}
55.0% {
clip-path: polygon(0 29px, 100% 29px, 100% 40px, 0 40px);
}
60.0% {
clip-path: polygon(0 40px, 100% 40px, 100% 39px, 0 39px);
}
65% {
clip-path: polygon(0 94px, 100% 94px, 100% 44px, 0 44px);
}
70% {
clip-path: polygon(0 2px, 100% 2px, 100% 78px, 0 78px);
}
75% {
clip-path: polygon(0 86px, 100% 86px, 100% 50px, 0 50px);
}
80% {
clip-path: polygon(0 2px, 100% 2px, 100% 46px, 0 46px);
}
85.0% {
clip-path: polygon(0 41px, 100% 41px, 100% 71px, 0 71px);
}
90% {
clip-path: polygon(0 75px, 100% 75px, 100% 15px, 0 15px);
}
95% {
clip-path: polygon(0 41px, 100% 41px, 100% 8px, 0 8px);
}
100% {
clip-path: polygon(0 23px, 100% 23px, 100% 75px, 0 75px);
}
}
/* Glitchy Text - End */
}}}<h1>Delayed and "Blinky" Text Code</h1>Here are two short examples of ways you can make more "active" text.
''Delayed text:''
First paragraph (non-delayed).
@@.delayed;Second paragraph.@@
@@.delayed;Third paragraph.@@
@@.delayed;Fourth paragraph.@@
''Blinky text:'' @@#blinky;Blinky text@@
[[Click to reload this passage so you can see the above changes again.|Delayed and Blinky Text]]
----
<h3>Delayed text code:</h3>For delayed text like that, you just need to wrap each section you want delayed in your passage within {{{@@.delayed; ... @@}}} like this:
{{{
First paragraph (non-delayed).
@@.delayed;Second paragraph.@@
@@.delayed;Third paragraph.@@
@@.delayed;Fourth paragraph.@@
}}}
To make that code work, you'll also need this CSS code in your project's {{{Stylesheet}}} section:
{{{
.delayed {
opacity: 0;
}
}}}
and this code in your project's {{{JavaScript}}} section:
{{{
/* Delayed Text code - Start */
$(document).on(":passagerender", function (ev) {
// Find all elements containing the delayed class.
let elems = $(ev.content).find(".delayed");
if (elems.length > 0) {
// Appearance delay (in milliseconds) between each delayed text block.
let delay = 1000; // 1000 ms = 1 second fade-in
elems.each(function (i) {
$(this).delay(delay * (i + 1)).fadeTo(delay, 1);
});
}
});
/* Delayed Text code - End */
}}}
You can change the value that "{{{delay}}}" is set to in the above code if you want a longer or shorter delay (default = 1000 milliseconds = 1 second).
----
<h3>Blinky text code:</h3>For Blinky Text you'd just do this in your passage:
{{{
Blinky text: @@.blinky;Blinky text@@
<<done>>
<<run $(".blinky").fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1)>>
<<run $(".blinky").css("color", "red")>>
<<run setTimeout( function () { $("#blinky").css("color", "green"); }, 1000)>>
<<run setTimeout( function () { $("#blinky").css("color", "blue"); }, 3200)>>
<<run setTimeout( function () { $("#blinky").css("color", "white"); }, 5400)>>
<</done>>
}}}
You can remove the last four {{{<<run>>}}} lines if you don't want it to change colors (which means it will just blink in white, by default).
The above code uses the jQuery <a href="https://api.jquery.com/fadeTo/" target="_blank">{{{.fadeTo()}}} method</a> to change the element's opacity over a period of milliseconds, as well as the jQuery <a href="https://api.jquery.com/delay/" target="_blank">{{{.delay()}}} method</a> to put in a delay before the next method is called. Calling the methods all in a row like that chains their effects to go off sequentially.
For the color changes it uses the jQuery <a href="https://api.jquery.com/css/" target="_blank">{{{.css()}}} method</a> to modify the target element's CSS styling. The JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout" target="_blank">{{{setTimeout()}}} method</a> is used to trigger the 2nd through 4th color changes after a delay that is synchronized to happen with the fades.
This should give you some ideas for how you can code your own kinds of timed display modifications.
<<done>>
<<run $(".blinky").fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1)>>
<<run $(".blinky").css("color", "red")>>
<<run setTimeout( function () { $(".blinky").css("color", "green"); }, 1000)>>
<<run setTimeout( function () { $(".blinky").css("color", "blue"); }, 3200)>>
<<run setTimeout( function () { $(".blinky").css("color", "white"); }, 5400)>>
<</done>><h1>Embedded Image Sample Code</h1>For very small images, such as icons, instead of having a separate image file, you can embed the image data within your Twine file. In order to do this, the image has to be converted into base64 data, which makes the file larger, hence why you only want to do this for very small images.
For example, here is a PNG image encoded as base64 data: <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gINCBMCngDqkgAABF1JREFUOMuFlFtsVFUUhr99zplz6ZyZztChLVCgTSPElJoW0aQBSVBAIyGi9sUXiEXB8AARo+iDEjFGHwRjfNA0BhIxRoxGfSCxpIEUCVabcgmoyK1CS1uY6XWu57p96CCiif7JylpZ2etfa+/1Z8N/YU/4r5S8hSnTsUfK8f9jsn4FPJe+O/me00BnthFApuMH5HiblCPMA5ALWpGbUn8dFQAD89cRV4tJTbiLKgdO/BR78lyiEK/qCGYlO4ibTSRcSJaoTI4yueIxUFtB3syxb+AzroztEZ87I7cJFYCGwcMUZGRzUVEP8nB/m+Y744FU9y7T+5reir8OQgcBLfY5IAoyAJm0ebHlBZabX8iXEfL7GULtZt1qaoa6mUZsmKsV77kqt5104gFT1RHun9NH/eRlsH3wBNJRQY6BbCw/qALtD6zkxpmVYmmmR17X0GqGugGoUyab4maOREUe7CwkpunPtXLLSaHqkqAQoX94GYQjQB6IQCgBFWoXd0HGFAv8mSsPzln3ran4CTQJWgCREIyQpYVLXLRW0FXdjpGH/EQdm4/ug/AEBEAYlM0w5GDbDgDlbOXGeQixWggJaghq2QvA9DHdIt8NrcTJKZAP2X9hB50/t0PpGHgF8DzwXHBFu7zSJrRQarORQp/Z9992r4RkqnRaj3/KqNUA0QIYEVAVth7ppCl/nuX3/QjqEsCCwGsW956Smh9q1UGoRGQoIFCQvsLYUIKvL63iVGwxTDnE3FEcx8bXTaSmIEON3b0ddKe2Q/I8aIshEJXyQM3bWsk3q7xQl4HUBJ5CadokrgfgKpy43kzMThNxHBwjhx/RCRUVIRRuTMVmRDcRgnENRC2oEVOTofKr4xuBF0Y0y1WwPCDusrXlMKlMhpNjzdQ3ZIilShw68ygXby1EotJYex4sIA34BZBZqA5Oa05gjJf8ilLJt+xokEV1BQiBO1bDE/PO8lRLLyRCplSDXNonfWEDuZJg17PfQBbwyyamQcqvtDXBu4M97s5Mzo3Ztj5FRdGDnIduBXx4+HEmA50JEdI/vBApTCyRpn3tcR5adBVuAS4zEhJONx6OAoKSa70yVUySd2P4TgSmDMhKti05ii9dRjJJaiNZ5lsjbF//Ja8+3QWjZX0XgbyAP2KHxEakkJ0gtsAPxo4jtfHhNTXRYWwji0gWYXYRavI4UXAqAqKpHKpd1uhtuAL5ewIxt1gvXitdEwDd2i6CUKlLmBOna2LDqVTFTSqMAiLhwKwSJEsQdyHmQYUPRgBKCL4KRQ10/3nxhveJfP/uXvToOxO2MX1qdnS0ocrKYOkFhO1CpQtxB6I+mAHoZcKSOkAkXC8+nvxF7jYQbzp3CI9FXmKVt5eDfKQ2x3s/SFiTmxLWuF2h5dEMF2GVpzMDsqFGOjenu7H3+FoQUnbMQuwfv/PB/hN90S3kgpioj19+xjZymy2t8KChlWxVCdJC93tCI3znt6rY6SVdffKftX8CBMXrIvMsmx4AAAAASUVORK5CYII=">
Here's what the code for the above line looks like:
{{{
For example, here is a PNG image encoded as base64 data: <img src="data:image/png;base64,iVBORw0
KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3R
JTUUH4gINCBMCngDqkgAABF1JREFUOMuFlFtsVFUUhr99zplz6ZyZztChLVCgTSPElJoW0aQBSVBAIyGi9sUXiEXB8AARo+i
DEjFGHwRjfNA0BhIxRoxGfSCxpIEUCVabcgmoyK1CS1uY6XWu57p96CCiif7JylpZ2etfa+/1Z8N/YU/4r5S8hSnTsUfK8f9
jsn4FPJe+O/me00BnthFApuMH5HiblCPMA5ALWpGbUn8dFQAD89cRV4tJTbiLKgdO/BR78lyiEK/qCGYlO4ibTSRcSJaoTI4
yueIxUFtB3syxb+AzroztEZ87I7cJFYCGwcMUZGRzUVEP8nB/m+Y744FU9y7T+5reir8OQgcBLfY5IAoyAJm0ebHlBZabX8i
XEfL7GULtZt1qaoa6mUZsmKsV77kqt5104gFT1RHun9NH/eRlsH3wBNJRQY6BbCw/qALtD6zkxpmVYmmmR17X0GqGugGoUya
b4maOREUe7CwkpunPtXLLSaHqkqAQoX94GYQjQB6IQCgBFWoXd0HGFAv8mSsPzln3ran4CTQJWgCREIyQpYVLXLRW0FXdjpG
H/EQdm4/ug/AEBEAYlM0w5GDbDgDlbOXGeQixWggJaghq2QvA9DHdIt8NrcTJKZAP2X9hB50/t0PpGHgF8DzwXHBFu7zSJrR
QarORQp/Z9992r4RkqnRaj3/KqNUA0QIYEVAVth7ppCl/nuX3/QjqEsCCwGsW956Smh9q1UGoRGQoIFCQvsLYUIKvL63iVGw
xTDnE3FEcx8bXTaSmIEON3b0ddKe2Q/I8aIshEJXyQM3bWsk3q7xQl4HUBJ5CadokrgfgKpy43kzMThNxHBwjhx/RCRUVIRR
uTMVmRDcRgnENRC2oEVOTofKr4xuBF0Y0y1WwPCDusrXlMKlMhpNjzdQ3ZIilShw68ygXby1EotJYex4sIA34BZBZqA5Oa05
gjJf8ilLJt+xokEV1BQiBO1bDE/PO8lRLLyRCplSDXNonfWEDuZJg17PfQBbwyyamQcqvtDXBu4M97s5Mzo3Ztj5FRdGDnId
uBXx4+HEmA50JEdI/vBApTCyRpn3tcR5adBVuAS4zEhJONx6OAoKSa70yVUySd2P4TgSmDMhKti05ii9dRjJJaiNZ5lsjbF/
/Ja8+3QWjZX0XgbyAP2KHxEakkJ0gtsAPxo4jtfHhNTXRYWwji0gWYXYRavI4UXAqAqKpHKpd1uhtuAL5ewIxt1gvXitdEwD
d2i6CUKlLmBOna2LDqVTFTSqMAiLhwKwSJEsQdyHmQYUPRgBKCL4KRQ10/3nxhveJfP/uXvToOxO2MX1qdnS0ocrKYOkFhO1
CpQtxB6I+mAHoZcKSOkAkXC8+nvxF7jYQbzp3CI9FXmKVt5eDfKQ2x3s/SFiTmxLWuF2h5dEMF2GVpzMDsqFGOjenu7H3+Fo
QUnbMQuwfv/PB/hN90S3kgpioj19+xjZymy2t8KChlWxVCdJC93tCI3znt6rY6SVdffKftX8CBMXrIvMsmx4AAAAASUVORK5
CYII=">
}}}
Note that the image source begins with {{{data:image/png;base64,}}}, the actual image data begins after that part.
As you can see, even for a tiny (20px by 20px) image like that, this is a bunch of text which looks like gibberish to add to your passages, so this is not a good way to add images to your project.
Again, it's strongly recommended that you ''do not'' use this method for a lot of image data. If you do embed large amounts of data, then the page will take a lot longer to load and, if you add too much, the browser may not even be able to load the page properly. Separate image files should be preferred.
So, when //should// you use this? Basically, only use this technique if you need to provide images or icons which you're sure will work, even if the image files are missing, so that the user can resolve the issue of why the rest of the images aren't working.
If you do want to do this, there are sites like <a href="https://www.base64-image.de/" target="_base">base64-image.de</a> which can convert your image files into base64 data.
<h1>Smart Checkboxes and Radio Buttons</h1>The sample code that was here has been obsolete since SugarCube v2.32.0, thus it has been removed from the main menu of this sample code collection. You can now use the "{{{autocheck}}}" parameter to do the same thing. See the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-checkbox" target="_blank">{{{<<checkbox>>}}}</a> and <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-radiobutton" target="_blank">{{{<<radiobutton>>}}}</a> macro documentation for details.
/* Old code:
<<if ndef $skills>><<set $skills = {}>><</if>>Select Skills:
<<checkbox "$skills.sword" false true `$skills.sword ? 'checked' : ''`>> Sword
<<checkbox "$skills.bow" false true `$skills.bow ? 'checked' : ''`>> Bow
<<checkbox "$skills.magic" false true `$skills.magic ? 'checked' : ''`>> Magic
Original values:
Sword - $skills.sword
Bow - $skills.bow
Magic - $skills.magic
[[Reload|Smart Checkboxes]]
Above code:
{{{
<<if ndef $skills>><<set $skills = {}>><</if>>Select Skills:
<<checkbox "$skills.sword" false true `$skills.sword ? 'checked' : ''`>> Sword
<<checkbox "$skills.bow" false true `$skills.bow ? 'checked' : ''`>> Bow
<<checkbox "$skills.magic" false true `$skills.magic ? 'checked' : ''`>> Magic
Original values:
Sword - $skills.sword
Bow - $skills.bow
Magic - $skills.magic
}}}
Note that the above code uses backticks ({{{`}}}), found on the upper-left key of most qwerty keyboards) as a way to insert the evaluation of the expression inside those backticks.
The expression inside the backticks works like this:
{{{
result = conditional ? true result output : false result output
}}}
So, if the conditional has a "truthy" value, then the result will get set to the true result output.
If the conditional has a "falsy" value, then the result will get set to the false result output.
"Falsy" values are:
* {{{false}}}
* {{{0}}} (zero)
* {{{null}}}
* {{{undefined}}}
* {{{NaN}}} (Not a Number)
* {{{""}}} (an empty string)
Any other values are "truthy" values.
To get back to the above code, then this code:
{{{
`$skills.sword ? 'checked' : ''`
}}}
becomes {{{checked}}} if {{{$skills.sword}}} has a "truthy" value, or it becomes nothing if {{{$skills.sword}}} has a "falsy" value.
----
You can do something similar for radio buttons. For example:
<<if ndef $class>><<set $class = "Fighter">><</if>>Select Class:
<<radiobutton "$class" "Fighter" `$class === "Fighter" ? "checked" : ""`>> Fighter
<<radiobutton "$class" "Archer" `$class === "Archer" ? "checked" : ""`>> Archer
<<radiobutton "$class" "Magic User" `$class === "Magic User" ? "checked" : ""`>> Magic User
Original Class: $class
[[Reload|Smart Checkboxes]]
Above code:
{{{
<<if ndef $class>><<set $class = "Fighter">><</if>>Select Class:
<<radiobutton "$class" "Fighter" `$class === "Fighter" ? "checked" : ""`>> Fighter
<<radiobutton "$class" "Archer" `$class === "Archer" ? "checked" : ""`>> Archer
<<radiobutton "$class" "Magic User" `$class === "Magic User" ? "checked" : ""`>> Magic User
Original Class: $class
}}}
*/<h1>Error Messages</h1>It's a good idea to clearly display error messages when your code encounters something that shouldn't happen, so that way you can easily spot when parts of your code are failing. And, if done properly, the messages should also help explain //why// things failed, making them easier to fix.
This section has two parts:
* <<link "How to throw custom error messages with Twine code">><<ScrollTo "pt1">><</link>>
* <<link "How to throw an error message from JavaScript code">><<ScrollTo "pt2">><</link>>
----
<h2 id="pt1">How to throw custom error messages with Twine code</h2>Doing:
{{{
@@#error;@@<<done>>
<<run throwError($('#error'),
'No player character.',
'You appear to be kind of mostly dead.\nSorry you had to find out like this.'
)>>
<</done>>
}}}
results in this:
@@#error;@@<<done>>
<<run throwError($('#error'), 'No player character.', 'You appear to be kind of mostly dead.\nSorry you had to find out like this.')>>
<</done>>
You can click the <<scicon caret-right>>/<<scicon caret-down>> toggle to show/hide the rest of the error message.
This works by calling the built-in SugarCube {{{throwError()}}} function within a {{{<<done>>}}} macro, which takes parameters for the target HTML element, title, and content values, as shown above, and displays them as an error message in the target HTML element once the passage is "done" with being added to the document.
The {{{@@#error;@@}}} part is <a href="https://www.motoslave.net/sugarcube/2/docs/#markup-custom-style" target="_blank">SugarCube markup</a>, equivalent to this:
{{{
<div id="error"></div>
}}}
Also, the {{{\n}}} is a way of signifying a "new line".
However, doing all of that each time you need it can be a bit cumbersome.
Instead, you can use the below {{{<<ErrMsg>>}}} widget to make displaying custom error messages easy. To use it, first create a non-story, non-special passage that has a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags), and then put this code within that widget passage:
{{{
/* <<ErrMsg>> Widget - Start
VARIABLES USED: _errNo
*/
<<widget "ErrMsg">><<nobr>>
<<if (_args.length == 2) && (typeof _args[0] === "string") && (typeof _args[1] === "string")>>
<<if ndef _errNo>>
<<set _errNo = 1>>
<<run $(document).one(":passagerender",
function (ev) {
$(ev.content).find(".errmsg").each(function (idx) {
throwError($(this), $(this).data("msg"), $(this).data("src"));
});
}
)>>
<<else>>
<<set _errNo += 1>>
<</if>>
<span class="errmsg" @data-msg="_args[0]" @data-src="_args[1]"></span>
<<else>>
<<ErrMsg '<<ErrMsg>> must have two string parameters.' 'Example: <<ErrMsg "Main text." "Details.">>'>>
<</if>>
<</nobr>><</widget>>
/* <<ErrMsg>> Widget - End */
}}}
Once you have that, doing code like this:
{{{
<<ErrMsg "Test error message #1." "Some details.">>
<<ErrMsg "Test error message #2." "More details.">>
}}}
results in this:
<<ErrMsg "Test error message #1." "Some details.">>
<<ErrMsg "Test error message #2." "More details.">>
You can use that widget within your own code to more clearly display your own custom error messages.
----
<h2 id="pt2">How to throw an error message from JavaScript code</h2>For a very simple example, let's say you have the following code in your JavaScript section:
{{{
window.ErrTest = function (text) {
if (typeof text === "string") {
console.log(text);
} else {
throw new Error("The value passed to this function (type = " + (typeof text) + ") must be a string.");
}
};
}}}
That uses the standard JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw" target="_blank">throw statement</a> to throw an error if the wrong type of data is passed in through the "text" parameter.
Now, if you call that function by doing:
{{{
<<run ErrTest()>>
}}}
it results in this:
<<run ErrTest()>>
By looking at the details of that error message, we can easily see that we forgot to pass a string to that function.
You can use that {{{throw new Error()}}} code within your own custom JavaScript functions whenever you need them to be able to properly throw an error.
<h1>Fullscreen Button Code</h1>Getting fullscreen mode in a variety of browsers can be tricky (though it's not as much of a problem now as it was when this was first written). This code allows you to programmatically trigger putting the page into fullscreen mode in just about any browser, such as with this button:
<<button "Fullscreen (direct toggleFullscreen() call)">><<run toggleFullscreen()>><</button>>
The code for that button is simply:
{{{
<<button "Fullscreen (direct toggleFullscreen() call)">><<run toggleFullscreen()>><</button>>
}}}
Note that the {{{toggleFullscreen()}}} function does require some user interaction to trigger it, such as clicking a button, due to browser security reasons. Thus this second button may or may not work in some browsers because it includes a 3 second delay before triggering, which means that the browser may not interpret the {{{toggleFullscreen()}}} call as being initiated by a user gesture after that much delay:
<<button "Fullscreen (delayed toggleFullscreen() call)">><<run window.setTimeout(window.toggleFullscreen, 3000)>><</button>>
You can also trigger the function from the "fullscreen" button ( <span class="svg"><svg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'><path d='m82 324v87c0 11 8 19 19 19h88c15 0 22-17 11-29l-27-28 83-83 83 83-27 28c-11 11-4 29 11 29h88c11 0 19-8 19-19v-87c0-15-17-23-29-12l-28 27-83-83 83-83 28 27c11 11 29 3 29-12v-87c0-11-8-19-19-19h-88c-15 0-22 17-11 29l27 28-83 83-83-83 27-28c11-11 4-29-11-29h-88c-11 0-19 8-19 19v87c0 15 17 23 29 12l28-27 83 83-83 83-28-27c-12-11-29-4-29 12zm374 188h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z' fill='#fff' /></svg></span> ) in the UI bar to the left. (See the bottom section for how to add it to your project.)
''Real-time status:'' (toggle fullscreen mode to see how these values change)
<<nobr>>
<<if setup.Fullscreen == false>>
Fullscreen not supported for this browser/OS combination.
<<else>>
{{{setup.Fullscreen.API}}} = <<= setup.Fullscreen.API>><br>
<span id="status"></span>
<<repeat 0.5s>>
<<replace "#status">>
{{{setup.Fullscreen.fullscreenElement()}}} = <<PrintType `setup.Fullscreen.fullscreenElement()`>><br>
{{{setup.Fullscreen.isFullscreen()}}} = <<PrintType `setup.Fullscreen.isFullscreen()`>><br>
{{{document.fullscreen}}} = <<PrintType `document.fullscreen`>><br>
{{{document.webkitIsFullScreen}}} = <<PrintType `document.webkitIsFullScreen`>><br>
{{{document.mozFullScreen}}} = <<PrintType `document.mozFullScreen`>><br>
{{{window.fullScreen}}} = <<PrintType `window.fullScreen`>>
<</replace>>
<</repeat>>
<</if>>
<</nobr>>
As you can see in the last four values above, there are a bunch of different ways that you can get the fullscreen status, some of which are particular to one browser or another. However, the {{{setup.Fullscreen.isFullscreen()}}} function (added by the code below) should return the correct value for all browsers.
In addition to the {{{toggleFullscreen()}}} function, the code below creates the {{{setup.Fullscreen}}} object, which normally contains these parts:
* {{{setup.Fullscreen.API}}} = "default"/"webkit"/"moz"/"ms" : Fullscreen API in use.
* {{{setup.Fullscreen.onfullscreenchangeHandler}}} : Set this property to a JavaScript function with an "event" parameter, and that function will be triggered on changes to fullscreen mode.
** For example: {{{setup.Fullscreen.onfullscreenchangeHandler = function(event) { console.log("Fullscreen mode changed to " + setup.Fullscreen.isFullscreen()); console.log(event); };}}}
* {{{setup.Fullscreen.isFullscreen()}}} = true/false : Whether you're in fullscreen mode or not.
* {{{setup.Fullscreen.fullscreenElement()}}} = The "HTML Element" object which is being displayed in fullscreen when in fullscreen mode; otherwise it's set to {{{null}}}.
* {{{setup.Fullscreen.fullscreenEnabled()}}} = true/false : Tells you if fullscreen mode is available or not.
* {{{setup.Fullscreen.requestFullscreen()}}} : Call this to try to go fullscreen.
* {{{setup.Fullscreen.exitFullscreen()}}} : Call this to try to exit fullscreen mode. It will be unable to work if {{{setup.Fullscreen.fullscreenElement()}}} returns {{{null}}}. If you calll it when it's unable to exit fullscreen mode, an alert message will appear telling the user how to exit fullscreen mode (assuming the alert message itself doesn't kick them out of fullscreen mode, which happens in some browsers).
Finally, any errors will be output to the console as "Fullscreen error:" followed by the error object.
''Note:'' If the code can't find a fullscreen API, then {{{setup.Fullscreen == false}}}.
For the the fullscreen code to work, simply add this to your project's ''JavaScript'' section:
{{{
/* Fullscreen toggle code v1.4 - Start */
/* For more information see: https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API */
// Works in: Firefox, Chrome, Edge, and (old) Presto-based Opera
// Failure/Untested cases:
// - Blink-based Opera: Can't trap F11 keypress, so toggleFullscreen() can't cancel it.
// - Internet Explorer: Can trap F11 keypress, but can't trigger toggleFullscreen() from keydown or keyup, so the keypress can't be emulated.
// - Safari: should work(?)
// - iOS (iPad + Safari): should work(?)
// - iOS (iPhone): unsupported
/* This code creates the setup.Fullscreen object, which normally contains these parts:
- setup.Fullscreen.API = "default"/"webkit"/"moz"/"ms" : Fullscreen API in use.
- setup.Fullscreen.onfullscreenchangeHandler : Set this property to a function with an "event" parameter, and that function will be triggered on changes to fullscreen mode.
For example: setup.Fullscreen.onfullscreenchangeHandler = function(event) { console.log("Fullscreen mode changed to " + setup.Fullscreen.isFullscreen()); console.log(event); };
- setup.Fullscreen.isFullscreen() = true/false : Whether you're in fullscreen mode or not. Check setup.Fullscreen.fullscreenElement() to see if you're in fullscreen using the API or not.
- setup.Fullscreen.fullscreen() = true/false : Obsolete method of checking if you're in fullscreen mode.
- setup.Fullscreen.fullscreenElement() = The Element object which is being displayed in fullscreen when in fullscreen mode; otherwise == null.
- setup.Fullscreen.fullscreenEnabled() = true/false : Tells you if fullscreen mode is available or not.
- setup.Fullscreen.requestFullscreen() : Call this to try to go fullscreen.
- setup.Fullscreen.exitFullscreen() : Call this to try to exit fullscreen mode. If it's unable to exit fullscreen mode an alert message will appear telling the user how to exit fullscreen mode.
It also creates:
- window.toggleFullscreen() -or- toggleFullscreen() : Call this to toggle fullscreen mode.
Finally, any errors will be output to the console as "Fullscreen error:" followed by the error object.
If the code can't find a fullscreen API, then setup.Fullscreen == false.
*/
function fullscreenchangeHandler (event) {
if (typeof setup.Fullscreen.isFullscreen === "function") {
if (setup.Fullscreen.isFullscreen() != setup.Fullscreen.wasFullscreen) {
setup.Fullscreen.wasFullscreen = setup.Fullscreen.isFullscreen();
if (typeof setup.Fullscreen.onfullscreenchangeHandler === "function") {
setup.Fullscreen.onfullscreenchangeHandler(event);
}
/* Fullscreen icon/button support - Start */
/* You can remove this part if you're not using the on-screen button. */
if (setup.Fullscreen.onfullscreenchangeButton && (typeof setup.Fullscreen.onfullscreenchangeButton === "function")) {
setup.Fullscreen.onfullscreenchangeButton(event);
}
/* Fullscreen icon/button support - End */
}
}
return true;
}
function fullscreenExit() {
if (setup.Fullscreen.fullscreenElement() == null) { // Handle situations where exiting fullscreen isn't possible.
setTimeout(alert("Press F11 once or twice to exit fullscreen."));
return false;
}
return true;
}
function fullscreenError (error) {
console.log("Fullscreen error:");
console.log(error);
}
if (typeof document.documentElement.requestFullscreen === "function") {
// Default fullscreen support.
setup.Fullscreen = {
API : "default",
onfullscreenchangeHandler : null,
fullscreen : function () { return document.fullscreen; },
fullscreenEnabled : function () { return document.fullscreenEnabled; },
fullscreenElement : function () { return document.fullscreenElement; },
requestFullscreen : function () { return document.documentElement.requestFullscreen(); }
};
if (typeof document.exitFullscreen === "function") {
setup.Fullscreen.exitFullscreen = function () { return fullscreenExit() ? document.exitFullscreen() : true; };
} else if (typeof document.cancelFullScreen === "function") {
setup.Fullscreen.exitFullscreen = function () { return fullscreenExit() ? document.cancelFullScreen() : true; };
}
document.onfullscreenchange = function (event) { return fullscreenchangeHandler(event); };
document.onfullscreenerror = function (error) { fullscreenError(error); };
} else if (typeof document.documentElement.webkitRequestFullscreen === "function") {
if (typeof document.webkitExitFullscreen === "function") {
// Blink/Chrome/Opera/Edge/Safari fullscreen support.
setup.Fullscreen = {
API : "webkit",
onfullscreenchangeHandler : null,
fullscreen : function () { return document.webkitIsFullScreen; },
fullscreenEnabled : function () { return document.webkitFullscreenEnabled; },
fullscreenElement : function () { return document.webkitFullscreenElement; },
requestFullscreen : function () { return document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); },
exitFullscreen : function () { return fullscreenExit() ? document.webkitExitFullscreen() : true; }
};
} else if (typeof document.webkitCancelFullScreen === "function") {
// Old Safari fullscreen support.
setup.Fullscreen = {
API : "webkit",
onfullscreenchangeHandler : null,
fullscreen : function () { return Boolean(document.webkitIsFullScreen); },
fullscreenEnabled : function () { return document.webkitCancelFullScreen; },
fullscreenElement : function () { return document.webkitCurrentFullScreenElement; },
requestFullscreen : function () { return document.documentElement.webkitRequestFullscreen(); },
exitFullscreen : function () { return fullscreenExit() ? document.webkitCancelFullScreen() : true; }
};
}
document.onwebkitfullscreenchange = function (event) { return fullscreenchangeHandler(event); };
document.onwebkitfullscreenerror = function (error) { fullscreenError(error); };
} else if (typeof document.documentElement.mozRequestFullScreen === "function") {
// Firefox fullscreen support.
setup.Fullscreen = {
API : "moz",
onfullscreenchangeHandler : null,
fullscreen : function () { return document.mozFullScreen; },
fullscreenEnabled : function () { return document.mozFullScreenEnabled; },
fullscreenElement : function () { return document.mozFullScreenElement; },
requestFullscreen : function () { return document.documentElement.mozRequestFullScreen(); },
exitFullscreen : function () { return fullscreenExit() ? document.mozCancelFullScreen() : true; },
};
document.onmozfullscreenchange = function (event) { return fullscreenchangeHandler(event); };
document.onmozfullscreenerror = function (error) { fullscreenError(error); };
} else if (typeof document.documentElement.msRequestFullscreen === "function") {
// Internet Explorer fullscreen support.
setup.Fullscreen = {
API : "ms",
onfullscreenchangeHandler : null,
fullscreen : function () { return (setup.Fullscreen.fullscreenElement() != null) || // IE workaround
(window.fullScreen === true) || (screen.height === window.innerHeight); }, // F11 workaround
// Note: (screen.height === window.innerHeight) won't work in IE if the console window is open.
fullscreenEnabled : function () { return document.msFullscreenEnabled; },
fullscreenElement : function () { return document.msFullscreenElement; },
requestFullscreen : function () { return document.documentElement.msRequestFullscreen(); },
exitFullscreen : function () { return fullscreenExit() ? document.msExitFullscreen() : true; },
};
document.MSFullscreenChange = function (event) { return fullscreenchangeHandler(event); };
document.MSFullscreenError = function (error) { fullscreenError(error); };
} else {
// Fullscreen not supported.
setup.Fullscreen = false;
}
if (setup.Fullscreen) {
setup.Fullscreen.isFullscreen = function () { return (setup.Fullscreen.fullscreenElement() != null) ||
(window.fullScreen === true) || (screen.height === window.innerHeight); }; // F11 workaround
setup.Fullscreen.wasFullscreen = setup.Fullscreen.isFullscreen();
$(window).on("resize", function(event) { // F11 workaround
fullscreenchangeHandler(event);
return true;
} );
}
window.toggleFullscreen = function () {
if (setup.Fullscreen) {
if (!setup.Fullscreen.isFullscreen()) {
setup.Fullscreen.requestFullscreen();
setTimeout( function () { // Firefox workaround
$("body").css("top", "0px");
} );
} else {
setup.Fullscreen.exitFullscreen();
}
} else {
alert("Fullscreen mode not supported for this browser.");
}
};
$(document).on("keydown", function(e) {
if (e.key == "F11") { // F11 workaround
if (setup.Fullscreen && (setup.Fullscreen.API !== "ms")) {
return false; // tries to cancel F11 keypress
}
}
return true;
});
$(document).on("keyup", function(e) {
if (e.key == "F11") { // F11 workaround
if (setup.Fullscreen && (setup.Fullscreen.API !== "ms")) { // Can't trigger fullscreen from a keypress in IE.
if ((!setup.Fullscreen.isFullscreen()) || (setup.Fullscreen.fullscreenElement() != null)) { // Try to toggle fullscreen if possible.
window.toggleFullscreen();
return false; // tries to cancel F11 keypress
}
}
}
return true;
});
/* Fullscreen toggle code - End */
}}}
You can also add the "fullscreen" button to your project's UI bar by putting this in your project's ''StoryCaption'' passage:
{{{
<input type="checkbox" id="fullscreen"><label for="fullscreen" class="gofullscreen"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m82 324v87c0 11 8 19 19 19h88c15 0 22-17 11-29l-27-28 83-83 83 83-27 28c-11 11-4 29 11 29h88c11 0 19-8 19-19v-87c0-15-17-23-29-12l-28 27-83-83 83-83 28 27c11 11 29 3 29-12v-87c0-11-8-19-19-19h-88c-15 0-22 17-11 29l27 28-83 83-83-83 27-28c11-11 4-29-11-29h-88c-11 0-19 8-19 19v87c0 15 17 23 29 12l28-27 83 83-83 83-28-27c-12-11-29-4-29 12zm374 188h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z' fill='%23fff' /%3E%3C/svg%3E" alt="Go full screen" title="Go full screen" class="fullscreenImg"></label><label for="fullscreen" class="exitfullscreen"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m65 99 83 83-27 28c-11 11-4 29 11 29h88c11 0 19-8 19-19v-87c0-15-17-23-29-12l-28 27-83-83zm117 265 28 27c11 11 29 3 29-12v-87c0-11-8-19-19-19h-88c-15 0-22 17-11 29l27 28-83 83 34 34zm265 49-83-83 27-28c11-11 4-29-11-29h-88c-11 0-19 8-19 19v87c0 15 17 23 29 12l28-27 83 83zm-117-265-28-27c-12-11-29-4-29 12v87c0 11 8 19 19 19h88c15 0 22-17 11-29l-27-28 83-83-34-34zm126 364h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-17 0-20 10-20 20v390c0 16 10 20 20 20h390c8 0 20-4 20-20v-390c0-18-12-20-20-20z' fill='%23fff' /%3E%3C/svg%3E" alt="Exit full screen" title="Exit full screen" class="fullscreenImg"></label>
}}}
adding this to your ''Stylesheet'' section:
{{{
/* Fullscreen Button - Start */
html, body {
position: absolute;
min-width: 100%;
min-height: 100vh;
}
:-webkit-full-screen { /* Chrome/Blink fullscreen fix */
background-color: #111; /* Change to match your game's background color. */
}
input#fullscreen {
display: none;
}
.exitfullscreen img {
display: none;
}
input#fullscreen:checked ~ .exitfullscreen img {
display: inline;
}
input#fullscreen:checked ~ .gofullscreen img {
display: none;
}
.fullscreenImg {
cursor: pointer;
transition: 0.3s;
border-radius: 3px;
position: absolute;
top: 40px;
left: 252px;
height: 25px;
vertical-align: text-bottom;
visibility: visible;
}
.fullscreenImg:hover {
background: #444444;
}
/* Fullscreen Button - End */
}}}
and adding this additional bit of code to the ''JavaScript'' section:
{{{
/* Fullscreen icon/button support - Start */
$(document).on("click", "#fullscreen", function (event) {
window.toggleFullscreen();
if (setup.Fullscreen && ($("#fullscreen").prop("checked") != setup.Fullscreen.isFullscreen())) {
$("#fullscreen").prop("checked", setup.Fullscreen.isFullscreen());
}
} );
$(document).one(":passageend", function (event) {
if (setup.Fullscreen && $("#fullscreen").length) {
setup.Fullscreen.onfullscreenchangeButton = function (event) {
if ($("#fullscreen").prop("checked") != setup.Fullscreen.isFullscreen()) {
$("#fullscreen").prop("checked", setup.Fullscreen.isFullscreen());
}
};
}
} );
/* Fullscreen icon/button support - End */
}}}
''NOTE:'' The JavaScript {{{alert()}}} function will also kick you out of fullscreen mode on some browsers. <<button "Test 'alert()'">><<run alert("This is an alert message.")>><</button>><h1>Random Link Sample Code</h1>Creating a random link can be fairly simple. If you just need to show a random passage name as a link which sends you to that passage, then you can just use a JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array" target="_blank">array</a> of passage names, the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#methods-array-prototype-method-random" target="_blank">Array.random() method</a> to pick one at random, and a <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-link" target="_blank">{{{<<link>>}}} macro</a> to display the link. For example:
{{{
''Random link:'' <<set _link = ["Backgrounds", "Settings", "Replace"].random()>>\
<<link _link _link>><</link>>
}}}
Which is displayed as:
''Random link:'' <<set _link = ["Backgrounds", "Settings", "Replace"].random()>>\
<<link _link _link>><</link>>
You can [[reload this passage|Random Link]] to see that it shows a different link most of the time (it can randomly pick the same link).
If you want the text of the links to be different from the passages' names, you can do that fairly similarly by using a basic JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object" target="_blank">object</a> to store the list of passage names and the link text for them, then the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys" target="_blank">Object.keys() method</a> to get the array of the passage names from that object, which you can then randomly pick from and then use to create the link. For example:
{{{
''Random link:'' <<set _linkList = { "Settings": "Game Settings",
"Backgrounds": "Passage Backgrounds",
"Replace": "{{{<<replace>>}}} macro" }>>\
<<set _link = Object.keys(_linkList).random()>>\
<<link _linkList[_link] _link>><</link>>
}}}
''Random link:'' <<set _linkList = { "Settings": "Game Settings",
"Backgrounds": "Passage Backgrounds",
"Replace": "{{{<<replace>>}}} macro" }>>\
<<set _link = Object.keys(_linkList).random()>>\
<<link _linkList[_link] _link>><</link>>
Now, because the above methods aren't using square brackets for the links, the Twine editor will not show arrows connecting those passages. If you'd like to have the connecting arrows show within the Twine editor anyways, you can create an additional "passage comment block" (e.g. {{{<!-- ... -->}}}) with the links inside, like this:
{{{
<!-- [[Settings]] [[Backgrounds]] [[Replace]] -->
}}}
Anything within a passage comment block like that won't be displayed or do anything, it's only there to make the Twine editor show the connecting arrows to those other passages.
<!-- [[Settings]] [[Backgrounds]] [[Replace]] -->/* --- Sample Code widgets --- */
/* SugarCube icon widget */
<<widget "scicon">><span @class="'sc-icon sc-' + _args.raw" alt="' + _args.raw + '"></span><</widget>>
/* <<ErrMsg>> Widget - Start
VARIABLES USED: _errNo
*/
<<widget "ErrMsg">><<nobr>>
<<if (_args.length == 2) && (typeof _args[0] === "string") && (typeof _args[1] === "string")>>
<<if ndef _errNo>>
<<set _errNo = 1>>
<<run $(document).one(":passagerender",
function (ev) {
$(ev.content).find(".errmsg").each(function (idx) {
throwError($(this), $(this).data("msg"), $(this).data("src"));
});
}
)>>
<<else>>
<<set _errNo += 1>>
<</if>>
<span class="errmsg" @data-msg="_args[0]" @data-src="_args[1]"></span>
<<else>>
<<ErrMsg '<<ErrMsg>> must have two string parameters.' 'Example: <<ErrMsg "Main text." "Details.">>'>>
<</if>>
<</nobr>><</widget>>
/* <<ErrMsg>> Widget - End */
/* HoverTxt : Show notepad icon shows some wikified text in a window of width X pixels above an icon when it's hovered over.
EXAMPLES:
<<HoverTxt 100 "text">>
<<HoverTxt "More text.">>
<<HoverTxt 300 `someFunction()`>>
VARIABLES USED: _HoverTxtCount, _HTLeft, _HTTxt, _HTWidth
*/
<<widget "HoverTxt">><<nobr>>
<<set _HTTxt = _args.length >= 2 ? _args[1] : _args[0]>>
<<if !Number.isInteger(_args[0])>>
<<set _HTWidth = 200>> /* Default to a width of 200 if an invalid width is passed. */
<<else>>
<<set _HTWidth = _args[0]>>
<</if>>
<<set _HTLeft = Math.trunc(_HTWidth / 2) - 11>>
<<if ndef _HoverTxtCount>>
<<set _HoverTxtCount = 1>>
<<else>>
<<set _HoverTxtCount += 1>>
<</if>>
<span class="hoverTxt" tabindex="0"><img class="hoverIco" @src="setup.NoteIcon">
<span @id="'hoverTxt' + _HoverTxtCount" @style="'left: -' + _HTLeft + 'px; width: ' + _HTWidth + 'px;'">
_HTTxt
</span>
</span>
<</nobr>><</widget>>
/* <<SetFlag>> widget: Sets Flag X to value Y (Y defaults to true).
Flag names are NOT case sensitive.
EXAMPLE: <<SetFlag "Mentor" "Bob">>
EXAMPLE: <<SetFlag "TrialMed">>
VARIABLES USED: _Fnam, $Flags
*/
<<widget "SetFlag">><<nobr>>
<<set _Fnam = _args[0].toLowerCase()>>
<<if ndef $Flags>>
<<set $Flags = {}>>
<</if>>
<<if def _args[1]>>
<<if _args[1] == false>>
<<if def $Flags[_Fnam]>>
<<run delete $Flags[_Fnam]>>
<</if>>
<<else>>
<<set $Flags[_Fnam] = _args[1]>>
<</if>>
<<else>>
<<set $Flags[_Fnam] = true>>
<</if>>
/* Event flags: */
/* Keep track of your flags here, for example: */
/* Mentor = Bob or Joe */
/* TrialMed = denotes whether you've agreed to take Dr. Acula's medication */
<</nobr>><</widget>>
/* <<SetFlag>> widget - End */
/* <<SetGender>> widget - Start
Usage (defaults to male):
for "he": <<SetGender>> or <<SetGender "m">>
for "she": <<SetGender "f">>
for "they": <<SetGender "b">>
for "it": <<SetGender "n">>
$pgen: 0 = male, 1 = female, 2 = gender neutral, 3 = no gender
VARIABLES USED: $pgen
*/
<<widget "SetGender">><<nobr>>
<<switch _args[0]>>
<<case "f">>
<<set $pgen = 1>>
<<case "b">>
<<set $pgen = 2>>
<<case "n">>
<<set $pgen = 3>>
<<default>>
<<set $pgen = 0>>
<</switch>>
<</nobr>><</widget>>
/* <<SetGender>> widget - End */
/* <<checkboxPlus>> widget
This widget allows you to display a custom checkbox which sets a
SugarCube variable, displays a (clickable) label, and satisfies
accessibility guidelines for users with impairments (usable via the
keyboard with TAB, SHIFT+TAB, and SPACE keys). The checkboxes are
also larger, to make them easier to see and to click on for mobile
devices.
Usage: <<checkboxPlus "variableName" "text" ["className"]>>
The value of the checkbox would then be tied to a variable, which
is passed to the widget as a string. All story variables passed to
the widget will be set to either a Boolean true or false. If the
variable had a "truthy" value, then the checkbox will be checked.
The "className" is an optional parameter, which adds that CSS class
to the text.
Example: <<checkboxPlus "$EnabledOp" "Enable Option" "blueText">>
VARIABLES USED: _cbStyle, _checkboxData, _checkboxIDno
*/
<<widget "checkboxPlus">><<nobr>>
/* Make sure the variable passed in is a boolean. */
<<set State.setVar(_args[0], !!State.getVar(_args[0]))>>
<<if ndef _checkboxIDno>>
/* Start checkbox IDs at 1. */
<<set _checkboxIDno = 1>>
<<else>>
/* Next checkbox ID. */
<<set _checkboxIDno++>>
<</if>>
<<set _checkboxData = "'" + _args[0] + "'">>
<<if def _args[2]>>
<<set _cbStyle = " " + _args[2]>>
<<else>>
<<set _cbStyle = "">>
<</if>>
/* Display checkbox. */
<span class="chkbox" tabindex="0" onkeypress="if ((event.key == ' ') || (event.key == 'Spacebar')) { $(this).find('input[type=\'checkbox\']').trigger('click'); return false; }">
<<print '<input type="checkbox" id="checkbox_' + _checkboxIDno + '" tabindex="-1" class="cbhidden" onchange="SugarCube.State.setVar(' + _checkboxData + ', this.checked)"' + (State.getVar(_args[0]) ? ' checked' : '') + '>'>>
<label @for="'checkbox_' + _checkboxIDno" @class="'chklabel' + _cbStyle">
_args[1]
</label>
</span>
<</nobr>><</widget>>
/* <<checkboxPlus>> Widget - End */
/* <<toggleLink>> Widget - Start
VARIABLES USED: _destAddr, _linkStr, _tlink
*/
<<widget "toggleLink">><<nobr>>
<<if ndef _tlink>>
<<set _tlink = 1>>
<<else>>
<<set _tlink++>>
<</if>>
<<set _destAddr = _args[1] ? _args[1] : _args[0]>>
<<set _linkStr = "$('.togglelink[id!=\\'tlink" + _tlink + "\\']')">>
<span @id="'tlink' + _tlink" class="togglelink"
@onmouseenter="_linkStr + '.addClass(\'disabled\')'"
@onmouseleave="_linkStr + '.removeClass(\'disabled\')'">
<<link _args[0] _destAddr>><</link>>
</span>
<</nobr>><</widget>>
/* <<toggleLink>> Widget - End */
/* <<countdownTimer>> Widget - Start
VARIABLES USED: _ctminutes, _ctseconds, _replacementPassage
*/
<<widget "countdownTimer">><<nobr>>
<<set _ctseconds = _args[0]>>
<<set _ctminutes = Math.floor(_ctseconds / 60)>>
<<set _replacementPassage = _args[1]>>
<div id="timer" class="timergreen">Time remaining _ctminutes:<<= (_ctseconds - (_ctminutes * 60)).toString().padStart(2, '0')>></div><<script>>
if (!recall("countdown", undefined)) {
setup.countdown = { startTime: new Date(), lastStr: "", passage: passage() };
memorize("countdown", setup.countdown);
} else {
setup.countdown = recall("countdown");
if (setup.countdown.passage !== passage()) {
setup.countdown = { startTime: new Date(), lastStr: "", passage: passage() };
memorize("countdown", setup.countdown);
}
}
setup.countdown.intervalID = setInterval(function () {
if (setup.countdown.passage !== passage()) {
clearInterval(setup.countdown.intervalID);
forget("countdown");
setup.countdown.passage = "";
} else {
let curtime = new Date(), str, seconds = State.temporary.ctseconds;
let diff = Math.floor(seconds - ((curtime - setup.countdown.startTime) / 1000)), min = Math.floor(diff / 60);
if ((diff >= 0) && (diff < seconds)) {
if ($("#timer").length) {
str = "Time remaining " + min + ":" + (diff - (min * 60)).toString().padStart(2, '0');
if (str != setup.countdown.lastStr) {
$("#timer").empty().wiki(str);
setup.countdown.lastStr = str;
}
if (diff <= 10) {
if (!$("#timer").hasClass("timerred")) {
$("#timer").removeClass("timeramber").addClass("timerred");
}
} else if (diff <= 20) {
if (!$("#timer").hasClass("timeramber")) {
$("#timer").removeClass("timergreen").addClass("timeramber");
}
} else {
if (!$("#timer").hasClass("timergreen")) {
$("#timer").removeClass("timeramber timerred").addClass("timergreen");
}
}
}
}
if (diff < 0) {
clearInterval(setup.countdown.intervalID);
forget("countdown");
$("#passages div.passage").empty().wiki('<<include "' + State.temporary.replacementPassage + '">>');
delete setup.countdown.passage;
}
}
}, 200);
<</script>>
<</nobr>><</widget>>
/* <<countdownTimer>> Widget - End */
/* <<textboxPlus>> widget v1.3.2 - Start
Usage:
<<textboxPlus "Label: " "$variableName" `{
default: "Default value",
passage: "Passage name",
placeholder: "Placeholder text",
maxlength: 10,
spellcheck: false,
autofocus: true,
autocomplete: "off",
password: true,
readonly: true,
disabled: true,
onchange: "<<run alert('Text was changed.')>>",
oninput: "<<run alert('Input event triggered.')>>",
onreturn: "<<run alert('User hit RETURN.')>>"
}`>>
NOTE: If you put a space as the last character for the label then, instead
of the textbox appearing to the right of the label, the textbox will
appear on the line BELOW the label. Also, all of the options shown
within the third parameter above (after "$variableName") are optional.
For a list of all "autocomplete" options see:
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
VARIABLES USED: _textboxPlusName, _textboxPlusOptions
*/
<<widget "textboxPlus">><<nobr>>
<<if (_args[1][0] !== "$") && (_args[1][0] !== "_")>>
/* Show error message for bad variable name. */
<span class="errmsg" data-msg="<<textboxPlus>> - Invalid variable name." @data-src="_args[1]"></span>
<<run $(document).one(":passagerender",
function (ev) {
$(ev.content).find(".errmsg").each(function (idx) {
throwError($(this), $(this).data("msg"), $(this).data("src"));
});
}
)>>
<<else>>
/* Create textboxPlus input box. */
<<if _args[1][0] === "$">>
<<set _textboxPlusName = "textbox-" + _args[1].substr(1).toLowerCase()>>
<<else>>
<<set _textboxPlusName = "textbox--" + _args[1].substr(1).toLowerCase()>>
<</if>>
<<if ndef _args[2]>>
<<set _textboxPlusOptions = {}>>
<<else>>
<<set _textboxPlusOptions = _args[2]>>
<</if>>
<<if ndef _textboxPlusOptions.placeholder>>
<<set _textboxPlusOptions.placeholder = "">>
<</if>>
<<if ndef _textboxPlusOptions.maxlength>>
<<set _textboxPlusOptions.maxlength = "">>
<</if>>
<<if ndef _textboxPlusOptions.spellcheck>>
<<set _textboxPlusOptions.spellcheck = true>>
<</if>>
<<if ndef _textboxPlusOptions.autocomplete>>
<<set _textboxPlusOptions.autocomplete = "">>
<</if>>
<<if ndef _textboxPlusOptions.password>>
<<set _textboxPlusOptions.password = "">>
<</if>>
<<if ndef _textboxPlusOptions.readonly>>
<<set _textboxPlusOptions.readonly = "">>
<</if>>
<<if ndef _textboxPlusOptions.disabled>>
<<set _textboxPlusOptions.disabled = "">>
<</if>>
<<if ndef _textboxPlusOptions.onchange>>
<<set _textboxPlusOptions.onchange = "">>
<</if>>
<<if ndef _textboxPlusOptions.oninput>>
<<set _textboxPlusOptions.oninput = "">>
<</if>>
<<if ndef _textboxPlusOptions.onreturn>>
<<set _textboxPlusOptions.onreturn = "">>
<</if>>
<span class="textboxplus" @data-variable="_args[1]" @data-placeholder="_textboxPlusOptions.placeholder" @data-maxlength="_textboxPlusOptions.maxlength" @data-spellcheck="_textboxPlusOptions.spellcheck" @data-autocomplete="_textboxPlusOptions.autocomplete" @data-password="_textboxPlusOptions.password" @data-readonly="_textboxPlusOptions.readonly" @data-disabled="_textboxPlusOptions.disabled" @data-onchange="_textboxPlusOptions.onchange" @data-oninput="_textboxPlusOptions.oninput" @data-onreturn="_textboxPlusOptions.onreturn">
<label @for="_textboxPlusName">_args[0]</label>
<<if _args[0].length > 0 && _args[0][_args[0].length - 1] === " ">>
<br>
<</if>>
<<if ndef _textboxPlusOptions.default>>
<<set _textboxPlusOptions.default = "">>
<</if>>
<<if ndef _textboxPlusOptions.passage>>
<<if _textboxPlusOptions.autofocus>>
<<textbox _args[1] _textboxPlusOptions.default autofocus>>
<<else>>
<<textbox _args[1] _textboxPlusOptions.default>>
<</if>>
<<else>>
<<if _textboxPlusOptions.autofocus>>
<<textbox _args[1] _textboxPlusOptions.default _textboxPlusOptions.passage autofocus>>
<<else>>
<<textbox _args[1] _textboxPlusOptions.default _textboxPlusOptions.passage>>
<</if>>
<</if>>
</span>
<</if>>
<</nobr>><</widget>>
<<script>>
$(document).on(":passagerender", function (event) {
/* Update textboxPlus input boxes. */
$(event.content).find(".textboxplus").each(function () {
let options = {}, props = {};
let data = $(this).data("placeholder");
if (data) {
options.placeholder = data;
}
data = $(this).data("maxlength");
if (data) {
options.maxlength = data;
}
data = $(this).data("spellcheck");
if (data.toString().toLowerCase() === "false") {
options.spellcheck = "false";
}
data = $(this).data("autocomplete");
if (data) {
options.autocomplete = data;
}
data = $(this).data("password");
if (data) {
props.type = "password";
}
data = $(this).data("readonly");
if (data) {
props.readonly = data;
}
data = $(this).data("disabled");
if (data) {
props.disabled = data;
}
$(this).find("input").each(function () {
if (props.type) {
$(this).removeProp("type").attr(options).prop(props);
} else {
$(this).attr(options).prop(props);
}
});
let changeCode = $(this).data("onchange");
if (changeCode) {
$(this).find("input").on("change", function (event) {
$.wiki(changeCode);
});
}
let inputCode = $(this).data("oninput"), parent = this;
if (inputCode) {
$(this).find("input").on("input", function (event) {
State.setVar($(parent).data("variable"), $(this).val());
$.wiki(inputCode);
});
}
let returnCode = $(this).data("onreturn");
if (returnCode) {
$(this).on("keyup", function (event) {
if (event.key === "Enter") {
$.wiki(returnCode);
}
});
}
});
});
<</script>>
/* <<textboxPlus>> widget - End */
/* <<SetPronouns>> widget
Usage... (defaults to male)
for "he": <<SetPronouns>>
for "she": <<SetPronouns "f">>
for "they": <<SetPronouns "b">>
for "it": <<SetPronouns "n">>
*/
<<widget "SetPronouns">><<nobr>>
<<switch $args[0]>>
<<case "f">>
<<set $they = "she">>
<<set $them = "her">>
<<set $themself = "herself">>
<<set $themselves = "themselves">>
<<set $their = "her">>
<<set $theirs = "hers">>
<<set $theyre = "she's">>
<<set $youngPerson = "girl">>
<<set $youngPeople = "girls">>
<<set $adultPerson = "woman">>
<<set $adultPeople = "women">>
<<set $generalPerson = "girl">>
<<set $generalPeople = "girls">>
<<set $pal = "chick">>
<<set $pals = "chicks">>
<<set $They = "She">>
<<set $Them = "Her">>
<<set $Themself = "Herself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Her">>
<<set $Theirs = "Hers">>
<<set $Theyre = "She's">>
<<set $YoungPerson = "Girl">>
<<set $YoungPeople = "Girls">>
<<set $AdultPerson = "Woman">>
<<set $AdultPeople = "Women">>
<<set $GeneralPerson = "Girl">>
<<set $GeneralPeople = "Girls">>
<<set $Pal = "Chick">>
<<set $Pals = "Chicks">>
<<case "b">>
<<set $they = "they">>
<<set $them = "them">>
<<set $themself = "themself">>
<<set $themselves = "themselves">>
<<set $their = "their">>
<<set $theirs = "theirs">>
<<set $theyre = "they're">>
<<set $youngPerson = "person">>
<<set $youngPeople = "people">>
<<set $adultPerson = "person">>
<<set $adultPeople = "people">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "pal">>
<<set $pals = "pals">>
<<set $They = "They">>
<<set $Them = "Them">>
<<set $Themself = "Themself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Their">>
<<set $Theirs = "Theirs">>
<<set $Theyre = "They're">>
<<set $YoungPerson = "Person">>
<<set $YoungPeople = "People">>
<<set $AdultPerson = "Person">>
<<set $AdultPeople = "People">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Pal">>
<<set $Pals = "Pals">>
<<case "n">>
<<set $they = "it">>
<<set $them = "it">>
<<set $themself = "itself">>
<<set $themselves = "themselves">>
<<set $their = "its">>
<<set $theirs = "its">>
<<set $theyre = "it's">>
<<set $youngPerson = "person">>
<<set $youngPeople = "people">>
<<set $adultPerson = "person">>
<<set $adultPeople = "people">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "pal">>
<<set $pals = "pals">>
<<set $They = "It">>
<<set $Them = "It">>
<<set $Themself = "Itself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Its">>
<<set $Theirs = "Its">>
<<set $Theyre = "It's">>
<<set $YoungPerson = "Person">>
<<set $YoungPeople = "People">>
<<set $AdultPerson = "Person">>
<<set $AdultPeople = "People">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Pal">>
<<set $Pals = "Pals">>
<<default>>
<<set $they = "he">>
<<set $them = "him">>
<<set $themself = "himself">>
<<set $themselves = "themselves">>
<<set $their = "his">>
<<set $theirs = "his">>
<<set $theyre = "he's">>
<<set $youngPerson = "boy">>
<<set $youngPeople = "boys">>
<<set $adultPerson = "man">>
<<set $adultPeople = "men">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "dude">>
<<set $pals = "dudes">>
<<set $They = "He">>
<<set $Them = "Him">>
<<set $Themself = "Himself">>
<<set $Themselves = "Themselves">>
<<set $Their = "His">>
<<set $Theirs = "His">>
<<set $Theyre = "He's">>
<<set $YoungPerson = "Boy">>
<<set $YoungPeople = "Boys">>
<<set $AdultPerson = "Man">>
<<set $AdultPeople = "Men">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Dude">>
<<set $Pals = "Dudes">>
<</switch>>
<</nobr>><</widget>>
/* <<SetPronouns>> widget - End */
/* --- Internal use widgets --- */
<<widget "mom">>Dad<</widget>>
/* SugarCube Icons passage widgets - Start */
<<widget "updateIcons">><<nobr>>
<<set _txt = "", _unselectedIcons = []>>
<<set _csstxt = '/* SugarCube\'s built-in Font Awesome icons - Start */\n.sc-fa {\n font-family: sc-icons;\n font-style: normal;\n font-weight: normal;\n font-variant: normal;\n text-transform: none;\n line-height: 1;\n speak: none;\n}\n.sc-icon {\n padding-left: 2px;\n padding-right: 2px;\n}\n.sc-icon::before {\n font-family: sc-icons;\n font-style: normal;\n font-weight: normal;\n font-variant: normal;\n text-transform: none;\n line-height: 1;\n speak: none;\n}\n'>>
<<set _searchItems = []>>
<<if _searchText != "">>
<<set _searchItems = _searchText.split(" ")>>
<</if>>
<<for _key, _val range _icons>>
<<if $selectedIcons.includes(_key)>>
<<set _txt += '<span class="iconbox" title="' + _key + '"><span class="sc-icon sc-' + _key + '" alt="' + _key + '"></span><span class="iconname">' + _key + '</span></span>'>>
<<set _csstxt += '.sc-' + _key + '::before {\n content: "\\f' + _val.id + '";\n}\n'>>
<<else>>
<<if _searchItems.length === 0>>
<<run _unselectedIcons.push(_key)>>
<<else>>
<<set _numMatches = 0>>
<<for _i = 0; _i < _val.desc.length; _i++>>
<<for _j = 0; _j < _searchItems.length; _j++>>
<<if _j < _searchItems.length - 1>>
<<if _val.desc[_i] === _searchItems[_j]>>
<<set _numMatches++>>
<</if>>
<<else>>
<<if (_searchItems[_j] != "") && (_val.desc[_i].indexOf(_searchItems[_j]) === 0)>>
<<set _numMatches++>>
<</if>>
<</if>>
<</for>>
<</for>>
<<if _numMatches>>
<<run _unselectedIcons.push({ key: _key, num: _numMatches })>>
<</if>>
<</if>>
<</if>>
<</for>>
<<if (_searchText.trim() != "") && (_unselectedIcons.length > 0)>>
<<run _unselectedIcons.sort(function (a, b) { return b.num - a.num; })>>
<<set _unselectedIcons = _unselectedIcons.map(function (el) { return el.key; })>>
<</if>>
<<run $("#selectedicons").html(_txt)>>
<<set _csstxt += '/* SugarCube\'s built-in Font Awesome icons - End */'>>
<<run $("#csscode").html(_csstxt)>>
<<set _txt = "">>
<<for _val range _unselectedIcons>>
<<set _txt += '<span class="iconbox" title="' + _val + '"><span class="sc-icon sc-' + _val + '" alt="' + _val + '"></span><span class="iconname">' + _val + '</span></span>'>>
<</for>>
<<run $("#unselectedicons").html(_txt)>>
<<run setup.activateIcons()>>
/*
$selectedIcons
_searchText
_icons["star-solid"] = { id: "00", desc: ["star", "*"]}
#unselectedicons
#selectedicons
#csscode
*/
<</nobr>><</widget>>
/* SugarCube Icons passage widgets - End */
/* Fullscreen passage widgets - Start */
<<widget "PrintType">><<nobr>>
<<set _n = _args[0]>>
<<if ndef _n>><<set _n = "@@.invert;undefined@@">><</if>>
<<if _n === null>><<set _n = "@@.invert;null@@">><</if>>
<<if Number.isNaN(_n)>><<set _n = "@@.invert;NaN@@">><</if>>
<<if _n === "">><<set _n = "@@.invert;(empty string)@@">><</if>>
<<if !!_n && typeof _n === "object">><<set _n = "[Object " + _n.constructor.name + "]">><</if>>
<<= _n>>
<</nobr>><</widget>>
<<widget "HighlightTrue">><<nobr>>
<<if _args[0]>>
@@.invert;true@@
<<else>>
false
<</if>>
<</nobr>><</widget>>
/* Fullscreen passage widgets - End */
/* Main menu widgets - Start */
<<widget "showLinks">><<nobr>>
<<set _category = _args[0]>>
<<for _i = 0; _i < setup.passages[_category].length; _i++>>
<<set _unread = "">>
<<if setup.passages[_category][_i].new>>
<<set _unread = " - <span style='fill: gold'>''<<=setup.star>>NEW!<<=setup.star>>''</span>">>
<<elseif setup.passages[_category][_i].updated>>
<<set _unread = " - <span style='fill: gold'>''<<=setup.star>>Updated!<<=setup.star>>''</span>">>
<<elseif setup.passages[_category][_i].unread>>
<<set _unread = " - ''<<=setup.star>>Unread''">>
<</if>>
<<if setup.passages[_category][_i].link>>
<li><<link setup.passages[_category][_i].link setup.passages[_category][_i].title>><</link>> (<<= setup.passages[_category][_i].type>>) _unread</li>
<<else>>
<li><<link setup.passages[_category][_i].title setup.passages[_category][_i].title>><</link>> (<<= setup.passages[_category][_i].type>>) _unread</li>
<</if>>
<</for>>
<</nobr>><</widget>>
<<widget "catInfo">><<nobr>>
<<set _category = _args[0], _unread = 0, _updated = 0, _new = 0>>
<<for _i = 0; _i < setup.passages[_category].length; _i++>>
<<if setup.passages[_category][_i].unread>>
<<set _unread++>>
<<set _noUnread = false>>
<</if>>
<<if setup.passages[_category][_i].updated>>
<<set _updated++>>
<</if>>
<<if setup.passages[_category][_i].new>>
<<set _new++>>
<</if>>
<</for>>
<<set _txt = "">>
<<if _unread>>
<<set _txt = "(" + _unread + " Unread">>
<<if _updated>>
<<set _txt += ", " + _updated + " Updated">>
<</if>>
<<if _new>>
<<set _txt += ", " + _new + " New">>
<</if>>
<<set _txt += ")">>
<</if>>
_txt
<</nobr>><</widget>>
/* Main menu widgets - End */<h1>{{{<<print>>}}} Macro Differences</h1>This code shows the differences in output from using "<a href="http://www.motoslave.net/sugarcube/2/docs/#markup-naked-variable" target="_blank">naked variables</a>", the {{{<<print>>}}} macro, the {{{<<=>>}}} macro, and the {{{<<->>}}} macro.
Additionally, this shows the results for the custom {{{<<raw>>}}} macro, which can be used for debugging purposes. See <<link "the code for the {{{<<raw>>}}} macro">><<ScrollTo "code">><</link>> at the bottom of this page.
As you can see below, the {{{<<print>>}}} macro and the {{{<<=>>}}} macro are equivalent in all cases, and show the test value affected by any markup within that value. "Naked variables" are equivalent to them in all //simple// cases, but fail in more complex cases (such as the "Son" and "Daughter" examples). And the {{{<<->>}}} macro is equivalent to them in all cases, except for those which contain HTML or Twine markup (such as the first two cases).
<<set $test = "Hi, <<mom>>.">>
Code: {{{<<set $test = "Hi, <<mom>>.">>}}} (widget test; where {{{<<mom>>}}} is a widget that just prints the word "Dad")
{{{$test}}}: $test
{{{<<print $test>>}}}: <<print $test>>
{{{<<= $test>>}}}: <<= $test>>
{{{<<- $test>>}}}: <<- $test>>
{{{<<raw $test>>}}}: <<raw $test>>
<<set $test = "Hi, <a>Mom</a>.">>
Code: {{{<<set $test = "Hi, <a>Mom</a>.">>}}} (HTML test)
{{{$test}}}: $test
{{{<<print $test>>}}}: <<print $test>>
{{{<<= $test>>}}}: <<= $test>>
{{{<<- $test>>}}}: <<- $test>>
{{{<<raw $test>>}}}: <<raw $test>>
<<set $test = { test: "Hi, Bro." }>>
Code: {{{<<set $test = { test: "Hi, Bro." }>>}}} (object test)
{{{$test.test}}}: $test.test
{{{<<print $test.test>>}}}: <<print $test.test>>
{{{<<= $test.test>>}}}: <<= $test.test>>
{{{<<- $test.test>>}}}: <<- $test.test>>
{{{<<raw $test.test>>}}}: <<raw $test.test>>
<<set $test = ["Hi, Sis."]>>
Code: {{{<<set $test = ["Hi, Sis."]>>}}} (array test)
{{{$test[0]}}}: $test[0]
{{{<<print $test[0]>>}}}: <<print $test[0]>>
{{{<<= $test[0]>>}}}: <<= $test[0]>>
{{{<<- $test[0]>>}}}: <<- $test[0]>>
{{{<<raw $test[0]>>}}}: <<raw $test[0]>>
<<set setup.test = ["Hi, Son."]>>
Code: {{{<<set setup.test = ["Hi, Son."]>>}}} ("setup" object test)
{{{setup.test}}}: setup.test
{{{<<print setup.test>>}}}: <<print setup.test>>
{{{<<= setup.test>>}}}: <<= setup.test>>
{{{<<- setup.test>>}}}: <<- setup.test>>
{{{<<raw setup.test>>}}}: <<raw setup.test>>
<<set $test = "Hi, Daughter.">>
Code: {{{<<set $test = "Hi, Daughter.">>}}} (function test)
{{{$test.toUpperCase()}}}: $test.toUpperCase()
{{{<<print $test.toUpperCase()>>}}}: <<print $test.toUpperCase()>>
{{{<<= $test.toUpperCase()>>}}}: <<= $test.toUpperCase()>>
{{{<<- $test.toUpperCase()>>}}}: <<- $test.toUpperCase()>>
{{{<<raw $test.toUpperCase()>>}}}: <<raw $test.toUpperCase()>>
<<set _name = "stranger">><<set $test = "Howdy, _name.">>
<span id="code">Code:</span> {{{<<set _name = "stranger">><<set $test = "Howdy, _name.">>}}} (variable name in a variable test)
{{{$test}}}: $test
{{{<<print $test>>}}}: <<print $test>>
{{{<<= $test>>}}}: <<= $test>>
{{{<<- $test>>}}}: <<- $test>>
{{{<<raw $test>>}}}: <<raw $test>>
As you can see in that last example, only the {{{<<raw>>}}} macro would show you that the {{{$test}}} variable actually holds the {{{_name}}} variable. This makes it quite useful for debugging variables, to make sure that your variables actually hold the values that you think they hold.
To use the {{{<<raw>>}}} macro in your game, simply add the following code to your JavaScript section:
{{{
/* raw macro - Start */
Macro.add("raw", {
skipArgs : true,
handler : function () {
function reparse (str) {
return str.toString().replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
if (this.args.full.length > 0) {
if (["$", "_"].includes(this.args.raw[0])) {
try {
$(this.output).append(reparse(State.getVar(this.args.raw)));
} catch(error) {
$(this.output).append(reparse(this.args.raw));
}
} else if (this.args.raw.indexOf("setup") === 0) {
try {
$(this.output).append(reparse(eval(this.args.raw)));
} catch(error) {
$(this.output).append(reparse(this.args.raw));
}
} else {
$(this.output).append(reparse(this.args.raw));
}
}
}
});
/* raw macro - End */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
In the macro, anything included after {{{<<raw }}}, all the way up to the first {{{>>}}} part, is treated as a single parameter. If the parameter passed to the {{{<<raw>>}}} macro starts witha {{{$}}}, {{{_}}}, or {{{setup}}}, then it will attempt to give you the raw value of that parameter, assuming it's a variable. Otherwise, it will output the whole parameter as raw HTML output.
Any {{{<}}}s in the output will be converted to {{{<}}} ("<") and any {{{>}}}s will be converted to {{{>}}} (">") in order to avoid creating actual HTML elements.
If the parameters would end with a "{{{>}}}", then you need to put a space after it to prevent the macro from being ended prematurely by the first "{{{>>}}}". For example:
{{{
<<raw <strong>$test</strong> >>
}}}
will be displayed like this: <<raw <strong>$test</strong> >>
To show another example, this:
{{{
<<raw This is a $test.>>
}}}
will output this: <<raw This is a $test.>>
If you want it to instead show the raw //value// of the {{{$test}}} variable, then you have to put that variable by itself as the only parameter, like this:
{{{
<<set _name = "stranger">><<set $test = "Howdy, _name.">>
Then I said, "<<raw $test>>"
Then I said, "<<print $test>>"
}}}
which would output this:
Then I said, "<<raw $test>>"
Then I said, "<<print $test>>"
Nonexistent variables will simply be displayed as raw text:
{{{<<raw $xyz>>}}}: <<raw $xyz>>
{{{<<raw _xyz>>}}}: <<raw _xyz>>
{{{<<raw setupxyz>>}}}: <<raw setupxyz>>
{{{<<raw setup.xyz>>}}}: <<raw setup.xyz>>
Hope that helps!<h1>Font Sizing Buttons Code</h1>If you'd like to add buttons to the UI bar that lets people increase or decrease the font size, then this section shows you how to do this. The first example uses separate PNG image files and the second example uses embedded SVG images.
For an example, you can click these buttons to grow or shrink the font:
<img @src="setup.ImagePath+'GrowF.png'" alt="Larger font" title="Larger font" class="fullscreenImg" style="position: inherit;" onclick="fontSize(1)"> <img @src="setup.ImagePath+'ShrinkF.png'" alt="Smaller font" title="Smaller font" class="fullscreenImg" style="position: inherit;" onclick="fontSize(-1)">
That requires these two files:
* images/GrowF.png
* images/ShrinkF.png
To make that work in the UI bar, you'd need to put this in your "{{{StoryCaption}}}" passage using external image files:
{{{
<img @src="setup.ImagePath+'GrowF.png'" alt="Larger font" title="Larger font" class="fullscreenImg" style="top: 70px;" onclick="fontSize(1)" tabindex="0"><img @src="setup.ImagePath+'ShrinkF.png'" alt="Smaller font" title="Smaller font" class="fullscreenImg" style="top: 100px;" onclick="fontSize(-1)" tabindex="0">
}}}
You can adjust their vertical position on the UI bar by changing the {{{top}}} position in the {{{style}}} attributes of these two images.
The above code assumes "{{{setup.ImagePath}}}" is set to your image path (e.g. {{{setup.ImagePath = "images/"}}}). See the JavaScript in the [[Displaying Images in Twine]] for how this is useful.
Alternately, instead of using external images files like in the above code, you could use SVG images embedded directly in the code itself, like these:
<img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" opacity="0.5" fill="white" /></svg>' alt="Larger font" title="Larger font" class="fontSizeImg" style="position: inherit;" onclick="fontSize(1)" tabindex="0"> <img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" opacity="0.5" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" fill="white" /></svg>' alt="Smaller font" title="Smaller font" class="fontSizeImg" style="position: inherit;" onclick="fontSize(-1)" tabindex="0">
So the alternative "StoryCaption" code for embedded SVG images would be this:
{{{
<img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" opacity="0.5" fill="white" /></svg>' alt="Larger font" title="Larger font" class="fontSizeImg" style="top: 70px;" onclick="fontSize(1)" tabindex="0"> <img src='data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m456 512h-400c-30 0-56-26-56-56v-400c0-30 26-56 56-56h400c30 0 56 26 56 56v400c0 30-27 56-56 56zm-5-471h-390c-14 0-20 10-20 20v390c0 19 15 20 20 20h390c12 0 20-7 20-20v-390c0-17-11-20-20-20z" fill="white" /><path d="m163 79v163h10c15 0 30-2 37-5 18-9 22-19 25-33h8v97h-8c-3-15-14-27-25-32-12-5-20-5-37-5h-10v123c0 20 1 33 3 39 2 5 5 9 11 14 5 4 12 6 19 6h9v11h-133v-11h8c8 0 14-2 18-6 3-2 6-7 8-14 2-5 2-18 2-38v-264c0-20-1-33-2-39-3-13-15-19-26-19h-8c-1 0 0-11 0-11h220v116h-8c-2-27-6-47-14-60s-17-22-32-27c-8-4-25-5-48-5z" opacity="0.5" fill="white" /><path d="m271 155v129h8c12 0 24-2 30-4 14-7 18-15 20-26h6v78h-6c-2-12-11-22-20-26-10-4-16-4-30-4h-8v99c0 16 1 26 2 31 2 4 4 7 9 11 4 3 10 5 15 5h7v9h-106v-9h6c6 0 11-2 14-5 2-2 5-6 6-11 2-4 2-14 2-30v-211c0-16-1-26-2-31-2-10-12-15-21-15h-6c-1 0 0-9 0-9h176v93h-6c-2-22-5-38-11-48s-14-18-26-22c-6-3-20-4-38-4z" opacity="0.75" fill="white" /><path d="m361 217v102h6c10 0 19-1 23-3 11-6 14-12 16-21h5v62h-5c-2-10-9-17-16-20-8-3-13-3-23-3h-6v78c0 13 1 21 2 25 1 3 3 6 7 9 3 3 8 4 12 4h6v7h-84v-7h5c5 0 9-1 11-4 2-1 4-4 5-9 1-3 1-11 1-24v-168c0-13-1-21-1-25-2-8-10-12-17-12h-5c-1 0 0-7 0-7h140v74h-5c-1-17-4-30-9-38s-11-14-20-17c-5-3-16-3-30-3z" fill="white" /></svg>' alt="Smaller font" title="Smaller font" class="fontSizeImg" style="top: 100px;" onclick="fontSize(-1)" tabindex="0">
}}}
Either way, you'll also need this in your JavaScript section:
{{{
// fontSize: Increase or decrease passage font size by "value" pixels.
window.fontSize = function(value) {
$("#story").css("font-size", Math.max(1, parseInt($("#story").css("font-size")) + value) + "px");
};
}}}
and you'll also need this in your Stylesheet section:
{{{
/* Font Size Buttons - Start */
.fontSizeImg {
cursor: pointer;
transition: 0.3s;
border-radius: 3px;
position: absolute;
top: 40px;
left: 252px;
height: 25px;
vertical-align: text-bottom;
visibility: visible;
}
.fontSizeImg:hover, .fontSizeImg:focus {
background: #aaa;
}
#story {
margin-left: 320px;
margin-right: 40px;
}
@media screen and (max-width: 1136px) {
#story {
margin-left: 290px;
margin-right: 24px;
}
#ui-bar.stowed~#story {
margin-left: 42px;
}
}
/* Font Size Buttons - End */
}}}
<h1>Browser Test</h1>This code allows you to detect what browser your code is running in, and whether it was launched from the Twine development environment or not.
Here's what the relevent functions detect currently:
''isAndroid:'' <<HighlightTrue setup.Engine.isAndroid>>
''isBlackBerry:'' <<HighlightTrue setup.Engine.isBlackBerry>>
''isChrome:'' <<HighlightTrue setup.Engine.isChrome>>
''isEdge:'' <<HighlightTrue setup.Engine.isEdge>>
''isFirefox:'' <<HighlightTrue setup.Engine.isFirefox>>
''isIE:'' <<HighlightTrue setup.Engine.isIE>>
''isiOS:'' <<HighlightTrue setup.Engine.isiOS>>
''isOpera:'' <<HighlightTrue setup.Engine.isOpera>>
''isSafari:'' <<HighlightTrue setup.Engine.isSafari>>
''isVivaldi:'' <<HighlightTrue setup.Engine.isVivaldi>>
''isMobile:'' <<HighlightTrue setup.Engine.isMobile>>
''isTwine:'' <<HighlightTrue setup.Engine.isTwine>>
''User Agent:'' <<=navigator.userAgent>>
Once you've added the engine detection code (below) to your game's JavaScript section, you can then write code like this:
{{{
<<if setup.Engine.isTwine>>
Launched from the Twine editor.
<</if>>
}}}
The text within that {{{<<if>>}}} will only be displayed if the game was launched from within the Twine editor. This can be handy for debugging or other purposes.
''NOTE:'' If you're using this code on an OS other than Windows, you may need to tweak the {{{isTwine}}} code below to properly detect when the HTML is being launched from that OS's "temp" directory. (I.e. change {{{"/temp/"}}} to detect a part of the URL which indicates that OS's "temp" directory.)
JavaScript section:
{{{
/* Engine detection code - Start */
setup.Engine = {
// Opera 8.0+ & Opera Touch
isOpera: (!!window.opr && !!opr.addons) || !!window.opera || (navigator.userAgent.indexOf(' OPR/') >= 0) || (navigator.userAgent.indexOf(' OPT/') >= 0),
// Firefox 1.0+ & Firefox for iOS
isFirefox: (typeof InstallTrigger !== 'undefined') || (navigator.userAgent.indexOf(' FxiOS/') >= 0),
// Safari 3.0+ "[object HTMLElementConstructor]"
isSafari: /constructor/i.test(window.HTMLElement) || (function(p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.safari || (typeof safari !== 'undefined' && safari.pushNotification)) || (/iP(ad|hone|od).+Version\/[\d\.]+.*Safari/i.test(navigator.userAgent)), // eslint-disable-line no-useless-escape
// Internet Explorer 6-11
isIE: /*@cc_on!@*/false || !!document.documentMode, // eslint-disable-line no-constant-binary-expression
// Android engine detection
isAndroid: Browser.isMobile.Android,
// iOS engine detection
isiOS: Browser.isMobile.iOS,
// BlackBerry engine detection
isBlackBerry: Browser.isMobile.BlackBerry,
// Vivaldi browser
isVivaldi: navigator.userAgent.toLowerCase().includes('vivaldi'),
// Twine engine detection
isTwine: document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat") /* Assumes Windows and Mac default temp paths. */
};
// Edge 20+
setup.Engine.isEdge = (!setup.Engine.isIE && !!window.StyleMedia) || (navigator.userAgent.indexOf(' Edg/') >= 0);
// Chrome 1+ & "Headless" Chrome
setup.Engine.isChrome = (!!window.chrome || (navigator.userAgent.indexOf(' HeadlessChrome/') >= 0)) && !setup.Engine.isOpera && !setup.Engine.isVivaldi;
// Mobile engine detection
setup.Engine.isMobile = ( setup.Engine.isAndroid || setup.Engine.isiOS || setup.Engine.isBlackBerry || Browser.isMobile.Windows || navigator.userAgent.toLowerCase().includes('windows phone') || navigator.userAgent.toLowerCase().includes('opera mini') );
/* Engine detection code - End */
}}}
<h1>Object Tests</h1>There are multiple kinds of objects in JavaScript, such as an Array object, a Date object, a Map object, a Set object, or a generic object. So how do you tell one object apart from another? Or tell a function apart from an object?
Here are some examples of different types of objects and how to tell them apart using various JavaScript functions.
The code for each of these sections follow this pattern (each section is the same except for the first line):
{{{
<<set $test = {}>>''{}'': (a generic object)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
}}}
<<set $test = {}>>''{}'': (a generic object)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
<<set $test = []>>''[]'': (an array)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
<<set $test = new Date('August 19, 1975 23:15:30')>>''new Date()'': (a Date object)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
<<set $test = new Map()>>''new Map()'': (a Map object)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
<<set $test = new Set()>>''new Set()'': (a Set object)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
<<set $test = window.isGenericObject>>''window.isGenericObject'': (a function)
isGenericObject() = <<- isGenericObject($test)>>
isObject() = <<- isObject($test)>>
variableType() = <<- variableType($test)>>
typeof = <<- typeof $test>>
Object.prototype.toString.call() = <<- Object.prototype.toString.call($test)>>
The above methods use the following JavaScript code:
{{{
// "Object Tests" functions
window.isGenericObject = function (Value) {
return !!Value && typeof Value === 'object' && Value.constructor === Object;
};
window.isObject = function (Value) {
return !!Value && typeof Value === 'object';
};
window.variableType = function (Value) {
return isObject(Value) ? Object.prototype.toString.call(Value).slice(8, -1) : typeof Value;
};
}}}
Add the above to your own project's JavaScript section if you want to use those functions.<h1>Keyboard Link Navigation</h1>If you want links to be automatically marked so that you can navigate them using the keyboard, you can use the JavaScript code below. Here are the keys you can use with this code:
* If there is only one link in a passage, you can use the {{{1}}} or {{{→}}} keys to go to the next passage.
* If there are anywhere from 2 to 10 links in a passage, the links will get marked like this<sup>[1]</sup>, and you can use the {{{0}}} through {{{9}}} keys to go to the corresponding passage. (''Note:'' In Opera you will need to have the page open in its own window with no other tabs on that window, otherwise the number keys will select which tab is visible in that window.)
* If there are more than 10 links, the links will not be marked.
* Keyboard navigation will be ignored if an {{{<input>}}} element has focus.
* The {{{.}}} and {{{r}}} keys will click a random link.
* The {{{←}}} and {{{`}}} keys can be used to go back a passage.
* Also, in passages with a {{{DisableKeyLinks}}} tag, no links will be marked and keyboard navigation will be disabled. You can use this tag for passages that need those keys for other things, such as passages with text input.
* You can tag a passage with {{{IgnoreArrowKeys}}} to disable just the {{{←}}} and {{{→}}} keys for navigation in that passage. For example, you could use that tag on passages that need horizontal scrolling.
* You can add a {{{data-nokeys="true"}}} element to a link or a link's parent to have that link be ignored. For example:
** {{{<a data-nokeys="true" href="http://google.com" target="_blank">Google</a>}}} which displays as: <a data-nokeys="true" href="http://google.com" target="_blank">Google</a>
** {{{<span data-nokeys="true">[[Main Menu]]</span>}}} which displays as: <span data-nokeys="true">[[Main Menu]]</span>
Try using the keyboard link navigation with the [[Replace]] sample code, or go back to the [[Main Menu]].
JavaScript section:
{{{
/* Keyboard links v1.3.1 - Start */
let KBIntervalID = 0;
$(document).on(":passagerender", function (ev) {
clearInterval(KBIntervalID);
UpdateLinks(ev.content);
// Search passages for links every 300ms, just in case they get updated, and marks them for key clicks
KBIntervalID = setInterval(UpdateLinks, 300);
});
// Adds key shortcut indicators to links in passage if there are less than 11 links in the passsage.
function UpdateLinks(Container) {
// Enables keyboard shortcuts on passages that do not have the "DisableKeyLinks" tag
if (!tags().includes("DisableKeyLinks")) {
let Links, i;
if (typeof Container === "undefined") {
Container = document;
Links = $("#passages a").toArray();
} else {
Links = $(Container).find("a").toArray();
}
if (Links.length > 0) {
for (i = Links.length - 1; i >= 0; i--) {
if ($(Links[i]).data("nokeys") || $(Links[i]).parent().data("nokeys")) {
Links.deleteAt(i);
}
}
}
if (Links.length === 1) {
if (!Links[0].id.includes("Link") && !Links[0].id.includes("NextLnk")) {
Links[0].id = "NextLnk";
}
} else if (Links.length >= 1 && Links.length <= 10) {
if ($("#NextLnk").length > 0) { // Remove "NextLnk" ID since the passage now has more than one link.
$("#NextLnk").removeAttr("id");
}
let n = 1;
for (i = 0; i < Links.length; i++) {
if (!Links[i].id.includes("Link")) {
while ($(Container).find("#Link" + n).length) {
++n;
if (n > 10) {
break;
}
}
if (n < 10) {
$("<sup>[" + n + "]</sup>").appendTo(Links[i]);
Links[i].id = "Link" + n;
} else if (n === 10) {
$("<sup>[0]</sup>").appendTo(Links[i]);
Links[i].id = "Link0";
break;
} else {
break;
}
}
}
}
}
}
$(document).on("keyup", function (e) {
// Enables keyboard shortcuts on passages that do not have the "DisableKeyLinks" tag and when you're not entering text
if (!tags().includes("DisableKeyLinks") && ($("input:focus").length === 0) && ($("textarea:focus").length === 0) && ($("div[contenteditable='true']:focus").length == 0)) {
// Trigger next link click on right arrow key or "1" (normal and numpad)
if (((e.key == "ArrowRight") || (e.key == "1") || (e.keyCode === 97)) && ($("#NextLnk").length > 0)) {
if (!(tags().includes("IgnoreArrowKeys") && (e.key == "ArrowRight"))) {
e.preventDefault();
$("#NextLnk").click();
return false;
}
} else {
// Trigger link click on keys "0" through "9"
if ((e.keyCode > 47) && (e.keyCode < 58)) {
if ($("#Link" + (e.keyCode - 48)).length) {
e.preventDefault();
$("#Link" + (e.keyCode - 48)).click();
return false;
}
}
// Trigger link click on numpad keys "0" through "9"
if ((e.keyCode > 95) && (e.keyCode < 106)) {
if ($("#Link" + (e.keyCode - 96)).length) {
e.preventDefault();
$("#Link" + (e.keyCode - 96)).click();
return false;
}
}
}
// Trigger random click on "." or "r" key
if ((e.key == ".") || (e.key == "r")) {
e.preventDefault();
let Links = $("#passages a"), n, UsableLinks = [];
if (Links.length > 0) {
for (n = 0; n < Links.length; n++) {
if (!$(Links[n]).data("nokey")) {
UsableLinks.push(n);
}
}
if (UsableLinks.length > 0) {
n = random(UsableLinks.length - 1);
Links[UsableLinks[n]].click();
return false;
}
}
}
// Trigger back click on left arrow key or backquote
if ((e.key == "ArrowLeft") || (e.key == "`")) {
if ((!tags().includes("IgnoreArrowKeys")) || (e.key != "ArrowLeft")) {
e.preventDefault();
Engine.backward();
return false;
}
}
}
});
/* Keyboard links - End */
}}}<h1>Displaying Object Contents Tool</h1>Have an object in your code that's not working properly or is behaving strangely? You can simplify your debugging of complicated objects by being able to quickly take a look at the whole object using the {{{getObjectProperties()}}} function code provided below.
For example, once you have that code in your project, you can then do something like this:
{{{
SugarCube.Browser = <<= getObjectProperties(SugarCube.Browser)>>
}}}
which will display the {{{SugarCube.Browser}}} object like this:
SugarCube.Browser = <<= getObjectProperties(SugarCube.Browser)>>
If you'd like to use the {{{getObjectProperties()}}} function to help with debugging your own code as shown above, then you simply need to add the code below into your project's JavaScript section:
{{{
/* getObjectProperties - Start */
// isObject: Returns if a value is some type of object.
window.isObject = function (Value) {
return !!Value && typeof Value === 'object';
};
// isGenericObject: Returns if a value is a generic object.
window.isGenericObject = function (Value) {
return !!Value && typeof Value === 'object' && Value.constructor === Object;
};
// isString: Returns if a value is a string.
window.isString = function (Value) {
return typeof Value === 'string' || Value instanceof String;
};
// getObjectProperties: Returns all of the properties and values of an object as a string. Returns the type of non-objects.
window.getObjectProperties = function (Obj, Ext) {
if (Ext === undefined) {
Ext = "";
} else {
Ext += ".";
}
let Txt, i;
if (Array.isArray(Obj)) {
Txt = "[ ";
for (i = 0; i < Obj.length; i++) {
if (isObject(Obj[i])) {
Txt += getObjectProperties(Obj[i]);
} else {
if (isString(Obj[i])) {
Txt += '"' + Obj[i] + '"';
} else {
Txt += Obj[i];
}
}
if (i < Obj.length - 1) {
Txt += ", ";
}
}
Txt += " ]";
return Txt;
} else if (isObject(Obj) && (Obj instanceof Date)) {
return "Date: { " + (Obj.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" } )) + " }";
} else if (isObject(Obj)) {
let Keys = Object.keys(Obj);
Txt = "{ ";
for (i = 0; i < Keys.length; i++) {
if (isObject(Obj[Keys[i]])) {
Txt += Ext + Keys[i] + " : " + getObjectProperties(Obj[Keys[i]], Ext + Keys[i]);
} else {
if (isString(Obj[Keys[i]])) {
Txt += Ext + Keys[i] + ' = "' + Obj[Keys[i]] + '"';
} else {
Txt += Ext + Keys[i] + ' = ' + Obj[Keys[i]];
}
}
if (i < Keys.length - 1) {
Txt += ", ";
}
}
Txt += " }";
return Txt;
} else {
return "(" + Ext + "type = " + typeof Obj + ")";
}
};
/* getObjectProperties - End */
}}}
Hope that helps!<h1>{{{<<HoverTxt>>}}} Widget</h1>''⚠ NOTE:'' The [[Hovertip Macro]] is a more sophisticated version of this, which highlights text instead of providing a "note" icon as this widget does.
Now, normally you can get text to pop up when you hover over something by setting the "title" attribute. For example:
{{{
<img title="This text will appear as a tooltip if you hover your mouse over the image." style="height: 32px;" src="Prism64-2.png" tabindex="0">
}}}
Which appears like this: <img title="This text will appear as a tooltip if you hover your mouse over the image." style="height: 32px;" @src="setup.ImagePath + 'Prism64-2.png'" tabindex="0">
(Hold your mouse over the image.)
With HoverTxt you would do this:
{{{
<<HoverTxt 300 "This text will appear as a tooltip if you click on or hover your mouse over the image.">>
}}}
Which appears like this: <<HoverTxt 300 "This text will appear as a tooltip if you click on or hover your mouse over the image.">>
Note that the hover text will also appear if you click on the icon, which means that this works on mobile devices or when being "tabbed" to as well. Clicking outside of the icon will make the hover text disappear.
The first parameter passed to the HoverTxt widget is normally the width in pixels, which I recommend you not go above 600 to avoid going wider than most screens. The second parameter is the content that you want to appear inside the note. If the text is longer than the width you set, it will get wrapped appropriately and the height will change to fit the wrapped text.
That said, you can skip the first parameter if the default width of 200 pixels is fine for you. Like this:
{{{
<<HoverTxt "This text defaults to a width of 200 pixels.">>
}}}
Which appears like this: <<HoverTxt "This text defaults to a width of 200 pixels.">>
However, unlike the "title" attribute, you can display more than simply text. If you want the ability to use markup, images, etc. in the hover text, you can use HoverTxt can do that. For example:
{{{
<<HoverTxt 135 'This is a prism: <img style="position: relative; vertical-align: text-bottom; height: 20px;" src="Prism64-2.png">'>>
}}}
That will display an image inside the text, like this: <<HoverTxt 135 'This is a prism: <img style="position: relative; vertical-align: text-bottom; height: 20px;" @src="setup.ImagePath+\'Prism64-2.png\'">'>>
If you want to display the results of a function, use the backtick "{{{`}}}" (found on the tilde "~" key on the upper left part of the keyboard) like this:
{{{
<<HoverTxt 300 `Date(Date.now()).toLocaleString("en-US", { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' })`>>
}}}
Which is displayed like this: <<HoverTxt 300 `Date(Date.now()).toLocaleString("en-US", { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' })`>>
To use this widget yourself, put the following code into a non-special story passage with a "''widget''" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
/* HoverTxt : Show notepad icon shows some wikified text in a window of width X pixels above an icon when it's hovered over.
EXAMPLES:
<<HoverTxt 100 "text">>
<<HoverTxt "More text.">>
<<HoverTxt 300 `someFunction()`>>
VARIABLES USED: _HoverTxtCount, _HTLeft, _HTTxt, _HTWidth
*/
<<widget "HoverTxt">><<nobr>>
<<set _HTTxt = _args.length >= 2 ? _args[1] : _args[0]>>
<<if !Number.isInteger(_args[0])>>
<<set _HTWidth = 200>> /* Default to a width of 200 if an invalid width is passed. */
<<else>>
<<set _HTWidth = _args[0]>>
<</if>>
<<set _HTLeft = Math.trunc(_HTWidth / 2) - 11>>
<<if ndef _HoverTxtCount>>
<<set _HoverTxtCount = 1>>
<<else>>
<<set _HoverTxtCount += 1>>
<</if>>
<span class="hoverTxt" tabindex="0"><img class="hoverIco" @src="setup.NoteIcon">
<span @id="'hoverTxt' + _HoverTxtCount" @style="'left: -' + _HTLeft + 'px; width: ' + _HTWidth + 'px;'">
_HTTxt
</span>
</span>
<</nobr>><</widget>>
}}}
add this JavaScript code to your JavaScript section:
{{{
/* HoverTxt - Start */
setup.NoteIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH4gISEC8NZwM7/QAAAs5JREFUOMttkktoXHUUxn//153exjvJdDShSR/SCLVpQaVUkKBUupBakSrYp64EEVdduKsLu3LlohuLuPAJlkKLiJWWWl8IQktrDBaKlBBDm6QZJ2MnyczkztxzXNw8KvjB2Z3vxznfOQawgB7du9tvHnh413OHX9195eL5X+Lygz2PbNu+/s/fR25v2b5j0/XLF0afeeng0z+cPfPTiVMf/QoYAA/osQP7164JoVZeVwql7iILjQalPo/pdDAoSRzTbKb0FBOMc7zx4t6psemZzd9dudYxAG/u33ewp1w+/da7JygmJWZnZxCBJElYaCygCt4pXV09TP41xvvvHCe1bsuX5y+OWwDjbNw70E8IgUplCu897XaLWq1KO21Rq1YQVaZmJmldv8bJQ0d5+9k9pwH1AMF7vHesiWOcc4gIzjtEMrIsI0QBEWFxoc74/B0Gdj7JHzdGEsDlgBDwzrM27kKiGGstxlisNSxLVUm/PssTGzdydfQ3bkzf+RzIPEAUAoVCgSgUUC+AwZj/mjGGvq/OUZuZZp5F5oeGLy1fgRAFoijKm0XBLJlQVPODtX+8THPqNg0Vxvr6iUul5gogigJRFAAQVcyyEUU1L3fyPVLvmV5sUt/6GHHI+1cyKBTyCYzhPjMoCnP3qE5MkIny9+Ag3UlClgksfSFRFAg+J1rrVkI0BkyIkC8+ppUucrdRp/uFV3DW4L27DxACYWkkkQwRQVUQERDhajpHp38D410xQy8fIXiPc251Be8DIfIAZJKB5lmoCrYDbmgHDO+iXO2wvvchQhTQdrYKANXWXJ00TfPdVBCVPECrjE7M0jtXZM/z+7j3Tw0jGS5/ITxg7laq3z+6dZBvP/mAZrOFtQZlVUXJSCt1Lnz6IeuKRZ7a+bh8c+nnScAYY4xRVY6//trwpg39hzLJHuB/ZAAFY4ydHb1567NTZ86NAPwLAMw3Qb12tl0AAAAASUVORK5CYII=";
let HTIntervalID = 0;
$(document).on(":passagerender", function (ev) {
UpdateHoverTxt(ev.content);
});
$(window).on("resize scroll", function (ev) {
clearInterval(HTIntervalID);
HTIntervalID = setInterval(UpdateHoverTxt, 300);
});
$("#ui-bar-toggle").on("click", function (ev) {
clearInterval(HTIntervalID);
HTIntervalID = setInterval(UpdateHoverTxt, 300);
});
/* Waits for passage to be fully rendered before doing anything. */
function UpdateHoverTxt(Container) {
if (typeof Container === "undefined") {
Container = document;
}
if (Engine.isIdle()) {
clearInterval(HTIntervalID);
let i = 1, sum, positionInfo, element = Container.getElementById("hoverTxt" + i);
while (element !== null) {
positionInfo = element.getBoundingClientRect(); /* Refresh rect */
element.style.left = ((Math.round(positionInfo.width / 2) - 11) * -1) + "px"; // Center hoverTxt horizontally over icon.
element.style.top = (-1 * parseInt(positionInfo.height) - 3) + "px"; // Position bottom of hoverTxt just above the icon.
positionInfo = element.getBoundingClientRect(); /* Refresh rect */
sum = Math.round(positionInfo.top + positionInfo.height + 5);
if (sum > window.innerHeight) { /* Make sure the text isn't outside the bottom of the screen. */
element.style.top = (parseInt(element.style.top) + window.innerHeight - sum) + "px";
positionInfo = element.getBoundingClientRect(); /* Refresh rect */
}
if (positionInfo.top < 5) { /* Make sure the text isn't outside the top of the screen. */
element.style.top = (parseInt(element.style.top) - positionInfo.top + 5) + "px";
}
sum = Math.round(positionInfo.left + positionInfo.width + 26);
if (sum > window.innerWidth) { /* Make sure the text isn't outside the right of the screen. */
element.style.left = (parseInt(element.style.left) + window.innerWidth - sum) + "px";
positionInfo = element.getBoundingClientRect(); /* Refresh rect */
}
if (positionInfo.left + window.pageXOffset < 10) { /* Make sure the text isn't outside the left of the screen. */
element.style.left = (parseInt(element.style.left) - positionInfo.left - window.pageXOffset + 10) + "px";
}
element = document.getElementById("hoverTxt" + (++i));
}
} else {
clearInterval(HTIntervalID);
HTIntervalID = setInterval(UpdateHoverTxt, 300);
}
}
/* HoverTxt - End */
}}}
The note icon is embedded as base64 image data in the {{{setup.NoteIcon}}} variable. See the [[Embed Image]] section for details on how to use base64 encoded image data if you'd like to use a different icon.
And add this CSS code to your Stylesheet section:
{{{
/* HoverTxt code - Start */
:root {
--hovertxtbkg: #e5e5f5; /* HoverTxt background color */
--hovertxtbdr: #666; /* HoverTxt border color */
--hovertxttxt: black; /* HoverTxt text color */
}
.hoverTxt {
position: relative;
top: 5px;
vertical-align: text-bottom;
height: 30px;
}
.hoverTxt:hover span:first-of-type, .hoverTxt:focus span:first-of-type {
visibility: visible;
}
.hoverTxt span:first-of-type {
position: absolute;
top: -100px;
padding: 3px;
background-color: var(--hovertxtbkg);
border: 1px solid var(--hovertxtbdr);
border-radius: 10px;
visibility: hidden;
color: var(--hovertxttxt);
text-align: left;
box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.5);
z-index: 100;
}
.hoverTxt span:first-of-type::after {
content: "";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -10px;
border-width: 8px;
border-style: solid;
border-color: var(--hovertxtbkg) transparent transparent transparent;
z-index: 100;
}
/* HoverTxt code - End */
}}}
You can change the color values in the {{{:root}}} section if you'd like the hover text to appear using other colors.
<h1>Combining Story Passages Sample Code</h1>You can combine multiple linear story passages (ones with no branching passages) into a single passage by using code like this:
{{{
<<if ndef $textGroup>><<set $textGroup = 1>><</if>><<switch $textGroup>>
<<case 1>>Initial story text.
<<link "Link Text 1" `passage()`>><</link>>
<<case 2>>Next story text.
<<link "Link Text 2" `passage()`>><</link>>
<<default>>Final story text.
<<unset $textGroup>>[[Link Text 3|Next Passage]]
<</switch>><<if def $textGroup>><<set $textGroup += 1>><</if>>
}}}
If you want to add more passages, just add more cases, following the same format as above. Also, make sure that the final passage, in the {{{<<default>>}}} case, does {{{<<unset $textGroup>>}}} to clear out that variable. The final passage can branch to multiple passages without a problem.
The story text doesn't need to be only one line. It can be mutiple paragraphs. I just used one line here to keep the sample code short.
If you want another passage to go to a specific case above, then set {{{$textGroup}}} to that case number before they go there, like this:
{{{
[[Click here to skip to the second case|Combined Passage][$textGroup = 2]]
}}}
[[Click here to see the above code in action|Combined Passage]]
[[Click here to skip to the second case|Combined Passage][$textGroup = 2]]
Want to make your combined passages even simpler? Add this code to your JavaScript section for the {{{<<chunkText>>}}} macro:
{{{
/* <<chunkText>> macro v1.2.1 - Start */
Macro.add('chunkText', {
tags: [ 'next' ],
handler: function() {
let tmp = '<<if ndef $textGroup>><<set $textGroup = 1>><</if>><<switch $textGroup>>', linkText, n, cls, txt, psg;
for (let i = 0; i < this.payload.length; ++i) {
if ((i == this.payload.length - 1) && (this.payload[i].args.length < 2)) {
return this.error('Final section missing link text and passage name parameters.');
}
cls = "";
txt = "";
psg = "";
if (this.payload[i].args.length == 0) {
linkText = '<<link "Next" `passage()`>><</link>>';
} else {
for (n = 0; n < this.payload[i].args.length; ++n) {
if (this.payload[i].args[n].indexOf(".") == 0) {
if (cls) {
cls += ' ' + this.payload[i].args[n].substring(1);
} else {
cls = '<span class="' + this.payload[i].args[n].substring(1);
}
} else if (txt == "") {
txt = this.payload[i].args[n];
} else if (psg == "") {
psg = this.payload[i].args[n];
}
}
if (txt == "") {
txt = "Next";
}
if (cls) {
cls += '">';
}
if (psg) {
linkText = '<<unset $textGroup>>' + cls + '[' + '[' + txt + '|' + psg + ']]';
} else {
linkText = cls + '<<link "' + txt + '" `passage()`>><</link>>';
}
if (cls) {
linkText += '</span>';
}
}
tmp += '<<case ' + (i + 1) + '>>' + this.payload[i].contents + linkText;
}
$(this.output).wiki(tmp + '<</switch>><<if def $textGroup>><<set $textGroup += 1>><</if>>');
}
});
/* <<chunkText>> macro - End */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
Now that you've done that, the {{{<<chunkText>>>}}} macro will let you do that same passage we did up top, but in a much cleaner and simpler way like this:
{{{
<<chunkText "Link Text 1">>
Initial story text.
<<next "Link Text 2">>
Next story text.
<<next "Link Text 3" "Next Passage">>
Final story text.
<</chunkText>>
}}}
If there are no parameters in the {{{<<chunkText>>}}} or {{{<<next>>}}} macros, then there will be a "Next" link to the next chunk of text.
Any parameters which start with a period will be added as a class to the link.
If there's one parameter which __doesn't__ start with a period, then the link to the next chunk of text will be whatever that parameter was, instead of "Next".
If there are two or more parameters that don't start with periods, then the first two parameters will be the link text and passage name for the link, respectively. (Additional parameters like that will be ignored.)
''NOTE:'' The last chunk must always have those two parameters or it will throw an error.
You can skip directly to a particular chunk of text the same as described earlier, by setting the {{{$textGroup}}} variable to the number which represents which chunk of text you want displayed (starting with {{{1}}}) before going to the passage.<<if ndef $textGroup>><<set $textGroup = 1>><</if>><<switch $textGroup>>
<<case 1>>Initial story text.
<<link "Link Text 1" `passage()`>><</link>>
<<case 2>>Next story text.
<<link "Link Text 2" `passage()`>><</link>>
<<default>>Final story text.
<<unset $textGroup>>Back to [[Combining Story Passages]].
<</switch>><<if def $textGroup>><<set $textGroup += 1>><</if>><<done>>
<<if tags().includes("tbar")>>
<<if $("#tbtext").length == 0>>
<<run $("#ui-bar").after($('<div id="topbar"><div id="tbblock"><div id="tbtext"></div></div></div>'))>>
<</if>>
<<run $("#tbtext").empty().wiki('<<include "TopBar">>')>>
<<elseif $("#topbar").length > 0>>
<<run $("#topbar").remove()>>
<</if>>
<</done>><h1>Linking to Passages by URL</h1>If you have a Twine HTML page where you want to be able to get URL links to jump straight to a particular passage, like how this Twine page does, then you can use the following code. It causes Twine to look at the <a href="https://en.wikipedia.org/wiki/Fragment_identifier" target="_blank">fragment identifier</a> in the URL (the text after a {{{#}}} hash mark in a URL) to direct the page to a passage with a matching name.
''NOTE:'' The URL should be "<<hovertip "URI = Universal Resource Indicator">>URI<</hovertip>> encoded", which changes certain characters in the URL into characters which are legal in the URL. For example, a space will get changed into {{{%20}}} within the URL.
To do this, just put this in your JavaScript section:
{{{
/* Anchor Link to Passage - Start */
if ("onhashchange" in window) { // event supported
window.onhashchange = function () {
hashChanged();
};
} else { // event not supported
window.setInterval(function () {
if (window.location.hash != setup.storedHash) {
hashChanged();
}
}, 100);
}
function hashChanged() {
if (Engine.isIdle()) {
if (window.location.hash && (setup.storedHash != window.location.hash)) {
setup.storedHash = window.location.hash;
let anchor = decodeURI(window.location.hash.substring(1));
if (Story.has(anchor) && (passage() !== anchor)) {
Engine.play(anchor);
}
} else {
// Comment out the following line of code if you don't want the
// anchor link of the current passage displayed in the URL bar.
window.location.hash = encodeURI(passage());
}
// Comment out the following line of code if you don't want the
// title of the page set to the passage name.
document.title = passage();
} else {
setTimeout(hashChanged, 100);
}
}
$(document).on(':passageend', function () { hashChanged(); });
/* Anchor Link to Passage - End */
}}}
As noted in the code above, there's one line you can comment out if you don't want the current passage name shown in the page title.
There's also another line you can comment out if you don't want the anchor to change in the URL bar in the browser as you change passages. If you comment out that line, then you can add a directly link to each page by creating a {{{PassageHeader}}} passage with this in it:
{{{
<div style="float: right;"><a @href="encodeURI(decodeURI(document.location.href.match(/(^[^#]*)/)[0])+'#'+passage())" alt="A link directly to this passage." title="A link directly to this passage.">#</a></div>
}}}
That will display a "#<<scicon external-link>>" link in the upper-right corner of every page, from which one can get a direct link to the passage.
That's it! You're good to go!(finish writing up this sample code)
<<nobr>>
<<set $inventory = {}>>
<<set _itemName = "Princess Dress">>
<<set _itemObject = { type: "outfit", description: "a dress that looks like it's suited for a Disney princess" }>>
<<set _safeName = _itemName.replace(/[^a-z0-9]/gi, "_")>>
<span @id="_safeName" @title="_itemObject.description">
<<set _safeName = "#" + _safeName>>
<<if ndef $inventory[_itemName]>>
<<capture _itemName _itemObject _safeName>>
Buy <<link _itemName>>
<<set $inventory[_itemName] = _itemObject>>
<<remove _safeName>>
<</link>>
<</capture>>
<</if>>
</span>
<</nobr>><h1>Simple Update Without Reload</h1>Let's say that you want the player to be able to push some buttons to update something displayed within the current passage, but you don't want to reload the current passage. For example:
<<set _a = 0>><<set _b = 0>><<button "Add 1 to a">>
<<set _a++>>
<<redo>>
<</button>> <<do>>a = _a<</do>>
<<button "Add 1 to b">>
<<set _b++>>
<<redo "b">>
<</button>> <<do tag "b">>b = _b<</do>>
<<button "Add 1 to a and b">>
<<set _a++; _b++>>
<<redo>>
<</button>>
The above uses the <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-do" target="_blank">{{{<<do>>}}} macro</a> to mark out a section of the passage which can be re-run later on, and the <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-redo" target="_blank">{{{<<redo>>}}} macro</a> to trigger those sections to be re-run.
Here's the code for the above:
{{{
<<set _a = 0>><<set _b = 0>><<button "Add 1 to a">>
<<set _a++>>
<<redo>>
<</button>> <<do>>a = _a<</do>>
<<button "Add 1 to b">>
<<set _b++>>
<<redo "b">>
<</button>> <<do tag "b">>b = _b<</do>>
<<button "Add 1 to a and b">>
<<set _a++; _b++>>
<<redo>>
<</button>>
}}}
(FYI - {{{variable++}}} means "add 1 to this variable after this". Also, you can use "{{{;}}}" as a separator to let you "set" multiple things within a single {{{<<set>>}}} macro.)
The {{{<<do>>a = _a<</do>>}}} is initially displayed as: {{{a = 0}}}
When the "Add 1 to a" button is clicked, it first does {{{<<set _a++>>}}}, which adds 1 to the variable {{{_a}}}. Then the {{{<<redo>>}}} macro triggers the {{{<<do>>a = _a<</do>>}}} again, this time showing: {{{a = 1}}}
Each time you click that button, it will add one more, then refresh that content. It also refreshes the {{{<<do tag "b">>b = _b<</do>>}}}, but you won't see any change, since that button doesn't change the value of {{{_b}}}.
The "Add 1 to b" button does something similar, but instead of {{{<<redo>>}}}, it uses {{{<<redo "b">>}}}, thus this button //only// updates display of the content within {{{<<do tag "b">>b = _b<</do>>}}}, because it has the "b" tag.
You //could// wrap the entire passage (other than the parts that set the variables to zero) within a {{{<<do>>}}} macro, but this is bad since you're making the code to extra and unnecessary work, plus you're setting yourself up for creating errors, such as if you forgot to exclude the parts which initialize variables.
__''NOTE:''__ Any changes made to any variables by the user within the current passage will //''not''// normally get saved, because the save only stores the values of story variables from //just prior// to entering the current passage.
<h1>Displaying Images in Twine</h1>If you want to display images in your Twine story with SugarCube, it's recommended that you include them in an "images" directory (also known as a "folder"). The "images" directory should be in the same parent directory as the HTML file itself. If you want music or sounds, then you should make a separate directory for them as well. For example:
<img @src="setup.ImagePath+'Example.png'" style="display: block; margin: auto;">/* <div style="text-align: center;">[img[setup.ImagePath+'Example.png']]</div> */
One issue with that is that, when you launch your game from the Twine editor, the HTML file won't be run from that directory. This means that the path to your images will be wrong, thus will not be displayed in that case.
''NOTE:'' The following assumes you're using the ''desktop app version'' of the Twine editor. If you're using the ''online version'' of the Twine editor, then you will have to use online media resources, due to browser security measures.
Rather than having to publish your game to that parent directory and then launch it from there to get your images to display, with a few tweaks to your images and some code, you can make your images automatically display correctly, regardless of how it was launched. This section shows you how.
First, put something like this at the top of your JavaScript section:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
}}}
It's important that this code be above any other JavaScript code which tries to use those paths, otherwise it won't work.
<<include PathNote>>
Once you've done that you can now load images two different ways within your passages:
1 - Using Twine code:
{{{
[img[setup.ImagePath+'image.jpg']]
}}}
2 - Using HTML code (see note below):
{{{
<img @src="setup.ImagePath+'image.jpg'">
- or -
<div @style="'background-image: url(\''+setup.ImagePath+'image.jpg\');'">...text and code goes here...</div>
}}}
The {{{\'}}} in the final example above puts an apostrophe inside of a {{{'...'}}}‑type string, instead of closing the string as the apostrophe normally would.
''IMPORTANT:'' For some operating systems and some browsers __//capitalization matters//__, while for others it doesn't. Make sure that the capitalization of your files and your path __//always//__ match. This is because loading the file {{{xyz.jpg}}} as {{{images/XYZ.jpg}}} may work for you, however, other people will see a broken image because their system can only find the version of that filename which exactly matches the original capitalization (which in this example was all lowercase). This applies to //all// file access from the browser.
If you want to turn an image displayed using the HTML method (shown above) into a clickable link, you can do that like this:
<a data-passage="Main Menu" class="link-internal link-image">
\<img @src="setup.ImagePath+'Example.png'" style="display: block; margin: auto;">
\</a>
To do that you just need to wrap an HTML {{{<a>}}} link (an "anchor" link) around your image, set the ''data-passage'' property to the name of the passage you want the link to go to in that link, and add the "link-internal link-image" classes. For example:
{{{
<a data-passage="Main Menu" class="link-internal link-image">
\<img @src="setup.ImagePath+'Example.png'" style="display: block; margin: auto;">
\</a>
}}}
That's the source used to display the above image link. In this example the ''style'' property horizontally centers the image.
You can also set a ''title'' property if you want a tooltip with some text to appear when you hover the mouse over the image/link. Also, setting the ''alt'' property to a description of the image for blind users is highly recommended.
Enjoy!<<nobr>>Your inventory:<br>
<<for _key range $inventory>>
- _key.quantity _key.description<<if def _key.bullets>> (with _key.bullets bullets in it)<</if>>.<br>
<</for>><</nobr>>
[[Back to Using Objects|Using Objects]]
(Note: The numbers of bullets gets reset when going back to the "Using Objects" passage, because that's what the code there says to do.)<h1>Event Flags</h1>The {{{<<SetFlag>>}}} widget and {{{Flag()}}} function shown here will make it easier for you to track whether events have happened in your game or not.
Using these means that you don't have to initialize a bunch of separate flag variables in your StoryInit if you want to keep track of what events have happen in your game. Instead, it's all tracked in one place. Also, it's structured to help minimize the save/history size by deleting flags which are set to "false".
{{{<<SetFlag "FlagName" _FlagValue>>}}} sets a flag named "FlagName" to the value of "_FlagValue". The flag name should be a string. The "_FlagValue" part is optional, and defaults to {{{true}}} (the value, not the string) if you don't include it. If the second parameter is set to {{{false}}} (the value, not the string) then the flag gets deleted.
{{{Flag("FlagName")}}} returns the value of a flag named "FlagName", or {{{false}}} (the value, not the string) if the flag can't be found. This means that all flags will default to {{{false}}}.
Flag //''names''// are ''NOT'' case sensitive in order to prevent problems if you enter the flag name with the wrong case (e.g. a flag called "Test" is treated the same as "test" or "TEST"; flag names are stored in all lowercase).
Flag //''values''//, on the other hand, will ''NOT'' have their case changed.
As an example, you can easily add/change/delete flags like this:
{{{
<<SetFlag "GotShot">> /* sets flag "GotShot" to true */
<<SetFlag "Owwie" "head">> /* sets flag "Owwie" to "head" */
<<SetFlag "healthy" false>> /* deletes the "healthy" flag */
}}}
And you can check the value of flags like this:
{{{
<<if Flag("Healthy")>>
Looks like he missed.
<<else>>
You got shot in the <<print Flag("Owwie")>>.
<</if>>
}}}
You could also check the value of flags like the following, just remember the flag names have to be in all lowercase:
{{{
<<if $Flags["gotshot"]>>
You shot me in the $Flags["owwie"]!
<</if>>
}}}
So, if we run those three chunks of sample code (shown above) all together, then we get this output:
<<nobr>>
<<SetFlag "GotShot">> /* sets flag "GotShot" to true */
<<SetFlag "Owwie" "head">> /* sets flag "Owwie" to "head" */
<<SetFlag "healthy" false>> /* deletes the "healthy" flag */
<<if Flag("Healthy")>>
Looks like he missed.
<<else>>
You got shot in the <<print Flag("Owwie")>>.
<</if>><br>
<<if $Flags["gotshot"]>>
You shot me in the $Flags["owwie"]!
<</if>>
<</nobr>>
----
As another simple example, we could test to see if you've been to this passage before:
<<if Flag("SeenIt")>>\
''This text will be different than the first time you saw this passage.''
<<else>>\
''This is the first time you've been to this passage.''
<</if>>\
<<SetFlag "SeenIt">>
The code used above:
{{{
<<if Flag("SeenIt")>>\
''This text will be different than the first time you saw this passage.''
<<else>>\
''This is the first time you've been to this passage.''
<</if>>\
<<SetFlag "SeenIt">>
}}}
[[Click this link go to this passage again, to see how that output changes.|Event Flags]]
----
To add this functionality to your Twine game, first, create a passage for the ''SetFlag'' widget and add the "{{{widget}}}" tag to it (click the "🏷 Tags" button at the top of the passage to add tags). Then put this code in that passage:
{{{
/* <<SetFlag>> widget: Sets Flag X to value Y (Y defaults to true).
Flag names are NOT case sensitive.
EXAMPLE: <<SetFlag "Mentor" "Bob">>
EXAMPLE: <<SetFlag "TrialMed">>
VARIABLES USED: _Fnam, $Flags
*/
<<widget "SetFlag">><<nobr>>
<<set _Fnam = _args[0].toLowerCase()>>
<<if ndef $Flags>>
<<set $Flags = {}>>
<</if>>
<<if def _args[1]>>
<<if _args[1] == false>>
<<if def $Flags[_Fnam]>>
<<run delete $Flags[_Fnam]>>
<</if>>
<<else>>
<<set $Flags[_Fnam] = _args[1]>>
<</if>>
<<else>>
<<set $Flags[_Fnam] = true>>
<</if>>
/* Event flags: */
/* Keep track of your flags here, for example: */
/* Mentor = Bob or Joe */
/* TrialMed = denotes whether you've agreed to take Dr. Acula's medication */
<</nobr>><</widget>>
/* <<SetFlag>> widget - End */
}}}
Next, for the ''Flag'' function, add the following code to your project's ''JavaScript'' section:
{{{
// If flag exists then return value, else return false
window.Flag = function (Fnam) {
if (State.variables.Flags == undefined) {
State.variables.Flags = {};
} else if (State.variables.Flags[Fnam.toLowerCase()] !== undefined) {
return State.variables.Flags[Fnam.toLowerCase()];
};
return false;
};
}}}
That's it! Enjoy!<h1>Swapping Items</h1>Here's some sample code showing how you can let the player swap which weapon they're using. This could easily be modified to handle swapping which clothes the player is wearing or other such things.
<<set $weapons = [ {name: "sword"}, {name: "axe"}, {name: "dagger"}, {name: "fist"}, {name: "rubber chicken"}]>>
Weapons:\
<<for _i = 0; _i < $weapons.length; _i++>>\
<span @id="'inv' + (_i + 1)" title="Click to make this the primary weapon">\
<<capture _i>>\
<<link $weapons[_i].name>>
<<set [$weapons[0], $weapons[_i]] = [$weapons[_i], $weapons[0]]>>
<<run $("#inv1 a").empty().wiki(" " + $weapons[0].name)>>
<<run $("#inv" + (_i + 1) + " a").empty().wiki(" " + $weapons[_i].name)>>
<</link>>\
<<if _i == 0>> (primary)<</if>>\
<</capture>>\
</span>\
<<if _i < $weapons.length - 1>> |<</if>>\
<</for>>
The code for the above line:
{{{
<<set $weapons = [ {name: "sword"}, {name: "axe"}, {name: "dagger"}, {name: "fist"}, {name: "rubber chicken"}]>>
Weapons:\
<<for _i = 0; _i < $weapons.length; _i++>>\
<span @id="'inv' + (_i + 1)" title="Click to make this the primary weapon">\
<<capture _i>>\
<<link $weapons[_i].name>>
<<set [$weapons[0], $weapons[_i]] = [$weapons[_i], $weapons[0]]>>
<<run $("#inv1 a").empty().wiki(" " + $weapons[0].name)>>
<<run $("#inv" + (_i + 1) + " a").empty().wiki(" " + $weapons[_i].name)>>
<</link>>\
<<if _i == 0>> (primary)<</if>>\
<</capture>>\
</span>\
<<if _i < $weapons.length - 1>> |<</if>>\
<</for>>
}}}
This sets {{{$weapons}}} to an array of objects, with a "{{{.name}}}" property for each weapon. You can add more properties than just the {{{.name}}} property (e.g. damage amounts or types).
The code then shows that list of weapons as links, and makes it so that clicking the link with the weapon's name will swap the selected weapon to the first entry in the {{{$weapons}}} array. Thus you can use {{{$weapons[0].name}}} to get the name of the primary weapon.
Note that the code uses the {{{[$x, $y] = [$y, $x]}}} method to swap the values of two variables.
<h1>Sample Code Updates</h1>''Sept. 4th, '26:''
* Updated the jQuery UI files from v1.12.1 to v1.14.2.
* Added a "Copy to Clipboard" icon ( <span class="svg"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44.2 48" fill-rule="evenodd"><path fill="#fff" d="M21.7 23.2h16.8v2H21.7Zm0 5.3h16.8v2H21.7Zm0 5.2h16.8v2H21.7Zm0 5.3h16.8v2H21.7Z"/><path fill="#fff" d="M40.4 19.8H20v24.4h20.4ZM44.2 48h-28V16h28z"/><path fill="#fff" d="M14.5 48H6a6 6 0 0 1-6-6V10.5a6 6 0 0 1 6-6h4.5V4a1 1 0 0 1 1-1h5.6s0-3 2.9-3 2.8 3 2.8 3h5.6c.5 0 1 .4 1 1v.5H34a6 6 0 0 1 6 6v3.9h-4v-2.9a3 3 0 0 0-3-3h-3.6v1c0 .6-.5 1-1 1h-17a1 1 0 0 1-1-1v-1H7a3 3 0 0 0-3 3V41a3 3 0 0 0 3 3h7.5zM20 1.4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z"/></svg></span> ) to all code blocks.
* Added a one-time-per-session "Main Menu" link flash to make sure that people see the link. This can be disabled from the "Settings" button.
* The accordion on the [[Main Menu]] window will now remember which section you had previously expanded when returning to it during the same session.
* Changed all external links to open in a new window.
* Majorly improved the [[<<hovertip>> Macro|Hovertip Macro]] (v3.0) behavior to reduce redraws and unnecessary scrolling, minimize width, work better within container elements, added a "swap" option, and fixed a potential "click through" bug where a clickable element in the hovertip window appearing under where the user tapped on mobile could get triggered.
* Improved the older [[<<HoverTxt>>> widget|Hover Text]] by making it support mobile use, embedding the icon, making the width parameter optional, using a span instead of a link, simplified how to change its colors, and added a "pointer" at the bottom of the tip bubble. Made some minor improvements to the parts explaining how to use the widget as well.
* Improved the [[<<selectRange>> macro|selectRange macro]] by adding the "upto" tag, as well as making it so that the two parameters for the "between" and "upto" tags can work properly in either order. Added some clarifications to the section's documentation as well.
* Improved the [[Bottom and Top Bars]] section, making the code simpler, and also fixed the bottom bar so that it appears at the bottom of the page even for passages which are shorter than the page.
* Minor bugfix to the [[TextboxPlus widget]] code (v1.3.2; fixes error on empty labels) and removed "{{{nobr}}}" tag dependency. Also improved the examples.
* Bugfix to the {{{SugarCubeInput()}}} function in the [[How to Use Sliders]] section, fixing it so that it also works with temporary variables. Also rewrote the section to be a bit clearer.
* Minor improvement to the [[FlagBit code]] to make the lowbit/highbit order no longer matter. Also improved the documentation and added a "Bit Visualizer" section.
* Minor CSS fix to the [[CheckboxPlus Widget]] to improve alignments.
* Minor fix to [[Scroll Blocks]] to deal with some cases where content could appear over top of scroll blocks.
* Multiple minor improvements to [[Font Size Buttons]] to limit minimum font size, allow tab focus on the buttons, and make focus and hover events more noticeable.
* Edited the [[Settings]] section to be a bit clearer, changed videos to be muted by default (instead of unmuted), fixed a broken setting, and added example code for how to fix "broken" settings yourself.
* Updated the [[Browser Tab Icon]] section to reflect the changes to the "<a href="https://realfavicongenerator.net/" target="_blank">RealFaviconGenerator</a>" site and prevented an unnecessary console error message when the page is loaded locally (i.e. when not online).
* Updated the Twine detection code for ''isTwine'' in the [[Browser Test]] section.
* Fixed the "fullscreen" icon depcited in the [[Fullscreen]] section being invisible in "Day" mode due to not changing the icon's color. (This didn't affect the fullscreen code.)
* Improved/simplified the code in the [[Delayed and Blinky Text]], [[Health Bar]], [[Image Toggle]], [[Level Calculator]], [[Listbox Tricks]] (and combined the old "Dropdown Select" and "Multiselect Listboxes" sections into this section), [[Maximum Checked Checkboxes]], [[Multicolor Links]], [[Object Tests]], and [[Swapping Items]] sections.
* Removed the old "Smart Checkboxes and Radio Buttons" section because SugarCube updates have made these examples irrelevant.
* Numerous other sections had minor improvements and clarifications.
* Updated multiple sections to comply with SugarCube v2.37.0 changes:
** Updated the [[Alerts and Dialogs]] sample code and further explained what the code does.
** Updated the [[Custom Save Titles]] sample code.
** Updated the [[Health Bar]] and the [[<<seen>> Macro|seen Macro]] sections to switch from the depreciated SugarCube {{{number.clamp()}}} method to SugarCube's {{{Math.clamp()}}} method.
** Updated [[Table of Links]] to change from using {{{Story.lookupWith()}}} to {{{Story.filter()}}}.
** Almost entirely rewrote [[Simple Update Without Reload]] to demonstrate how to use the added {{{<<do>>}}} and {{{<<redo>>}}} macros.
* Updated all widgets to remove the "{{{nobr}}}" tag requirement, switched from using {{{$args}}} to {{{_args}}} (per the SugarCube v2.36.0 changes, except for in the legacy [[SetPronouns Widget]]), and included a "VARIABLES USED" comment to indicate which variables each widget uses to prevent potential conflicts.
** This affected the following sections: [[CheckboxPlus Widget]], [[Countdown Timer]], [[Error Messages]], [[Event Flags]], [[<<HoverTxt>>> widget|Hover Text]], [[Pronoun Templates]], [[SugarCube Icons]], [[TextboxPlus widget]], and [[toggleLink Widget|toggleLink Macro]].
** If you were previously using the [[Countdown Timer]], it has changed from using the {{{_seconds}}} temporary variable to using {{{_ctseconds}}} instead, so make sure to change that in your own code if you update your copy of the {{{<<countdownTimer>>}}} widget to the current version.
* Changed almost all usage of the JavaScript "var" to "let", since Internet Explorer support has been dropped. Hypothetically, this change shouldn't cause any problems outside of the IE browser, and may actually reduce problems with conflicting variable names.
** This affected the following sections: [[Browser Tab Icon]], [[Clicking Parts of Images]], [[Combining Story Passages]], [[Countdown Timer]], [[Custom Save Titles]], [[Delayed and Blinky Text]], [[Display Object Contents]], [[Embed Image]], [[FlagBit code]], [[Geolocation Access]], [[Health Bar]], [[<<hovertip>> Macro|Hovertip Macro]], [[<<HoverTxt>>> widget|Hover Text]], [[How to Use Sliders]], [[Image Toggle]], [[imageExists() Function]], [[Keyboard Link Navigation]], [[Linking to Passages by URL]], [[Loading and Saving Data Files]], [[Loading External Scripts]], [[Maximum Checked Checkboxes]], [[Music]], [[ScrollTo Macro]], [[<<seen>> Macro|seen Macro]], [[<<selectRange>> macro|selectRange macro]], [[Setting Toggles Text Changer]], [[Simple Password Hiding]], [[SlideWin Overlay]], [[Speech Boxes]], [[TextboxPlus widget]], [[Using JavaScript with SugarCube]], [[Using jQuery UI in Twine]], [[Video Background]], and [[weightedEither() Function]].
''Nov. 8, '25:''
* Added the [[Scroll Blocks]] section.
''Oct. 23, '25:''
* A few additional notes added to the [[Video Background]] section.
''Sept. 4, '25:''
* Added [[Cycle Link Identifier]] section.
''Apr. 5, '25:''
* Fixed the JavaScript for [[Speech Boxes]] so that the avatars were properly displayed when playing from the Twine editor. No change if you weren't using that part.
''Mar. 11, '25:''
* Slightly tweaked the CSS for the [[Dumb Terminal]] font to make it look better.
''Mar. 7, '25:''
* Updated the [[SugarCube Icons]] page and the CSS generator there to work with the new SugarCube v2.37+ icon format. (NOTE: Some old icons no longer exist, such as the image, tag, hollow-star, checkbox, and radio button icons, while others have changed their appearance, such as the magic wand, sun, and lightbulb icons.)
''Nov. 27, '24:''
* Corrected the example code for [[Pronoun Templates]] and clarified the meaning of the {{{<<SetGender>>}}} widget parameters.
''Oct. 2, '24:''
* Fixed code in multiple entries to work with Twine's new "Scratch" folder. This updates [[Displaying Images in Twine]], [[Loading External Scripts]], [[Hover Text]], [[Speech Boxes]], [[imageExists() Function]], [[Music]], [[Using jQuery UI in Twine]], [[Fullscreen]], [[Font Size Buttons]], and [[Browser Tab Icon]].
* Fixed a potential issue with the health bar display in the [[Health Bar]] passage.
* Minor change to [[Image Toggle]] to improve text position within toggle.
* Updated links and code to work at new website.
* Dropped attempts to support Internet Explorer in my sample code. IE is no longer supported and the market share of its users has finally dropped to insignificance.
* NOTE: SugarCube v2.37.0 removed a lot of depreciated features and broke the icons used in the [[SugarCube Icons]] section. Fixes for this are planned, but not implemented yet.
* More fixes to come...
''Mar. 25, '23:''
* Very minor change to [[FlagBit code]].
* Major fixes to [[Keyboard Link Navigation]] to make it work properly when links go from one to more than one link on a page, and to prevent it from triggering when entering text in a passage.
''Feb. 27, '22:''
* Updated the [[Lined Paper and Handwriting]] section to explain different methods to load fonts and swapped the "Brush Script MT" font for "Dancing Script".
* Very minor text cleanup in a few passages.
''Jan. 23, '22:''
* Minor tweak to the [[CheckboxPlus Widget]] code so that, when used together with the {{{SugarCubeInput()}}} function found in the [[How to Use Sliders]] section, it wouldn't set variables incorrectly.
''Jan. 22, '22:''
* Updated [[Browser Test]] to detect mobile versions of Safari as well.
''Dec. 23, '21:''
* Fixed a major bug in the [[Day and Night Mode Setting]] section's code.
''Dec. 20, '21:''
* Added the [[SugarCube Icons]] section.
* Added the [[Setting Toggles Text Changer]] section.
* Added the [[Day and Night Mode Setting]] section.
* Updated the [[TextboxPlus widget]] code to add "oninput" event handling.
''Dec. 10, '21:''
* Added the [[Random Events]] section.
* Added the [[Loading and Saving Data Files]] section.
''Nov. 15, '21:''
* Added the [[How to Use Sliders]] section.
''Nov. 2, '21:''
* Fixed [[Custom Save Titles]] so that hitting "Cancel" uses the default save name, instead of saving with a blank name.
''Oct. 22, '21:''
* Switched to absolute positioning in the CSS for the [[Paper Doll Images]] section.
''Oct. 14, '21:''
* Corrected the <a href="https://drive.google.com/file/d/1bK6-QZOf2i8cdV710iiaDvPwooIQxV88/view?usp=sharing" target="_blank">Twine_Sample_Code.zip</a> download link (click the "download" icon in the upper-right corner of that page).
''Oct. 4, '21:''
* Added the [[SlideWin Overlay]] section.
''Oct. 2, '21:''
* Added the [[Paper Doll Images]] section.
* Fixed typos in the "data-keys" examples within the [[Keyboard Link Navigation]] section.
''Sept. 13, '21:''
* Updated Chapel's Volume Slider code in the [[Music]] section.
''Aug. 16, '21:''
* Added the [[Dream Background]] section.
* Added the [[Listbox Tricks]] section.
* Added the [[Outlined Text]] section.
* Added the [[Pointy Buttons]] section.
''June 6, '21:''
* Added the [[Speech Boxes]] section.
''May 30, '21:''
* Fixed the {{{setFlag()}}} function when disabling flags in the [[FlagBit code]] section.
''Feb. 23, '21:''
* Added detection for the MacOS "{{{/private/}}}" directory, in addition to the Windows "{{{/temp/}}}" directory in the Twine launch detection code used throughout this sample code.
''Feb. 7, '21:''
* Minor improvement to the [[Keyboard Link Navigation]] navigation code to prevent it from triggering when {{{<input>}}} elements have focus.
''Jan. 25, '21:''
* Fixed some display code.
''Jan. 17, '21:''
* Added the ability to style {{{<<chunkText>>}}} links in the [[Combining Story Passages]] section.
''Jan. 10, '21:''
* Added the "Force Update" button in the UI bar so you can force your browser to show the latest version of the Sample Code page. This is intended for use in browsers which have trouble fully refreshing their cache of the page.
* Fixed [[Fullscreen]] code so that it no longer throws errors in unsupported browsers.
* Fixed the [[TextboxPlus widget]] so that hitting {{{RETURN}}} without a passage parameter being set didn't send it to an "unknown" passage. Added new functionality for triggering code upon textbox change events or upon hitting {{{ENTER}}} or {{{RETURN}}}.
* Improved [[Browser Test]] code to better detect browser type.
* Various cleanup for improved display on mobile devices.
''December 30, '20:''
* Minor fixes for Internet Explorer.
''November 20, '20:''
* Minor internal code cleanup.
''November 17, '20:''
* Added additional methods for working with the [[Countdown Timer]], i.e. adding time, pausing/unpausing the timer, and making it work across multiple passages.
''November 6, '20:''
* Improved the [[Custom Save Titles]] code so that it only asks for titles when saving to a save slot.
''October 26, '20:''
* Added the [[Pronoun Templates]] section, which is a replacement for the <a data-passage="SetPronouns Widget" class="link-internal">{{{<<setPronouns>>}}} widget</a>.
''October 16, '20:''
* Cleaned up the [[Settings]] section and added a video which actually has audio.
''September 26, '20:''
* Added the [[Remembering Scroll Position]] section.
''September 19, '20:''
* Minor fix to [[Bottom and Top Bars]] to correct the z-index so that passage elements should stay below it (such as the {{{<sup>}}} keyboard navigation indicators).
* Minor improvement in starting speed due to using <a href="https://github.com/typekit/webfontloader" target="_blank">Web Font Loader</a> to load fonts in a non-blocking way.
* Various other minor typos fixed.
''September 14, '20:''
* Minor correction to the [[Keyboard Link Navigation]] code to better detect the {{{data-nokeys="true"}}} attribute on the parent elements of links.
''September 12, '20:''
* Various minor cleanup of section titles and such.
''September 9, '20:''
* Minor correction to the [[Hovertip Macro]] CSS to allow spans within the hovertip text to work properly.
''September 7, '20:''
* Added the [[seen Macro]] section.
''August 29, '20:''
* Noted that the "Smart Checkboxes and Radio Buttons" code is no longer needed for SugarCube v2.32.0 or higher (now removed).
''August 18, '20:''
* Minor improvement to the {{{<<raw>>}}} macro in the [[Print Differences]] section.
''August 14, '20:''
* Added Date support to the [[Display Object Contents]] {{{getObjectProperties()}}} function.
''August 2, '20:''
* Major improvment of the [[Hovertip Macro]] to work in more situations.
''July 31, '20:''
* Updated the [[Print Differences]] section to add the {{{<<raw>>}}} macro code.
''July 29, '20:''
* Added the [[weightedEither() Function]] section.
''July 25, '20:''
* Added the [[Browser Tab Icon]] section.
''July 20, '20:''
* Added the [[FlagBit code]] section.
''July 14, '20:''
* Major interface update for the [[Main Menu]]. This should make it easier for you to find out which sections you haven't read yet and which sections have been added or updated since the last time you checked.
''July 5, '20:''
* Added the [[Clicking Parts of Images]] section.
''June 30, '20:''
* Added the [[imageExists() Function]] section.
''May 29, '20:''
* Added the [[TextboxPlus widget]] section.
''May 10, '20:''
* Added the [[Custom Save Titles]] section.
''May 9, '20:''
* Added the [[Image Toggle]] section.
''May 2, '20:''
* Fixed display problem on narrower screens.
''Apr. 25, '20:''
* Added a note on IE support to the [[Level Calculator]] section.
''Mar. 30, '20:''
* Updated the [[Table of Links]] code to work in SugarCube v2.31.0, since the {{{Story.passages}}} property was removed.
* Fixed a problem where the stylesheet section had old code, so it wasn't showing the "Top Bar" or video background correctly. The code shown in the [[Bottom and Top Bars]] and [[Video Background]] sections, however, were fine.
''Mar. 26, '20:''
* Fixed a problem where the stylesheet section had old code for the Glass_TTY_VT220 font somehow, causing that font to be displayed wrong. The code shown in the [[Dumb Terminal]] section, however, was fine.
''Mar. 14, '20:''
* Some minor fixes/improvements to the [[Keyboard Link Navigation]] code for better browser support.
''Mar. 10, '20:''
* Added radio buttons to Smart Checkboxes and Radio Buttons (now removed due to SugarCube changes making these examples obsolete).
''Feb. 26, '20:''
* Changed [[Countdown Timer]] to a more flexible widget.
''Feb. 4, '20:''
* Minor fix to the previous improvement in the [[Fullscreen]] CSS code.
''Jan. 14, '20:''
* Minor improvement to the [[Fullscreen]] CSS code.
''Jan. 13, '20:''
* Added additional details [[Class Macro]] section.
''Jan. 5, '20:''
* Added code for dealing with tall bars in the [[Bottom and Top Bars]] section.
* Added the [[Lined Paper and Handwriting]] section.
* Added the [[Countdown Timer]] section.
''Dec. 6, '19:''
* Some minor improvements to the [[Fullscreen]] code.
''Dec. 4, '19:''
* Clarified the point of the [[Print Differences]] section.
* Some additions to the [[Linking to Passages by URL]] code.
* Added the [[Video Background]] section.
* Added how to put an input textbox inside of [[Bottom and Top Bars]].
''Nov. 20, '19:''
* Improved font support for the "Glass TTY VT220" font in the [[Dumb Terminal]] section.
''Nov. 6, '19:''
* Some fixes to the [[Browser Test]] section.
* Related fix to Twine detection in various other sections.
* Modified the "Bottom Bar" section into [[Bottom and Top Bars]] by adding information on how to create a top bar as well.
''Oct. 16, '19:''
* Clarifications and additional details added to the [[Using JavaScript with SugarCube]] section.
''Oct. 6, '19:''
* Added the [[Geolocation Access]] section.
''Sept. 30, '19:''
* Added the [[Using jQuery UI in Twine]] section.
''Sept. 22, '19:''
* Added some additional information to the [[Invisible Access to SugarCube Output]] section.
''Sept. 15, '19:''
* Minor improvements to the [[Linking to Passages by URL]] section.
* Minor improvements to the [[Hovertip Macro]] section and the macro's CSS.
''Sept. 11, '19:''
* Added the [[Bottom and Top Bars]] section.
''Sept. 8, '19:''
* Minor improvements to the [[CheckboxPlus Widget]] CSS and an IE fix.
''Aug. 30, '19:''
* Added the [[Dumb Terminal]] section.
''Aug. 27, '19:''
* Added the [[selectRange macro]] section.
''Aug. 26, '19:''
* Added another example to the [[Loading External Scripts]] section.
''Aug. 17, '19:''
* Added a CSS class parameter to the [[CheckboxPlus Widget]].
''Aug. 16, '19:''
* Added the [[Loading External Scripts]] section.
''Aug. 15, '19:''
* Added the [[Level Calculator]] section.
* Added the [[toggleLink Macro]] section.
''Aug. 14, '19:''
* Added some additional sample code to the [[Simple Password Hiding]] section.
''Aug. 13, '19:''
* Added the [[Multicolor Links]] section.
''Aug. 10, '19:''
* [[Scroll to Top]] now displays the source code for the buttons.
''Aug. 8, '19:''
* Updated to the //correct// version of the [[CheckboxPlus Widget]] code.
* Added the [[Invisible Access to SugarCube Output]] section.
''Aug. 6, '19:''
* Added the [[Drunk Text]] section.
* Added the [[CheckboxPlus Widget]] section.
* Updated [[Displaying Images in Twine]] to work in Twine v2.3.2+.
''July 30, '19:''
* Added the [[Class Macro]] section.
* Added the [[Hovertip Macro]] section.
* Added more details to the [[Maximum Checked Checkboxes]] section.
* Bugfix to [[Keyboard Link Navigation]] page.
* Minor code cleanup in [[Table of Links]].
* Started cleanup of the main menu page.
''Apr. 21, '19:''
* Further improvements to the [[Fullscreen]] section's JavaScript.
''Mar. 31, '19:''
* Minor change to the [[Fullscreen]] section's JavaScript to fix a problem for anyone using Babel's buggy transpiler on it. (Use if you're getting a "too much recursion" / "Maximum callstack size exceeded" error from Babel's {{{_typeof}}} function.)
''Mar. 28, '19:''
* Updates and fixes to the [[Fullscreen]] section's JavaScript.
''Mar. 26, '19:''
* Added the [[Using JavaScript with SugarCube]] section.
''Mar. 21, '19:''
* Added the [[Newgrounds Fix]] section to show how to fix problems playing Twine games on the Newgrounds website.
* Some minor cleanup of various CSS code.
''Mar. 3, '19:''
* Some minor cleanup and code improvement in the [[Health Bar]] section.
* Some more code dumped into the [[Music]] section.
''Feb. 26, '19:''
* Added the {{{<<chunkText>>}}} macro code to the [[Combining Story Passages]] section.
''Feb. 25, '19:''
* Improved the code in the [[Table of Links]] passage, added a description, and added visible source code.
''Feb. 24, '19:''
* Added code for a color shifting [[Health Bar]].
''Feb. 17, '19:''
* Major cleanup of the "Dropdown Select" section (now within the [[Listbox Tricks]] section), adding explanations and visible source code. Also, fixed an example there.
''Feb. 12, '19:''
* Added the [[SetPronouns Widget]] section.
''Feb. 10, '19:''
* Added {{{<<ErrMsg>>}}} widget to the [[Error Messages]] section.
''Feb. 5, '19:''
* Added more sample code to the [[Time]] section.
''Feb. 2, '19:''
* Showed code examples in [[Glitchy Text]] section.
''Jan. 31, '19:''
* Updated {{{TrackExists()}}} and {{{isPlaying()}}} functions in the [[Music]] section to use the new (v2.28.0+) SugarCube API.
''Jan. 27, '19:''
* Added the "Multiselect Listboxes" section (now within the [[Listbox Tricks]] section).
''Jan. 23, '19:''
* Added the [[Simple Password Hiding]] section.
''Jan. 5, '19:''
* Added the [[ScrollTo Macro]] section.
* Fixed a bug in [[Hover Text]].
* Added donation buttons to [[Main Menu]].
''Dec. 21, '18:''
* Minor fixes to the CSS code in the [[Health Bar]].
* Added another example to the [[Displaying Images in Twine]] section.
''Dec. 18, '18:''
* Displayed the code used in the [[Time]] section.
''Dec. 16, '18:''
* Updated Chapel's audio slider in the [[Music]] section to use the new (v2.28.0+) SugarCube API for volume control.
''Dec. 14, '18:''
* Adjusted audio path from "sound" to "sounds".
* Fixed incorrect "newline" marker in the [[Error Messages]] section.
* Added a note on how to adjust the color of a [[Health Bar]].
''Dec. 11, '18:''
* Added the [[Swapping Items]] section.
* Added the [[Maximum Checked Checkboxes]] section.
* Added this section.
* Added code and explanation in the "Smart Checkboxes" section (now removed).
<h1>Maximum Checked Checkboxes</h1>This code limits the number of checkboxes that can be checked (2 in this case). Try putting a checkmark in more than two of the below checkboxes:
<label><input type="checkbox" id="cb1" class="chkGroup" value="Good"> Good</label>
<label><input type="checkbox" id="cb2" class="chkGroup" value="Fast"> Fast</label>
<label><input type="checkbox" id="cb3" class="chkGroup" value="Cheap"> Cheap</label>
<<done>><<script>>
function selectiveCheck (event) {
$("#result").empty();
let checked = $(".chkGroup:checked");
for (let i = 0; i < checked.length; i++) {
if (checked.length <= max || event.target != checked[i]) {
$("#result").wiki($(checked[i]).val() + " ");
}
}
if (checked.length > max) return false;
}
let checks = $(".chkGroup"), max = 2;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = selectiveCheck;
}
<</script>><</done>>
''Results:'' <span id="result"></span>
The above code is:
{{{
<label><input type="checkbox" id="cb1" class="chkGroup" value="Good"> Good</label>
<label><input type="checkbox" id="cb2" class="chkGroup" value="Fast"> Fast</label>
<label><input type="checkbox" id="cb3" class="chkGroup" value="Cheap"> Cheap</label>
<<done>><<script>>
function selectiveCheck (event) {
$("#result").empty();
let checked = $(".chkGroup:checked");
for (let i = 0; i < checked.length; i++) {
if (checked.length <= max || event.target != checked[i]) {
$("#result").wiki($(checked[i]).val() + " ");
}
}
if (checked.length > max) return false;
}
let checks = $(".chkGroup"), max = 2;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = selectiveCheck;
}
<</script>><</done>>
''Results:'' <span id="result"></span>
}}}
Changing {{{max = 2}}} changes the maximum number of checkboxes. Also note that this code uses the jQuery <a href="https://api.jquery.com/val/" target="_blank">''.val()'' method</a> to get the value of the "{{{value}}}" attribute of each checkbox.
The above code is only that complex because it also has to display text showing which items are checked. However, if you don't need that part, then things get much simpler.
Here's a less complicated example of code, using the SugarCube {{{<<checkbox>>}}} macro, which limits the maximum number of selectable checkboxes in a group:
<label class="chkGroup2"><<checkbox "$good" false true>> Good</label>
<label class="chkGroup2"><<checkbox "$fast" false true>> Fast</label>
<label class="chkGroup2"><<checkbox "$cheap" false true>> Cheap</label>
<<done>>
<<run $(".chkGroup2 input").each(function () {
this.onclick = () => $(".chkGroup2 input:checked").length <= 2;
})>>
<</done>>
Doing that only needs this code:
{{{
<label class="chkGroup2"><<checkbox "$good" false true>> Good</label>
<label class="chkGroup2"><<checkbox "$fast" false true>> Fast</label>
<label class="chkGroup2"><<checkbox "$cheap" false true>> Cheap</label>
<<done>>
<<run $(".chkGroup2 input").each(function () {
this.onclick = () => $(".chkGroup2 input:checked").length <= 2;
})>>
<</done>>
}}}
In this version, instead of putting the class ("{{{chkGroup2}}}") on the textboxes themselves, we put the class on the labels which //contain// the textboxes. To use this code yourself, you'd simply need to make sure you use the same name for the labels' class as you do for the code within the {{{<<done>>}}} macro. Also, you would want the "{{{2}}}" to be set to the maximum number of checkboxes you want to allow to be selected at once.
Now, if you //aren't// using the SugarCube {{{<<checkbox>>}}} macro, then you'll need some way to access the values of your checkboxes. To do that, you can use code like this:
{{{
$("#CheckboxID").prop("checked")
}}}
where {{{CheckboxID}}} is the ID of the specific checkbox you're referring to.
In this code from the top of the page:
{{{
<input type="checkbox" id="cb1" class="chkGroup" value="Good">
}}}
the checkbox's ID is set to {{{cb1}}}, so to access the value of that checkbox you'd do:
{{{
<<set $Good = $("#cb1").prop("checked")>>
}}}
and that would set {{{$Good}}} variable to either {{{true}}} or {{{false}}}, depending on whether the checkbox was checked or not, respectively. See the jQuery <a href="https://api.jquery.com/prop/" target="_blank">''.prop()'' method</a> for details.
If you wanted to know how many checkboxes in that group of checkboxes with a "{{{chkGroup}}}" class are currently checked, then you can get that number from {{{$(".chkGroup:checked").length}}}.
For a usage example:
<label><input type="checkbox" id="cb1" class="chkGroup3" value="Good"> Good</label>
<label><input type="checkbox" id="cb2" class="chkGroup3" value="Fast"> Fast</label>
<label><input type="checkbox" id="cb3" class="chkGroup3" value="Cheap"> Cheap</label>
<<button "Get values">>
<<run $("#values").empty()>>
<<set _cbox = $(".chkGroup3")>>
<<for _i = 0; _i < _cbox.length; _i++>>
<<run $("#values").wiki("<br>" + $(_cbox[_i]).val() + " = " + $(_cbox[_i]).prop("checked"))>>
<</for>>
<<run $("#values").wiki("<br>Total Selected = " + $(".chkGroup3:checked").length)>>
<</button>>
Values: <span id="values"></span>
The above code is:
{{{
<label><input type="checkbox" id="cb1" class="chkGroup3" value="Good"> Good</label>
<label><input type="checkbox" id="cb2" class="chkGroup3" value="Fast"> Fast</label>
<label><input type="checkbox" id="cb3" class="chkGroup3" value="Cheap"> Cheap</label>
<<button "Get values">>
<<run $("#values").empty()>>
<<set _cbox = $(".chkGroup3")>>
<<for _i = 0; _i < _cbox.length; _i++>>
<<run $("#values").wiki("<br>" + $(_cbox[_i]).val() + " = " + $(_cbox[_i]).prop("checked"))>>
<</for>>
<<run $("#values").wiki("<br>Total Selected = " + $(".chkGroup3:checked").length)>>
<</button>>
Values: <span id="values"></span>
}}}
Thus, you can use code like this to get both the values of your checkboxes and how many are currently selected.<h1>{{{<<ScrollTo>>}}} Macro</h1>This macro allows you to easily scroll an element into view so that it's aligned at the top or bottom of the window.
For example, the "Scroll to bottom element" button uses this code:
{{{
<<button "Scroll to bottom element">>
<<ScrollTo "bottomElement" false>>
<</button>>
}}}
The "false" parameter tells {{{<<ScrollTo>>>}}} to scroll until that element is fully visible at the bottom of the window.
<<button "Scroll to bottom element">>
<<ScrollTo "bottomElement" false>>
<</button>>
Clicking that button will scroll you down to this line of code:
{{{
@@#bottomElement;(Bottom element)@@
}}}
That code places a {{{<div>}}} element with an ID of "bottomElement" around the "(Bottom element)" text.
If you don't want to use a {{{<div>}}} element, which produces a separate block of HTML elements, you can instead use a {{{<span>}}} element, which allows it to be inline with other HTML elements, and set the element ID directly, like this:
{{{
<span id="topElement">(Top element)</span>
}}}
Which just looks like this:
<span id="topElement">(Top element)</span>
To add this functionality to your Twine story, just add the following code to your JavaScript section:
{{{
/* <<ScrollTo>> macro: Scrolls the window to the given element ID.
Waits for element to exist.
Accepts parameters for scrollIntoView(). See:
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
Usage:
Scroll element into view aligned to the top of the window:
<<ScrollTo "ElementID">>
Scroll element into view aligned to the bottom of the window:
<<ScrollTo "ElementID" false>>
*/
Macro.add('ScrollTo', {
skipArgs : false,
handler : function () {
if (this.args.length > 0) {
let Value = this.args[0];
if (typeof Value === "string" || Value instanceof String) {
let element = null, params = undefined;
if (this.args.length > 1) {
params = this.args[1];
}
// wait for element
let elementWaitID = setInterval(function () {
element = document.getElementById(Value);
if (element != null) {
// stop waiting and set scroll position
clearInterval(elementWaitID);
if (params != undefined) {
element.scrollIntoView(params);
} else {
element.scrollIntoView();
}
}
}, 100);
}
}
}
});
/* <<ScrollTo>> macro - End */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
For another example, the "Scroll to top element" button uses this code:
{{{
<<button "Scroll to top element">>
<<ScrollTo "topElement">>
<</button>>
}}}
Since there's no "false" parameter, {{{<<ScrollTo>>>}}} to scroll until that element is fully visible at the top of the window.
<<button "Scroll to top element">>
<<ScrollTo "topElement">>
<</button>>
Clicking that button will scroll you up to this earlier line of code:
{{{
<span id="topElement">(Top element)</span>
}}}
This example shows how it works with a {{{<span>}}} element, but it would work with {{{<img>}}}, {{{<a>}}}, and other elements, as long as you set the element's ID.
''NOTE:'' Element IDs should be unique on the page. If you have more than one element with the same ID then this may not work the way you expect.
@@#bottomElement;(Bottom element)@@
You can have other stuff below that element.<h1>Simple Password Hiding</h1>If you want to have passwords or cheat codes in your game, but you don't want them to be found simply by looking at your HTML code, then you can use the code below to "hash" those strings into a number.
''For example:''
Enter text here: <<textbox "_txt" "">><<button "Hash Text">><<set _val = hashStr(_txt)>><<run $("#test").empty().wiki(_val)>><</button>>
Hash: <span id="test"></span>
If you type the word {{{Swordfish}}} into the above textbox and hit the button, it will give you the hash value of {{{334046645}}} every time. This means that instead of doing:
{{{
<<if _txt == "Swordfish">>
}}}
in your code, you can do:
{{{
<<if hashStr(_txt) == 334046645>>
}}}
and it will effectively work the same, but without giving away what the password is to anyone who reads the HTML code.
To use the {{{hashStr()}}} function in your own project, just add the following to your it's JavaScript section:
{{{
/* hashStr - Start */
window.hashStr = function(txt) {
let hash = 0, i, chr;
if (txt.length === 0) return hash;
for (i = 0; i < txt.length; i++) {
chr = txt.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
/* hashStr - End */
}}}
''NOTE:'' This is by no means a strong hash. It should be fine for a game, but there are far better methods if you need a secure hash. Do not use this code for anything which requires strong security.
OK, so now that you've got that, you'll need some code to make it work in your game. Try this...
__''Password Test (password = oceanus)''__
<<set $answer to ''>>\
<<textbox '$answer' ''>>\
<span id='textbox-submit'>\
<<button 'Send Intel'>>
<<set $answer to $answer.trim().toLowerCase()>>
<<if hashStr($answer) == -1635745764>>
<<replace '#textbox-submit'>>\
/* Removes the button */
<</replace>>
<<replace '#textbox-reply'>>\
Correct!
[[Read the secret document.|Main Menu]]
<</replace>>
<<run $('#textbox-answer').attr('readonly', 'true');>>
<<else>>
<<replace '#textbox-reply'>>\
@@.alert;Incorrect.@@ Please try again.\
<</replace>>
<</if>>
<</button>>\
</span>
<<script>>
$(document).one(":passagerender", function (ev) {
$(ev.content).find("#textbox-answer").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-submit button").trigger("click");
}
});
});
<</script>>
<span id='textbox-reply'></span>
Here's the above code (with the addition of setting "autofocus" for the textbox):
{{{
<<set $answer to ''>>\
<<textbox '$answer' '' autofocus>>\
<span id='textbox-submit'>\
<<button 'Send Intel'>>
<<set $answer to $answer.trim().toLowerCase()>>
<<if hashStr($answer) == -1635745764>>
<<replace '#textbox-submit'>>\
/* Removes the button */
<</replace>>
<<replace '#textbox-reply'>>\
Correct!
[[Read the secret document.|Main Menu]]
<</replace>>
<<run $('#textbox-answer').attr('readonly', 'true');>>
<<else>>
<<replace '#textbox-reply'>>\
@@.alert;Incorrect.@@ Please try again.\
<</replace>>
<</if>>
<</button>>\
</span>
<<script>>
$(document).one(":passagerender", function (ev) {
$(ev.content).find("#textbox-answer").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-submit button").trigger("click");
}
});
});
<</script>>
<span id='textbox-reply'></span>
}}}
Note that the above code converts whatever was typed in to all lowercase (using {{{.toLowerCase()}}}) and trims off any whitespace at the beginning or end of the text (using {{{.trim()}}}). This makes things a bit easier on the player if they make a minor copying mistake on the password.
It also allows the user to hit the {{{ENTER}}} key to trigger entering the text (using the code inside the {{{<<script>>>}}} section) and disables the textbox after you enter the correct password (using the {{{<<run $('#textbox-answer').attr('readonly', 'true');>>}}} line).
If you use a different variable instead of {{{$answer}}}, then you'll need to replace the "answer" part of {{{"#textbox-answer"}}}, in the two places it's in above code, with the name of the story variable you use instead (not including the {{{$}}}). If you use a temporary variable, then you'll need to put a dash in front of the name where the underscore ({{{_}}}) would be. For example, using:
{{{
<<textbox '_code' ''>>\
}}}
would mean that you'd have to use {{{"#textbox--code"}}} instead.
Also, for the "alert" class that makes the message blink red, add this to your Stylesheet section:
{{{
/* Alert Text - End */
.alert {
animation: alertblink linear 2s;
}
@keyframes alertblink {
0% { color: white; }
17% { color: red; }
33% { color: white; }
50% { color: red; }
76% { color: white; }
83% { color: red; }
100% { color: white; }
}
/* Alert Text - End */
}}}<h1>{{{<<SetPronouns>>}}} Widget</h1>This widget makes it fairly easy to write text which automatically displays gendered pronouns correctly based on the gender you set.
''NOTE:'' If you have SugarCube v2.29.0 or later then you should use [[Pronoun Templates]] instead.
For example, you could write this text:
{{{
* $They got $themself a new toy for $their collection and $theyre very happy about it.
}}}
And if you had previously done {{{<<SetPronouns "f">>}}}, then that text would display as:<<SetPronouns "f">>
* $They got $themself a new toy for $their collection and $theyre very happy about it.
Alternately, {{{<<SetPronouns>>}}} or {{{<<SetPronouns "m">>}}} would give you:<<SetPronouns>>
* $They got $themself a new toy for $their collection and $theyre very happy about it.
And {{{<<SetPronouns "n">>}}} would give you:<<SetPronouns "n">>
* $They got $themself a new toy for $their collection and $theyre very happy about it.
{{{<<SetPronouns "b">>}}} would give you:<<SetPronouns "b">>
* $They got $themself a new toy for $their collection and $theyre very happy about it.
Note that the variables are case sensitive, so (uppercase) {{{$They}}} shows as "$They" and (lowercase) {{{$they}}} shows as "$they". Also, you have to use {{{$Theyre}}} and {{{$theyre}}}, because variables can't have an apostrophe in their name.
To use this, add the following code to a non-story non-special passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
<<widget "SetPronouns">><<nobr>>
/* Usage... (defaults to male) */
/* for "he": <<SetPronouns>> or <<SetPronouns "m">> */
/* for "she": <<SetPronouns "f">> */
/* for "they": <<SetPronouns "b">> */
/* for "it": <<SetPronouns "n">>*/
<<switch $args[0]>>
<<case "f">>
<<set $they = "she">>
<<set $them = "her">>
<<set $themself = "herself">>
<<set $themselves = "themselves">>
<<set $their = "her">>
<<set $theirs = "hers">>
<<set $theyre = "she's">>
<<set $youngPerson = "girl">>
<<set $youngPeople = "girls">>
<<set $adultPerson = "woman">>
<<set $adultPeople = "women">>
<<set $generalPerson = "girl">>
<<set $generalPeople = "girls">>
<<set $pal = "chick">>
<<set $pals = "chicks">>
<<set $They = "She">>
<<set $Them = "Her">>
<<set $Themself = "Herself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Her">>
<<set $Theirs = "Hers">>
<<set $Theyre = "She's">>
<<set $YoungPerson = "Girl">>
<<set $YoungPeople = "Girls">>
<<set $AdultPerson = "Woman">>
<<set $AdultPeople = "Women">>
<<set $GeneralPerson = "Girl">>
<<set $GeneralPeople = "Girls">>
<<set $Pal = "Chick">>
<<set $Pals = "Chicks">>
<<case "b">>
<<set $they = "they">>
<<set $them = "them">>
<<set $themself = "themself">>
<<set $themselves = "themselves">>
<<set $their = "their">>
<<set $theirs = "theirs">>
<<set $theyre = "they're">>
<<set $youngPerson = "person">>
<<set $youngPeople = "people">>
<<set $adultPerson = "person">>
<<set $adultPeople = "people">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "pal">>
<<set $pals = "pals">>
<<set $They = "They">>
<<set $Them = "Them">>
<<set $Themself = "Themself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Their">>
<<set $Theirs = "Theirs">>
<<set $Theyre = "They're">>
<<set $YoungPerson = "Person">>
<<set $YoungPeople = "People">>
<<set $AdultPerson = "Person">>
<<set $AdultPeople = "People">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Pal">>
<<set $Pals = "Pals">>
<<case "n">>
<<set $they = "it">>
<<set $them = "it">>
<<set $themself = "itself">>
<<set $themselves = "themselves">>
<<set $their = "its">>
<<set $theirs = "its">>
<<set $theyre = "it's">>
<<set $youngPerson = "person">>
<<set $youngPeople = "people">>
<<set $adultPerson = "person">>
<<set $adultPeople = "people">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "pal">>
<<set $pals = "pals">>
<<set $They = "It">>
<<set $Them = "It">>
<<set $Themself = "Itself">>
<<set $Themselves = "Themselves">>
<<set $Their = "Its">>
<<set $Theirs = "Its">>
<<set $Theyre = "It's">>
<<set $YoungPerson = "Person">>
<<set $YoungPeople = "People">>
<<set $AdultPerson = "Person">>
<<set $AdultPeople = "People">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Pal">>
<<set $Pals = "Pals">>
<<default>>
<<set $they = "he">>
<<set $them = "him">>
<<set $themself = "himself">>
<<set $themselves = "themselves">>
<<set $their = "his">>
<<set $theirs = "his">>
<<set $theyre = "he's">>
<<set $youngPerson = "boy">>
<<set $youngPeople = "boys">>
<<set $adultPerson = "man">>
<<set $adultPeople = "men">>
<<set $generalPerson = "guy">>
<<set $generalPeople = "guys">>
<<set $pal = "dude">>
<<set $pals = "dudes">>
<<set $They = "He">>
<<set $Them = "Him">>
<<set $Themself = "Himself">>
<<set $Themselves = "Themselves">>
<<set $Their = "His">>
<<set $Theirs = "His">>
<<set $Theyre = "He's">>
<<set $YoungPerson = "Boy">>
<<set $YoungPeople = "Boys">>
<<set $AdultPerson = "Man">>
<<set $AdultPeople = "Men">>
<<set $GeneralPerson = "Guy">>
<<set $GeneralPeople = "Guys">>
<<set $Pal = "Dude">>
<<set $Pals = "Dudes">>
<</switch>>
<</nobr>><</widget>>
}}}
Then set the pronouns appropriately by calling the widget when the gender is chosen, and after that you just use the pronoun variables listed above in your text.<h1>Newgrounds Fix</h1>OK, you put your shiny new Twine game up on <a href="https://www.newgrounds.com" target="_blank">Newgrounds</a>, only to find that your scrollbars are broken. How do you fix it?
Simple. Add this to your JavaScript section:
{{{
if (document.location.href.toLowerCase().indexOf("newgrounds") !== -1) {
$("html").css({ width: "640px", height: "480px", "overflow-y": "auto" });
$("#ui-overlay").css({ width: "640px", height: "480px", top: "0", left: "0", position: "absolute" });
$("#ui-bar").css({ height: "480px" });
$("#story").css({ width: "590px", height: "480px", margin: "0 0 0 50px", "overflow-y": "auto" });
$("#ui-bar.stowed ~ #story").css({ margin: "0 0 0 50px" });
$("#passages").css({ margin: "50px 16px 50px 0" });
$(document).on(":passagerender", function (ev) { $("#story").scrollTop(0); });
}
}}}
That code checks to see if your game is being played on Newgrounds, and if it is, then it makes sure everything fits properly in their 640x480 <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe" target="_blank">iframe</a> and also adds some code to make sure that going to a new passage puts the scrollbar at the top of the screen.
Not playing the game on Newgrounds? No problem, as long as the word "newgrounds" isn't in the game's URL or filepath, then your game will play as normal.
Enjoy!
<h1>Using JavaScript with SugarCube</h1>Basically, you can write JavaScript code into Twine in four different ways:
''1)'' You can put the JavaScript code inside a passage using the {{{<<script>>}}} macro. For example:
{{{
<<script>>
alert("This is JavaScript.");
<</script>>
}}}
See the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-script" target="_blank"><<script>> macro documentation</a>.
''2)'' You can use JavaScript within just about any macro that takes parameters. For example:
{{{
<<run alert("This is also JavaScript.")>>
<<if $("#elementID").length>>
The above jQuery JavaScript in the "if" will look to see if an element with that ID exists.
If it does then this text will be displayed.
<</if>>
}}}
In the context here, an "element" refers to an HTML element, basically anything in the format of {{{<...>}}} or {{{<...>...</...>}}}.
The "element ID" refers to the value of the ID attribute in that element, if there is one. For example:
{{{
<img id="someimage" class="imagestyle" src="/images/image.jpg">
}}}
So within the above {{{<img>}}} element, the value of the {{{id}}} attribute is set to "someimage" (which you could refer to in CSS using {{{#someimage}}}), the value of the {{{class}}} attribute is set to "imagestyle" (which you could refer to in CSS using {{{.imagestyle}}}), and the value of the {{{src}}} attribute is set to "/images/image.jpg".
Just think of the format as:
{{{
<element attribute=value>
}}}
''3)'' You can write JavaScript in the JavaScript section. Any JavaScript put there will get run when the game is started or the page is reloaded (by hitting RELOAD or F5 in the browser). You can also create functions there which you can call from the passages, and you can set up triggers which occur on certain events. For example:
{{{
/* The following line is run immediately when the program first starts. */
let timeout = setTimeout(triggeredCode, 2000); // calls the "triggeredCode" function after 2 seconds
/* The code inside "triggeredCode" is run only when the function is called. */
window.triggeredCode = function() {
alert("This is JavaScript.");
};
}}}
You could then call a function, like the above {{{triggeredCode()}}} function, from any of your passages like this:
{{{
<<button "Click me">><<run triggeredCode()>><</button>>
}}}
''4)'' You can write JavaScript into the event handlers of HTML elements. For example:
Click this image: <img @src="setup.ImagePath+'Example.png'" onclick="alert('Image was clicked on.');">
{{{
Click this image: <img @src="setup.ImagePath+'Example.png'" onclick="alert('Image was clicked on.');">
}}}
When the image is clicked on, that will trigger the "onclick" event, which will call the JavaScript there.
Within your JavaScript code, you can access the values of your Twine variables one of two ways:
''1)'' You can get and set the values of Twine story variables and temporary variables within JavaScript code by using the {{{State.variables}}} and {{{State.temporary}}} objects, respectively. For example:
{{{
State.variables.test1 = "test-1";
State.temporary.test2 = "test-2";
}}}
would set the value of the Twine {{{$test1}}} story variable to "test-1", and the value of the Twine {{{_test2}}} temporary variable to "test-2". You can read the variables the same way:
{{{
let someVariable = State.variables.test1;
}}}
That would set the JavaScript variable {{{someVariable}}} to the same value as the {{{$test1}}} variable.
(There are other similar methods for doing this, such as using the <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-variables" target="_blank">{{{variables()}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-temporary" target="_blank">{{{temporary()}}}</a> functions or the <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-method-getvar" target="_blank">{{{State.getVar()}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-method-setvar" target="_blank">{{{State.setVar()}}}</a> methods to indirectly access those variables, but it's most optimal to directly access the Twine variables using the <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-getter-variables" target="_blank">{{{State.variables}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-getter-temporary" target="_blank">{{{State.temporary}}}</a> object variables.)
''2)'' You can pass values to functions using function parameters. For example if you have this code in your JavaScript section:
{{{
window.MyFunction = function (someVar) {
alert(someVar);
}
}}}
then you could call that from a Twine passage like this:
{{{
<<set $anotherVar = "This is a test.">>
<<run MyFunction($anotherVar)>>
}}}
and that would pass the value of {{{$anotherVar}}} into the {{{someVar}}} parameter on {{{MyFunction()}}}, and then display that text.
<h1>{{{<<hovertip>>}}} Macro</h1>This code gives you an easy way to display information to help your beginner users, without getting in the way of more experienced users.
Note that, if the underlined text or the window that pops up is clicked on, the tip will also appear and stay visible until you click elsewhere, so this works on mobile devices as well. Additionally, this hovertip meets accessibility standards for "<a href="https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html" target="_blank">content on hover or focus</a>" and "<a href="https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/" target="_blank">tooltip patterns</a>", so the tooltips can be hidden using the "ESC" key.
For example, <<hovertip 'And now you can see the "hovertip" text.'>>hover your mouse over this text.<</hovertip>>
Also, <<hovertip `'Like this one: <img style="height: 32px;" src="' + setup.ImagePath + 'Prism64-2.png">'`>>tips can include images.<</hovertip>>
Here's the code for the two above examples:
{{{
For example, <<hovertip 'And now you can see the "hovertip" text.'>>hover your mouse over this text.<</hovertip>>
Also, <<hovertip `'Like this one: <img style="height: 32px;" src="' + setup.ImagePath + 'Prism64-2.png">'`>>tips can include images.<</hovertip>>
}}}
As you can see, the macro works like this:
{{{
<<hovertip "Hovertip text.">>Text you normally see.<</hovertip>>
}}}
Wherever you need it, just wrap the {{{<<hovertip>>}}} macro around the text you want displayed, and pass the "tip" as a parameter to the macro.
You can also reverse how you pass the tip and text to the macro with the "{{{swap}}}" option:
{{{
<<hovertip "Text you normally see." swap>>Hovertip text.<</hovertip>>
}}}
If you want the {{{<<hovertip>>}}} to swap by default, then you can put this setting in your {{{StoryInit}}} special passage:
{{{
<<set setup.hovertipSwap = true>>
}}}
Note that if you use both the swap setting and the swap parameter at the same time, then it will use the default behavior.
By default the maximum width of the tip window is 330 pixels. You can change the defaults in the CSS below. Also, you can override that by setting the optional second parameter of the {{{<<hovertip>>}}} macro to the maximum pixel width for that individual {{{<<hovertip>>}}}.
<<hovertip 'By default, the maximum width of the tip window is 330 pixels as set in the ".hoverBox" CSS.'>>Default 330px width.<</hovertip>>
<<hovertip 'The maximum width is overriden to 500 pixels in this case by passing "500".' 500>>Custom 500px width.<</hovertip>>
The above code looks like this:
{{{
<<hovertip 'By default, the maximum width of the tip window is 330 pixels as set in the ".hoverBox" CSS.'>>Default 330px width.<</hovertip>>
<<hovertip 'The maximum width is overriden to 500 pixels in this case by passing "500".' 500>>Custom 500px width.<</hovertip>>
}}}
Because the hovertips try to minimize the width as much as possible without increasing their height, even if you set the maximum width to 500 pixels, if the text isn't 500 pixels or more in width, then the hovertip's width will be shrunk to fit the text. <<hovertip "Max-width = 500 pixels." 500>>For example.<</hovertip>>
That code looks like this:
{{{
<<hovertip "Max-width = 500 pixels." 500>>For example.<</hovertip>>
}}}
Finally, the if the {{{<<hovertip>>}}} macro is contained within an element that has the "overflow" CSS property set to "auto", "clip", "hidden", "overlay", or "scroll", then the tip text itself will also be contained inside that element and will constrain itself to the element's width. For example:
<div id="test" style="width: 200px; height: 100px; overflow: auto; padding-left: 8px; background-color: #0000FF">200px by 100px window
overflow = "auto"
Test: <<hovertip "By default, the maximum width of the tip window is 330 pixels.">>Default 330px width<</hovertip>>
</div>
''NOTE:'' If the macro is used in a passage and within an element which has its CSS "position" set to "sticky" or "fixed", then the tip text window will appear behind the UI bar if it would overlap it, due to how HTML implements <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Positioned_layout/Stacking_context" target="_blank">stacking contexts</a>. Thus it's recommended that you do not use this macro within "sticky" or "fixed" positioned elements unless they're inside of the UI bar.
To add the {{{<<hovertip>>}}} macro to your project, just add the following code to your project's ''JavaScript'' section:
{{{
/* <<hovertip>> macro v3.0 - Start */
/**
* **UpdateHoverTipTxt():** Handles repositioning and resizing the
* hovertip so that it remains onscreen and doesn't have
* unnecessary whitespace along the right side of the hovertip.
*
* USES: _HoverTipCount
*
* @param {JQuery<JQueryStatic|HTMLElement>} container
**/
async function UpdateHoverTipTxt (container) { // Async to prevent setTimeout violations from debouncing code. This function does not need to be "await"ed.
HTTLastUpdate = Date.now();
if (Engine.isIdle()) {
// Debounce stuff.
clearInterval(HTTIntervalID);
HTTIntervalID = 0;
// Make sure `container` is a jQuery object.
if (typeof container !== "object") {
container = $(document); // Not an object, so use the document object.
} else if (!("jquery" in container)) {
container = $(container); // Not a jQuery object, so try to convert it to one.
}
/** All `id="hoverTipTxt##"` elements. */
let elementList = container.find('span[id^="hoverTipTxt"]');
let scroll = { x: window.scrollX, y: window.scrollY }; // Remember current scroll position.
for (let i = 0; i < elementList.length; i++) {
/** Current "hoverTipTxt" element. */
let element = $(elementList[i]);
element.removeClass("hoverDismissed");
if (element.css("display") === "none") { // Skip hidden elements.
continue;
}
/** Current "hoverTipTxt" bounding rect. */
let hoverPos = element[0].getBoundingClientRect();
/** Parent of current "hoverTipTxt" element. */
let parent = $(container).find("#hoverTip" + (elementList[i].id.substring("hoverTipTxt".length)));
// Find the view area (i.e. the closest ancestor which limits containment based on the "overflow" property).
let view = $(parent).parent(), sticky = false;
while (view.length > 0 && view[0].nodeName != "#document-fragment" && !["auto", "clip", "hidden", "overlay", "scroll"].includes($(view).css("overflow"))) {
if (["sticky"].includes($(view).css("position"))) {
sticky = true; // Element is within a "sticky" element.
}
view = view.parent();
}
if (view.length > 0 && view[0].nodeName != "#document-fragment") {
// Handle cases where the hovertip element is limited to smaller view window.
if (parseFloat(element.css("max-width")) > view.innerWidth() - 35) {
element.css("max-width", (view.innerWidth() - 35) + "px");
}
view.limited = view[0].nodeName != "BODY";
} else {
// No view window found.
view = $(window);
view.limited = false;
}
if (view.limited && sticky) { // Ignore "sticky" case.
view.limited = false;
}
// Position bottom of hoverTipTxt just above the parent.
let top = Math.round(-element.outerHeight() - 4);
// Prevent it from going off the bottom side of the view.
if (hoverPos.bottom - parseFloat(element.css("top")) + top > view.height() + 12) {
top += view.height() + 12 - hoverPos.bottom + parseFloat(element.css("top")) - top;
top = Math.round(top);
}
// Prevent it from going off the top side of the view.
if (hoverPos.top - parseFloat(element.css("top")) + top < 4) {
top += 4 - hoverPos.top + parseFloat(element.css("top")) - top;
top = Math.round(top);
}
// Center hoverTipTxt horizontally over parent.
let left = Math.round((parent.outerWidth() - element.outerWidth()) / 2);
// Prevent it from going off the right side of the view.
if (hoverPos.right - parseFloat(element.css("left")) + left > view.width() - 4) {
left += view.width() - 4 - hoverPos.right + parseFloat(element.css("left")) - left;
left = Math.round(left);
}
// Prevent it from going off the left side of the view.
if (hoverPos.left - parseFloat(element.css("left")) + left < 4) {
left += 4 - hoverPos.left + parseFloat(element.css("left")) - left;
left = Math.round(left);
}
if (view.limited) { // Don't put the top-left outside of a limited view window.
let viewPos = view.position(), parentPos = parent.position();
let viewRect = view[0].getBoundingClientRect(), parentRect = parent[0].getBoundingClientRect();
top = Math.max(viewPos.top - parentPos.top, viewRect.top - parentRect.top, top);
left = Math.max(viewPos.left - parentPos.left, viewRect.left - parentRect.left, left);
}
// Update position if it doesn't match the current position.
if (parseFloat(element.css("top")) != top || parseFloat(element.css("left")) != left) {
element.css({ top: top, left: left });
}
}
// Prevent window scrolling.
if (scroll.x != window.scrollX || scroll.y != window.scrollY) {
window.scrollTo({ left: scroll.x, top: scroll.y, behavior: "smooth" });
}
} else { // SugarCube is working, so wait.
UpdateHoverTipTxtDebounce();
}
}
/** HoverTipText interval ID. */
let HTTIntervalID = 0;
/** HoverTipText last update time. */
let HTTLastUpdate = 0;
/** HoverTipText delay interval in milliseconds. */
const HTTDelay = 300;
/** Prevent calling UpdateHoverTipTxt() too often. */
function UpdateHoverTipTxtDebounce () {
if (HTTIntervalID == 0) {
if (Date.now() - HTTDelay > HTTLastUpdate) { // Go immediately.
UpdateHoverTipTxt();
} else { // Debounce.
HTTIntervalID = setInterval(UpdateHoverTipTxt, HTTDelay);
}
} else { // Debounce.
clearInterval(HTTIntervalID);
HTTIntervalID = setInterval(UpdateHoverTipTxt, HTTDelay);
}
}
window.UpdateHoverTipTxtDebounce = UpdateHoverTipTxtDebounce; // Make it global.
// Waits for passage to be fully rendered before doing anything.
$(document).on(":passageend", UpdateHoverTipTxtDebounce);
// Updates on resize or scroll events.
$(window).on("resize scroll", UpdateHoverTipTxtDebounce);
// Updates on UI bar being toggled.
$("#ui-bar-toggle").on("click", UpdateHoverTipTxtDebounce);
// Handle hovertip dismissal by hitting the ESCAPE key.
let HTTTipEscaped = false;
document.addEventListener("keydown", function (ev) {
let tips = $('span[id^="hoverTipTxt"]:visible');
if (ev.key === "Escape" && tips.length > 0) {
HTTTipEscaped = true;
tips.addClass("hoverDismissed");
ev.stopImmediatePropagation(); // Prevent hovertip dismissal from affecting other elements.
ev.preventDefault(); // Prevent hovertip dismissal from affecting the browser.
} else {
HTTTipEscaped = false;
}
}, true);
document.addEventListener("keyup", function (ev) {
if (ev.key === "Escape" && HTTTipEscaped) {
HTTTipEscaped = false;
ev.stopImmediatePropagation(); // Prevent hovertip dismissal from affecting other elements.
ev.preventDefault(); // Prevent hovertip dismissal from affecting the browser.
}
}, true);
/* <<hovertip>> macro */
Macro.add("hovertip", {
tags : null,
handler : function () {
if (this.args.length > 0) {
let mw = "", text = this.args[0];
let tip = this.payload[0].contents;
if (setup.hovertipSwap == true) {
[text, tip] = [tip, text];
}
if (this.args.indexOf("swap") > 0) {
[text, tip] = [tip, text];
}
if ((this.args.length > 1) && (!isNaN(parseInt(this.args[1], 10)))) {
mw = parseInt(this.args[1], 10);
} else if ((this.args.length > 2) && (!isNaN(parseInt(this.args[2], 10)))) {
mw = parseInt(this.args[2], 10);
}
if (State.temporary.HoverTipCount == undefined) {
State.temporary.HoverTipCount = 1;
} else {
State.temporary.HoverTipCount++;
}
while ($("#hoverTip" + State.temporary.HoverTipCount).length) {
// Found an existing hoverTip.
State.temporary.HoverTipCount++;
}
// Scale down max-width to best fit content.
let count = State.temporary.HoverTipCount;
let el = $('<span id="hoverTip' + count + '" class="hoverTip"' +
' role="tooltip" aria-describedby="hoverTipTxt' + count + '" tabindex="0"' +
' onfocus="UpdateHoverTipTxtDebounce();" onmouseenter="UpdateHoverTipTxtDebounce();">' +
'<span id="hoverTipTxt' + count + '" class="hoverBox"></span></span>')[0];
el.style.visibility = "hidden"; // Add the "visibility: hidden" style.
if (($("#ui-bar:visible").length > 0 || UIBar.isHidden() === null) && $("#passages > div")) { // If the page is ready...
$("#passages > div")[0].appendChild(el); // ...add a hidden test element to a passage.
} else { // If this is the first passage and hasn't loaded yet...
$("body")[0].appendChild(el); // ...add the hidden test element to body instead.
}
$(el).wiki(tip);
let elchild = $(el).find(".hoverBox")[0];
$(elchild).wiki(text);
if (!mw) { // Get the default "max-width" in pixels when it's not passed as a parameter.
mw = parseInt($(elchild).css("max-width"), 10);
}
$(elchild).css("max-width", mw + "px");
let height = $(elchild).outerHeight();
while (height == $(elchild).outerHeight() && mw > 0) {
$(elchild).css("max-width", --mw + "px");
}
if (++mw < $(elchild).outerWidth() + 2) { // Deal with cases where the text is a single word.
mw = $(elchild).outerWidth() + 2;
}
$(elchild).css("max-width", mw + "px");
$(elchild).data("maxWidth", mw + "px"); // Track the original max-width.
el.remove(); // Remove the element from the document.
el.removeAttribute("style"); // Remove the "visibility: hidden" style.
this.output.append(el);
} else {
$(this.output).wiki(this.payload[0].contents);
}
}
});
/* <<hovertip>> macro - End */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
You'll also need to add this CSS code to your ''Stylesheet'' section:
{{{
/* <<hovertip>> macro v3.0 styling - Start */
.hoverTip {
display: inline-block;
position: relative;
border-width: 0px;
border-bottom: 2px dotted white; /* If you want dots under the hoverable text */
line-height: initial;
}
.hoverTip .hoverBox {
display: none;
position: absolute;
}
.hoverTip:hover .hoverBox, .hoverTip:focus .hoverBox {
display: block;
animation: hoverDisable linear 0.5s; /* Prevent accidental clicks */
}
@keyframes hoverDisable {
0% { pointer-events: none; }
100%{ pointer-events: inherit; }
}
.hoverTip .hoverBox.hoverDismissed {
display: none;
}
.hoverBox {
padding: 3px 10px;
width: intrinsic; /* Safari/WebKit uses a non-standard name */
width: -webkit-max-content; /* Chrome */
width: -moz-max-content; /* Firefox/Gecko */
width: max-content;
max-width: 330px;
font-size: initial;
font-weight: normal;
line-height: initial;
text-align: left;
color: white;
background-color: black;
border: #7f7f7f solid 2px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.75);
box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.75);
z-index: 100;
}
.hoverBox::after {
content: "";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 8px;
border-style: solid;
border-color: #7f7f7f transparent transparent transparent;
z-index: 100;
}
.hoverBox a {
-webkit-transition-duration: unset;
-moz-transition-duration: unset;
-o-transition-duration: unset;
transition-duration: unset;
}
/* <<hovertip>> macro styling - End */
}}}
If the text is too high above or too low below the dotted line, then just adjust the {{{line-height}}} property within {{{.hoverTipTxt}}} to match your game's styling.
Enjoy!<h1>"Class" Macro</h1>Here is a simple example macro that just shows how to add a class to some text. This is not only useful for that, but as a basic template when creating your own macros.
Here's an example of how it looks:
<<c>>Some text<</c>>
<<c green>>Some text<</c>>
The above code:
{{{
<<c>>Some text<</c>>
<<c green>>Some text<</c>>
}}}
That will add the "default" class to the text inside unless some other class is specified, like in the second one where it applies the "green" class.
To use this, just add this to your JavaScript section:
{{{
/* Class macro - Start */
Macro.add("c", {
skipArgs : true,
tags : null,
handler : function () {
if (this.payload[0].args.full.length > 0) {
$(this.output).wiki('<span class="' + this.payload[0].args.full + '">' + this.payload[0].contents + '</span>');
} else {
$(this.output).wiki('<span class="default">' + this.payload[0].contents + '</span>');
}
}
});
/* Class macro - End */
}}}
You can use whatever classes you want, however, the two used in the example above (found in this Stylesheet) are:
{{{
.default {
color: white;
}
.green {
color: green;
}
}}}<h1>"Drunk" Text Example</h1>The "drunk" class causes text to look blurry and as though you have double-vision, like you're drunk. For example:
<span class="drunk">Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci velit, sed quia non-numquam eius modi tempora incidunt, ut labore et dolore magnam aliquam quaerat voluptatem.</span>
To use it put this in your Stylesheet section:
{{{
/* Drunk Text - Start */
.drunk {
animation: drunkCam 10s infinite alternate;
color: white;
}
@keyframes drunkCam {
0% { filter: blur(0px); text-shadow: 0 0 0 grey; }
20% { filter: blur(1px); text-shadow: 8px 0 0 grey; }
24% { filter: blur(0px); text-shadow: 0 0 0 grey; }
26% { filter: blur(0px); text-shadow: 0 0 0 grey; }
28% { filter: blur(1px); text-shadow: 10px 0 0 grey; }
30% { filter: blur(0px); text-shadow: 0 0 0 grey; }
60% { filter: blur(1px); text-shadow: 5px 0 0 grey; }
62% { filter: blur(0px); text-shadow: 0 0 0 grey; }
65% { filter: blur(2px); text-shadow: 8px 0 0 grey; }
67% { filter: blur(0px); text-shadow: 0 0 0 grey; }
80% { filter: blur(0px); text-shadow: 8px 0 0 grey; }
85% { filter: blur(2px); text-shadow: 10px 0 0 grey; }
88% { filter: blur(1px); text-shadow: 5px 0 0 grey; }
90% { filter: blur(0px); text-shadow: 0 0 0 grey; }
}
/* Drunk Text - End */
}}}
Then just wrap any text you want to look like this inside of a span with the "drunk" class, like this:
{{{
<span class="drunk">Drunk text goes here.</span>
}}}
and that will be displayed like this:
<span class="drunk">Drunk text goes here.</span><h1>{{{<<checkboxPlus>>}}} Widget</h1>The {{{<<checkboxPlus>>}}} widget allows you to display a custom checkbox which sets a SugarCube variable, displays a (clickable) label, and satisfies accessibility guidelines for users with impairments (usable via the keyboard with {{{TAB}}}, {{{SHIFT+TAB}}}, and {{{SPACE}}} keys). The checkboxes are also larger and the checkbox label is also clickable, to make them both easier to see and to click on for mobile devices. Basically, it's a visual upgrade to the SugarCube {{{<<checkbox>>}}} widget, but with "autocheck" always enabled, a built-in label, and easier styling.
Usage: {{{<<checkboxPlus "variableName" "checkbox text" ["className"]>>}}}
The value of the checkbox would then be tied to the variable name in the string passed to the widget. All variables passed to the widget will be set to either a Boolean {{{true}}} or {{{false}}}, and if the variable isn't defined, then it will default to {{{false}}}.
The ''className'' parameter is optional. If it's included then that CSS class will be applied to the text next to the checkbox.
''Example:''
CheckboxPlus checkboxes:<<set $check1 = true>><<set $check2 = false>>
<<checkboxPlus "$check1" "Checkbox 1">>\
<<checkboxPlus "$check2" "Checkbox 2">>\
<<checkboxPlus "_check3" "Checkbox 3" "boldGreen">>
<<button "Show $$check1 value">>
<<run alert($check1)>>
<</button>> <<button "Show $$check2 value">>
<<run alert($check2)>>
<</button>> <<button 'Show """_check3""" value'>>
<<run alert(_check3)>>
<</button>>
Here's a standard SugarCube {{{<<checkbox>>}}} widget for comparison:
<label><<checkbox "$test" false true>> Separate text</label>
The above code is just:
{{{
CheckboxPlus checkboxes:<<set $check1 = true>><<set $check2 = false>>
<<checkboxPlus "$check1" "Checkbox 1">>\
<<checkboxPlus "$check2" "Checkbox 2">>\
<<checkboxPlus "_check3" "Checkbox 3" "boldGreen">>
<<button "Show $$check1 value">>
<<run alert($check1)>>
<</button>> <<button "Show $$check2 value">>
<<run alert($check2)>>
<</button>> <<button 'Show """_check3""" value'>>
<<run alert(_check3)>>
<</button>>
Here's a standard SugarCube {{{<<checkbox>>}}} widget for comparison:
<label><<checkbox "$test" false true>> Separate text</label>
}}}
As you can see above, it works with both story variables and temporary variables, the checkbox is checked based on the value of the variable, and it even works if the variable wasn't previously defined.
The "boldGreen" class is defined like this in the CSS:
{{{
.boldGreen {
color: green;
font-weight: bold;
}
}}}
To use the {{{<<checkboxPlus>>}}} widget, add the following to a non-special non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
/* <<checkboxPlus>> widget
This widget allows you to display a custom checkbox which sets a
SugarCube variable, displays a (clickable) label, and satisfies
accessibility guidelines for users with impairments (usable via the
keyboard with TAB, SHIFT+TAB, and SPACE keys). The checkboxes are
also larger, to make them easier to see and to click on for mobile
devices.
Usage: <<checkboxPlus "variableName" "text" ["className"]>>
The value of the checkbox would then be tied to a variable, which
is passed to the widget as a string. All story variables passed to
the widget will be set to either a Boolean true or false. If the
variable had a "truthy" value, then the checkbox will be checked.
The "className" is an optional parameter, which adds that CSS class
to the text.
Example: <<checkboxPlus "$EnabledOp" "Enable Option" "blueText">>
VARIABLES USED: _cbStyle, _checkboxData, _checkboxIDno
*/
<<widget "checkboxPlus">><<nobr>>
/* Make sure the variable passed in is a boolean. */
<<set State.setVar(_args[0], !!State.getVar(_args[0]))>>
<<if ndef _checkboxIDno>>
/* Start checkbox IDs at 1. */
<<set _checkboxIDno = 1>>
<<else>>
/* Next checkbox ID. */
<<set _checkboxIDno++>>
<</if>>
<<set _checkboxData = "'" + _args[0] + "'">>
<<if def _args[2]>>
<<set _cbStyle = " " + _args[2]>>
<<else>>
<<set _cbStyle = "">>
<</if>>
/* Display checkbox. */
<span class="chkbox" tabindex="0" onkeypress="if ((event.key == ' ') || (event.key == 'Spacebar')) { $(this).find('input[type=\'checkbox\']').trigger('click'); return false; }">
<<print '<input type="checkbox" id="checkbox_' + _checkboxIDno + '" tabindex="-1" class="cbhidden" onchange="SugarCube.State.setVar(' + _checkboxData + ', this.checked)"' + (State.getVar(_args[0]) ? ' checked' : '') + '>'>>
<label @for="'checkbox_' + _checkboxIDno" @class="'chklabel' + _cbStyle">
_args[1]
</label>
</span>
<</nobr>><</widget>>
/* <<checkboxPlus>> Widget - End */
}}}
and add the following to your Stylesheet section:
{{{
/* checkboxPlus widget - Start */
input[type="checkbox"].cbhidden {
display: none;
}
input[type="checkbox"] + label.chklabel {
padding: 4px 4px 4px 30px;
position: relative;
user-select: none;
cursor: pointer;
}
:focus > input[type="checkbox"] + label.chklabel::before {
border: #135BCF 2px solid; /* Bright blue */
margin-left: 1px;
top: 2px; /* Use this to adjust the height of the checkbox box when it has focus. */
line-height: 15px; /* Use this to adjust the height of the checkmark when it has focus. */
}
input[type="checkbox"] + label.chklabel::before {
padding: 0 3px;
height: 15px;
width: 11px;
background: white;
border: 1px solid #999; /* Gray */
border-radius: 5px;
content: ' ';
color: black;
line-height: 16px; /* Use this to adjust the normal height of the checkmark. */
position: absolute;
text-align: center;
transform: translate(-130%, 20%); /* Use this to adjust the normal position of the checkbox box. */
}
input[type="checkbox"]:checked + label.chklabel::before {
content: '\2713';
font-weight: bold;
font-size: 78.75%;
}
input[type="checkbox"]:focus + label.chklabel::before {
border: #135BCF 2px solid; /* Bright blue */
margin-left: 1px;
}
.chkbox {
display: inline-block;
margin: 0 10px 0 0;
line-height: 150%;
}
.chkbox:focus {
outline: 2px dotted #135BCF; /* Bright blue */
}
/* checkboxPlus widget - End */
}}}
If you want the checkbox to look different, just modify the above CSS as needed. Certain parts have been marked to indicate what they can be adjusted for.<h1>Invisible Access to SugarCube Output</h1>If you need to access the HTML output from some SugarCube code, but want to do it in a way that the user won't see it, here's how you'd do that:
{{{
<<set $output = $(document.createElement("div")).wiki($input)[0].innerHTML>>
}}}
If, for example, {{{$input}}} was set to "{{{<<button 'Test'>><</button>>}}}" then {{{$output}}} would get set to this:
<<= "{{{\n" + $(document.createElement("div")).wiki("<<button 'Test'>><</button>>")[0].innerHTML + "\n}}}">>''Note:'' The output above will be significantly different if you run this in "Test" mode, as opposed to playing the story normally.
The above code works by first creating an HTML {{{<div>}}} element in memory (not on the screen). Then it uses the {{{.wiki()}}} method to "wikify" the {{{$input}}} string into HTML content through SugarCube, and then puts that content within the {{{<div>}}} we created in the first step. And then, finally, it copies the contents from within that {{{<div>}}} to the {{{$output}}} variable.
Just be careful when using this method, since the output you get might not be quite what you expect it to be. I'd recommend testing it first like this:
{{{
<<set $output = $(document.createElement("div")).wiki("<<someWidget $someVar>>")[0].innerHTML>>
"<<- $output>>"
}}}
The {{{<<- ...>>}}} version of the {{{<<print ...>>}}} macro used above will actually display HTML elements as text, instead of using them for display. So that will display the exact output of that widget on the screen, so you can see any hidden spaces or HTML elements you might not have expected from the widget.
For another example, if you had a passage named "Test Passage" with this content:
<div id="original"></div>Then this code:
{{{
<<set _content = $(document.createElement("div")).wiki(Story.get("Test Passage").text)[0].innerHTML>>
}}}
would set {{{_content}}} to this:
<div id="output"></div>
Odds are that you're unlikely to need this trick, but if you do, the above code should save you the hassle of trying to figure out how to accomplish it.
<<script>>
$(document).one(":passagerender", function(event) {
$(event.content).find("#original").empty().wiki("{{{\n" + Story.get("Test Passage").text + "\n}}}");
$(event.content).find("#output").empty().wiki("{{{" + $(document.createElement("div")).wiki(Story.get("Test Passage").text)[0].innerHTML + "}}}");
});
<</script>><!-- [[Test Passage]] -->
/* Include the undocumented Scripting.evalTwineScript() and Scripting.evalJavaScript() methods. */
<h1>Multicolor Links</h1>If you want to have certain words within a link be a different color, then you can use something like the following code to do that.
''Example:''
<<nobr>><a data-passage="Main Menu" class="link-internal" tabindex="0">This
<span class="redlink"> multicolor </span>
<span class="orangelink">link </span>
<span class="yellowlink">takes </span>
<span class="greenlink">you </span>
<span class="bluelink">to </span>
<span class="purplelink">the </span>
"Main Menu" passage.</a><</nobr>>
''Source:''
{{{
<<nobr>><a data-passage="Main Menu" class="link-internal" tabindex="0">This
<span class="redlink"> multicolor </span>
<span class="orangelink">link </span>
<span class="yellowlink">takes </span>
<span class="greenlink">you </span>
<span class="bluelink">to </span>
<span class="purplelink">the </span>
"Main Menu" passage.</a><</nobr>>
}}}
(''Note:'' You con't have to use {{{<<nobr>>}}} or put each item on separate lines. It's done here merely for readability purposes.)
The "{{{<a>...</a>}}}" above is the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a" target="_blank">"anchor" HTML element</a>, which is used for links. The "{{{data-passage}}}" attribute determines which Twine passage the link takes the player to when clicked. It also needs the {{{class="link-internal"}}} and {{{tabindex="0"}}} attributes to be set within the anchor element in order to work as a proper SugarCube link. The text between the anchor element's "head" ({{{<a ...>}}}) and "tail" ({{{</a>}}}) is what gets displayed as the link's text, so that's where you'd put the colored text.
The "{{{<span>}}}" elements, as shown above, allow you to make a specially tagged section of other HTML elements and/or text within them. In the above case they're each tagged with a particular "{{{class}}}" which is defined using CSS in your game's Stylesheet section. Elements can have multiple classes, where each class name is separated by a space.
For the class values used to produce the colors shown above, you'll also need some CSS like this in your Stylesheet section:
{{{
.redlink {
color: red;
}
a:hover .redlink, a:focus .redlink {
color: pink;
}
.orangelink {
color: orange;
}
a:hover .orangelink, a:focus .orangelink {
color: #FFE1AB;
}
.yellowlink {
color: yellow;
}
a:hover .yellowlink, a:focus .yellowlink {
color: #FFFBD8;
}
.greenlink {
color: green;
}
a:hover .greenlink, a:focus .greenlink {
color: lightgreen;
}
.bluelink {
color: blue;
}
a:hover .bluelink, a:focus .bluelink {
color: lightblue;
}
.purplelink {
color: purple;
}
a:hover .purplelink, a:focus .purplelink {
color: orchid;
}
.magentalink {
color: magenta;
}
a:hover .magentalink, a:focus .magentalink {
color: #FFC0FF;
}
}}}
The "{{{.}}}" in the "{{{.classname}}}" lines above stands for "class". So, for example, the styling within the "{{{.magentalink}}}" section affects elements where the HTML element's class is set to "{{{magentalink}}}". For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS" target="_blank">the MDN's CSS documentation</a>.
Let's take a look at a simpler example:
<a data-passage="Main Menu" class="link-internal" tabindex="0">Normal text\
<span class="redlink">red text</span>\
normal text.\
</a>
The code for that link is:
{{{
<a data-passage="Main Menu" class="link-internal" tabindex="0">Normal text\
<span class="redlink">red text</span>\
normal text.\
</a>
}}}
Again, the value of the "{{{data-passage}}}" attribute tells SugarCube which passage the link will send you to. In this case it's the "Main Menu" passage. (The backslashes ({{{\}}}) prevent line breaks, but you can remove those and make that all one line of code for the same effect.)
The text inside of the {{{<a>}}} (anchor) element determines what the text of the link will say. Any text you want to have as another color should be within a {{{<span>}}} element that has its "{{{class}}}" attribute set to the appropriate class from the CSS in your Stylesheet section.
In the Stylesheet section you'd need this for the "{{{redlink}}}" class:
{{{
.redlink {
color: red;
}
a:hover .redlink, a:focus .redlink {
color: pink;
}
}}}
The first three lines determine what color font the text with the "redlink" class will normally have. The last three does the same thing, but gives a different color for when the user hovers their mouse over the link or if the link has focus. This hover/focus color should be different from the non-hover/focus color so that the user can tell this is a clickable link when they hover their mouse over it or the link has tab focus (as in, the focus has been set to that link through the use of the TAB key, which is only possible if the {{{tabindex="0"}}} attribute is set in the link element).
You can create your own link colors using that same format. You don't have to use <a href="https://www.w3schools.com/colors/colors_names.asp" target="_blank">color names</a> for each color, you can use <a href="https://www.w3schools.com/colors/colors_rgb.asp" target="_blank">RGB color values</a> instead. For example:
{{{
.magentalink {
color: #FF00FF;
}
a:hover .magentalink, a:focus .magentalink {
color: #FFC0FF;
}
}}}
Gives you a <a data-passage="Main Menu" class="link-internal" tabindex="0">link in <span class="magentalink">this color</span></a>.<h1>Level Calculator</h1>Here's a simple tool for calculating a player's level based on their experience points.
First, set up the levels in your ''StoryInit'' passage like this:
{{{
<<set setup.levels = [Number.NEGATIVE_INFINITY, 100, 200, 300, 500, 800, Infinity]>>
}}}
That transates to:
EXP < 100 = ''Level 1''
100 - 199 = ''Level 2''
200 - 299 = ''Level 3''
300 - 499 = ''Level 4''
500 - 799 = ''Level 5''
EXP >= 800 = ''Level 6''
For different EXP to level translations, just change the values in the above array in the parts between {{{Number.NEGATIVE_INFINITY}}} and {{{Infinity}}}.
Then, to get those values, you can simply use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex" target="_blank">Array.findIndex() method</a>, like this:
{{{
<<set _level = setup.levels.findIndex(value => value > $exp)>>
}}}
That assumes that {{{$exp}}} is the player's experience points.
To make that simpler, you could put this in your JavaScript section:
{{{
/* getLevel - Start */
window.getLevel = function(exp) {
return setup.levels.findIndex(value => value > exp);
};
/* getLevel - End */
}}}
which would allow you to simplify the earlier code to just this:
{{{
<<set _level = getLevel($exp)>>
}}}
''You can try out the above example code here:''
EXP = <<textbox "$exp" "0" autofocus>><<button "Get Level">><<run alert("level = " + getLevel(parseInt($exp)))>><</button>>
<<done>>
<<run $("#textbox-exp").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-exp + button").trigger("click");
}
})>>
<</done>>
Here's the the code for that:
{{{
EXP = <<textbox "$exp" "0" autofocus>><<button "Get Level">><<run alert("level = " + getLevel(parseInt($exp)))>><</button>>
<<done>>
<<run $("#textbox-exp").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-exp + button").trigger("click");
}
})>>
<</done>>
}}}
Enjoy!<h1>{{{<<toggleLink>>}}} Widget</h1>The {{{<<toggleLink>>}}} widget lets you make links where, if you hover your mouse over one of them, the rest darken (or whatever you want).
For example:
<<toggleLink "Main Menu">>
<<toggleLink "Custom Link Text" "Table of Links">>
<<toggleLink "here" "toggleLink Macro">>
The above code is simply:
{{{
<<toggleLink "Main Menu">>
<<toggleLink "Custom Link Text" "Table of Links">>
<<toggleLink "here" "toggleLink Macro">>
}}}
To use this, add the following code to a non-special non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
/* <<toggleLink>> Widget - Start
VARIABLES USED: _destAddr, _linkStr, _tlink
*/
<<widget "toggleLink">><<nobr>>
<<if ndef _tlink>>
<<set _tlink = 1>>
<<else>>
<<set _tlink++>>
<</if>>
<<set _destAddr = _args[1] ? _args[1] : _args[0]>>
<<set _linkStr = "$('.togglelink[id!=\\'tlink" + _tlink + "\\']')">>
<span @id="'tlink' + _tlink" class="togglelink"
@onmouseenter="_linkStr + '.addClass(\'disabled\')'"
@onmouseleave="_linkStr + '.removeClass(\'disabled\')'">
<<link _args[0] _destAddr>><</link>>
</span>
<</nobr>><</widget>>
/* <<toggleLink>> Widget - End */
}}}
and add this CSS to your ''Stylesheet'' section:
{{{
.disabled a {
color: grey;
}
}}}
That CSS determines how the other links look when you hover over one of the links. You can modify that CSS if you want the other links to look differently.
<h1>Loading External Scripts</h1>If you need to load external JavaScript or CSS files, then you'll probably need some code like this at the top of your JavaScript section:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
setup.JSLoaded = false;
let lockID = LoadScreen.lock(); // Lock loading screen
importStyles(setup.Path + "jquery-ui-fix.css"); // Your CSS files go here
importScripts(setup.Path + "jquery-ui.js") // Your JavaScript files go here
.then(function() {
setup.JSLoaded = true;
// Reload current passage since imported scripts can function now.
Engine.play(passage(), true);
LoadScreen.unlock(lockID); // Unlock loading screen
}).catch(function(error) {
alert(error);
}
);
}}}
<<include PathNote>>
The sample code above loads the <a href="https://api.jqueryui.com/category/all/" target="_blank">jQuery UI</a> CSS and JavaScript, but you can put other things in the <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-importstyles" target="_blank">''importStyles()''</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-importscripts" target="_blank">''importScripts()''</a> function calls above if you need to load other scripts.
The above code also locks the loading screen on until all of the JavaScript scripts have finished loading, that way the user won't see the page refresh once everything has loaded (see the <a href="http://www.motoslave.net/sugarcube/2/docs/#loadscreen-api" target="_blank">LoadScreen API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#engine-api-method-play" target="_blank">Engine.play()</a> for details). Code elsewhere, which is run at the start of your game, may need to check {{{setup.JSLoaded}}} first, in order to tell whether or not the external libraries have finished loading. That check should make sure you don't cause an error by trying to use anything in the scripts before they've finished loading.
Note that if the above code fails to be able to load all of the JavaScript files that you're trying to load using the ''importScripts()'' function, then it will display an error message about the first failed file and the loading screen will remain visible. You can change it to unlock the loading screen after showing the error message (the {{{alert(error);}}} line) if you want to, but your code likely won't work properly without the missing scripts, so there's probably not much point.
If you need help understanding how the ''.then()'' and ''.catch()'' parts work, see the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" target="_blank">''Promise'' object</a> documentation.This is a test:
<<button "Button">><</button>>
[[Main Menu]]<h1>{{{<<selectRange>>}}} Macro</h1>This macro is much like the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-switch" target="_blank">{{{<<switch>>}}} macro</a>, except that it allows you to have various options triggered based upon ranges of numbers. This makes it a bit easier to get certain outputs based on ranges of inputs.
Here's some pseudocode showing the basic format to use this macro:
{{{
<<selectRange $X>>
#0) This text appears first if any of the "between" or "upto" ranges are matched.
<<upto 0 3>>
#1) This text appears if $X is >= 0 and $X < 3.
<<between 3 5>>
#2) This text appears if $X is >= 3 and $X <= 5.
<<between 5 6>>
#3) This text appears if $X is >= 5 and $X <= 6.
<<selectDefault>>
#4) This text appears if no previous "between" or "upto" range is matched.
<</selectRange>>
}}}
It's possible to trigger more than one "upto" or "between" at a time, though "selectDefault" will only trigger if no previous sections have triggered. In the above example, if {{{$X}}} = 5 then both lines #2 and #3 will be shown, in addition to line #0.
Here's a more realistic example:
{{{
You're wearing <<selectRange $winterClothesVal>>\
your \
<<between 1 3>>\
hat\
<<between 2 3>>\
<<print " and gloves">>\
<<selectDefault>>\
no winter clothes\
<</selectRange>>.
}}}
If {{{$winterClothesVal}}} = {{{1}}} it outputs:
{{{
You're wearing your hat.
}}}
If {{{$winterClothesVal}}} = {{{2}}} or {{{3}}} it outputs:
{{{
You're wearing your hat and gloves.
}}}
Otherwise it outputs:
{{{
You're wearing no winter clothes.
}}}
If you want to use the {{{<<selectRange>>}}} macro yourself, just add the following code to your project's JavaScript section:
{{{
/* <<selectRange>> macro - Start */
Macro.add('selectRange', {
skipArgs : false,
tags : ["between", "upto", "selectDefault"],
handler : function () {
if (this.args.length < 1) {
throw new Error("macro needs a value parameter to test.");
}
let val = parseFloat(this.args[0]);
if (Number.isNaN(val)) {
throw new Error("macro's value parameter must be a number.");
}
console.log(val);
let noMatch = true, i, hi, lo, header = "", shownHeader = true;
for (i = 0; i < this.payload.length; ++i) {
switch (this.payload[i].name) {
case "between": // Handles X >= Y and X <= Z cases.
if (this.payload[i].args.length < 2) {
throw new Error("<<between>> tag needs two value parameters to test the range.");
}
lo = parseFloat(this.payload[i].args[0]);
hi = parseFloat(this.payload[i].args[1]);
if (Number.isNaN(lo) || Number.isNaN(hi)) {
throw new Error("<<between>> tag's value parameters must be numbers.");
}
if (lo > hi) { // Swap if the values are in the wrong order.
[lo, hi] = [hi, lo];
}
if ((val >= lo) && (val <= hi)) {
if (!shownHeader) {
shownHeader = true;
$(this.output).wiki(header);
}
$(this.output).wiki(this.payload[i].contents);
noMatch = false;
}
break;
case "upto": // Handles X >= Y and X < Z cases.
if (this.payload[i].args.length < 2) {
throw new Error("<<upto>> tag needs two value parameters to test the range.");
}
lo = parseFloat(this.payload[i].args[0]);
hi = parseFloat(this.payload[i].args[1]);
if (Number.isNaN(lo) || Number.isNaN(hi)) {
throw new Error("<<upto>> tag's value parameters must be numbers.");
}
if (lo > hi) { // Swap if the values are in the wrong order.
[lo, hi] = [hi, lo];
}
if ((val >= lo) && (val < hi)) {
if (!shownHeader) {
shownHeader = true;
$(this.output).wiki(header);
}
$(this.output).wiki(this.payload[i].contents);
noMatch = false;
}
break;
case "selectDefault": // Displayed if no previous "between" or "upto" has found a match.
if (noMatch) {
$(this.output).wiki(this.payload[i].contents);
}
break;
default: // Set the initial text to be displayed if a "between" or "upto" section has a match.
if (i == 0) {
header = this.payload[0].contents;
shownHeader = false;
}
}
}
}
});
/* <<selectRange>> macro - End */
}}}
For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.
<h1>Dumb Terminal</h1>Here's a style for people who want an old-school computer terminal look for their game.
<div class="dumb_terminal">Here's an example of how the "Glass TTY VT220" font looks.
Note that it also gives the text a subtle kind of glow like you used to see on old monochrome CRT monitors.
This code needs the <a href="Glass_TTY_VT220_Font.zip">Glass_TTY_VT220 font files</a> (<a href="https://caglrc.cc/glasstty/" target="_blank">original source</a>) in the same directory as your HTML file. As a fallback it uses the <a href="https://fonts.google.com/specimen/VT323" target="_blank">VT323 font</a> from <a href="https://fonts.google.com/?category=Sans+Serif" target="_blank">Google Fonts</a>.
</div>
To use this style, put this in your Stylesheet section:
{{{
/* Dumb terminal VT323 font loader */
@import url("https://fonts.googleapis.com/css?family=VT323&display=swap");
/* Dumb terminal font definitions - Start */
@font-face {
/* Font source: http://sensi.org/~svo/glasstty/ */
font-display: swap;
font-family: Glass;
src: url("Glass_TTY_VT220.eot");
src: url("Glass_TTY_VT220.eot?#iefix") format("embedded-opentype"),
url("Glass_TTY_VT220.woff2") format("woff2"),
url("Glass_TTY_VT220.woff") format("woff"),
url("Glass_TTY_VT220.svg#Glass_TTY_VT220") format("svg"),
url("Glass_TTY_VT220.ttf") format("truetype"),
url("file:///D:/Games/MyGame/Glass_TTY_VT220.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
.dumb_terminal {
color: #a3ff99;
text-shadow: 0px 0px 20px #5dff4b;
font-family: Glass, VT323, monospace;
font-size: 16px;
line-height: 20px;
}
.dumb_terminal a, .dumb_terminal a:hover, .dumb_terminal a:active {
color: #a3ff99;
text-shadow: 0px 0px 20px #5dff4b;
text-decoration: underline;
}
/* Dumb terminal font definitions - End */
}}}
''IMPORTANT:'' In order for any <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@import" target="_blank">{{{@import}}}</a> lines to work, they //must// be at the top of the CSS in your Stylesheet section. Only <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@charset" target="_blank">{{{@charset}}}</a> lines (which must be first), <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@layer" target="_blank">{{{@layer}}}</a> lines, and comments ({{{/* ... */}}}) can be before the {{{@import}}} lines. You can have the rest of that CSS elsewhere in your Stylesheet section.
<<include PathNote>>
If you want the links to remain the normal blue color, then you can replace that last part with this:
{{{
.dumb_terminal a, .dumb_terminal a:hover, .dumb_terminal a:active {
text-shadow: 0px 0px 20px #618eff;
}
}}}
Now that you have that, if you want a section of text to appear like that in a part of the game, then you can display that kind of text like this:
{{{
<div class="dumb_terminal">Your "dumb terminal" text goes here.
More text goes here.</div>
}}}
If you instead want //all// passages to look like that by default, change all of the parts with "{{{.dumb_terminal}}}" in the CSS code above to "{{{.passage}}}" instead.
For reference, the VT323 font looks different, like this:
<div class="vt_font">Here's an example of what the "VT323" font looks like at "font-size: 16px".</div>
<div class="vt_font" style="font-size: 20px">And here's what the "VT323" font looks like at "font-size: 20px".</div>
<h1>Bottom Bar and Top Bar sample code</h1>Here's some code to add a "bottom bar" at the bottom of any passage with a "{{{bbar}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags). It will remain visible at the very bottom of the page, regardless of how long or short the passage is or how you move the scrollbar. See the bottom of this window for an example.
With a slight modification, you could make this into a "top bar" for passages with a "{{{tbar}}}" tag as well (see below).
Test links:
[[A "tbar" Tagged Passage]]
[[A "bbar" and "tbar" Tagged Passage]]
[[A short "bbar" Tagged Passage]]
To add a "bottom bar" to your game, first, create a passage named ''{{{PassageFooter}}}'' with this code inside of it:
{{{
<<done>>
<<if tags().includes("bbar")>>
<<if $("#bbtext").length == 0>>
<<run $("body").append($('<div id="bottombar"><div id="bbblock"><div id="bbtext"></div></div></div>'))>>
<</if>>
<<run $("#bbtext").empty().wiki('<<include "BottomBar">>')>>
<<run $("#story").css("min-height", "calc(100vh - " + Math.max(0, Math.ceil($("#story").position().top)) + "px - " + $("#story").css("margin-top") +
" - " + $("#story").css("margin-bottom") + " - " + Math.ceil($("#bottombar").outerHeight()) + "px)")>>
<<elseif $("#bottombar").length > 0>>
<<run $("#bottombar").remove()>>
<<run $("#story").css("min-height", "")>>
<</if>>
<</done>>
}}}
Once you have that (and the CSS code below), you can now add a passage named "{{{BottomBar}}}" with whatever content you want in it, and that content will appear in the bottom bar.
If you want to reverse that logic, so that the bottom bar appears on every passage which //doesn't// have a "''bbar''" tag, just add an exclamation point in front of the {{{tags()}}} function. (The exclamation point stands for "not" in cases like this, so {{{<<if !tags().includes("bbar")>>}}} would mean "if this passage's tags do NOT include {{{bbar}}}".)
If you want the content in your bottom bar to vary based on the passage, then you could have that "{{{BottomBar}}}" passage display a temporary variable (e.g. {{{_bbcontent}}}) and then you'd set that variable from within each story passage which uses a bottom bar to whatever content you want to appear in that bottom bar. For example, if you had this in one of your story passages:
{{{
<<set _bbcontent = `<<button "Test">><<run alert("It works!")>><</button>>`>>
}}}
and if the content of the "{{{BottomBar}}}" passage was simply {{{_bbcontent}}}, then that "Test" button would be displayed in that story passage's bottom bar. (Note that this example used backticks, the "{{{`}}}" character on the {{{~}}} key on most English keyboards, to encapsulate the content, but you can use single- or double-quotes for that if you want. However, if you do that, then each of those same kinds of quotes inside the starting and ending quotes will need a backslash ({{{\}}}) before them to distinguish them from the actual ending quote.)
You'll also need to put this CSS code in your project's ''Stylesheet'' section:
{{{
/* Bottom and Top Bar styling - Start */
#topbar, #bottombar {
position: sticky;
width: 100%;
padding-top: 6px;
padding-bottom: 8px;
background-color: #333;
border-top: 1px solid #444;
box-shadow: 0 0 10px 0 #333;
line-height: 1.75;
z-index: 40;
}
#topbar {
top: 0;
}
#bottombar {
bottom: -1px;
}
#bbblock, #tbblock {
margin-left: 20em;
margin-right: 2.5em;
-o-transition: margin .2s ease-in;
transition: margin .2s ease-in;
}
#bbtext, #tbtext {
text-align: center;
max-width: 54em;
margin: 0 auto;
}
#ui-bar.stowed ~ #topbar #tbblock,
#ui-bar.stowed ~ #bottombar #bbblock {
margin-left: 4.5em;
}
@media screen and (max-width: 1136px) {
#bbblock, #tbblock {
margin-left: 19em;
margin-right: 1.5em;
}
#ui-bar.stowed ~ #topbar #tbblock,
#ui-bar.stowed ~ #bottombar #bbblock {
margin-left: 3.5em;
}
}
@media screen and (max-width: 768px) {
#bbblock, #tbblock {
margin-left: 3.5em;
}
}
/* Bottom and Top Bar styling - End */
}}}
Now that you have both of those, in any passage which has a "{{{bbar}}}" tag, you'll see a bar at the bottom of the passage with the content of the "{{{BottomBar}}}" passage in it.
If you wanted a top bar instead, include the above CSS in your project's ''Stylesheet'' section, create a passage named ''{{{PassageHeader}}}'' and put this code inside of it:
{{{
<<done>>
<<if tags().includes("tbar")>>
<<if $("#tbtext").length == 0>>
<<run $("#ui-bar").after($('<div id="topbar"><div id="tbblock"><div id="tbtext"></div></div></div>'))>>
<</if>>
<<run $("#tbtext").empty().wiki('<<include "TopBar">>')>>
<<elseif $("#topbar").length > 0>>
<<run $("#topbar").remove()>>
<</if>>
<</done>>
}}}
and then add a passage named "{{{TopBar}}}" with whatever content you want in it, and that content will appear in the top bar, and it will remain at the top of the page even if you scroll up or down.
Enjoy!
[[Bottom and Top Bars]]
[[A "tbar" Tagged Passage]]
[[A short "bbar" Tagged Passage]]
<<include "Lorem ipsum">>[[Bottom and Top Bars]]
[[A "bbar" and "tbar" Tagged Passage]]
<<include "Lorem ipsum">><<done>>
<<if tags().includes("bbar")>>
<<if $("#bbtext").length == 0>>
<<run $("body").append($('<div id="bottombar"><div id="bbblock"><div id="bbtext"></div></div></div>'))>>
<</if>>
<<run $("#bbtext").empty().wiki('<<include "BottomBar">>')>>
<<run $("#story").css("min-height", "calc(100vh - " + Math.max(0, Math.ceil($("#story").position().top)) + "px - " + $("#story").css("margin-top") +
" - " + $("#story").css("margin-bottom") + " - " + Math.ceil($("#bottombar").outerHeight()) + "px)")>>
<<elseif $("#bottombar").length > 0>>
<<run $("#bottombar").remove()>>
<<run $("#story").css("min-height", "")>>
<</if>>
<</done>><h1>Using jQuery UI in Twine</h1><a href="https://jqueryui.com/demos/" target="_blank">''jQuery UI'' lets you add a bunch of neat user interface (UI) elements into your game</a>, as you can see from the examples at that link. SugarCube includes jQuery, but //not// jQuery UI, so you'll have to add jQuery UI to your project yourself if you want to use it.
Unfortunately, it's a bit more complicated to use jQuery UI in Twine than it would be in a normal HTML page. This section helps explain how to implement jQuery UI elements within Twine.
To use jQuery UI in Twine with the SugarCube story format, first you'll first want to <a href="https://jqueryui.com/themeroller/" target="_blank">create a ''jQuery UI'' theme and download it</a>. You should extract the files from that download into your game's directory.
Once you've done that, to load jQuery UI into Twine, you'll want to put something like this at the top of your JavaScript section:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
/* Load jQuery UI - Start */
setup.JSLoaded = false;
importStyles(setup.Path + "jquery-ui.min.css");
importScripts(setup.Path + "jquery-ui.min.js")
.then(function() {
setup.JSLoaded = true;
}).catch(function(error) {
alert(error);
}
);
/* Load jQuery UI - End */
}}}
<<include PathNote>>
Now, let's say, for example, that you want to make an element with an ID of "draggable", like this:
{{{
<div id="draggable" class="ui-widget-content" style="width: 150px; height: 150px; padding: 0.5em;">
Drag me around.
</div>
}}}
into a <a href="https://jqueryui.com/draggable/" target="_blank">jQuery UI draggable element</a> in some passages. Then, instead of using the standard JavaScript code, which looks like something like this:
{{{
<script>
$(function() {
$("#draggable").draggable();
});
</script>
}}}
you'd have to put something more like this in the passages where you wanted to use a draggable element (preferably at the bottom of the passage):
{{{
<<script>>
$(document).one(":passagerender", function(event) {
// Passage is about to be displayed.
let dragID = "#draggable", jQIntervalID = 0;
function ActivatejQueryUI() { // Function to see if jQuery UI is loaded yet.
if (setup.JSLoaded) {
clearInterval(jQIntervalID); // Stop looking, it's loaded.
if ($(dragID).length > 0) { // Make sure an element with a "draggable" ID exists.
$(dragID).draggable(); // Make it draggable.
}
}
}
if (setup.JSLoaded) { // See if jQuery UI is loaded yet.
// It's loaded, so set up the draggable element.
$(event.content).find(dragID).draggable();
} else { // Wait for jQuery UI to load using the ActivatejQueryUI function.
jQIntervalID = setInterval(ActivatejQueryUI, 100);
}
});
<</script>>
}}}
That code would wait for the passage to be ready to be displayed, and just before SugarCube displays it, the code would connect jQuery UI's {{{draggable()}}} functionality to the element with the {{{id="draggable"}}} attribute in the passage. However, if the jQuery UI's JavaScript isn't loaded yet, that code would wait for jQuery UI to be loaded before trying to connect it to that element.
Other than in the starting passage, all of that extra work to make sure that jQuery UI is loaded first is //''probably''// unnecessary, since it should be loaded by the time you get to the second passage. Thus you could probably shorten that code to just this in any of the other passages:
{{{
<<script>>
$(document).one(":passagerender", function(event) {
// Passage is about to be displayed, so make "draggable" element draggable.
$(event.content).find("#draggable").draggable();
});
<</script>>
}}}
However, that //will// throw a "draggable is not a function" error if jQuery UI hasn't loaded by that point.
I'm sure that all looks a bit complicated, but really, that's just removed all of the hard parts for you. All you'd need to do now is copy the code to the appropriate locations, put in your file path and element IDs, style the elements in the passages so that they look the way you want, and it should all work fine.
Additionally, if you wanted to have //multiple// draggable elements in a single passage, then instead of looking for an HTML element's ID (which has to be unique on any page), you could add a "draggable" class to all of the elements you want to be draggable by using something like {{{class="draggable ui-widget-content"}}}, and then search for that "draggable" class (since classes don't have to be unique and you can have multiple classes on a single element, each class name separated by a space). So that might look something like this:
{{{
<div id="draggable-1" class="draggable ui-widget-content" style="width: 150px; height: 150px; padding: 0.5em;">
Drag me around #1.
</div>
<div id="draggable-2" class="draggable ui-widget-content" style="width: 150px; height: 150px; padding: 0.5em;">
Drag me around #2.
</div>
}}}
Then you'd just change {{{"#draggable"}}} to {{{".draggable"}}} in the earlier code, because {{{#}}} represents an "id" attribute on an HTML element, while {{{.}}} represents a "class" attribute instead (the same way you'd refer to elements by ID or class in CSS code that you'd put in your Stylesheet section).
For another example, if you wanted to use multiple elments of both "draggable" and "<a href="https://jqueryui.com/droppable/" target="_blank">droppable</a>" types on the same page, then you'd do something more like this:
{{{
<<script>>
$(document).one(":passagerender", function(event) {
// Passage is about to be displayed.
let dragClass = ".draggable", dropClass = ".droppable", jQIntervalID = 0;
function ActivatejQueryUI() { // Function to see if jQuery UI is loaded yet.
if (setup.JSLoaded) {
clearInterval(jQIntervalID); // Stop looking, it's loaded.
if ($(dragClass).length > 0) { // Make sure an element with a "draggable" class exists.
$(dragClass).draggable(); // Make them draggable.
}
if ($(dropClass).length > 0) { // Make sure an element with a "droppable" class exists.
$(dropClass).droppable(); // Make them droppable.
}
}
}
if (setup.JSLoaded) { // See if jQuery UI is loaded yet.
// It's loaded, so set up draggable and droppable elements.
$(event.content).find(dragClass).draggable();
$(event.content).find(dropClass).droppable();
} else { // Wait for jQuery UI to load using the ActivatejQueryUI function.
jQIntervalID = setInterval(ActivatejQueryUI, 100);
}
});
<</script>>
}}}
And that would set all elements with a "draggable" class to be draggable, and all elements with a "droppable" class to be droppable, once the passage is displayed.
The short version, of course, would just be this:
{{{
<<done>>
<<run $(".draggable").draggable()>> /* Make "draggable" elements draggable. */
<<run $(".droppable").droppable()>> /* Make "droppable" elements droppable. */
<</done>>
}}}
''NOTE:'' If you're using jQuery UI in your project, then when distributing your project you'll need to make sure you include both the "{{{jquery-ui.min.css}}}" and "{{{jquery-ui.min.js}}}" files along with the "{{{images}}}" directory and the jQuery UI images in that directory.
Have fun!<h1>Geolocation Access</h1>If you've ever wanted to make your own "Pokemon Go"-like game, where the player has to move around in the real world to find things, you can use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API" target="_blank">Geolocation API</a> built into most (but not all) browsers, to do that yourself.
''Please note:'' In most browsers the Geolocation API now requires two things:
# The page is hosted on a secure website (one using HTTPS). Thus this feature will likely not work with locally hosted Twine/HTML files.
# The user has enabled the use of geolocation on the site where the Twine/HTML file is located (which may require reloading the page after enabling that feature for that site).
This makes this feature much more difficult to use than when I originally wrote this section.
For example: <<button "Find my location">>
<<run setup.geoFindMe()>>
<</button>>
<span id="status"></span>
That button calls the {{{setup.geoFindMe()}}} function, which you'd need to add to your JavaScript section:
{{{
/* geoFindMe() function - Start
USES: $latitude, $longitude
*/
setup.geoFindMe = function() {
function success(position) {
let latitude = position.coords.latitude;
let longitude = position.coords.longitude;
State.variables.latitude = latitude;
State.variables.longitude = longitude;
$('#status').empty().wiki('<a href="https://www.openstreetmap.org/#map=18/' + latitude + '/' + longitude + '" target="_blank">Latitude: ' + latitude + '°, Longitude: ' + longitude + '°</a>');
}
function error() {
$('#status').empty().wiki('Unable to retrieve your location.');
}
State.variables.latitude = 0;
State.variables.longitude = 0;
if (!navigator.geolocation) {
$('#status').empty().wiki('Geolocation is not supported by your browser.');
} else {
$('#status').empty().wiki('Locating…');
navigator.geolocation.getCurrentPosition(success, error);
}
};
/* geoFindMe() function - End */
}}}
That example also sets the {{{$latitude}}} and {{{$longitude}}} variables with the coordinates, and will also display the current coordinates in a {{{<span id="status"></span>}}} as a clickable link, which takes you to the OpenStreetMap.org page at those coordinates. You can edit the above code to do other things with that data instead.
''Note:'' The geoFindMe() function works asynchronously, which means that the values won't be available immediately after the geoFindMe() function is called. They'll only be available after the success() function is triggered. A simple way to deal with this is to get the location in one passage, and then use those values in the next passage. (There are alternate ways to handle things if you don't want to change passages, but they're a bit more complicated.)
<video autoplay muted loop id="myVideo">
<source @src='setup.ImagePath + "rain.mp4"' type="video/mp4">
</video><div class="content">\
<h1>Video Background</h1>\
This is an example of how to play a video in the background of a Twine game, based on <a href="https://www.w3schools.com/howto/howto_css_fullscreen_video.asp" target="_blank">the W3Schools example here</a>. Here's the code:
{{{
<video autoplay muted loop id="myVideo">
<source @src='setup.ImagePath + "rain.mp4"' type="video/mp4">
</video><div class="content">\
<h1>Heading</h1>\
Text goes here.
<span id="btn"><<button "Pause">>
<<script>>
// Get the video
let video = document.getElementById("myVideo");
// Pause and play the video, and change the button text
if (video.paused) {
video.play();
$("#btn button")[0].innerHTML = "Pause";
} else {
video.pause();
$("#btn button")[0].innerHTML = "Play";
}
<</script>>
<</button>></span>
</div>
}}}
Note that the {{{setup.ImagePath}}} part requires the "[[Displaying Images in Twine]]" code. If you're not using that code in your project, then you should change that line to something more like:
{{{
<source src="images/rain.mp4" type="video/mp4">
}}}
assuming your videos are in an "images" directory.
That code also includes a button like the below which pauses/plays the video.
<span id="btn"><<button "Pause">>
<<script>>
// Get the video
let video = document.getElementById("myVideo");
// Pause and play the video, and change the button text
if (video.paused) {
video.play();
$("#btn button")[0].innerHTML = "Pause";
} else {
video.pause();
$("#btn button")[0].innerHTML = "Play";
}
<</script>>
<</button>></span>
You can leave out the whole {{{<span id="btn">...</span>}}} part if you don't need the pause button.
You'll also need this Stylesheet code to position the video correctly:
{{{
/* Style the video: 100% width and height to cover the entire window */
#myVideo {
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
}
.content {
position: relative;
width: -webkit-calc(100% - 40px);
width: -moz-calc(100% - 40px);
width: calc(100% - 40px);
padding: 20px;
color: #f1f1f1;
background: rgba(0, 0, 0, 0.5);
}
}}}
Keep in mind that you don't need to use this exact method, this is merely intended as an example of one way it could be done.
</div><h1>Lined Paper and Handwriting</h1><<set $font = 0>>This section shows you how you can display text which looks like handwriting on lined paper.
For example:
<section class="paper">\
<article class="paperheader"></article>\
<div class="papertext">Hello, World!
Here's some text that looks like handwriting on lined paper.
This is a really, really, really, really long line to demonstrate that the text wraps properly when the line length is wider than the page.
<span class="beanie">Multiple different fonts can be used at the same time as well.</span>
</div>\
</section>
''Choose a font:'' <label><<radiobutton "$font" 0 checked>> "Waiting for the Sunrise"</label> / <label><<radiobutton "$font" 1>> "Reenie Beanie"</label> / <label><<radiobutton "$font" 2>> "Homemade Apple"</label> / <label><<radiobutton "$font" 3>> generic "cursive"</label>
Use the above radio buttons to see what some other fonts look like.
These fonts are just some examples of the ones available. You can find many, many others at the <a href="https://fonts.google.com/?categoryFilters=Calligraphy:%2FScript%2FHandwritten" target="_blank">Google Fonts</a> site. Simply pick the filter for fonts you want on the left, scroll through the example fonts, and find the one(s) that you want. Once you've done that, click on that font example, click the "Get font" button in the upper-right, and from there you can do "Download all" if you want to include the font files with your game (useful for local installs on a person's computer), and/or you can do "Get embed code" to get the font name and import path (under the "Web" tab, select the "@import" radio button; useful for online versions of the game). For further details on how to handle adding fonts to your game and then using them, see below.
Here's the code for the above example, showing something like what you'd put in your passages to use it:
{{{
<section class="paper">\
<article class="paperheader"></article>\
<div class="papertext">Hello, World!
Here's some text that looks like handwriting on lined paper.
This is a really, really, really, really long line to demonstrate that the text wraps properly when the line length is wider than the page.
<span class="beanie">Multiple different fonts can be used at the same time as well.</span>
</div>\
</section>
}}}
The "paper" section sets up the background, with the "paperheader" adding the gap at the top, and the "papertext" section being where you'd put the text that goes on the paper.
In order for that to work, you'll need to add this CSS code to your Stylesheet section:
{{{
/* Paper - Start */
.paperheader {
background-color: #FFF;
margin-top: -5px;
margin-left: -42px;
min-height: 60px;
padding-left: 42px;
}
.paper {
font: italic 16px / 20px cursive;
font: bold 15px / 20px "Homemade Apple";
font: bold italic 25px / 20px "Reenie Beanie";
font: bold italic 20px / 20px "Waiting for the Sunrise";
line-height: 20px;
position: relative;
margin: 0 auto;
padding: 8px 5px 4px 42px;
color: #444;
border: 1px solid #d2d2d2;
background: #fff;
background: -webkit-gradient(linear, 0 0, 0 100%, from(#d9eaf3), color-stop(4%, #fff)) 0 4px;
background: -webkit-gradient(linear, left top, left bottom, from(#d9eaf3), color-stop(8%, #fff)) 0 4px;
background: -webkit-linear-gradient(#d9eaf3 0%, #fff 8%) 0 4px;
background: -moz-linear-gradient(#d9eaf3 0%, #fff 8%) 0 4px;
background: -o-linear-gradient(#d9eaf3 0%, #fff 8%) 0 4px;
background: linear-gradient(#d9eaf3 0%, #fff 8%) 0 4px;
-webkit-background-size: 100% 20px;
-moz-background-size: 100% 20px;
-ms-background-size: 100% 20px;
-o-background-size: 100% 20px;
background-size: 100% 20px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.07);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.07);
box-shadow: 0 1px 2px rgba(0,0,0,0.07);
}
.paper::before {
content: '';
position: absolute;
top: 0;
left: 30px;
width: 4px;
bottom: 0;
border: 1px solid;
border-color: transparent #efe4e4;
}
.papertext {
display: inline-block;
position: relative;
top: 4px;
}
/* Paper - End */
}}}
The "{{{font:}}}" lines within the "<code>.paper { ... }</code>" section determine what font is used, going from first to last being the lowest to highest priority. In this example, the "Waiting for the Sunrise" font has the highest priority.
Thus, if you would prefer that a different font besides "Waiting for the Sunrise" be the default font, either put the preferred font at the bottom of the list of fonts found near the top of the {{{.paper}}} styling section or simply remove the other {{{font:}}} lines.
Note also that different font sizes (e.g. {{{15px}}}), font weights (e.g. {{{bold}}}), and font styles (e.g. {{{italic}}}) work better or worse for the various fonts you use, so you may want to experiment with your fonts to find what settings look best for each font in your game.
Additionally, you'll want to create classes in the CSS in your Stylesheet section for any alternate (non-default) fonts you want to use, like this:
{{{
.apple {
font: bold 15px / 20px "Homemade Apple";
}
.beanie {
font: bold italic 25px / 20px "Reenie Beanie";
}
.cursive {
font: italic 16px / 20px cursive;
}
.sunrise {
font: bold italic 20px / 20px "Waiting for the Sunrise";
}
}}}
''Note:'' It's recommended that class names only be composed of the characters {{{a}}}-{{{z}}}, {{{A}}}-{{{Z}}}, {{{0}}}-{{{9}}}, {{{-}}}, or {{{_}}} and cannot start with a number or a hyphen. CSS is case-sensitive, meaning that using consistent capitalization is required. The {{{.}}} at the start indicates that the name which follows is the name of a class.
Having CSS like that makes it easier to style your text, since then you can simply include the name of the class you created (without the starting "{{{.}}}") in the text's class(es), as was demonstrated in the following line from the example at the top of the page:
{{{
<span class="beanie">Multiple different fonts can be used at the same time as well.</span>
}}}
If you're putting your game online, then you'll need to add something like this to the top of your Stylesheet section for the fonts to work:
{{{
/* Handwriting font loader - Start */
@import url("https://fonts.googleapis.com/css2?family=Waiting+for+the+Sunrise&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Reenie+Beanie&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Homemade+Apple&display=swap");
/* Handwriting font loader - End */
}}}
''IMPORTANT:'' In order for any <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@import" target="_blank">{{{@import}}}</a> lines to work, they //must// be at the top of the CSS in your Stylesheet section. Only <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@charset" target="_blank">{{{@charset}}}</a> lines (which must be first), <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@layer" target="_blank">{{{@layer}}}</a> lines, and comments ({{{/* ... */}}}) can be before the {{{@import}}} lines.
That loads the "Waiting for the Sunrise", "Reenie Beanie", and "Homemade Apple" fonts from Google Fonts, which means that the user will have to be online for those fonts to work.
If the game is run locally (i.e. run entirely on their computer) and the player may not have Internet access, then you'll need to include the font files in your game's download. If you have a choice of font formats for the font that you want, then WOFF (Web Open Font Format) is the most widely supported font format, closely followed by TTF/OTF (TrueType/OpenType Font). Create a "fonts" folder in the same directory as the game's HTML file, and put any font files you're using into that folder.
Then, set up a {{{@font-face}}} for each font family you want to use in your game's Stylesheet section, like this example:
{{{
@font-face {
/* Font source: http://sensi.org/~svo/glasstty/ */
font-display: swap;
font-family: "Glass";
src: url("fonts/Glass_TTY_VT220.eot");
src: url("fonts/Glass_TTY_VT220.eot?#iefix") format("embedded-opentype"),
url("fonts/Glass_TTY_VT220.woff2") format("woff2"),
url("fonts/Glass_TTY_VT220.woff") format("woff"),
url("fonts/Glass_TTY_VT220.svg#Glass_TTY_VT220") format("svg"),
url("fonts/Glass_TTY_VT220.ttf") format("truetype"),
url("file:///D:/Games/MyGame/fonts/Glass_TTY_VT220.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
}}}
Just pare down the list of sources to only the font type(s) that you downloaded and change the {{{font-family}}} to the name of the font. You can also set the {{{font-weight}}} and {{{font-style}}} to how you plan to use them with that font, but it's not required.
Additionally, you may want to have a second copy of the URL for the font you're using, but with the full path to the game's font on your computer, so that way it will also work when you launch the game from the Twine editor.
For example, with the "<a href="https://fonts.google.com/specimen/Reenie+Beanie" target="_blank">Reenie Beanie</a>" font, you'd download it from that link using the "Download family" button, extract the TTF file from the downloaded ZIP file, put it into the game's "fonts" folder, and then put this in the game's Stylesheet section:
{{{
@font-face {
font-display: swap;
font-family: "Reenie Beanie";
src: url("fonts/ReenieBeanie-Regular.ttf") format("truetype"),
url("file:///D:/Games/MyGame/fonts/ReenieBeanie-Regular.ttf") format("truetype");
font-weight: bold;
font-style: italic;
}
}}}
<<include PathNote>>
Enjoy!
<<done>><<script>>
$("[id^=radiobutton-font-]").on("change", function (event) {
let ft = 'bold italic 20px / 20px "Waiting for the Sunrise"';
switch (State.variables.font) {
case 1:
ft = 'bold italic 25px / 20px "Reenie Beanie"';
break;
case 2:
ft = 'bold 15px / 20px "Homemade Apple"';
break;
case 3:
ft = 'italic 16px / 20px cursive';
break;
default:
}
$(".paper").css("font", ft);
});
<</script>><</done>><h1>Countdown Timer</h1>If you'd like to limit the viewing time of a particular passage, you can use this countdown timer code to produce a timer like the one shown in the upper-right.
Note that once it gets to 20 to 10 seconds remaining it will change to yellow, and then at 10 to 0 seconds remaining it will change to red.
''IMPORTANT:'' The "anti-cheat" code, which prevents people from just hitting {{{F5}}} to reset the timer, requires ''SugarCube v2.29.0'' or later.
To add this to your code, put this in your ''Stylesheet'' section:
{{{
/* Timer - Start */
/*
Makes sure that elements stay within the page width - scrollbar width.
*/
html, body {
width: -webkit-calc(100vw - 16px);
width: -moz-calc(100vw - 16px);
width: calc(100vw - 16px);
min-height: 100%;
}
#timer {
position: fixed;
top: 10px;
right: 40px;
width: 146px;
line-height: 1.55;
padding: 0 14px;
color: rgb(238, 238, 238);
-webkit-border-radius: 16px;
-moz-border-radius: 16px;
border-radius: 16px;
border: 2px solid white;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.72);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.72);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.72);
margin-right: 20px;
z-index: 20;
-webkit-transition: background-color 1s;
-o-transition: background-color 1s;
-moz-transition: background-color 1s;
transition: background-color 1s;
}
.timergreen {
background-color: darkgreen;
}
.timeramber {
background-color: #8E7306;
}
.timerred {
background-color: darkred;
}
/* Timer - End */
}}}
Then, in a non-special non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags), put this:
{{{
/* <<countdownTimer>> Widget - Start
VARIABLES USED: _ctminutes, _ctseconds, _replacementPassage
*/
<<widget "countdownTimer">><<nobr>>
<<set _ctseconds = _args[0]>>
<<set _ctminutes = Math.floor(_ctseconds / 60)>>
<<set _replacementPassage = _args[1]>>
<div id="timer" class="timergreen">Time remaining _ctminutes:<<= (_ctseconds - (_ctminutes * 60)).toString().padStart(2, '0')>></div><<script>>
if (!recall("countdown", undefined)) {
setup.countdown = { startTime: new Date(), lastStr: "", passage: passage() };
memorize("countdown", setup.countdown);
} else {
setup.countdown = recall("countdown");
if (setup.countdown.passage !== passage()) {
setup.countdown = { startTime: new Date(), lastStr: "", passage: passage() };
memorize("countdown", setup.countdown);
}
}
setup.countdown.intervalID = setInterval(function () {
if (setup.countdown.passage !== passage()) {
clearInterval(setup.countdown.intervalID);
forget("countdown");
setup.countdown.passage = "";
} else {
let curtime = new Date(), str, seconds = State.temporary.ctseconds;
let diff = Math.floor(seconds - ((curtime - setup.countdown.startTime) / 1000)), min = Math.floor(diff / 60);
if ((diff >= 0) && (diff < seconds)) {
if ($("#timer").length) {
str = "Time remaining " + min + ":" + (diff - (min * 60)).toString().padStart(2, '0');
if (str != setup.countdown.lastStr) {
$("#timer").empty().wiki(str);
setup.countdown.lastStr = str;
}
if (diff <= 10) {
if (!$("#timer").hasClass("timerred")) {
$("#timer").removeClass("timeramber").addClass("timerred");
}
} else if (diff <= 20) {
if (!$("#timer").hasClass("timeramber")) {
$("#timer").removeClass("timergreen").addClass("timeramber");
}
} else {
if (!$("#timer").hasClass("timergreen")) {
$("#timer").removeClass("timeramber timerred").addClass("timergreen");
}
}
}
}
if (diff < 0) {
clearInterval(setup.countdown.intervalID);
forget("countdown");
$("#passages div.passage").empty().wiki('<<include "' + State.temporary.replacementPassage + '">>');
delete setup.countdown.passage;
}
}
}, 200);
<</script>>
<</nobr>><</widget>>
/* <<countdownTimer>> Widget - End */
}}}
Then, in the passage where you want the countdown timer, put something like this at the bottom of the passage:
{{{
<<countdownTimer 60 "Timeout Passage">>
}}}
You can change the number {{{60}}} to a different number of seconds, if you want something other than a one minute countdown.
The {{{"Timeout Passage"}}} should be replaced with the name of a passage which holds the content you want to appear after the coundown is runs out. The timeout doesn't actually navigate you to that passage, it just displays the contents of that passage, replacing whatever was visible before.
''NOTE:'' It's recommend that you do:
{{{
<<run forget("countdown")>>
}}}
in the passage which is prior to the passage with the countdown timer in order to make sure that the anti-cheat data is cleared.
It's also highly recommend that you put a link to exit the page somewhere in the passage where you use this. That way, people who want to skip the timer can do so.
If you want to add some time to the counter, simply add that many seconds to the {{{_ctseconds}}} variable. For example:
<<button "Add 1 min">>
<<set _ctseconds += 60>>
<</button>>
The code for that button:
{{{
<<button "Add 1 min">>
<<set _ctseconds += 60>>
<</button>>
}}}
If you want to be able to pause/unpause the timer like this:
<span id="pause"><<button "Pause">>
<<if ndef _int>>
<<set _int = setInterval(function () {
State.temporary.ctseconds += 0.2;
}, 200)>>
<<run $("#pause button")[0].innerHTML = "Unpause">>
<<else>>
<<run clearInterval(_int)>>
<<unset _int>>
<<run $("#pause button")[0].innerHTML = "Pause">>
<</if>>
<</button>></span>
Then you can use code like this:
{{{
<span id="pause"><<button "Pause">>
<<if ndef _int>>
<<set _int = setInterval(function () {
State.temporary.ctseconds += 0.2;
}, 200)>>
<<run $("#pause button")[0].innerHTML = "Unpause">>
<<else>>
<<run clearInterval(_int)>>
<<unset _int>>
<<run $("#pause button")[0].innerHTML = "Pause">>
<</if>>
<</button>></span>
}}}
When you leave that passage you'll also need to do {{{clearInterval(_int)}}} to make sure that the interval stops. For example:
{{{
[[Repeat][clearInterval(_int)]]
}}}
And finally, if you want to be able to use the countdown timer across multiple passages, then in the first passage you'd set it up like this:
{{{
[[Next][$sec = _ctseconds]]
<<countdownTimer 60 "Done">>
}}}
or, if you need to, you can combine that with clearing the pause/unpause interval like this:
{{{
[[Next][$sec = _ctseconds; clearInterval(_int)]]
<<countdownTimer 60 "Done">>
}}}
Either way, that keeps track of the number of seconds (by keeping track of the value of {{{_ctseconds}}} in a story variable) so that you can use it in subsequent passages like this:
{{{
[[Repeat][$sec = _ctseconds]]
<<set _diff = Math.floor($sec - (((new Date()) - setup.countdown.startTime) / 1000))>>\
<<countdownTimer _diff "Done">>
}}}
That will work even if you change the number of seconds. If the number of seconds instead remains unchanging, then you don't need to keep track of {{{_ctseconds}}}, and simply set the seconds like this:
{{{
[[Repeat]]
<<set _diff = Math.floor(60 - (((new Date()) - setup.countdown.startTime) / 1000))>>\
<<countdownTimer _diff "Done">>
}}}
Simply change "60" in that code to whatever number of seconds your countdown started at.
Enjoy!
<<countdownTimer 60 "Timeout Passage">>Time’s up!
[[Go Back|Countdown Timer]]<h1>Image Toggle</h1>If you want to give your players the option to disable images, without needing to rewrite all of your code, then this code should make that easy.
This code demonstrates two different ways you can toggle images off. By either putting a toggle in the UI bar or by adding a toggle in the Settings dialog (see the "[[Settings]]" section for more info on how to create settings). They can be found in the locations on the UI bar indicated here:
<div style="text-align: center; margin-bottom: -30px;"><span style="display: block; position: relative;">(This image is hidden.)</span><img @src="setup.ImagePath+'Image_toggles.png'" style="position: relative; top: -24px; width: 200px;"></div>
For example, you can toggle the display of this image using the "Images" toggle on the UI bar or the "Hide images?" option in the game's "Settings" window:
[img[setup.ImagePath+"Prism64-2.png"]]
If you want to protect an image from being hidden like that, you can either add the {{{nohide}}} class to the image, like this:
<img @src="setup.ImagePath+'Prism64-2.png'" class="nohide">
Source: {{{<img @src="setup.ImagePath+'Prism64-2.png'" class="nohide">}}}
Or you can add the {{{nohide}}} class to the image's parent element, like this:
<span class="nohide">[img[setup.ImagePath+"Prism64-2.png"]]</span>
Source: {{{<span class="nohide">[img[setup.ImagePath+"Prism64-2.png"]]</span>}}}
Because this uses a SugarCube setting, that setting will be remembered, even if you close the game and open it later on. If you need to, you can check the {{{settings.images}}} variable, and that will be set to {{{true}}} if images are hidden, or {{{false}}} if they're not hidden.
Also note that, if images are disabled, this prevents the page from even trying to load the images in a passage. This way no bandwidth, time, or memory is wasted loading images which aren't displayed within a passage. (Images in the UI bar or elsewhere are still loaded, but not displayed.)
To add this ability to your game, simply put the following in your Stylesheet section:
{{{
/* Hidden image toggle */
.hideImages img {
display: none !important;
}
.hideImages .nohide img, .hideImages img.nohide {
display: inherit !important;
}
}}}
and add this to your JavaScript section:
{{{
/* Toggle images JavaScript - Start */
function toggleImg (val) {
if (val === undefined) { // Handle manual toggling.
settings.images = !settings.images;
}
Setting.save();
if (settings.images) {
$("html").addClass("hideImages");
$(".toggle-wrapper").removeClass("pushed");
} else {
$("html").removeClass("hideImages");
$(".toggle-wrapper").addClass("pushed");
}
return settings.images;
}
Setting.addToggle("images", {
label: "Image hiding:",
onChange: toggleImg,
onInit: toggleImg
});
$(document).on("click", ".toggle_handler", function () { toggleImg(); });
/* Toggle images JavaScript - End */
}}}
However, if you also want to add a toggle switch for this to the UI bar, then add this to your ''StoryCaption'' passage:
{{{
<<nobr>>''Images:'' <div @class="'toggle-wrapper' + (settings.images ? '' : ' pushed')">
<div class="rect_2"></div>
<div class="rect_1">
<div class="rect_1_inset"></div>
Off On
</div>
<div class="rect_3"></div>
<div class="toggle_handler">
<div class="toggle_ellipse"></div>
</div>
</div><</nobr>>
}}}
and add this to your Stylesheet section:
{{{
/* Toggle Switch - Start */
.toggle-wrapper {
position: relative;
top: 8px;
width: 91px;
height: 29px;
display: inline-flex;
}
.rect_1 {
position: absolute;
left: 4px;
top: 4px;
width: 80px;
height: 20px;
background-color: #d6503a;
border-radius: 9px;
box-shadow: 0 0 1px 0px #e1e1e1;
overflow: hidden;
transition: all 0.3s;
will-change: background-color;
line-height: 1.3;
}
.rect_1_inset {
position: absolute;
left: 32px;
top: -4px;
width: 48px;
height: 28px;
background-color: #3362a8;
background-image: linear-gradient(to top, #b6b8b3 0%, #b6b8b3 18%, #e1e2e1 62%, #e1e2e1 74%, #e1e2e1 100%, #e1e2e1 100%);
border-radius: 16px;
box-shadow: inset 0 0 2px rgba(255,255,255,0.35);
transition: all 0.2s;
will-change: transform;
}
.rect_2 {
position: absolute;
left: 0;
top: 0;
width: 84px;
height: 22px;
background-color: #3362a8;
background-image: linear-gradient(to right, #cbcbcb 0%, #cbcbcb 0%, #cdcdcd 100%);
border: 3px solid #e4e4e3;
border-radius: 14px;
}
.rect_3 {
position: absolute;
left: 4px;
top: 4px;
width: 80px;
height: 20px;
border-radius: 10px;
background-color: rgba(0,0,0,0);
box-shadow: inset 0 5px 25px 0 rgba(0,0,0,0.6);
}
.toggle_handler {
position: absolute;
left: 39px;
top: 3px;
width: 48px;
height: 22px;
background-color: #3362a8;
background-image: linear-gradient(to top, #bfbfbe 0%, #e1e2e1 64%, #f3f3f3 77%, #f3f3f3 100%, #f3f3f3 100%);
border-radius: 11px;
box-shadow: inset 0 0 2px rgba(255,255,255,0.35), 0 3px 3px rgba(0,0,0,0.5), 0 0 3px rgba(0,0,0,0.5);
transition: all 0.2s;
will-change: transform;
}
.toggle_ellipse {
position: absolute;
left: 28px;
top: 4px;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: #3362a8;
background-image: linear-gradient(to top, #e1e2e1 0%, #e1e2e1 0%, #b6b8b3 100%, #c1c1c1 100%);
}
.toggle-wrapper.pushed .toggle_handler {
-webkit-transform: translate3d(-75%, 0, 0);
transform: translate3d(-75%, 0, 0);
}
.toggle-wrapper.pushed .rect_1_inset {
-webkit-transform: translate3d(-60%, 0, 0) scale(1, 1);
transform: translate3d(-60%, 0, 0) scale(1, 1);
}
.toggle-wrapper.pushed .rect_1 {
background-color: #4fade3;
}
/* Toggle Switch - End */
}}}
Enjoy!<h1>Custom Save Titles</h1>If you would like to make it so that the SugarCube save dialog prompts the user to enter in a description for the save, it's actually pretty simple to do that.
To do this, we just need to set a function for the "<a href="https://www.motoslave.net/sugarcube/2/docs/#config-api-property-saves-descriptions" target="_blank">Config.saves.descriptions</a>" method (see link for details) which will control how the title/description of each save slot gets set.
You do that by adding something like the following code to your JavaScript section:
{{{
Config.saves.descriptions = function (saveType) {
if (saveType === Save.Type.Slot) { // If this is for a manual save slot (won't trigger on autosaves)...
let title = prompt("Enter Save Slot Title:", passage()); // ...ask the user for the save's title, defaulting to the passage name.
if (title !== null) {
return title; // Use the user's title.
}
}
return passage(); // Use the passage name.
};
}}}
(NOTE: Although the code calls the save descriptions a "title," in the save object it's actually the "desc" (description) property of the <a href="https://www.motoslave.net/sugarcube/2/docs/#save-api-objects" target="_blank">the save object</a>. The "title" property in the save object's history will actually be the passage name of each moment in the history.)
Now, when the user chooses to save in a save slot, it will ask them to enter in their own save slot title, defaulting to the current passage name, or they can just click "OK" and let it use the default title. Clicking "Cancel" will also use the default title, because there's no way to prevent saving at that point.
You can try it out by saving the current "game" to see how it works.
''NOTE:'' This code requires the use of SugarCube v2.37.0 or later, as this feature changed in this version.
Enjoy!
**********
Current code is broken. Try these:
{{{
/* Save file naming 2.37.3 */
Config.saves.descriptions = function (saveType) {
if (saveType === Save.Type.Slot) {
const playerDesc = prompt('Save Description:', '');
playerDesc.trim();
return playerDesc ? playerDesc : undefined;
}
};
/* Save naming 2.37.3 */
Save.onSave.add(function (save) {
var title = prompt("Enter Save Slot Title:", save.title);
if (title !== null) save.desc = title;
});
}}}
Also, see if they can have a working "Cancel" button.<h1>{{{<<textboxPlus>>}}} Widget</h1>The {{{<<textboxPlus>>}}} widget allows you to quickly set up a textbox which meets accessibility requirements, plus it has a number of options that the default {{{<<textbox>>}}} macro doesn't have.
''Usage:''
{{{
<<textboxPlus "Label: " "$variableName" `{
default: "Default value",
passage: "Passage name",
placeholder: "Placeholder text",
maxlength: 10,
spellcheck: false,
autofocus: true,
autocomplete: "off",
password: true,
readonly: true,
disabled: true,
onchange: "<<run alert('Text was changed.')>>",
oninput: "<<run alert('Input event triggered.')>>",
onreturn: "<<run alert('User hit RETURN.')>>"
}`>>
}}}
''NOTE:'' The use of multiple lines above is for //illustrative purposes only//. The actual code must all be on one single line (not including linewrap) with no line breaks.
For the first parameter, which is the text of the label for the textbox, if you put a space as the last character of the label, then, instead of the textbox appearing to the //right// of the label, the textbox will appear on the line //below// the label.
The second parameter is the name of the variable which will get set to whatever is typed into the textbox. This parameter must contain a valid variable name as a string (i.e. within quotes).
All of the other options in the third parameter are optional. The non-default versions of those options are shown above. If any options are used, they must be contained within backticks and curly brackets (i.e. {{{`{ ... }`}}}), and multiple options must be separated by commas, as shown above (but all in a single line of code). Alternately, you can create a variable as an object with your options, and then pass that variable as the third parameter (see the "No spellcheck or autocomplete" example below).
The "{{{default: "Default value"}}}" option sets the default value for the variable and shows up as the default text within the textbox.
The "{{{passage: "Passage name"}}}" option sets the name of the passage to go to when the {{{RETURN}}} or {{{ENTER}}} key is hit while focus is within the textbox.
The "{{{onreturn: "CODE"}}}" option silently runs the included code when the {{{RETURN}}} or {{{ENTER}}} key is hit while focus is within the textbox.
The "{{{onchange: "CODE"}}}" option silently runs the included code when the focus leaves the textbox (per the ''MDN - <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event" target="_blank">{{{<input type="text">}}} change event</a>'').
The "{{{oninput: "CODE"}}}" option silently runs the included code whenever the text typed into the textbox changes (per the ''MDN - <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event" target="_blank">{{{<input type="text">}}} input event</a>'').
The "{{{password: true}}}" option hides the characters typed within the textbox.
For an explanation of the rest of the various options see: ''MDN - "<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text" target="_blank">{{{<input type="text">}}}</a>" and "<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text" target="_blank">{{{<input>}}}: The Input (Form Input) element</a>"''
For a list of all potential "{{{autocomplete}}}" values see: ''MDN - "<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete" target="_blank">The HTML autocomplete attribute</a>"''
''Examples:''
<<textboxPlus "Label: " "$variableName">>
Source: {{{<<textboxPlus "Label: " "$variableName">>}}}
<<textboxPlus "Search:" "$search" `{ placeholder: "Search text here..." }`>>
Source: {{{<<textboxPlus "Search:" "$search" `{ placeholder: "Search text here..." }`>>}}}
<<set _options = { spellcheck: false, autocomplete: "off" }>><<textboxPlus "No spellcheck or autocomplete: " "_text" _options>>
Source: {{{<<set _options = { spellcheck: false, autocomplete: "off" }>><<textboxPlus "No spellcheck or autocomplete: " "_text" _options>>}}}
<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to go to the Main Menu: " "$menu" `{ passage: "Main Menu", default: "Default text" }`>>
Source: {{{<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to go to the Main Menu: " "$menu" `{ passage: "Main Menu", default: "Default text" }`>>}}}
<<set $input1 = "Sample text">><<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to have that text displayed: " "$input1" `{ onreturn: "<<run alert('You entered: ' + $input1)>>", default: $input1 }`>>
Source: {{{<<set $input1 = "Sample text">><<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to have that text displayed: " "$input1" `{ onreturn: "<<run alert('You entered: ' + $input1)>>", default: $input1 }`>>}}}
<<set $input2 = "Sample text">><<textboxPlus "Enter or change some text and then click outside the textbox to have that text displayed: " "$input2" `{ onchange: "<<run alert('You entered: ' + $input2)>>", default: $input2 }`>>
Source: {{{<<set $input2 = "Sample text">><<textboxPlus "Enter or change some text and then click outside the textbox to have that text displayed: " "$input2" `{ onchange: "<<run alert('You entered: ' + $input2)>>", default: $input2 }`>>}}}
<<textboxPlus "Enter PIN: (max 4 characters) " "$PIN" `{ maxlength: 4, password: true }`>>
<<button "Show PIN Value">>
<<run alert($PIN)>>
<</button>>
Source:
{{{
<<textboxPlus "Enter PIN: (max 4 characters) " "$PIN" `{ maxlength: 4, password: true }`>>
<<button "Show PIN Value">>
<<run alert($PIN)>>
<</button>>
}}}
''Source:''
To use this widget, create a non-special, non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags), and put the following code inside that widget passage:
{{{
/* <<textboxPlus>> widget v1.3.2 - Start
Usage:
<<textboxPlus "Label: " "$variableName" `{
default: "Default value",
passage: "Passage name",
placeholder: "Placeholder text",
maxlength: 10,
spellcheck: false,
autofocus: true,
autocomplete: "off",
password: true,
readonly: true,
disabled: true,
onchange: "<<run alert('Text was changed.')>>",
oninput: "<<run alert('Input event triggered.')>>",
onreturn: "<<run alert('User hit RETURN.')>>"
}`>>
NOTE: If you put a space as the last character for the label then, instead
of the textbox appearing to the right of the label, the textbox will
appear on the line BELOW the label. Also, all of the options shown
within the third parameter above (after "$variableName") are optional.
For a list of all "autocomplete" options see:
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
VARIABLES USED: _textboxPlusName, _textboxPlusOptions
*/
<<widget "textboxPlus">><<nobr>>
<<if (_args[1][0] !== "$") && (_args[1][0] !== "_")>>
/* Show error message for bad variable name. */
<span class="errmsg" data-msg="<<textboxPlus>> - Invalid variable name." @data-src="_args[1]"></span>
<<run $(document).one(":passagerender",
function (ev) {
$(ev.content).find(".errmsg").each(function (idx) {
throwError($(this), $(this).data("msg"), $(this).data("src"));
});
}
)>>
<<else>>
/* Create textboxPlus input box. */
<<if _args[1][0] === "$">>
<<set _textboxPlusName = "textbox-" + _args[1].substr(1).toLowerCase()>>
<<else>>
<<set _textboxPlusName = "textbox--" + _args[1].substr(1).toLowerCase()>>
<</if>>
<<if ndef _args[2]>>
<<set _textboxPlusOptions = {}>>
<<else>>
<<set _textboxPlusOptions = _args[2]>>
<</if>>
<<if ndef _textboxPlusOptions.placeholder>>
<<set _textboxPlusOptions.placeholder = "">>
<</if>>
<<if ndef _textboxPlusOptions.maxlength>>
<<set _textboxPlusOptions.maxlength = "">>
<</if>>
<<if ndef _textboxPlusOptions.spellcheck>>
<<set _textboxPlusOptions.spellcheck = true>>
<</if>>
<<if ndef _textboxPlusOptions.autocomplete>>
<<set _textboxPlusOptions.autocomplete = "">>
<</if>>
<<if ndef _textboxPlusOptions.password>>
<<set _textboxPlusOptions.password = "">>
<</if>>
<<if ndef _textboxPlusOptions.readonly>>
<<set _textboxPlusOptions.readonly = "">>
<</if>>
<<if ndef _textboxPlusOptions.disabled>>
<<set _textboxPlusOptions.disabled = "">>
<</if>>
<<if ndef _textboxPlusOptions.onchange>>
<<set _textboxPlusOptions.onchange = "">>
<</if>>
<<if ndef _textboxPlusOptions.oninput>>
<<set _textboxPlusOptions.oninput = "">>
<</if>>
<<if ndef _textboxPlusOptions.onreturn>>
<<set _textboxPlusOptions.onreturn = "">>
<</if>>
<span class="textboxplus" @data-variable="_args[1]" @data-placeholder="_textboxPlusOptions.placeholder" @data-maxlength="_textboxPlusOptions.maxlength" @data-spellcheck="_textboxPlusOptions.spellcheck" @data-autocomplete="_textboxPlusOptions.autocomplete" @data-password="_textboxPlusOptions.password" @data-readonly="_textboxPlusOptions.readonly" @data-disabled="_textboxPlusOptions.disabled" @data-onchange="_textboxPlusOptions.onchange" @data-oninput="_textboxPlusOptions.oninput" @data-onreturn="_textboxPlusOptions.onreturn">
<label @for="_textboxPlusName">_args[0]</label>
<<if _args[0].length > 0 && _args[0][_args[0].length - 1] === " ">>
<br>
<</if>>
<<if ndef _textboxPlusOptions.default>>
<<set _textboxPlusOptions.default = "">>
<</if>>
<<if ndef _textboxPlusOptions.passage>>
<<if _textboxPlusOptions.autofocus>>
<<textbox _args[1] _textboxPlusOptions.default autofocus>>
<<else>>
<<textbox _args[1] _textboxPlusOptions.default>>
<</if>>
<<else>>
<<if _textboxPlusOptions.autofocus>>
<<textbox _args[1] _textboxPlusOptions.default _textboxPlusOptions.passage autofocus>>
<<else>>
<<textbox _args[1] _textboxPlusOptions.default _textboxPlusOptions.passage>>
<</if>>
<</if>>
</span>
<</if>>
<</nobr>><</widget>>
<<script>>
$(document).on(":passagerender", function (event) {
/* Update textboxPlus input boxes. */
$(event.content).find(".textboxplus").each(function () {
let options = {}, props = {};
let data = $(this).data("placeholder");
if (data) {
options.placeholder = data;
}
data = $(this).data("maxlength");
if (data) {
options.maxlength = data;
}
data = $(this).data("spellcheck");
if (data.toString().toLowerCase() === "false") {
options.spellcheck = "false";
}
data = $(this).data("autocomplete");
if (data) {
options.autocomplete = data;
}
data = $(this).data("password");
if (data) {
props.type = "password";
}
data = $(this).data("readonly");
if (data) {
props.readonly = data;
}
data = $(this).data("disabled");
if (data) {
props.disabled = data;
}
$(this).find("input").each(function () {
if (props.type) {
$(this).removeProp("type").attr(options).prop(props);
} else {
$(this).attr(options).prop(props);
}
});
let changeCode = $(this).data("onchange");
if (changeCode) {
$(this).find("input").on("change", function (event) {
$.wiki(changeCode);
});
}
let inputCode = $(this).data("oninput"), parent = this;
if (inputCode) {
$(this).find("input").on("input", function (event) {
State.setVar($(parent).data("variable"), $(this).val());
$.wiki(inputCode);
});
}
let returnCode = $(this).data("onreturn");
if (returnCode) {
$(this).on("keyup", function (event) {
if (event.key === "Enter") {
$.wiki(returnCode);
}
});
}
});
});
<</script>>
/* <<textboxPlus>> widget - End */
}}}
Enjoy!<h1>imageExists() Function</h1>If you need to be able to tell if images can load properly, then the {{{imageExists()}}} function makes this easy using the code given below.
Here are some examples of how the {{{imageExists()}}} function can be used:
<<button "Test 1 pass">>
<<set _img = imageExists("woodtexture5.jpg")>>
<<repeat 10ms>>
<<if _img.status != "waiting">>
<<if _img.status>>
<<run $("#imag").attr("src", _img.src)>>
<<else>>
<<run alert("Image not found!")>>
<</if>>
<<stop>>
<</if>>
<</repeat>>
<</button>>
<<button "Test 1 fail">>
<<set _img = imageExists("test.jpg")>>
<<repeat 10ms>>
<<if _img.status != "waiting">>
<<if _img.status>>
<<run $("#imag").attr("src", _img.src)>>
<<else>>
<<run alert("Image not found!")>>
<</if>>
<<stop>>
<</if>>
<</repeat>>
<</button>> (this tries to load an invalid image)
<<button "Test 2 pass">>
<<run imageExists("woodtexture7.jpg", "#imag", '<<run alert("Image not found!")>>')>>
<</button>>
<<button "Test 2 fail">>
<<run imageExists("test.jpg", "#imag", '<<run alert("Image not found!")>>')>>
<</button>> (this tries to load an invalid image)
<img id="imag" width="300">
The code for the first button looks like this:
{{{
<<button "Test 1 pass">>
<<set _img = imageExists("woodtexture5.jpg")>>
<<repeat 10ms>>
<<if _img.status != "waiting">>
<<if _img.status>>
<<run $("#imag").attr("src", _img.src)>>
<<else>>
<<run alert("Image not found!")>>
<</if>>
<<stop>>
<</if>>
<</repeat>>
<</button>>
<img id="imag" width="300">
}}}
The "Test 1" buttons use the {{{.status}}} property of the returned value to watch the status of the image loading. That property will have the values of {{{"waiting"}}} while the image is waiting to load, and then it will either be {{{true}}} or {{{false}}} depending on whether or not the image loaded correctly, respectively. This version waits for the image to load by using the {{{<<repeat>>}}} macro to keep checking the image status until it loads.
The code for the first "Test 2" button is:
{{{
<<button "Test 2 pass">>
<<run imageExists("woodtexture7.jpg", "#imag", '<<run alert("Image not found!")>>')>>
<</button>>
<img id="imag" width="300">
}}}
The "Test 2" buttons pass the selector of the image element ({{{#imag}}} refers to the {{{id="imag"}}} in the {{{<img>}}} element) where you want the image to appear upon success as the second parameter, and pass SugarCube code to run upon failure as the third parameter.
The {{{imageExists()}}} function works like this:
!!!imageObject = imageExists(imageFilename, [imageElementSelector], [failureCode])
''imageFilename:'' The filename of the image to try loading.
''imageElementSelector:'' (optional) A CSS selector for an HTML {{{<img>}}} element to load the image into if it loads successfully.
''failureCode:'' (optional) This is the wiki code to execute if the image fails to load.
''imageObject:'' The object returned from this function has the following properties:
* ''status:'' Which will be {{{"waiting"}}} while the image is loading, and then will become either {{{true}}} or {{{false}}}, indicating if the image was able to be loaded successfully or not, respectively.
* ''src:'' This is the full URL string for the image.
* ''fname:'' This is the filename that was passed to the function.
* ''img:'' This is the actual {{{<img>}}} element this function uses.
To be able to use this function in your project, simply add the code below to your project's JavaScript section:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
/* imageExists() function - Start */
window.imageExists = function(image_url, selector, failcode) {
function Loaded() {
ret.status = true; // Successful load status.
if (selector) {
$(selector).attr("src", image.src);
}
}
function Failure() {
ret.status = false; // Failed load status.
if (failcode) {
$.wiki(failcode);
}
}
let image = new Image();
// Return an object to indicate the image's loading state.
let ret = { status: "waiting", src: setup.ImagePath + image_url, fname: image_url, img: image };
// Set up event handlers on the image object.
$(image).on("load", Loaded).on("error abort", Failure);
// other events: loadstart onprogress progress loadend
image.src = setup.ImagePath + image_url; // Start loading the image.
return ret;
};
/* imageExists() function - End */
}}}
<<include PathNote>>
Enjoy!<h1>Clicking Parts of Images</h1>If you'd like to make images which have different parts you can click on to make different things happen, then what you're looking for is called an <a href="https://www.w3schools.com/html/html_images_imagemap.asp" target="_blank" tabindex="0">image map</a>.
As an example, if you click on the head, neck, torso, waist, legs, or feet of the below image, then you'll see how that image map works. Note that clicking on the feet takes you to a different passage.
<<nobr>>
<map name="infographic" id="bodyoutline">
<area alt="Head" title="Head" onclick="SugarCube.Dialog.setup('Head');SugarCube.Dialog.wiki('Thinking...');SugarCube.Dialog.open();" shape="rect" coords="152,10,298,129" tabindex="0" />
<area alt="Neck" title="Neck" onclick="alert($(this).attr('title'))" shape="rect" coords="111,129,349,185" tabindex="0" />
<area alt="Torso" title="Torso" onclick="alert('Torso')" shape="rect" coords="79,186,390,374" tabindex="0" />
<area alt="Waist" title="Waist" onclick="alert('Waist')" shape="rect" coords="321,374,113,438" tabindex="0" />
<area alt="Legs" title="Legs" onclick="alert('Legs')" shape="rect" coords="101,807,327,441" tabindex="0" />
<area alt="Feet" title="Feet" onclick="$.wiki('<'+'<goto "Image Clicked">>')" shape="rect" coords="355,891,100,809" tabindex="0" />
</map>
<div class="resizable imageMapObserve" style="width: 200px;">
<img usemap="#infographic" alt="Body Outline" @src="setup.ImagePath + 'BodyOutline.png'" />
</div>
<</nobr>>
The above code looks like this:
{{{
<<nobr>>
<map name="infographic" id="bodyoutline">
<area alt="Head" title="Head" onclick="SugarCube.Dialog.setup('Head');SugarCube.Dialog.wiki('Thinking...');SugarCube.Dialog.open();" shape="rect" coords="152,10,298,129" tabindex="0" />
<area alt="Neck" title="Neck" onclick="alert($(this).attr('title'))" shape="rect" coords="111,129,349,185" tabindex="0" />
<area alt="Torso" title="Torso" onclick="alert('Torso')" shape="rect" coords="79,186,390,374" tabindex="0" />
<area alt="Waist" title="Waist" onclick="alert('Waist')" shape="rect" coords="321,374,113,438" tabindex="0" />
<area alt="Legs" title="Legs" onclick="alert('Legs')" shape="rect" coords="101,807,327,441" tabindex="0" />
<area alt="Feet" title="Feet" onclick="$.wiki('<<goto "Image Clicked">>')" shape="rect" coords="355,891,100,809" tabindex="0" />
</map>
<div class="resizable imageMapObserve" style="width: 200px;">
<img usemap="#infographic" alt="Body Outline" @src="setup.ImagePath + 'BodyOutline.png'" />
</div>
<</nobr>>
}}}
If you want to make your own image map for an image, you can use one of the various online "image map generators" to simplify generating the image map and areas (<a href="http://maschek.hu/imagemap/imgmap/" target="_blank" tabindex="0">example 1</a>, <a href="https://imagemap.org/" target="_blank" tabindex="0">example 2</a>, <a href="https://www.image-map.net/" target="_blank" tabindex="0">example 3</a>). (Also, see the [[Displaying Images in Twine]] section for details on how the {{{@src="setup.ImagePath + 'BodyOutline.png'"}}} part of the above code works.)
The value of the {{{alt}}} attribute in an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area" target="_blank" tabindex="0">{{{<area>}}} element</a> should be a brief description of the contents of that area, which is useful for blind users. The value of the {{{title}}} attribute will appear as tooltip text if you hover your mouse over the area (though this does nothing on mobile or other touch-screen devices).
If you add an {{{onclick}}} attribute to the {{{<area>}}} element, then you can use that to trigger JavaScript code when that area is clicked on. If you want to call SugarCube code from a click, then use the {{{$.wiki()}}} method, as seen in this code:
{{{
<area alt="Feet" title="Feet" onclick="$.wiki('<<goto "Image Clicked">>')"
shape="rect" coords="355,891,100,809" tabindex="0" />
}}}
That code uses the {{{<<goto>>}}} to trigger going to the "''Image Clicked''" passage if you click on that area.
Note that {{{"}}} is used to "escape" the problems with putting a double-quote inside of a double-quoted string. To escape a single-quote you'd use {{{'}}} instead like this:
{{{
<area alt="Feet" title="Feet" onclick='$.wiki("<<goto 'Image Clicked'>>")'
shape="rect" coords="355,891,100,809" tabindex="0" />
}}}
Yet another way to link to another passage would be by using the {{{data-passage}}} attribute, like this:
{{{
<area alt="Feet" title="Feet" data-passage="Image Clicked"
shape="rect" coords="355,891,100,809" tabindex="0" />
}}}
That would //also// take you to the "''Image Clicked''" passage if you clicked on it.
The "''resizable''" class on the above {{{<div>}}} element allows you to resize the image using the bottom-right corner of that {{{<div>}}}. To make the "''resizable''" class work it needs the following "''Resizable element style''" CSS code in the Stylesheet section:
{{{
/* Resizable element style */
.resizable {
display: inline-block;
overflow: hidden;
line-height: 0;
resize: both;
}
.resizable img {
width: 100%;
height: 100%;
}
/* Area element style */
area {
cursor: pointer;
}
area:focus {
outline: 2px solid black;
}
}}}
The "''Area element style''" part of that CSS makes it so that, if you move your mouse over an area, then the cursor turns into a pointer, indicating that that area can be clicked on. Also, that CSS makes it so that, if the {{{<area>}}} element has {{{tabindex="0"}}} in it, you can then {{{TAB}}} or {{{SHIFT+TAB}}} to the {{{<area>}}}, and it will show the outline defined in the {{{area:focus}}} style above. You can leave that part of the CSS out if you don't want an outline to be displayed like that.
Now, normally image maps don't resize automatically with the image like you see in the above example. In order to make //that// work you'll also need to add the following code your JavaScript section:
{{{
/*! Image Map Resizer
* Desc: Resize HTML imageMap to scaled image.
* Copyright: (c) 2014-15 David J. Bradshaw - dave@bradshaw.net
* License: MIT
* Modified for use in Twine/SugarCube by: HiEv (2026) v1.0.1
*/
(function() {
function scaleImageMap() {
function resizeMap() {
function resizeAreaTag(cachedAreaCoords, idx) {
function scale(coord) {
let dimension = 1 === (isWidth = 1 - isWidth) ? "width" : "height";
return (padding[dimension] + Math.floor(Number(coord) * scalingFactor[dimension]));
}
let isWidth = 0;
areas[idx].coords = cachedAreaCoords.split(",").map(scale).join(",");
}
let scalingFactor, padding;
if (image) {
scalingFactor = {
width: $(image).width() / image.naturalWidth,
height: $(image).height() / image.naturalHeight
};
padding = {
width: parseInt(window.getComputedStyle(image, null).getPropertyValue("padding-left"), 10),
height: parseInt(window.getComputedStyle(image, null).getPropertyValue("padding-top"), 10)
};
cachedAreaCoordsArray.forEach(resizeAreaTag);
}
}
function getCoords(e) {
// Normalize coord-string to csv format without any space chars
return e.coords.replace(/ *, */g, ",").replace(/ +/g, ",");
}
function debounce() {
clearTimeout(timer);
timer = setTimeout(resizeMap, 250);
}
function start() {
function setupMap () {
if ($("img").width()) {
resizeMap();
} else {
if (++tries < 100) {
setTimeout(setupMap, 20);
}
}
}
let tries = 0;
if (image) {
setupMap();
let imo = $(context).find(".imageMapObserve");
if (imo.length) {
$(context).off("mouseup", ".imageMapObserve", resizeMap);
imo.on("mouseup", resizeMap);
if (window.ResizeObserver) {
new ResizeObserver(debounce).observe($(context)[0].querySelector(".imageMapObserve"));
}
}
}
}
function addEventListeners() {
if (image) {
image.addEventListener("load", resizeMap, false); // Detect late image loads in IE11
}
window.addEventListener("focus", resizeMap, false); // Cope with window being resized whilst on another tab
window.addEventListener("resize", debounce, false);
window.addEventListener("readystatechange", resizeMap, false);
document.addEventListener("fullscreenchange", resizeMap, false);
$("#ui-bar-toggle").click(debounce);
}
function beenHere() {
return "function" === typeof map._resize;
}
function getImg(name) {
return $(context).find('img[usemap="' + name + '"]')[0];
}
function setup() {
areas = map.getElementsByTagName("area");
cachedAreaCoordsArray = Array.prototype.map.call(areas, getCoords);
image = getImg("#" + map.name) || getImg(map.name);
map._resize = resizeMap; // Bind resize method to HTML map element
}
let map = this, areas = null, cachedAreaCoordsArray = null, image = null, timer = null;
if (!beenHere()) {
setup();
addEventListeners();
start();
} else {
let imo = $(context).find(".imageMapObserve");
if (imo.length) {
$(context).off("mouseup", ".imageMapObserve", resizeMap);
imo.on("mouseup", resizeMap);
if (window.ResizeObserver) {
new ResizeObserver(debounce).observe($(context)[0].querySelector(".imageMapObserve"));
}
}
map._resize(); // Already setup, so just resize map
}
}
function factory() {
function chkMap(element) {
if (!element.tagName) {
throw new TypeError("Object is not a valid DOM element");
} else if ("MAP" !== element.tagName.toUpperCase()) {
throw new TypeError("Expected <MAP> tag, found <" + element.tagName + ">.");
}
}
function init(element) {
if (element) {
chkMap(element);
scaleImageMap.call(element);
maps.push(element);
}
}
let maps;
return function imageMapResizeF(target) {
maps = []; // Only return maps from this call
switch (typeof target) {
case "undefined":
case "string":
Array.prototype.forEach.call(context.querySelectorAll(target || "map"), init);
break;
case "object":
init(target);
break;
default:
throw new TypeError("Unexpected data type (" + typeof target + ").");
}
return maps;
};
}
let context = document;
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof module === "object" && typeof module.exports === "object") {
module.exports = factory(); // Node for browserfy
} else {
window.imageMapResize = factory();
}
if ("jQuery" in window) {
window.jQuery.fn.imageMapResize = function $imageMapResizeF() {
context = this.prevObject;
return this.filter("map").each(scaleImageMap).end();
};
}
})();
$(document).on(":passagerender", function (event) {
$(event.content).find("map").imageMapResize();
});
/* Image Map Resizer - End */
}}}
That code attempts to detect changes in any of the normal things which could potentially resize the screen, and thus possibly the image as well, so that it can keep the image map scaled properly to the image.
For anything //else//, such as things you add to a passage yourself which could resize the image, you'll need to add the "''imageMapObserve''" class there (either to the image itself or to the element which would resize it). Elements with that class will use a "''mouseup''" event and a <a href ="https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver" target="_blank" tabindex="0">resizeOberver</a> to detect any resizing of those elements. The above example uses that class on the {{{<div>}}} to fix the image map scaling when you resize the image. ''NOTE:'' The {{{imageObserver()}}} method isn't supported by Internet Explorer or some mobile browsers (<a href ="https://caniuse.com/#search=resizeObserver" target="_blank" tabindex="0">see here</a>).
That said, in most cases you'll simply need to add the "''Image Map Resizer''" code to your JavaScript section, add the:
{{{
area {
cursor: pointer;
}
}}}
code to your Stylesheet section, put your images and image maps into the passages where you need them, and then you're good to go!
Enjoy!You clicked on the feet.
Head back to [[Clicking Parts of Images]].<h1>FlagBit code</h1>If you want to optimize the performance of your Twine story during saves, loads, and passage transitions, then you'll want to store as little information in the game's history as possible. One way of doing that is by storing multiple pieces of information, such as small numbers and true/false flags, all on a single integer value.
To make doing that simpler you can add <<link 'the "FlagBit" code'>><<ScrollTo "code">><</link>> at the bottom of this page to your JavaScript section and then use the "FlagBit" functions to work with data like that.
To give an example, let's say you set up a dungeon with 20 types of rooms and 7 true/false data within each of those rooms that you may need to track, such as particular items, exits, etc...
That could all be tracked using a single integer value.
In JavaScript an integer is normally made up of 64 bits, however bitwise operators only work on the first 32 bits of that, which allows you to work with values from -2,147,483,648 to 2,147,483,647. However, you could break up those bits to store different chunks of information within that range of integers.
For example, you could use just the first five (5) bits (#0 to #4) of an integer to store an integer value with a range from 0 to 31 (because 2^5 - 1 = 31) representing the up to 32 room types, and then use the remaining 27 bits of that integer to store other values. To represent that visually, it would look like this:<span class="specialCode">
{{{
Bits: 00000000001111111111222222222233
01234567890123456789012345678901
└─┬─┘└──────────┬──────────────┘
room types flags
#0-#31 bits #5 - #31
}}}
</span>Normally, the first 31 bits (bits 0 to 30) determine the integer's numerical value, while the 32nd bit (bit 31) determines the integer's sign, positive or negative. Thus, the integer value {{{0}}} would translate to all bits being set to {{{0}}} or "false", and the integer value {{{-1}}} would translate to all bits being set to {{{1}}} or "true" (since negative values start at {{{-2147483648}}} when only the 32nd bit is set, and count up to {{{-1}}}).
But in the examples used above and below, we're turning that integer back into bits and groups of bits, as shown above, and using those to track multiple pieces of information for each of the rooms in a hypothetical 2D dungeon map.
If you need help visualizing how this works, see the "<<link "Bit Visualizer">><<ScrollTo "bitVisualizer">><</link>>" section below.
To make working with bits in an integer as described above a lot easier, this section provides code for the following five FlagBit functions: ''<<link "setFlagName()">><<ScrollTo "setFlagName">><</link>>'', ''<<link "setFlag()">><<ScrollTo "setFlag">><</link>>'', ''<<link "hasFlag()">><<ScrollTo "hasFlag">><</link>>'', ''<<link "setVal()">><<ScrollTo "setVal">><</link>>'', and ''<<link "getVal()">><<ScrollTo "getVal">><</link>>''.
----
<h2 id="setFlagName">setup.setFlagName(name, flag)</h2>Sets a named alias for the 32 flag numbers (0 to 31). This is to make working with flags easier, since then you can use them by name, instead of having to remember which number represents what flag.
''**IMPORTANT**:'' This function ''must'' be run when the game is started within either the ''StoryInit'' passage or the JavaScript section, because the flag names are ''not'' stored in the game's history, so the values set by this must be initialized whenever the game is started, restarted, or loaded.
''Parameters:''
* ''name'' [any] = The flag number's name. If it's not a string then it will be converted to a string.
* ''flag'' [integer] = The flag number (0 to 31).
''Return value:''
* ''none''
''Example:'' (within the ''StoryInit'' passage)
To make flags 11 to 14 represent exits for the room:
{{{
<<set setup.setFlagName("North", 11)>>
<<set setup.setFlagName("East", 12)>>
<<set setup.setFlagName("South", 13)>>
<<set setup.setFlagName("West", 14)>>
}}}
----
<h2 id="setFlag">setup.setFlag(oldValue, flag, [enabled]) ⇒ newValue</h2>Takes an existing "flagbit" value, sets a single bit within that value to either 1 or 0, and returns the modified "flagbit" value.
''Parameters:''
* ''oldValue'' [integer] = The existing "flagbit" value that is to be changed.
* ''flag'' [integer or string] = The flag number (0 to 31) or its alias as set by ''setFlagName''.
* ''enabled'' [any; optional, defaults to {{{true}}}] = a "<<hovertip '"Truthy" values include any values other than {{{false}}}, {{{undefined}}}, {{{null}}}, {{{NaN}}} (which stands for "Not a Number"), {{{0}}} (the number zero), and {{{""}}} (an empty string).'>>truthy<</hovertip>>" or "<<hovertip '"Falsy" values include {{{false}}}, {{{undefined}}}, {{{null}}}, {{{NaN}}} (which stands for "Not a Number"), {{{0}}} (the number zero), and {{{""}}} (an empty string).'>>falsy<</hovertip>>" value, which determines if the flag bit is set to 1 or 0, respectively. Normally it's best to use {{{true}}} or {{{false}}} here.
''Return value:''
* ''newValue'' [integer] = The new "flagbit" value after the flag is set.
''Example:''
To update a value in a "room" array to add a north exit (flag = 11) you could do:
{{{
<<set $room[1][5] = setup.setFlag($room[1][5], 11, true)>>
}}}
or:
{{{
<<set $room[1][5] = setup.setFlag($room[1][5], "North")>>
}}}
The first example explicitly sets bit 11 to {{{true}}} in the {{{$room}}} array of "flagbits", while the second example uses the alias set using {{{setup.setFlagName()}}} to set the same bit to {{{true}}}, since {{{true}}} is the default value if no third parameter is supplied.
----
<h2 id="hasFlag">setup.hasFlag(value, flag) ⇒ flagSet</h2>Returns a true/false value indicating whether a bit within the "flagbit" value passed to it is either 1 or 0, respectively.
''Parameters:''
* ''value'' [integer] = The value to look for the flag in.
* ''flag'' [integer or string] = The flag number (0 to 31) or its alias as set by setFlagName.
''Return value:''
* ''flagSet'' [Boolean] = true or false, depending on whether that bit was set to 1 or 0, respectively.
''Example:''
To see if a room in the array has a north exit (flag = 11) you could do:
{{{
<<if setup.hasFlag($room[1][5], 11)>>\
There is an exit to the north.
<</if>>
}}}
or, using an alias set using {{{setup.setFlagName()}}}:
{{{
<<if setup.hasFlag($room[1][5], "North")>>\
There is an exit to the north.
<</if>>
}}}
----
<h2 id="setVal">setup.setVal(oldValue, [lowBit,] highBit, number) ⇒ newValue</h2>Sets a range of bits within an integer to a number.
''Parameters:''
* ''oldValue'' [integer] = The value to set the number in.
* ''lowBit'' [integer; optional, defaults to 0] = The low bit (0 to 31) to be set.
* ''highBit'' [integer] = The high bit (lowBit to 31) to be set. (Note: lowBit and highBit will be swapped if lowbit is larger than highBit.)
* ''number'' [integer] = The value to be set within the returned value. The value of ''number'' must be within the 0 to (2^bits - 1) range. For example, if you're using five bits, then the range is 0 to 31. Use the following tool to see the available range for a particular number of bits: <<numberbox "_bits" 5>> bits = A range of 0 to <span id="max">31</span>
''Return value:''
* ''newValue'' [integer] = The modified "flagbit" value, with the bit range set to the new number.
''Example:''
To set a room's type using the first five bits (0 to 4) you could do this:
{{{
<<set $room[1][5] = setup.setVal($room[1][5], 0, 4, _roomType)>>
}}}
or, since the "lowBit" is zero in this case, you could leave it out like this:
{{{
<<set $room[1][5] = setup.setVal($room[1][5], 4, _roomType)>>
}}}
----
<h2 id="getVal">setup.getVal(value, [lowBit,] highBit) ⇒ number</h2>Gets the value of a range of bits within an integer as a number.
''Parameters:''
* ''value'' [integer] = The value to set the number in.
* ''lowBit'' [integer; optional, defaults to 0] = The low bit (0 to 31) to be set.
* ''highBit'' [integer] = The high bit (lowBit to 31) to be set. (Note: lowBit and highBit will be swapped if lowbit is larger than highBit.)
''Return value:''
* ''number'' [integer] = The number stored within the given bit range of value.
''Example:''
To get a room's type using the first five bits (0 to 4) you could do this:
{{{
<<set _roomType = setup.getVal($room[1][5], 0, 4)>>
}}}
or, since the "lowBit" is zero in this case, you could leave it out like this:
{{{
<<set _roomType = setup.getVal($room[1][5], 4)>>
}}}
----
<h2 id="code">BitFlags source code</h2>
''NOTE:'' If you need more than 32 bits to work with, then you can use all of the FlagBit functions on more than one variable. However, any flag //names// set using the {{{setFlagName()}}} function will act the same for all of those variables.
To use the FlagBit functions in your code, simply add this to your JavaScript section:
{{{
/* FlagBit Functions - Start */
(function () {
let i; // Initialize the flag names.
setup.flagName = [];
for (i = 0; i < 32; i++) {
setup.flagName.push(i.toString());
}
})();
/* setup.setFlagName(name, flag)
Sets a named alias for the 32 flag numbers (0 to 31). This is to make working with flags easier, since then you can use them by name, instead of having to remember which number represents what flag.
**IMPORTANT**: This should only be used in the 'StoryInit' passage because the flag names are not stored in the game's history.
Parameters:
name [any] = The flag number's name. If it's not a string then it will be converted to a string.
flag [integer] = The flag number (0 to 31).
*/
setup.setFlagName = function (name, flag) {
if (passage() != "") {
throw new Error("\nsetFlagName: Flag names should only be set during initialization in the 'StoryInit' passage.");
}
if (Number.isInteger(flag)) {
if ((flag >= 0) && (flag <= 31)) {
setup.flagName[flag] = name.toString();
} else {
throw new Error("\nsetFlagName: Flag value " + flag + " is out of bounds. Value should be from 0 to 31.");
}
} else {
throw new Error("\nsetFlagName: Invalid type for flag. '" + flag + "' = type " + typeof flag + ", but should be integer.");
}
};
/* setup.setFlag(oldValue, flag, [enable]) ⇒ newValue
Sets a single bit within the value passed to either 1 or 0.
Parameters:
oldValue [integer] = The value to set the flag in.
flag [integer or string] = The flag number (0 to 31) or its alias as set by ''setFlagName''.
enabled [any; optional, defaults to true] = a truthy of falsy value which determines if the flag bit is set to 1 or 0, respectively.
Return value:
newValue [integer] = The new value after the flag is set.
*/
setup.setFlag = function (value, flag, enable) {
if (enable === undefined) { // Set enable to true if a value wasn't passed for it.
enable = true;
}
if ((typeof flag === "string") || (flag instanceof String)) {
let tmp = setup.flagName.indexOf(flag);
if (tmp < 0) {
throw new Error("\nsetFlag: Unknown flag name '" + flag + "'.");
}
flag = tmp;
}
if (Number.isInteger(flag)) {
if (Number.isInteger(value)) {
if ((flag >= 0) && (flag <= 31)) {
if (enable) {
return value | Math.pow(2, flag); // bitwise OR
} else {
return value & ~Math.pow(2, flag); // bitwise AND NOT
}
} else {
throw new Error("\nsetFlag: flag value " + flag + " is out of bounds. Value should be from 0 to 31.");
}
} else {
throw new Error("\nsetFlag: Invalid type for value. '" + value + "' = type " + typeof value + ", but should be integer.");
}
} else {
throw new Error("\nsetFlag: Invalid type for flag. '" + flag + "' = type " + typeof flag + ", but should be string or integer.");
}
};
/* setup.hasFlag(value, flag) ⇒ flagSet
Determines if a bit within the value passed is either 1 or 0.
Parameters:
value [integer] = The value to look for the flag in.
flag [integer or string] = The flag number (0 to 31) or its alias as set by setFlagName.
Return value:
flagSet [Boolean] = true or false, depending on whether that bit was set to 1 or 0, respectively.
*/
setup.hasFlag = function (value, flag) {
if ((typeof flag === "string") || (flag instanceof String)) {
let tmp = setup.flagName.indexOf(flag);
if (tmp < 0) {
throw new Error("\nhasFlag: Unknown flag name '" + flag + "'.");
}
flag = tmp;
}
if (Number.isInteger(flag)) {
if (Number.isInteger(value)) {
if ((flag >= 0) && (flag <= 31)) {
return !!(value & Math.pow(2, flag));
} else {
throw new Error("\nhasFlag: Flag value " + flag + " is out of bounds. Value should be from 0 to 31.");
}
} else {
throw new Error("\nhasFlag: Invalid type for value. '" + value + "' = type " + typeof value + ", but should be integer.");
}
} else {
throw new Error("\nhasFlag: Invalid type for flag. '" + flag + "' = type " + typeof flag + ", but should be string or integer.");
}
};
/* setup.setVal(oldValue, [lowBit,] highBit, number) ⇒ newValue
Sets a range of bits within an integer to a number.
Parameters:
oldValue [integer] = The value to set the number in.
lowBit [integer; optional, defaults to 0] = The low bit (0 to 31) to be set.
highBit [integer] = The high bit (lowBit to 31) to be set. (Note: lowBit and highBit will be swapped if lowbit is larger than highBit.)
number [integer] = The value to be set within the returned value. The value of ''number'' must be within the 0 to (2^bits - 1) range. For example, if you're using five bits, then the range is 0 to 31.
Return value:
newValue [integer] = The oldValue, but with the bit range set to number.
*/
setup.setVal = function (oldValue, lowBit, highBit, value) {
if (value === undefined) { // Assume lowBit is zero if only three parameters are passed.
value = highBit;
highBit = lowBit;
lowBit = 0;
}
if (lowBit > highBit) { // Swap lowest number to the lowBit.
[lowBit, highBit] = [highBit, lowBit];
}
if (!Number.isInteger(oldValue)) {
throw new Error("\nsetVal: Invalid type for oldValue. '" + oldValue + "' = type " + typeof oldValue + ", but should be integer.");
}
if (!Number.isInteger(value)) {
throw new Error("\nsetVal: Invalid type for value. '" + value + "' = type " + typeof value + ", but should be integer.");
}
if (!Number.isInteger(lowBit)) {
throw new Error("\nsetVal: Invalid type for lowBit. '" + lowBit + "' = type " + typeof lowBit + ", but should be integer.");
}
if (!Number.isInteger(highBit)) {
throw new Error("\nsetVal: Invalid type for highBit. '" + highBit + "' = type " + typeof highBit + ", but should be integer.");
}
if ((lowBit < 0) || (lowBit > 31)) {
throw new Error("\nsetVal: lowBit " + lowBit + " is out of bounds. lowBit should be from 0 to 31.");
}
if ((highBit < lowBit) && (highBit > 31)) {
throw new Error("\nsetVal: highBit " + highBit + " is out of bounds. highBit should be from lowBit to 31.");
}
let maxVal = Math.pow(2, highBit - lowBit + 1) - 1;
if (value > maxVal) {
throw new Error("\nsetVal: value " + value + " is out of bounds (0 to " + maxVal + "). Not enough bits selected to represent that value.");
}
let result = oldValue, i, n = 0;
for (i = lowBit; i <= highBit; i++, n++) {
if (((result & Math.pow(2, i)) != 0) != ((value & Math.pow(2, n)) != 0)) { // Detect if bit needs to be flipped
result = result ^ Math.pow(2, i); // Flip the bit so it matches value
}
}
return result;
};
/* setup.getVal(value, [lowBit,] highBit) ⇒ number
Gets the value of a range of bits within an integer as a number.
Parameters:
value [integer] = The value to set the number in.
lowBit [integer; optional, defaults to 0] = The low bit (0 to 31) to be set.
highBit [integer] = The high bit (lowBit to 31) to be set. (Note: lowBit and highBit will be swapped if lowbit is larger than highBit.)
Return value:
number [integer] = The number stored within the given bit range of value.
*/
setup.getVal = function (value, lowBit, highBit) {
if (highBit === undefined) { // Assume lowBit is zero if only two parameters are passed.
highBit = lowBit;
lowBit = 0;
}
if (lowBit > highBit) { // Swap lowest number to the lowBit.
[lowBit, highBit] = [highBit, lowBit];
}
if (!Number.isInteger(value)) {
throw new Error("\ngetVal: Invalid type for value. '" + value + "' = type " + typeof value + ", but should be integer.");
}
if (!Number.isInteger(lowBit)) {
throw new Error("\ngetVal: Invalid type for lowBit. '" + lowBit + "' = type " + typeof lowBit + ", but should be integer.");
}
if (!Number.isInteger(highBit)) {
throw new Error("\ngetVal: Invalid type for highBit. '" + highBit + "' = type " + typeof highBit + ", but should be integer.");
}
if ((lowBit < 0) || (lowBit > 31)) {
throw new Error("\ngetVal: lowBit " + lowBit + " is out of bounds. lowBit should be from 0 to 31.");
}
if ((highBit < lowBit) && (highBit > 31)) {
throw new Error("\ngetVal: highBit " + highBit + " is out of bounds. highBit should be from lowBit to 31.");
}
let result = 0, i, n = 0;
for (i = lowBit; i <= highBit; i++, n++) {
result += (value & Math.pow(2, i)) ? Math.pow(2, n) : 0;
}
return result;
};
/* FlagBit Functions - End */
}}}
If you have any JavaScript code which uses the above functions and is also immediately triggered when the game starts, then you'll need to put that JavaScript code below the FlagBit code.
Also, if you want the FlagBit functions' error messages to display properly, add this to your Stylesheet section:
{{{
.error-view>.error { /* Show line breaks within error messages. */
white-space: pre;
}
}}}
Enjoy!
<<script>>
$(document).one(":passagerender", function (event) {
$(event.content).find("#numberbox--bits").css("width", 38).attr({ min: 1, max: 31 }).on("change input", function (event) {
if (isNaN(this.valueAsNumber)) {
this.valueAsNumber = 1;
} else {
if (this.valueAsNumber < this.min) {
this.valueAsNumber = this.min;
}
if (this.valueAsNumber > this.max) {
this.valueAsNumber = this.max;
}
}
$("#max").empty().append(Math.pow(2, this.valueAsNumber) - 1);
});
});
<</script>>
----
<h2 id="bitVisualizer">Bit Visualizer</h2>This should help you understand how the bits in an integer work a bit better.
<span style="display: block; font-family: monospace; white-space: pre; line-height: 15px"> Bits: 00000000001111111111222222222233
01234567890123456789012345678901</span><span id="bitstr" style="font-family: monospace">Value: 00000000000000000000000000000000 = 0</span>
<<set _int = 0>><<textboxPlus "''Integer Value:''" "_int" `{ default: "0", oninput: "<<run showbits()>>" }`>> (values from -2,147,483,648 to 2,147,483,647)
<span class="tallbtn">''Bit:'' <<set _opts = Array.from({ length: 32 }, (_, i) => i)>><<listbox "_n">><<optionsfrom _opts>><</listbox>> <<button "Set to 0">>
<<run _int = setup.setFlag(parseInt(_int), _n, 0); $("#textbox--int")[0].value = parseInt(_int); $("#textbox--int").trigger("input")>>
<</button>> <<button "Set to 1">>
<<run _int = setup.setFlag(parseInt(_int), _n, 1); $("#textbox--int")[0].value = parseInt(_int); $("#textbox--int").trigger("input")>>
<</button>></span>
''Numeric value of bits between:'' <<listbox "_a">><<optionsfrom _opts>><</listbox>> and <<listbox "_b">><<optionsfrom _opts>><</listbox>> = <span id="bitval" style="font-family: monospace">0</span>
<<script TwineScript>>
window.showbits = function () {
let txt = "", value = parseInt(_int);
if (Number.isNaN(value)) {
$("#bitstr").text("Value: ???????????????????????????????? = Not an integer");
return false;
} else {
for (let i = 0; i < 32; i++) {
txt += (value & Math.pow(2, i)) ? "1" : "0";
}
}
$("#bitstr").text("Value: " + txt + " = " + value);
$("#bitval").text(setup.getVal(value, _a, _b));
};
<</script>><<done>>
<<run $("#listbox--a, #listbox--b").on("change", showbits)>>
<</done>><h1>Browser Tab Icon</h1>If you look in your browser's tab on this page you should see this icon: <img @src="setup.ImagePath+'SugarCube.svg'" style="height: 16px">
Want to make your own "favicon" like that?
First, start off by creating the image you want displayed there. It's best to either create it as either a square transparent PNG image that's at least 512x512 pixels or as an SVG image. It's also recommended that the icon shouldn't be too elaborate, since it will usually be displayed //much// smaller (e.g. 16x16 pixels).
Next, use that image to generate a favicon at a site like the <a href="https://realfavicongenerator.net/" target="_blank">RealFaviconGenerator</a> site. If you use that particular site, after you've picked your favicon image, set your "Favicon path" to something like "{{{/images/favicon/}}}". You'll also want to turn on "Skip metadata injection" and set the "App name", "Name", and "Short name" values before clicking "Next".
That site will then generate a "favicon.zip" file that you should download and some HTML code which it will tell you to put in the header of your HTML file. Unfortunately, adding a header is not so easy if you're using the Twine 2 editor. Here's how you work around that.
The HTML that site will generate will look something like this:
{{{
<link rel="icon" type="image/png" href="/images/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/images/favicon/favicon.svg" />
<link rel="shortcut icon" href="/images/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="SampleCode" />
<link rel="manifest" href="/images/favicon/site.webmanifest" />
}}}
You'll then translate that into something like the following code in your JavaScript section:
{{{
/* Make the file path work when launched from within the Twine editor. */
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = encodeURI("file:///D:/Games/MyGame/"); // Path to parent directory; used when launched from the Twine application.
} else {
setup.Path = ""; // Path when launched normally. THIS MUST BE ""!
}
setup.ImagePath = setup.Path + encodeURI("images/"); // Images child directory name.
setup.SoundPath = setup.Path + encodeURI("sounds/"); // Sounds child directory name.
// Game icon in tab. Generated at: https://realfavicongenerator.net
let favIco = setup.ImagePath + "favicon/";
$(document.head).append('<link rel="icon" type="image/png" href="' + favIco + 'favicon-96x96.png" sizes="96x96" />');
$(document.head).append('<link rel="icon" type="image/svg+xml" href="' + favIco + 'favicon.svg" />');
$(document.head).append('<link rel="shortcut icon" href="' + favIco + 'favicon.ico">');
$(document.head).append('<link rel="apple-touch-icon" sizes="180x180" href="' + favIco + 'apple-touch-icon.png" />');
$(document.head).append('<meta name="apple-mobile-web-app-title" content="SampleCode" />');
if (document.location.href.toLowerCase().indexOf("http") === 0) { // Manifest can't be loaded if page isn't online.
$(document.head).append('<link rel="manifest" href="' + favIco + 'site.webmanifest" />');
}
}}}
Basically, after putting each line within a {{{$(document.head).append('...');}}}, you'll want to replace the {{{/images/favicon/}}} portion of the line with {{{' + favIco + '}}}.
<<include PathNote>>
Finally, once you've added the code to your JavaScript section, then just put the contents of the "favicon.zip" file you downloaded from that site into your "{{{images/favicon}}}" directory (which should exist where you set your path in the code above) and the icon should now appear in your browser's tab when playing this game!
Enjoy!<h1>weightedEither() Function</h1>The SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#functions-function-either" target="_blank">{{{either()}}} function</a> allows you to pass a list of items to it, and it will randomly pick one.
Unfortunately, if you want some choices to be more likely than others when using the {{{either()}}} function, you have to type in the same item multiple times.
The {{{weightedEither()}}} function makes it easier to both set the odds of each item and to do so with greater precision.
First, just create a list of options by creating an object like this:
{{{
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
}}}
Note that any items which either aren't entirely made up of alphanumeric characters or start with a number will need to put inside quotes. See the {{{"dire beast"}}} example above, which has a space in the name.
The larger the above number, the greater the odds of it being picked by the {{{weightedEither()}}} function. Then just pass that variable as a parameter to the function, something like this:
{{{
<<set _name = weightedEither(_WWName)>>
}}}
and the function will semi-randomly return one of those names.
Try the following example: <<button "Retry function">><<redo "example1">><</button>>
(''Note:'' If nothing changes below when the button above is clicked, that's because the function has picked the same string again.)
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <<do tag "example1">><<= weightedEither(_WWName)>><</do>> attacks you!''
The above code looks like this:
{{{
Try the following example: <<button "Retry function">><<redo "example1">><</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <<do tag "example1">><<= weightedEither(_WWName)>><</do>> attacks you!''
}}}
If you'd like it to ignore some of the options you list in the object, such as to prevent it showing the same name twice in a row, you can pass either the item name as a string or an array of strings listing the item names to be ignored in the second parameter of the {{{weightedEither()}}} function.
For example: <<button "Retry function">><<redo "example2">><</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <<do tag "example2">><<set _lastName = weightedEither(_WWName, _lastName)>>_lastName<</do>> attacks you!''
The code for that looks like this:
{{{
For example: <<button "Retry function">><<redo "example2">><</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <<do tag "example2">><<set _lastName = weightedEither(_WWName, _lastName)>>_lastName<</do>> attacks you!''
}}}
The {{{<<set _lastName = weightedEither(_WWName, _lastName)>>}}} part of the code will prevent the same name from being picked twice in a row.
In order to use this function in your own game, just add the following to your JavaScript section:
{{{
/* weightedEither() Function - Start */
window.weightedEither = function (types, ignore) {
/* Verify "types" parameter is a generic object. */
if ((!types) || (typeof types !== "object") || (types.constructor !== Object)) {
throw new Error("\nweightedEither: First parameter must be a generic object.");
}
/* Remove any ignored keys. */
types = clone(types);
let i;
if (ignore !== undefined) {
if (!Array.isArray(ignore)) {
ignore = [ ignore ]; // Turn "ignore" into an array.
}
for (i = 0; i < ignore.length; i++) {
if (hasOwnProperty.call(types, ignore[i])) {
delete types[ignore[i]];
}
}
}
/* Verify "types" object values are all numbers. */
let values = Object.values(types), val;
for (i = 0; i < values.length; i++) {
val = values[i];
if ((typeof val !== "number") || (!Number.isFinite(val)) || Number.isNaN(val)) {
throw new Error("\nweightedEither: All values for object keys must be numbers.");
}
}
/* Find total of weights. */
val = values.reduce(function (sum, item) {
return sum + item;
});
val = random(1, val); /* Get random position within range. */
for (let key in types) {
if (val <= types[key]) {
return key; /* Return matching position. */
}
val -= types[key]; /* Look further for matching position. */
}
};
/* weightedEither() Function - End */
}}}
Enjoy!<h1>{{{<<seen>>}}} Macro</h1>The {{{<<seen>>}}} macro allows you to have code trigger automatically when certain content is made visible and/or hidden by being scrolled onto or off of the page, respectively.
''USAGE:''
{{{
<<seen [visibility]>> Code to execute when the content becomes visible.
[<<hidden>> Code to execute when the content becomes hidden.]
[<<content>> Content to display on the screen.]
<</seen>>
}}}
(The square brackets merely indicate that those parts are optional, but those brackets should not actually be used like that with this macro.)
The optional {{{visibility}}} argument should be a value from 0 to 100. It represents the threshold percentage of the macro's content which needs to be visible on the screen, before the following "visible" code is triggered. Its default value is 0, meaning that any amount of the content being displayed on the screen counts as it being visible. For example, if you use {{{<<seen 50>>}}}, then the "visible" code will only trigger once over 50% of the macro's content is visible on the screen.
''NOTE:'' For large pieces of content on sufficiently small screens, it is possible that the content will //never// be able to be sufficiently visible for large {{{visibility}}} values, since some of the content may scroll off the top of the screen before the bottom becomes visible. Caution should be used with larger {{{visibility}}} values on large pieces of content.
Any code within the first part of the {{{<<seen>>}}} macro will be triggered when the macro's content is sufficiently visible. (This ''will'' trigger when the passage it's in is first displayed, if the content is sufficiently visible.)
Any code within the optional {{{<<hidden>>}}} section will get executed when the macro's content has completely scrolled off screen. (This will ''not'' trigger when the passage it's in is first displayed.)
Anything within the optional {{{<<content>>}}} section will be displayed normally on the screen (though within a {{{<span>}}} element, which is used to detect visibility). If no content is specified, then the content will be equivalent to a single space.
''EXAMPLE:''
At the bottom of this passage, the following code exists:
{{{
<<seen>>
<<run alert("Congratulations! You've reached the bottom of the page!")>>
<<hidden>>
<<run alert("Awww... You left.")>>
<<content>>
Enjoy the code!
<</seen>>
}}}
Scroll down to the bottom of the page to see it in action.
''SOURCE:''
To use this macro, simply add the following code to your JavaScript section:
{{{
/* <<seen>> macro - Start
USES: _SeenElementIndex
*/
setup.seenNewObservers = [];
setup.seenOldObservers = [];
Macro.add("seen", {
tags: [ "content", "hidden" ],
handler: function() {
let vis = 0;
if (this.args.length) {
vis = parseFloat(this.args[0]);
if (isNaN(vis)) {
vis = 0;
} else {
vis = Math.clamp(vis, 0, 99) / 100; // Fixes problems with 99% visibility when totally visible.
}
}
if (State.temporary.SeenElementIndex == undefined) {
State.temporary.SeenElementIndex = 0;
}
let n = ++State.temporary.SeenElementIndex;
let txt = '<span id="seen-macro-' + n + '">', hid = "";
if (this.payload.length > 1) {
for (let i = 1; i < this.payload.length; ++i) {
if (this.payload[i].name === "hidden") {
hid += this.payload[i].contents;
} else {
txt += this.payload[i].contents;
}
}
}
if (txt === '<span id="seen-macro-' + n + '">') {
txt += " ";
}
txt += '</span>';
$(this.output).wiki(txt);
if (window.IntersectionObserver) {
setup.seenNewObservers.unshift({ index: n, threshold: vis, viscode: this.payload[0].contents, hidcode: hid });
}
}
});
setup.seenHandler = function (entries, observer) {
if ($("html").attr("data-init") != "loading") {
if (entries[0].intersectionRatio > observer.threshold) {
if (observer.hidden) {
observer.hidden = false;
$.wiki(observer.viscode);
}
} else if (entries[0].intersectionRatio === 0) {
if (!observer.hidden) {
observer.hidden = true;
$.wiki(observer.hidcode);
}
}
}
};
$(document).on(":passageend", function (event) {
while (setup.seenOldObservers.length) {
// Clear out old observers
setup.seenOldObservers[0].disconnect();
setup.seenOldObservers.shift();
}
let obs;
while (setup.seenNewObservers.length) {
// Create new observers
if (setup.seenNewObservers[0].threshold === 0) {
obs = new IntersectionObserver(setup.seenHandler, { threshold: [0, 0.01, 0.99, 1] });
} else {
obs = new IntersectionObserver(setup.seenHandler, { threshold: [0, setup.seenNewObservers[0].threshold, 1] });
}
obs.observe($("#seen-macro-" + setup.seenNewObservers[0].index)[0]);
obs.index = setup.seenNewObservers[0].index;
obs.threshold = setup.seenNewObservers[0].threshold;
obs.viscode = setup.seenNewObservers[0].viscode;
obs.hidcode = setup.seenNewObservers[0].hidcode;
obs.hidden = true;
setup.seenOldObservers[0] = obs;
setup.seenNewObservers.shift();
}
});
/* <<seen>> macro - End */
}}}
For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation. For reference, this particular macro works through using the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" target="_blank">Intersection Observer API</a>. ("API" = Application Programming Interface; a set of rules and protocols that allows two different software programs to communicate with each other and exchange data.)
<<seen>>
<<run alert("Congratulations! You've reached the bottom of the page!")>>
<<hidden>>
<<run alert("Awww... You left.")>>
<<content>>
Enjoy the code!
<</seen>><h1>Remembering Scroll Position</h1>If you'd like your passages to be able to remember the current scroll position of the main window when using the "back" (<<scicon arrow-left>>) and "forward" (<<scicon arrow-right>>) buttons, then just add the following code to your game's JavaScript section:
{{{
/* TrackScroll code - Start
USES: $WinPosX, $WinPosY
*/
setup.currTurn = 1;
State.variables.WinPosX = 0;
State.variables.WinPosY = 0;
$(document).on(":passageinit", function () {
if (State.length) {
State.history[setup.currTurn - 1].variables.WinPosX = window.scrollX;
State.history[setup.currTurn - 1].variables.WinPosY = window.scrollY;
}
});
$(document).on(":passageend", function () {
if ((State.variables.WinPosX) || (State.variables.WinPosY)) {
window.scroll(State.variables.WinPosX, State.variables.WinPosY);
State.variables.WinPosX = 0;
State.variables.WinPosY = 0;
}
setup.currTurn = State.length;
});
/* TrackScroll code - End */
}}}
Note that this uses the {{{$WinPosX}}} and {{{$WinPosY}}} variables, as well as the {{{setup.currTurn}}} variable, so make sure you don't use those anywhere else in your code.
If you'd like, you can test this out by going to this long page of "[[Lorem ipsum]]" text, scrolling down, then hitting the "back" button and the "forward" button again, and the scroll position should be the same.
Enjoy!<h1>Pronoun Templates</h1>These templates, plus the optional {{{<<SetGender>>}}} widget, make it fairly easy to write text which automatically displays gendered pronouns correctly based on the gender you set.
''NOTE:'' <a href="https://www.motoslave.net/sugarcube/2/docs/#template-api" target="_blank">Templates</a> require SugarCube v2.29.0 or later. If you have an older version of SugarCube you should either update to the current version of SugarCube or you can use the <a data-passage="SetPronouns Widget" class="link-internal">{{{<<setPronouns>>}}} widget</a> instead.
To use these templates in your passages you could, for example, write this text:
{{{* ?They got ?themself a new toy for ?their collection and ?theyre very happy about it.}}}
And if you had previously done either {{{<<SetGender "f">>}}} or {{{<<set $pgen = 1>>}}}, then that text would display as:<<SetGender "f">>
* ?They got ?themself a new toy for ?their collection and ?theyre very happy about it.
Alternately, {{{<<SetGender>>}}}, {{{<<SetGender "m">>}}}, or {{{<<set $pgen = 0>>}}} would give you:<<SetGender>>
* ?They got ?themself a new toy for ?their collection and ?theyre very happy about it.
And {{{<<SetGender "b">>}}} or {{{<<set $pgen = 2>>}}} would give you:<<SetGender "b">>
* ?They got ?themself a new toy for ?their collection and ?theyre very happy about it.
{{{<<SetGender "n">>}}} or {{{<<set $pgen = 3>>}}} would give you:<<SetGender "n">>
* ?They got ?themself a new toy for ?their collection and ?theyre very happy about it.
"m" = male, "f" = female, "b" = both, "n" = neither
Note that the template names are case sensitive, so (uppercase) {{{?They}}} shows as "?They" and (lowercase) {{{?they}}} shows as "?they". Also, you have to use {{{?Theyre}}} and {{{?theyre}}}, because template names can't have an apostrophe in them.
See the "''Template Name List''" at the bottom of the page for all of the template names made available to you.
To use these templates, add the following code to your JavaScript section:
{{{
/* Pronoun Templates - Start
USES: $pgen: 0 = male, 1 = female, 2 = gender neutral, 3 = no gender
*/
$(document).one(":passagestart", function () {
if (State.variables.pgen === undefined) {
State.variables.pgen = 0; /* Default gender */
}
});
Template.add("they", function () { return ["he", "she", "they", "it"][State.variables.pgen]; });
Template.add("them", function () { return ["him", "her", "them", "it"][State.variables.pgen]; });
Template.add("themself", function () { return ["himself", "herself", "themself", "itself"][State.variables.pgen]; });
Template.add("themselves", function () { return ["themselves", "themselves", "themselves", "themselves"][State.variables.pgen]; });
Template.add("their", function () { return ["his", "her", "their", "its"][State.variables.pgen]; });
Template.add("theirs", function () { return ["his", "hers", "theirs", "its"][State.variables.pgen]; });
Template.add("theyre", function () { return ["he's", "she's", "they're", "it's"][State.variables.pgen]; });
Template.add("youngPerson", function () { return ["boy", "girl", "person", "person"][State.variables.pgen]; });
Template.add("youngPeople", function () { return ["boys", "girls", "people", "people"][State.variables.pgen]; });
Template.add("adultPerson", function () { return ["man", "woman", "person", "person"][State.variables.pgen]; });
Template.add("adultPeople", function () { return ["men", "women", "people", "people"][State.variables.pgen]; });
Template.add("generalPerson", function () { return ["guy", "girl", "guy", "guy"][State.variables.pgen]; });
Template.add("generalPeople", function () { return ["guys", "girls", "guys", "guys"][State.variables.pgen]; });
Template.add("pal", function () { return ["dude", "chick", "pal", "pal"][State.variables.pgen]; });
Template.add("pals", function () { return ["dudes", "chicks", "pals", "pals"][State.variables.pgen]; });
Template.add("They", function () { return ["He", "She", "They", "It"][State.variables.pgen]; });
Template.add("Them", function () { return ["Him", "Her", "Them", "It"][State.variables.pgen]; });
Template.add("Themself", function () { return ["Himself", "Herself", "Themself", "Itself"][State.variables.pgen]; });
Template.add("Themselves", function () { return ["Themselves", "Themselves", "Themselves", "Themselves"][State.variables.pgen]; });
Template.add("Their", function () { return ["His", "Her", "Their", "Its"][State.variables.pgen]; });
Template.add("Theirs", function () { return ["His", "Hers", "Theirs", "Its"][State.variables.pgen]; });
Template.add("Theyre", function () { return ["He's", "She's", "They're", "It's"][State.variables.pgen]; });
Template.add("YoungPerson", function () { return ["Boy", "Girl", "Person", "Person"][State.variables.pgen]; });
Template.add("YoungPeople", function () { return ["Boys", "Girls", "People", "People"][State.variables.pgen]; });
Template.add("AdultPerson", function () { return ["Man", "Woman", "Person", "Person"][State.variables.pgen]; });
Template.add("AdultPeople", function () { return ["Men", "Women", "People", "People"][State.variables.pgen]; });
Template.add("GeneralPerson", function () { return ["Guy", "Girl", "Guy", "Guy"][State.variables.pgen]; });
Template.add("GeneralPeople", function () { return ["Guys", "Girls", "Guys", "Guys"][State.variables.pgen]; });
Template.add("Pal", function () { return ["Dude", "Chick", "Pal", "Pal"][State.variables.pgen]; });
Template.add("Pals", function () { return ["Dudes", "Chicks", "Pals", "Pals"][State.variables.pgen]; });
/* Pronoun Templates - End */
}}}
Optionally, if want to be able to use the {{{<<SetGender>>}}} widget, you can add the following code to a non-special non-story passage with a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
/* <<SetGender>> widget - Start
Usage (defaults to male):
for "he": <<SetGender>> or <<SetGender "m">>
for "she": <<SetGender "f">>
for "they": <<SetGender "b">>
for "it": <<SetGender "n">>
$pgen: 0 = male, 1 = female, 2 = gender neutral, 3 = no gender
VARIABLES USED: $pgen
*/
<<widget "SetGender">><<nobr>>
<<switch _args[0]>>
<<case "f">>
<<set $pgen = 1>>
<<case "b">>
<<set $pgen = 2>>
<<case "n">>
<<set $pgen = 3>>
<<default>>
<<set $pgen = 0>>
<</switch>>
<</nobr>><</widget>>
/* <<SetGender>> widget - End */
}}}
If you don't want to use the {{{<<SetGender>>}}} widget to set the pronoun gender, then you can just set the {{{$pgen}}} variable to determine the pronoun gender instead. Setting it to 0 = male, 1 = female, 2 = gender neutral, and 3 = no gender. Any other value will likely throw an error.
<<nobr>>
<<set $pron = ["they", "them", "themself", "themselves", "their", "theirs", "theyre", "youngPerson", "youngPeople", "adultPerson", "adultPeople", "generalPerson", "generalPeople", "pal", "pals"]>>
<<set $gen = ["m", "f", "b", "n"]>>
<div id="list"><label>''Template Name List:''</label>
<table id="info">
<tr>
<th class="info-cell">Template Name</th>
<th class="info-cell">Male</th>
<th class="info-cell">Female</th>
<th class="info-cell">Gender Neutral</th>
<th class="info-cell">Non-Gendered</th>
</tr>
<<for _i = 0, _len = $pron.length; _i < _len; _i++>>
<<run $pron.push($pron[_i].toUpperFirst())>>
<</for>>
<<for _i = 0, _len = $pron.length; _i < _len; _i++>>
<<set _txt = '<tr><td>?' + $pron[_i] + '</td><td>'>>
<<for _j = 0; _j < 4; _j++>>
<<SetPronouns $gen[_j]>>
<<set _txt += State.variables[$pron[_i]] + '</td>'>>
<<if _j < 3>>
<<set _txt += '<td>'>>
<</if>>
<</for>>
<<set _txt += '</tr>'>>
_txt
<</for>>
</table></div>
<</nobr>>
If you don't like some of the above outputs, simply edit those templates to give whatever outputs you prefer instead.
Enjoy!<h1>{{{<<speech>>}}} Macro</h1>This macro makes it easy to show text in "speech boxes" like this:
<<speech "You" "David">>Hey, sis!<</speech>>
<<speech "Celestia">>Hey bro, what're you up to?<</speech>>
To do that you'd simply need to put this in your passage:
{{{
<<speech "You" "David">>Hey, sis!<</speech>>
<<speech "Celestia">>Hey bro, what're you up to?<</speech>>
}}}
The first parameter in the {{{<<speech>>}}} macro is the class name. If you include a second parameter then that will put that second parameter's text as the name at the top of the speech box, otherwise it will use the class name you gave as the first parameter as the character's name as well.
To add the {{{<<speech>>}}} macro to your own Twine gme, simply put this code into your JavaScript section:
{{{
/* speech macro - Start */
Macro.add('speech', {
tags : null,
handler : function () {
let id = this.args[0], name = id;
if (this.args.length > 1) name = this.args[1];
let output = '<div class="speech ' + id + '">';
output += '<span class="avatar"></span>';
output += name + '<hr>' + this.payload[0].contents + '</div>';
$(this.output).wiki(output);
}
});
/* speech macro - End */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
Then put this CSS code into your Stylesheet section:
{{{
/* speech macro - Start */
.speech {
min-height: 91 px;
color: white;
border: 2px solid white;
border-radius: 5px;
padding: 8px 8px 8px 8px;
box-shadow: 5px 5px 3px Black;
}
.avatar {
display: block;
padding: 1px;
height: 84px;
width: 84px;
float: left;
margin: 0px 10px 0px 0px;
border: 2px solid white;
border-radius: 5px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.You {
background-color: #333333;
}
.You .avatar {
background-image: URL("images/Silhouette_100.png");
}
.Celestia {
background-color: #554400;
}
.Celestia .avatar {
background-image: URL("images/Celestia_100.jpg");
}
/* speech macro - End */
}}}
Note that you'll need to add a {{{.Name}}} and {{{.Name .avatar}}} class for each character.
Also, if you'd like to shorten the gap around the horizontal line, you can add something like this:
{{{
.speech hr {
margin: 4px 0;
}
}}}
That will change the gap above and below the line from the default of {{{1em}}} (one line height) to 4 pixels. (You can use different values if you want.)
''Note:'' Because the CSS above uses relative paths, you won't be able to see those images if you launch your game from the Twine editor. If you want to see them when playing it from the Twine editor as well, then you can add this "hack" to your JavaScript section:
{{{
if (document.location.href.toLowerCase().includes("/twine/scratch/") || document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
/* Detect if you're running in Twine. */
$(document).on(":passagedisplay", function (ev) {
$(".avatar").each(function () {
let url = $(this).css("background-image"); // Grab the current avatar URL.
url = url.replace(/\"/gi, "").replace(')',''); // Remove the end of the URL we don't need.
url = url.substr(document.location.href.lastIndexOf("/") + 5); // Remove the unneeded front part.
$(this).css("background-image", "url('file:///D:/Games/MyGame/" + url + "')");
});
});
}
}}}
<<include PathNote>>
Now, when running that code in Twine, it will change the relative path of the avatar images into absolute paths, which will make them displayable in Twine. (When running that code outside of Twine it does nothing.)
If you're at all concerned about having your directory names in the code, then you should probably either change the path or take that chunk of code out of any versions you release to the public.<h1>Listbox Tricks</h1>Here are a few bits of sample code showing how to do some more advanced tricks using the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-listbox" target="_blank">{{{<<listbox>>}}} macro</a>:
# <<link "How to set the value of a listbox">><<ScrollTo "ex1">><</link>>
# <<link "Updating the passage when the listbox changes">><<ScrollTo "ex2">><</link>>
# <<link "Listbox groups">><<ScrollTo "ex3">><</link>>
# <<link "Multiselect listboxes">><<ScrollTo "ex4">><</link>>
----
!!<span id="ex1">Example 1: How to set the value of a listbox</span>
This example shows how to have a default value displayed ("{{{Cherry}}}" in this example), as well as how to use buttons to select particular options in a dropdown. Here's the example:
''Pie:'' <<set $pie ??= "cherry">><<listbox "$pie" autoselect>>
<<option "Blueberry" "blueberry">>
<<option "Cherry" "cherry">>
<<option "Coconut cream" "coconut cream">>
<</listbox>>
<<button "Set Pie to Blueberry by index">>
<<run $("#listbox-pie").val("0").trigger("change")>> /* "blueberry" is at index "0" */
<</button>>
<<button "Set Pie to Coconut cream by option name">>
<<run $("#listbox-pie").val($("#listbox-pie option:contains('Coconut cream')").val()).trigger("change")>>
<</button>>
Select a different option above and <<link [[try it out|Next Passage]]>>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<<set $FontName = $("#FontName").val()>>
<</link>>. (Use the back button to return to this position in the passage.)
Here's the source for the above example:
{{{
''Pie:'' <<set $pie ??= "cherry">><<listbox "$pie" autoselect>>
<<option "Blueberry" "blueberry">>
<<option "Cherry" "cherry">>
<<option "Coconut cream" "coconut cream">>
<</listbox>>
<<button "Set Pie to Blueberry by index">>
<<run $("#listbox-pie").val("0").trigger("change")>> /* "blueberry" is at index "0" */
<</button>>
<<button "Set Pie to Coconut cream by option name">>
<<run $("#listbox-pie").val($("#listbox-pie option:contains('Coconut cream')").val()).trigger("change")>>
<</button>>
}}}
This part of the code: {{{<<set $pie ??= "cherry">>}}} will set the value of {{{$pie}}} to {{{"cherry"}}} //only if// the variable isn't defined yet (that's what the {{{??=}}} means). The {{{autoselect}}} parameter then causes the current value of {{{$pie}}} (at the time the listbox is created) to be the "default" value which is selected in the dropdown.
For the buttons, they set the current index of the listbox in two different ways, by directly setting the index or by finding the index by option text and then setting it to that index, respectively. The jQuery <a href="https://api.jquery.com/val/" target="_blank">.val()</a> method lets you get and set the value of the listbox (i.e. the selected index), and the <a href="https://api.jquery.com/trigger/" target="_blank">.trigger()</a> method is used to tell SugarCube that the value changed. To do something like this in your own code, the {{{"#listbox-pie"}}} part of the above code will need to be changed to {{{"#listbox-yourVariableName"}}} when you're using the {{{<<listbox>>}}} macro to set a ''story'' variable (one which starts with a {{{$}}}). If you're instead setting a ''temporary'' variable (one which starts with an {{{_}}}), then you'll need to use {{{"#listbox--yourVariableName"}}} (two hyphens).
----
!!<span id="ex2">Example 2: Updating the passage when the listbox changes</span>
If you want to trigger an action to occur when the user changes the option that is selected in a listbox, such as updating some text that's displayed, like this...
<blockquote>''Select class:'' <<listbox "$class" autoselect>>
<<optionsfrom ["Warrior", "Archer", "Sorcerer"]>>
<</listbox>>
''Your class:'' <span id="class-info"><<include `"Class Info " + $class`>></span>\
<<done>><<script TwineScript>>
/* Trigger text display upon listbox change. */
$("#listbox-class").on("change", function (event) {
/* Fade out text over 500ms (0.5 seconds). */
$("#class-info").fadeOut(500, function () {
/* Update text and then fade it back in over 500ms (0.5 seconds). */
$("#class-info").empty().wiki("<<include 'Class Info " + $class + "'>>").fadeIn(500);
});
});
<</script>><</done>></blockquote>...then you can use some code like this to do that:
{{{
''Select class:'' <<listbox "$class" autoselect>>
<<optionsfrom ["Warrior", "Archer", "Sorcerer"]>>
<</listbox>>
''Your class:'' <span id="class-info"><<include `"Class Info " + $class`>></span>\
<<done>><<script TwineScript>>
/* Trigger text display upon listbox change. */
$("#listbox-class").on("change", function (event) {
/* Fade out text over 500ms (0.5 seconds). */
$("#class-info").fadeOut(500, function () {
/* Update text and then fade it back in over 500ms (0.5 seconds). */
$("#class-info").empty().wiki("<<include 'Class Info " + $class + "'>>").fadeIn(500);
});
});
<</script>><</done>>
}}}
The above uses an array to fill in the options displayed by the {{{<<listbox>>}}} macro. The text that's displayed comes from the corresponding passages with names using the "{{{Class Info X}}}" format (where "{{{X}}}" stands for any class name in the array). See the [[Class Info Warrior]], [[Class Info Archer]], and [[Class Info Sorcerer]] passages for where the texts are pulled from.
You don't have to use the {{{<<include>>}}} macro to add the text, this is merely one example of how this can be done.
See the jQuery <a href="https://api.jquery.com/on/" target="_blank">.on()</a>, <a href="https://api.jquery.com/fadeOut/" target="_blank">.fadeOut()</a>, <a href="https://api.jquery.com/fadeIn/" target="_blank">.fadeIn()</a>, and <a href="https://api.jquery.com/empty/" target="_blank">.empty()</a> methods, as well as the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#methods-jquery-method-wiki" target="_blank">.wiki()</a> method for details on how they work.
----
!!<span id="ex3">Example 3: Listbox groups</span>
This dropdown uses pure HTML code, groups items by type (using {{{<optgroup>}}}), and attempts to set the font for each line:
''Font:'' <select id="FontName" data-uinv="dropdown"> /* Intentionally left out "Comic Sans" and "Impact". */
<optgroup label="Sans-Serif Fonts (recommended)">
<option style="font-family:Arial" value="Arial">Arial</option>
<option style="font-family:'Arial Black'" value="'Arial Black'">Arial Black</option>
<option style="font-family:Helvetica" value="Helvetica">Helvetica</option>
<option style="font-family:'Lucida Sans Unicode'" value="'Lucida Sans Unicode'">Lucida Sans Unicode</option>
<option style="font-family:Tahoma" value="Tahoma">Tahoma</option>
<option style="font-family:'Trebuchet MS'" value="'Trebuchet MS'">Trebuchet MS</option>
<option style="font-family:Verdana" value="Verdana">Verdana</option>
</optgroup>
<optgroup label="Serif Fonts">
<option style="font-family:Bookman" value="Bookman">Bookman</option>
<option style="font-family:Garamond" value="Garamond">Garamond</option>
<option style="font-family:Georgia" value="Georgia">Georgia</option>
<option style="font-family:Palatino" value="Palatino">Palatino</option>
<option style="font-family:'Palatino Linotype'" value="'Palatino Linotype'">Palatino Linotype</option>
<option style="font-family:Times" value="Times">Times</option>
<option style="font-family:'Times New Roman'" value="'Times New Roman'">Times New Roman</option>
</optgroup>
<optgroup label="Monospace Fonts">
<option style="font-family:Courier" value="Courier">Courier</option>
<option style="font-family:'Courier New'" value="'Courier New'">Courier New</option>
<option style="font-family:'Lucida Console'" value="'Lucida Console'">Lucida Console</option>
</optgroup>
</select>
<<done>>
<<run $("#FontName").val(State.variables.FontName ?? "Arial")>>
<</done>>(Note: Some browsers will show the different fonts in the dropdown and others won't.)
The above will try to select the correct font from the dropdown list based on the value of the {{{$FontName}}} variable. If that variable doesn't exist, then it defaults to the "''Arial''" font in the dropdown.
Because of how the above works, you would then have to set the {{{$FontName}}} value when exiting the passage, which you could do one of two ways:
{{{
[[Link text|Next Passage][$FontName = $("#FontName").val()]]
}}}
or
{{{
<<link [[Link text|Next Passage]]>>
<<set $FontName = $("#FontName").val()>>
<</link>>
}}}
Select a different option above and <<link [[try it out|Next Passage]]>>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<<set $FontName = $("#FontName").val()>>
<</link>>. (Use the back button to return to this position in the passage.)
Here is the source for that dropdown listbox:
{{{
''Font:'' <select id="FontName" data-uinv="dropdown"> /* Intentionally left out "Comic Sans" and "Impact". */
<optgroup label="Sans-Serif Fonts (recommended)">
<option style="font-family:Arial" value="Arial">Arial</option>
<option style="font-family:'Arial Black'" value="'Arial Black'">Arial Black</option>
<option style="font-family:Helvetica" value="Helvetica">Helvetica</option>
<option style="font-family:'Lucida Sans Unicode'" value="'Lucida Sans Unicode'">Lucida Sans Unicode</option>
<option style="font-family:Tahoma" value="Tahoma">Tahoma</option>
<option style="font-family:'Trebuchet MS'" value="'Trebuchet MS'">Trebuchet MS</option>
<option style="font-family:Verdana" value="Verdana">Verdana</option>
</optgroup>
<optgroup label="Serif Fonts">
<option style="font-family:Bookman" value="Bookman">Bookman</option>
<option style="font-family:Garamond" value="Garamond">Garamond</option>
<option style="font-family:Georgia" value="Georgia">Georgia</option>
<option style="font-family:Palatino" value="Palatino">Palatino</option>
<option style="font-family:'Palatino Linotype'" value="'Palatino Linotype'">Palatino Linotype</option>
<option style="font-family:Times" value="Times">Times</option>
<option style="font-family:'Times New Roman'" value="'Times New Roman'">Times New Roman</option>
</optgroup>
<optgroup label="Monospace Fonts">
<option style="font-family:Courier" value="Courier">Courier</option>
<option style="font-family:'Courier New'" value="'Courier New'">Courier New</option>
<option style="font-family:'Lucida Console'" value="'Lucida Console'">Lucida Console</option>
</optgroup>
</select>
<<done>>
<<run $("#FontName").val(State.variables.FontName ?? "Arial")>>
<</done>>(Note: Some browsers will show the different fonts in the dropdown and others won't.)
}}}
If you need more help with how to use option groups, see the MDN's documentation on <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/optgroup" target="_blank">the option group element</a>.
----
!!<span id="ex4">Example 4: Multiselect listboxes</span>
If you would like people to be able to select multiple items from a list, then you might need a listbox where you can select multiple items at once, like this one:
<select id="testList" multiple size="3" style="min-width: 200px;">
<option value="A">Apple</option>
<option value="B">Banana</option>
<option value="C">Coconut</option>
<option value="D">Durian Fruit</option>
</select>
(Hold the {{{[Ctrl]}}}, {{{[Shift]}}}, or {{{[Command]}}} keys to select multiple items.)
<<button "Display selected items by their labels + values">>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<<run alert($values.join(", "))>>
<</button>>
Here's the code for the above:
{{{
<select id="testList" multiple size="3" style="min-width: 200px;">
<option value="A">Apple</option>
<option value="B">Banana</option>
<option value="C">Coconut</option>
<option value="D">Durian Fruit</option>
</select>
(Hold the {{{[Ctrl]}}}, {{{[Shift]}}}, or {{{[Command]}}} keys to select multiple items.)
<<button "Display selected items by their labels + values">>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<<run alert($values.join(", "))>>
<</button>>
}}}
The "{{{size}}}" attribute on the {{{<select>}}} element roughly determines how many items in the list will be visible at once. If you don't include the "{{{size}}}" attribute, then the list will be tall enough to show all items.
The code above fills in the {{{$values}}} array with the value of each selected item. You can change the {{{opt.label + ": " + opt.value}}} part of the code if you only need the label or the value, instead of both.
As in example #3, you will need to grab the values upon exiting the passage, like this:
<<link [[Link text|Next Passage]]>>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<<set $FontName = $("#FontName").val()>>
<</link>>
Here's the code for how this can be done:
{{{
<<link [[Link text|Next Passage]]>>
<<set $values = [...$("#testList").prop("options")].filter(opt => opt.selected).map(opt => opt.label + ": " + opt.value)>>
<</link>>
}}}
The above code uses the <a href="http://api.jquery.com/prop/" target="_blank">jQuery .prop() method</a> to get the properties from the listbox. Putting the value returned from {{{.prop("options")}}} within {{{[...X]}}} (using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax" target="_blank">spread syntax</a> within array brackets) turns the output into a normal array. That array then uses the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter" target="_blank">.filter() method</a> to reduce the array to only the selected entries. And once you have that, then the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map" target="_blank">.map() method</a> can be used to get the labels and/or the values of those selected elements.
Furthermore, note that Twine/SugarCube defaults to selecting ''all'' items in the list. If you would like to have it default to selecting ''no'' items instead, then add this code to the same passage:
{{{
<<done>>
<<run [...$("#testList").prop("options")].forEach(opt => opt.selected = false)>>
<</done>>
}}}
If you would like some items selected by default and some not, you will need to use a modified version of the above code, since the normal method of putting the "selected" attribute in the {{{<option>}}} element doesn't work in Twine/SugarCube.
Warrior.
He's a good big fight dood.Archer.
Shoots arrows. Pew! Pew!Sorcerer.
"You attack the darkness."<h1>Outlined Text</h1>Here's some simple CSS to add to your game's Stylesheet section to make text more readable on complex backgrounds:
{{{
.bbwt { /* Black Border - White Text */
color: white;
text-shadow: 1px 0px rgba(0,0,0,0.7), -1px 0px rgba(0,0,0,0.7), 0px 1px rgba(0,0,0,0.7), 0px -1px rgba(0,0,0,0.7);
font-weight: bold;
}
.wbbt { /* White Border - Black Text */
color: black;
text-shadow: 1px 0px rgba(255,255,255,0.7), -1px 0px rgba(255,255,255,0.7), 0px 1px rgba(255,255,255,0.7), 0px -1px rgba(255,255,255,0.7);
font-weight: bold;
}
}}}
Once you've added the above CSS to your game's Stylesheet section, then simply add some code like this in your game's passages where you want them to be styled using those classes:
{{{
<span class="wbbt">Some sample black text using the "wbbt" class.</span>
<span class="bbwt">Some sample white text using the "bbwt" class.</span>
}}}
If you want text in //all// of your passages to be styled one of those ways, simply replace the ".bbwt" or ".wbbt" in the above CSS with "#passages" so that CSS affects all passages.
The "bbwt" class works best for "black background - white text" cases and the "wbbt" class works best for "white background - black text" cases. For example:
<div @style="'background: url(\'' + setup.ImagePath + 'space-1.jpg\') center center / cover repeat; background-position-x: -262px; background-position-y: -160px;'">
<blockquote id="bqbkg" style="padding: 8px; border: 3px white solid; border-radius: 10px;"><span style="font-weight: bold; color: black;">Some sample black text.</span>
<span class="wbbt">Some sample black text using the "wbbt" class.</span>
<span style="font-weight: bold;">Some sample white text.</span>
<span class="bbwt">Some sample white text using the "bbwt" class.</span>
</blockquote>
</div>
As you can see, the black text without the outline is barely visible, and the white text without the outline also becomes difficult to read when overlapping a bright portion of the background. On the other hand, the text with the outlines remain fairly readable in either case.
The "bbwt" class works best in most of the above image, since it's a generally darker image. However, it might be better to put a translucent background behind the text as well:
''Background:'' <<button "Dark">><<run $("#bqbkg").css("background-color", "rgba(0, 0, 0, 0.5)")>><</button>> <<button "Light">><<run $("#bqbkg").css("background-color", "rgba(255, 255, 255, 0.3)")>><</button>> <<button "none">><<run $("#bqbkg").css("background-color", "transparent")>><</button>>
For the border and backgrounds seen above, you can mix and match the classes in the following CSS code, which you'd need to add to your Stylesheet section:
{{{
.txtborder {
padding: 8px;
border: 3px white solid;
border-radius: 10px;
}
.txtdarkbkg {
background-color: rgba(0, 0, 0, 0.5);
}
.txtlightbkg {
background-color: rgba(255, 255, 255, 0.3);
}
}}}
You can modify the last <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb_colors" target="_blank">{{{rgba()}}}</a> parameter in that code to determine how translucent the background color is (i.e. 0 = completely transparent; 1 = completely opaque).
With that, the code in your passages might look like:
{{{
<div class="txtborder txtdarkbkg">
<span class="bbwt">White text with a black border on top of a dark translucent background.</span>
</div>
}}}<h1>Pointy Buttons</h1>Want hexagonal buttons that look like this?:
<span class="pointybtn"><<button "Pointy Button">><<run alert("Clicked.")>><</button>></span>
Then just add the following CSS to your game's Stylesheet section:
{{{
/* Pointy Buttons - Start */
:root { /* Palette */
--button-color : #35a;
--button-border : #57c;
--border-highlight : #79e;
}
.pointybtn {
cursor: pointer;
position: relative;
display: inline-block;
top: 4px;
height: 34px;
padding: 1px 0 0 0;
}
.pointybtn::before {
content: "";
display: inline-block;
position: relative;
left: 0;
top: -1.3px;
border-top: 17.6px solid transparent;
border-right: 20.6px solid var(--button-border);
border-bottom: 17.6px solid transparent;
}
.pointybtn:hover::before {
border-right-color: var(--border-highlight);
}
.pointybtn button {
position: relative;
top: -13px;
padding: .4em;
background-color: var(--button-color);
border-top: 1.7px solid var(--button-border);
border-bottom: 1.7px solid var(--button-border);
border-left: transparent;
border-right: transparent;
line-height: 1.12;
z-index: 20;
}
.pointybtn button:hover {
background-color: var(--button-border);
border-top: 1.7px solid var(--border-highlight);
border-bottom: 1.7px solid var(--border-highlight);
}
.pointybtn button::before {
content: "";
position: absolute;
right: 100%;
top: -0.9px;
border-top: 15.6px solid transparent;
border-right: 16.9px solid var(--button-color);
border-bottom: 15.6px solid transparent;
-webkit-transition-duration: 0.2s;
-moz-transition-duration: 0.2s;
-o-transition-duration: 0.2s;
transition-duration: 0.2s;
}
.pointybtn button:hover::before {
border-right-color: var(--button-border);
}
.pointybtn button::after {
content: "";
position: absolute;
left: 100%;
top: -0.9px;
border-top: 15.6px solid transparent;
border-left: 16.9px solid var(--button-color);
border-bottom: 15.6px solid transparent;
z-index: 10;
-webkit-transition-duration: 0.2s;
-moz-transition-duration: 0.2s;
-o-transition-duration: 0.2s;
transition-duration: 0.2s;
}
.pointybtn button:hover::after {
border-left-color: var(--button-border);
}
.pointybtn::after {
content: "";
display: inline-block;
position: relative;
left: 0;
top: -1.3px;
border-top: 17.6px solid transparent;
border-left: 20.6px solid var(--button-border);
border-bottom: 17.6px solid transparent;
z-index: 0;
}
.pointybtn:hover::after {
border-left-color: var(--border-highlight);
}
.pointybtn button:focus {
outline: none;
}
.pointybtn:focus-within {
outline: thin dotted;
}
@supports (-moz-appearance:none) {
/* Firefox only */
.pointybtn {
padding: 3px 0 0 0;
}
.pointybtn button::before {
top: 0.2px;
}
.pointybtn button::after {
top: 0.2px;
}
}
/* Pointy Buttons - End */
}}}
If you want different colors for your buttons, simply change the color values given in the {{{:root}}} section of the above CSS. If you want this to work properly in Internet Explorer as well, then I'd recommend changing all of the {{{var(--XXX)}}} parts to the actual color values which those bits of code represent.
Once you've finished doing the above, all you need to do is wrap your {{{<<button>>}}} macros within a span which adds that styling, like this:
{{{
<span class="pointybtn"><<button "Pointy Button">><<run alert("Clicked.")>><</button>></span>
}}}
and that will modify that button to look more hexagonal.
Enjoy!<h1>Dream Background</h1>If you'd like to use this animated background in your game, simply add a "dream" tag to any passages where you want it to appear, and add the following CSS code to the game's Stylesheet section:
{{{
/* Dream Background - Start
Based on the code found here: https://codepen.io/hylobates-lar/pen/bGEQXgm
*/
body.dream {
background-color: #111;
}
.dream #story {
background-color: #111c;
}
.dream #story::before, .dream #story::after,
body.dream::before, body.dream::after {
font: 5vmin/1.3 Serif;
color: transparent;
position: fixed;
top: 50%;
left: 50%;
width: 3em;
height: 3em;
content: ".";
mix-blend-mode: screen;
animation: 44s -27s move infinite ease-in-out alternate;
z-index: -1;
}
body.dream::before {
text-shadow: 0.9445em 2.4338em 7px rgba(33, 255, 0, 0.9), 0.5759em 0.54em 7px rgba(0, 255, 208, 0.9), -0.4559em 0.5912em 7px rgba(0, 255, 236, 0.9), 1.4961em 2.4687em 7px rgba(0, 255, 65, 0.9), 1.5417em 0.0384em 7px rgba(255, 80, 0, 0.9), 0.125em 1.2416em 7px rgba(0, 195, 255, 0.9), 2.3972em 0.181em 7px rgba(0, 221, 255, 0.9), 2.4624em 1.92em 7px rgba(255, 0, 49, 0.9), -0.0334em 0.44em 7px rgba(0, 237, 255, 0.9), 1.1526em 2.3128em 7px rgba(255, 240, 0, 0.9), 0.167em 1.038em 7px rgba(0, 255, 181, 0.9), 0.1229em 1.2339em 7px rgba(255, 165, 0, 0.9), 2.4996em 2.0089em 7px rgba(0, 255, 22, 0.9), -0.0446em 1.3232em 7px rgba(54, 0, 255, 0.9), -0.2259em 2.1849em 7px rgba(255, 216, 0, 0.9), 2.1445em 0.9129em 7px rgba(0, 255, 78, 0.9), 0.2714em -0.3698em 7px rgba(255, 254, 0, 0.9), 0.3873em 0.8664em 7px rgba(255, 0, 189, 0.9), 2.13em 2.206em 7px rgba(166, 255, 0, 0.9), 1.0059em 1.4956em 7px rgba(255, 94, 0, 0.9), -0.081em 1.3552em 7px rgba(255, 0, 112, 0.9), -0.2231em 0.8206em 7px rgba(0, 255, 176, 0.9), 1.1141em 2.154em 7px rgba(255, 0, 135, 0.9), 0.6443em 1.0032em 7px rgba(0, 210, 255, 0.9), 0.5579em 1.8526em 7px rgba(255, 211, 0, 0.9), -0.0934em 1.0921em 7px rgba(5, 0, 255, 0.9), 2.2169em -0.4125em 7px rgba(0, 224, 255, 0.9), 1.2373em 0.0968em 7px rgba(0, 226, 255, 0.9), 1.1008em -0.3961em 7px rgba(142, 0, 255, 0.9), 0.6563em 0.8088em 7px rgba(251, 0, 255, 0.9), 0.0568em 1.7335em 7px rgba(0, 255, 56, 0.9), 1.8487em 2.0343em 7px rgba(0, 255, 34, 0.9), 0.7157em 2.0317em 7px rgba(0, 12, 255, 0.9), -0.4807em 2.2297em 7px rgba(49, 0, 255, 0.9), -0.4254em 2.3987em 7px rgba(0, 255, 154, 0.9), 2.335em 1.8111em 7px rgba(255, 158, 0, 0.9), 1.8396em 1.0255em 7px rgba(4, 0, 255, 0.9), 1.4643em 1.2423em 7px rgba(0, 204, 255, 0.9), 1.6005em 1.2384em 7px rgba(194, 255, 0, 0.9), -0.0072em 1.458em 7px rgba(255, 36, 0, 0.9), -0.4817em 0.741em 7px rgba(119, 255, 0, 0.9);
animation-duration: 44s;
animation-delay: -27s;
}
body.dream::after {
text-shadow: 1.1478em 2.3162em 7px rgba(54, 255, 0, 0.9), 2.4372em 0.644em 7px rgba(252, 0, 255, 0.9), 1.3828em 0.3822em 7px rgba(255, 76, 0, 0.9), 1.2372em 2.0605em 7px rgba(78, 255, 0, 0.9), 1.136em 0.1495em 7px rgba(255, 0, 72, 0.9), 2.4944em 0.91em 7px rgba(255, 138, 0, 0.9), 1.6947em 1.6824em 7px rgba(0, 229, 255, 0.9), 1.2857em 2.2709em 7px rgba(172, 255, 0, 0.9), 1.5298em 1.2425em 7px rgba(255, 0, 244, 0.9), 0.6615em 1.3241em 7px rgba(255, 201, 0, 0.9), 1.6726em -0.031em 7px rgba(99, 0, 255, 0.9), 1.8822em 1.48em 7px rgba(255, 0, 144, 0.9), 1.8877em 2.124em 7px rgba(0, 147, 255, 0.9), 0.2285em 2.2208em 7px rgba(118, 0, 255, 0.9), 1.1214em 1.9097em 7px rgba(183, 255, 0, 0.9), 0.077em -0.438em 7px rgba(235, 255, 0, 0.9), 0.5525em 0.6064em 7px rgba(0, 255, 167, 0.9), 2.0679em -0.2284em 7px rgba(0, 90, 255, 0.9), 0.129em 1.4586em 7px rgba(0, 255, 196, 0.9), 0.5162em 1.3848em 7px rgba(0, 235, 255, 0.9), 0.5718em -0.169em 7px rgba(116, 255, 0, 0.9), -0.29em 0.0931em 7px rgba(0, 58, 255, 0.9), 0.8758em -0.1827em 7px rgba(0, 217, 255, 0.9), 0.317em 1.7079em 7px rgba(0, 255, 197, 0.9), 1.8475em -0.181em 7px rgba(0, 255, 81, 0.9), 1.2301em 0.7312em 7px rgba(0, 255, 162, 0.9), 1.1789em 0.3969em 7px rgba(122, 0, 255, 0.9), 0.8406em 0.1432em 7px rgba(0, 255, 37, 0.9), -0.4374em -0.4698em 7px rgba(255, 0, 222, 0.9), 0.2916em 0.549em 7px rgba(0, 68, 255, 0.9), 2.3653em 0.6267em 7px rgba(0, 255, 193, 0.9), 0.8062em 0.582em 7px rgba(181, 0, 255, 0.9), 1.1588em 1.1817em 7px rgba(240, 255, 0, 0.9), 1.3778em 2.18em 7px rgba(0, 255, 155, 0.9), 0.1705em 1.0881em 7px rgba(0, 209, 255, 0.9), 0.7739em 2.1074em 7px rgba(255, 0, 216, 0.9), 0.557em 1.0809em 7px rgba(255, 154, 0, 0.9), 2.461em 2.081em 7px rgba(123, 0, 255, 0.9), 1.028em 1.5213em 7px rgba(0, 52, 255, 0.9), 1.4168em 0.9101em 7px rgba(69, 0, 255, 0.9), 2.221em 2.3186em 7px rgba(0, 255, 143, 0.9);
animation-duration: 43s;
animation-delay: -32s;
}
head::before {
text-shadow: 0.8016em 2.3114em 7px rgba(245, 255, 0, 0.9), 1.126em 2.2075em 7px rgba(161, 255, 0, 0.9), 1.9574em 2.2538em 7px rgba(0, 255, 136, 0.9), 0.0691em 0.7518em 7px rgba(255, 218, 0, 0.9), 0.9464em -0.2514em 7px rgba(255, 0, 208, 0.9), 1.3681em 1.8335em 7px rgba(65, 0, 255, 0.9), 1.0312em 1.0682em 7px rgba(0, 255, 207, 0.9), 0.2964em 0.0637em 7px rgba(255, 0, 153, 0.9), 0.7568em -0.2568em 7px rgba(0, 25, 255, 0.9), 0.5288em 1.636em 7px rgba(0, 222, 255, 0.9), 0.9712em 1.6134em 7px rgba(255, 247, 0, 0.9), 0.0307em 1.1524em 7px rgba(100, 255, 0, 0.9), 0.5794em 0.5299em 7px rgba(0, 143, 255, 0.9), 0.093em 1.534em 7px rgba(81, 255, 0, 0.9), 0.2816em 0.7949em 7px rgba(111, 0, 255, 0.9), 0.7289em 0.8082em 7px rgba(0, 181, 255, 0.9), 0.9486em -0.3715em 7px rgba(255, 0, 108, 0.9), 1.3897em 1.7802em 7px rgba(229, 255, 0, 0.9), 0.7283em 1.5614em 7px rgba(255, 197, 0, 0.9), 2.0225em 2.1354em 7px rgba(255, 0, 110, 0.9), 1.7632em 1.6043em 7px rgba(0, 123, 255, 0.9), 2.4257em 2.3895em 7px rgba(237, 0, 255, 0.9), 0.77em 0.1143em 7px rgba(255, 49, 0, 0.9), 1.52em 2.1449em 7px rgba(143, 255, 0, 0.9), 1.9005em 2.3336em 7px rgba(0, 255, 8, 0.9), -0.1228em 1.6819em 7px rgba(3, 0, 255, 0.9), -0.0479em 0.5373em 7px rgba(0, 255, 72, 0.9), 0.1703em -0.1272em 7px rgba(0, 16, 255, 0.9), 1.999em -0.3338em 7px rgba(42, 255, 0, 0.9), 2.3526em 0.2166em 7px rgba(142, 255, 0, 0.9), 0.9688em 2.245em 7px rgba(255, 0, 152, 0.9), 2.111em -0.1589em 7px rgba(255, 0, 189, 0.9), 1.8395em 1.1002em 7px rgba(213, 0, 255, 0.9), -0.0805em 2.2244em 7px rgba(0, 255, 25, 0.9), 1.3164em 2.4778em 7px rgba(255, 62, 0, 0.9), -0.0494em 0.197em 7px rgba(24, 255, 0, 0.9), 0.0303em 0.5078em 7px rgba(136, 0, 255, 0.9), 0.32em 1.265em 7px rgba(0, 255, 81, 0.9), 0.134em 1.5171em 7px rgba(255, 0, 63, 0.9), 2.4128em 1.7454em 7px rgba(255, 0, 76, 0.9), -0.0657em 1.3949em 7px rgba(187, 0, 255, 0.9);
animation-duration: 42s;
animation-delay: -23s;
}
.dream #story::after {
text-shadow: 0.2921em 1.4574em 7px rgba(255, 232, 0, 0.9), 2.3749em -0.06em 7px rgba(0, 255, 230, 0.9), 1.667em 0.6827em 7px rgba(0, 255, 16, 0.9), 0.541em 1.0844em 7px rgba(255, 140, 0, 0.9), 0.8912em 1.1734em 7px rgba(0, 255, 89, 0.9), -0.4043em 1.937em 7px rgba(226, 255, 0, 0.9), 1.3839em 0.8385em 7px rgba(255, 0, 129, 0.9), 0.7206em 1.36em 7px rgba(255, 0, 62, 0.9), 0.8656em 0.7114em 7px rgba(255, 0, 49, 0.9), 2.0217em 2.2937em 7px rgba(121, 0, 255, 0.9), 0.8651em 2.3366em 7px rgba(255, 83, 0, 0.9), 0.5552em 0.2328em 7px rgba(255, 0, 49, 0.9), 0.454em 0.9135em 7px rgba(44, 255, 0, 0.9), 2.4277em 0.4088em 7px rgba(0, 128, 255, 0.9), -0.4559em 1.1927em 7px rgba(77, 0, 255, 0.9), 1.9425em 0.0319em 7px rgba(0, 75, 255, 0.9), 2.0776em 2.3312em 7px rgba(0, 255, 185, 0.9), 0.3179em 2.2526em 7px rgba(164, 0, 255, 0.9), 1.8339em 1.527em 7px rgba(0, 255, 247, 0.9), 0.133em -0.4561em 7px rgba(255, 238, 0, 0.9), 1.5383em -0.0102em 7px rgba(0, 11, 255, 0.9), 1.3749em 2.163em 7px rgba(116, 255, 0, 0.9), 2.312em -0.1272em 7px rgba(78, 255, 0, 0.9), 2.3737em 1.1575em 7px rgba(255, 161, 0, 0.9), 1.3794em -0.1936em 7px rgba(255, 0, 178, 0.9), 0.9091em 2.099em 7px rgba(255, 0, 198, 0.9), 1.5319em 0.981em 7px rgba(255, 0, 243, 0.9), 1.1046em 1.8253em 7px rgba(255, 0, 35, 0.9), -0.1624em 0.7596em 7px rgba(0, 255, 177, 0.9), 1.5148em 1.5667em 7px rgba(99, 0, 255, 0.9), 1.2774em 2.2866em 7px rgba(0, 33, 255, 0.9), 1.0745em 1.5588em 7px rgba(156, 0, 255, 0.9), 1.3882em 2.0661em 7px rgba(255, 0, 252, 0.9), 0.2575em 1.3612em 7px rgba(0, 48, 255, 0.9), 1.5928em 1.0365em 7px rgba(0, 255, 151, 0.9), 0.0494em 0.7136em 7px rgba(196, 0, 255, 0.9), 1.8106em -0.3614em 7px rgba(199, 0, 255, 0.9), 1.469em -0.2581em 7px rgba(0, 255, 215, 0.9), 2.2018em -0.2591em 7px rgba(0, 255, 69, 0.9), -0.235em 0.7107em 7px rgba(11, 255, 0, 0.9), 0.4233em -0.0835em 7px rgba(50, 255, 0, 0.9);
animation-duration: 41s;
animation-delay: -19s;
}
@keyframes move {
from {
transform: rotate(0deg) scale(12) translateX(-20px);
}
to {
transform: rotate(360deg) scale(18) translateX(20px);
}
}
/* Dream Background - End */
}}}
As noted above, this is based on the code found in <a href="https://codepen.io/hylobates-lar/pen/bGEQXgm" target="_blank">this CodePen page by Alison Quaglia</a>.
If you'd like to affect the opacity of the background behind the text, simply change the value of the last character in the {{{background-color}}} value here:
{{{
.dream #story {
background-color: #111c;
}
}}}
That value can go from {{{0}}} to {{{f}}}, where {{{0}}} is totally transparent and {{{f}}} is totally opaque. Use the slider below to try out the various opacities.
<label>Opacity: </label><input id="opacity" type="range" min="0" max="15" step="1" value="12" oninput="$('#opval').html(parseInt(this.value).toString(16)); $('#story').css('background-color', 'rgba(17, 17, 17, ' + (parseInt(this.value) / 15))"> = #111<span id="opval">c</span>;
''Note:'' It's strongly recommend that you do //not// go with a totally transparent background color, since that may make the text difficult to read at times.<h1>Paper Doll Images</h1><div id="paperDoll">\
<img id="dollBody" @src="setup.ImagePath + 'Part_Body.png'">\
<img id="dollShirt" @src="setup.ImagePath + 'Part_Shirt.png'">\
<img id="dollLeggings" @src="setup.ImagePath + 'Part_Leggings.png'">\
</div>The image to the left is actually made up of a container, with a main body image and separate shirt and leggings images overlaid on top of the body image.
Hover your mouse over the shirt or leggings images to hide those images.
The HTML for the images to the left looks like this:
{{{
<div id="paperDoll">\
<img id="dollBody" @src="setup.ImagePath + 'Part_Body.png'">\
<img id="dollShirt" @src="setup.ImagePath + 'Part_Shirt.png'">\
<img id="dollLeggings" @src="setup.ImagePath + 'Part_Leggings.png'">\
</div>
}}}
(''Note:'' That HTML code uses the {{{setup.ImagePath}}} generated by the code given in the [[Displaying Images in Twine]] section.)
To figure out the exact positioning for your images, in your browser, right-click on the image you want to modify, choose "Inspect" (or your browser's equivalent), and then (under the "Elements" tab in the "Styles" section) modify the {{{top}}} and {{{left}}} positions (and possibly the {{{height}}}) after setting the {{{position}}} to "{{{absolute}}}". Use the {{{z-index}}} to determine how the images will be layered, where higher z-indexes will appear on top of lower z-indexes.
Note that the container for your images (like the {{{<div id="paperDoll"> ... </div>}}} above) will need to have its {{{position}}} set to "{{{relative}}}", so that its contents' "{{{absolute}}}" positions will be positioned relative to that container, instead of the whole web page.
I'd also recommend scaling your browser window up and down ({{{CTRL}}}+{{{Mouse Wheel}}} usually works for this), plus testing in a few different browsers, just to make sure that it displays the images properly in all cases, since the images may shift a bit in some cases.
Once you've found the best property values, simply copy any added/modified CSS properties and their values to your game's Stylesheet section.
The CSS for the images above looks like this:
{{{
#paperDoll {
position: relative;
float: left;
width: 160px;
height: 620px;
margin-right: 20px;
}
#dollBody {
position: absolute;
height: 610px;
z-index: 10;
}
#dollShirt {
position: absolute;
top: 117px;
left: 17px;
height: 122px;
z-index: 20;
}
#dollLeggings {
position: absolute;
top: 235px;
left: 22px;
height: 199px;
z-index: 15;
}
#dollShirt:hover, #dollLeggings:hover {
opacity: 0;
}
}}}
Note that the "{{{float: left;}}}" and "{{{margin-right: 20px;}}}" parts in the {{{#paperDoll}}} section of the above CSS are just there so the images will stay to the left of the text with a bit of a gap between the images and the text. If you aren't displaying it next to text, then you won't need those parts.
The {{{:hover}}} section at the end of that CSS is just there so that the shirt and leggings will be invisible if the mouse hovers over them, so you can remove that part as well.<h1>SlideWin Overlay</h1>Do you need to allow your players to bring up some information and then, once they're done there, return back to the passage you were at before, //without// re-triggering that passage's code? If so, then the SlideWin overlay could be your solution.
For example, try this button:
<<button '"Alerts and Dialogs" Overlay'>>
<<run slideWin("Alerts and Dialogs")>>
<</button>>
When that button is clicked, it displays the contents of the [[Alerts and Dialogs]] passage within a SlideWin overlay that's displayed over top of this passage.
The code for that is simply:
{{{
<<button '"Alerts and Dialogs" Overlay'>>
<<run slideWin("Alerts and Dialogs")>>
<</button>>
}}}
A SlideWin overlay can be hidden by either clicking the "X" button in the upper-right of the overlay, hitting the {{{ESC}}} key, or by calling:
{{{
<<run slideWin("hide")>>
}}}
(''Note:'' Due to that, you can't open a passage named "hide" with the ''slideWin()'' function if the overlay is already open.)
In the case where you open a SlideWin overlay while one is already open, the current overlay will hide itself, and then the new overlay will appear. To see an example of this, try clicking the buttons within the overlay brought up by this:
<<button '"SlideWin Overlay" Overlay'>>
<<run slideWin("SlideWin Overlay")>>
<</button>>
If you need to know what passage is currently being displayed in the overlay, you can check the value of ''setup.slideWinPassage'' to get the passage name. It will be set to {{{undefined}}} if the SlideWin overlay is hidden.
Additionally, the "body" HTML element will have the "''slideWin''" class added to it while the overlay is fully visible. This is used to hide the main scrollbar while this overlay is up, but it also means that you can use the "''body.slideWin''" selector in your CSS if you'd like to do any additional styling while the overlay is fully displayed.
In the rare event where you create any event handlers in the passage shown by ''slideWin()'' which will need to be removed once the overlay is hidden, then you will need to add information about those event handlers to the ''setup.slidewinHandler'' array, so that they can be automatically removed when the slideWin overlay is hidden. For example:
{{{
<<set setup.slidewinHandler.push({ selector: "body", event: "keyup", function: setup.keyhandler })>>
}}}
Now, when the SlideWin overlay is hidden, that will tell ''slideWin()'' to automatically dispose of that "keyup" event on the "body" element which triggers the ''setup.keyhandler()'' function.
To add SlideWin overlays to your own project, add this to your JavaScript section:
{{{
/* SlideWin v1.0.1 - Start */
// Add the SlideWin window to the page.
let el = document.createElement("div");
el.id = "slideWin";
el.setAttribute("role", "main");
el.setAttribute("aria-labelledby", "slideTitle");
document.body.appendChild(el);
$(el).css({ transform: "translateX(-101vw)", "stroke-width": "101px" });
// Allow the ESC key to hide the SlideWin window.
$(document).on("keyup", function (event) {
if (($("#slideWin").css("stroke-width") !== "101px") && ((event.key === "Escape") || (event.key === "Esc"))) {
window.slideWin("hide");
return false;
}
});
// The slideWin() function.
setup.slideWinHandler = [];
window.slideWin = function (dir) {
if ($("#slideWin").css("stroke-width") !== "101px") { // Hide slide window.
setup.slideWinPassage = undefined;
$("#slideWin").attr("tabindex", null);
$("#slideWin").animate(
{ "stroke-width": "101px" }, // Hack to get animation to work.
{ step: function (now, fx) { $(this).css("transform", "translateX(" + (-now) + "vw)"); },
complete: function () {
$("#slideWin").empty();
if (dir !== "hide") {
window.slideWin(dir);
}
},
duration: 500
},
"swing"
);
let handler; // Remove event handlers.
while (setup.slideWinHandler.length) {
handler = setup.slideWinHandler.shift();
$(handler.selector).off(handler.event, "#slideWin", handler.function);
}
$("body").removeClass("slideWin");
} else { // Show slide window.
setup.slideWinPassage = dir;
$("#slideWin").empty().attr("tabindex", 0).wiki('<button class="ur" onclick="slideWin(\'hide\')" tabindex="0" aria-label="Close Save/Load Window">X</button><div id="slideWinContent" tabindex="0"><<include "' + dir + '">></div>');
$("#slideWin p").each(function () { // Remove any empty <p> elements.
if ($(this).text().trim() === "") {
$(this).remove();
} else { // Strip <p> elements from around their contents.
$(this).children().unwrap();
}
});
$("#slideWin").delay(1).animate( // Delay prevents animation stutter.
{ "stroke-width": "0" }, // Hack to get animation to work.
{ step: function (now, fx) { $(this).css("transform", "translateX(" + (-now) + "vw)"); },
complete: function () { $("body").addClass("slideWin"); },
duration: 500
},
"swing"
).focus();
}
};
/* SlideWin - End */
}}}
''Note:'' The above JavaScript uses the "''stroke-width''" CSS attribute to make the animation work. If you need that attribute to remain unchanged for some reason, then you'll have to find another CSS attribute you can use instead.
And you'll also need to add this CSS to your Stylesheet section:
{{{
/* SlideWin styling v1.0.1 */
body.slideWin {
overflow: hidden;
}
#slideWin {
position: fixed;
top: 0;
-webkit-transform: translateX(-101vw);
-moz-transform: translateX(-101vw);
-ms-transform: translateX(-101vw);
-o-transform: translateX(-101vw);
transform: translateX(-101vw);
stroke-width: 101px; /* Used for animation trick on the transform property. */
width: 80.1vw;
height: calc(100vh - 30px);
padding: 30px 10vw;
overflow: hidden auto;
background-color: rgba(17, 17, 17, 0.97);
line-height: 1.75;
z-index: 100;
}
#slideWin:focus {
outline: none;
}
#slideWinContent {
position: relative;
top: -46px;
padding: 24px;
background-color: #222;
}
.ur { /* Upper-right "close" button */
position: sticky;
top: 10px;
left: calc(80.1vw - 56px);
width: 26px;
height: 26px;
margin: 10px;
color: black;
cursor: pointer;
line-height: 14px;
-webkit-box-shadow: 1px 3px 0px 0px grey;
-moz-box-shadow: 1px 3px 0px 0px grey;
box-shadow: 1px 3px 0px 0px grey;
border: 1px solid #5F5F5F;
border-collapse: separate;
font-weight: bold;
background-color: #FFFFFF;
-webkit-border-radius: 13px;
-moz-border-radius: 13px;
border-radius: 13px;
z-index: 101;
}
.ur:hover {
background-color: #FFFFFF;
border-color: #5F5F5F;
}
.ur:focus, .ur:hover {
outline: transparent;
-webkit-animation: pulsing-glow-white 1s linear infinite alternate;
-moz-animation: pulsing-glow-white 1s linear infinite alternate;
-o-animation: pulsing-glow-white 1s linear infinite alternate;
animation: pulsing-glow-white 1s linear infinite alternate;
}
.ur:active {
top: 2px;
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
}
@-webkit-keyframes pulsing-glow-white {
0% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
}
100% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
}
}
@-moz-keyframes pulsing-glow-white {
0% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
-moz-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
}
100% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
-moz-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
}
}
@-o-keyframes pulsing-glow-white {
0% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
}
100% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
}
}
@keyframes pulsing-glow-white {
0% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
-moz-box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
box-shadow: 1px 3px 0px 0px grey, 0 2px 20px rgb(255, 255, 255), inset 0 2px 10px rgb(255, 255, 255);
}
100% {
-webkit-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
-moz-box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
box-shadow: 1px 3px 0px 0px grey, 0 2px 5px rgba(255, 255, 255, 0), inset 0 2px 5px rgb(26, 3, 3);
}
}
/* SlideWin styling v1.0.1 - End */
}}}
You can modify the above CSS code if you want to change how the overlay, passage content, or close button look.
<h1>How to Use Sliders</h1>Sliders, also known as "range inputs", are a handy way to allow your users to visually see and change a value within a certain range. For example:
<label for="slidetest">Slider Test</label>
<input type="range" id="slidetest" name="slidetest" min="0" max="100"
value="25" class="slider" data-var="$test" oninput="SugarCubeInput(this)">
''Test value:'' <span id="slideval">$test</span>
<<button "Show $$test value">>
<<run alert("$test = " + $test)>>
<</button>><<done>>
<<run $("#slideval").empty().text($test)>> /* Display initial value. */
<<run $("#slidetest").on("input", function () { $("#slideval").empty().text($test); })>>
<</done>>
The code for the above is just this:
{{{
<label for="slidetest">Slider Test</label>
<input type="range" id="slidetest" name="slidetest" min="0" max="100"
value="25" class="slider" data-var="$test" oninput="SugarCubeInput(this)">
''Test value:'' <span id="slideval">$test</span>
<<button "Show $$test value">>
<<run alert("$test = " + $test)>>
<</button>><<done>>
<<run $("#slideval").empty().text($test)>> /* Display initial value. */
<<run $("#slidetest").on("input", function () { $("#slideval").empty().text($test); })>>
<</done>>
}}}
The first three lines create a label for the slider and the slider itself, including the range it covers (from {{{min}}} to {{{max}}} inclusive) and its default value ({{{value}}}). The {{{oninput}}} event triggers the {{{SugarCubeInput()}}} function (code for that given below) whenever the slider changes, making that function set the SugarCube variable named in the {{{data-var}}} attribute to the slider's value as it changes.
''Important Note:'' Due to the way the code works, the SugarCube variable named in the {{{data-var}}} attribute will //''not''// actually be set until //after// the passage is created, meaning its initial value will be {{{undefined}}} unless you'd set it yourself previously. If you need the variable to have an initial value, then you can do that by doing something like this:
{{{
<<set $test = 75>><label for="slidetest">Slider Test</label>
<input type="range" id="slidetest" name="slidetest" min="0" max="100"
@value="$test" class="slider" data-var="$test" oninput="SugarCubeInput(this)">
}}}
The {{{@value="$test"}}} part will make sure that the default value of the slider matches the value that ''$$test'' was set to previously. (See the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute-directive" target="_blank">Attribute Directive</a> for details on how that works.)
Additionally, the {{{<<done>>}}} section at the end just makes sure that the value displayed within the {{{<span id="slideval">}}} element is set initially and that it's also updated by the slider's changes as they happen. You won't need the first line in the {{{<<done>>}}} section if the variable is set prior to being displayed initially.
Also, by adding the {{{class="slider"}}} to your sliders like that, you can then style how your sliders look by using the {{{.slider}}} CSS selector in your Stylesheet section, instead of having to target the {{{input[type=range]}}} selector. For a page which helps you customize the style for your own sliders, see the "<a href="http://danielstern.ca/range.css/?ref=css-tricks#/" target="_blank">range.css - CSS Style Generator for Range Inputs</a>" page.
In addition to the above, you'll need this code in your game's JavaScript section:
{{{
/* Input handler - Start */
/* SugarCubeInput() function:
Updates the SugarCube variable named in the element's "data-var"
attribute to get set to the element's value via the element's
events, using the 'onEvent="SugarCubeInput(this)"' format, where
"onEvent" is replaced with "on" + event name (in lowercase).
Example:
<input type="range" min="0" max="100" value="50"
data-var="$test" oninput="SugarCubeInput(this)">
*/
window.SugarCubeInput = function (element) {
let varname = $(element).data("var");
if ((varname.indexOf("$") === 0) || (varname.indexOf("_") === 0)) {
State.setVar(varname, $(element).val());
}
};
/*
Makes sure that the variable is properly initialized to match the
initial value of the <input> element.
*/
$(document).on(":passagerender", function (event) {
$(event.content).find("input").each(function () {
let varname = $(this).data("var");
if (varname !== undefined) {
if ((varname.indexOf("$") === 0) || (varname.indexOf("_") === 0)) {
State.setVar(varname, $(this).val());
}
}
});
});
/* Input handler - End */
}}}
That code allows you to easily make {{{<input>}}} elements which update SugarCube variables via the variable name you put in the element's {{{data-var}}} attribute. You just use the element's on-event handlers (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers" target="_blank">a list of global on-event handlers here</a>) to trigger the {{{SugarCubeInput()}}} function like this: {{{oninput="SugarCubeInput(this)}}}
Note that, within an on-event handler like that one, "{{{this}}}" is a JavaScript object which refers to the current element (<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#in_an_inline_event_handler" target="_blank">see here</a>), so don't replace the word "{{{this}}}" with something else.
For more information on sliders, see the MDN documentation on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range" target="_blank">{{{<input type="range">}}} element</a>.
Enjoy!
<h1>Random Events</h1>Let's say that you want to have it so that, while you're traveling through the forest, there's a random chance of having a monster encounter. However, you don't want to have to put the same random event code into every forest passage, and you don't want to break history navigation by automatically redirecting to another passage for those events, so how would you code that?
Because you're working with SugarCube, you can use a <a href="https://www.motoslave.net/sugarcube/2/docs/#config-api-property-navigation-override" target="_blank">''Config.navigation.override'' function</a> to redirect passage navigation for these kinds of random events. Below I'll give you an example of how you can use this function for random events.
To keep things simple for this example, let's say you have "normal" passages, "travel" passages, and "event" passages. When going to "normal" or "event" passages, there's no chance of random events, but for "travel" passages there's a 30% chance of ending up at an "event" passage instead, unless you're already coming from an "event" passage, in which case you'll always make it to the "travel" passage. That will prevent triggering multiple events in a row. To be able to recognize all of the "travel" and "event" passages, they'll all need to be marked with either a "''travel''" or "''event''" tag, respectively. (I'd also recommend picking colors for those tags, to make those passages easier to spot.) Any passages without either of those tags will be a "normal" passage.
Now that we have that structure figured out, you can implement that by putting this into your JavaScript section:
{{{
// This function is triggered just before each passage navigation event.
Config.navigation.override = function (dest) {
if (tags(dest).includes("travel") && !tags().includes("event")) {
// If the destination is a "travel" tagged passage and
// the current passage is NOT an "event" tagged passage, then...
if (random(1, 10) <= 3) { // ...there's a 30% chance of:
// Storing the original destination in the $continue variable.
State.variables.continue = dest;
// Changing to a random "event" tagged passage.
dest = Story.lookup("tags", "event").random().title;
} // ...otherwise continue to the original destination passage.
} else { // For non-"travel" tagged destination passages:
// Clear the value in $continue since it shouldn't be needed anymore.
delete State.variables.continue;
}
// Go to the passage named in the "dest" variable.
return dest;
};
}}}
Note that the code which looks at the tags is case sensitive, so tagging a passage with "''Travel''" instead of "''travel''" will not work, due to the capital "''T''" not matching.
That code uses the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#functions-function-tags" target="_blank">''tags()'' function</a> to get an array of tags in a passage, the <a href="https://www.motoslave.net/sugarcube/2/docs/#methods-array-prototype-method-includes" target="_blank">''.includes()'' method</a> to determine if a value is in an array, the <a href="https://www.motoslave.net/sugarcube/2/docs/#functions-function-random" target="_blank">''random()'' function</a> to get a random number, the <a href="https://www.motoslave.net/sugarcube/2/docs/#state-api-getter-variables" target="_blank">''State.variables'' object</a> to access SugarCube story variables (since you can't directly use SugarCube story or temporary variable names within JavaScript code), the <a href="https://www.motoslave.net/sugarcube/2/docs/#story-api-method-lookup" target="_blank">''Story.lookup()'' method</a> to get an array of passage objects with a particular tag, the <a href="https://www.motoslave.net/sugarcube/2/docs/#methods-array-prototype-method-random" target="_blank">''.random()'' method</a> to get a random element from an array, and the <a href="https://www.motoslave.net/sugarcube/2/docs/#passage-api-prototype-getter-title" target="_blank">''.title'' property</a> to get the title of a passage from a passage object. It also uses the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete" target="_blank">''delete'' operator</a> to remove the {{{$continue}}} story variable from the ''State.variables'' object (it's the JavaScript equivalent of the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#macros-macro-unset" target="_blank">{{{<<unset>>}}} macro</a>).
Once you've added that to your code and added "''travel''" and "''event''" tags to the appropriate passages, it will then automatically have a 30% chance of redirecting any movement to a "''travel''" tagged passage to an "''event''" tagged passage, unless you were just in an "''event''" tagged passage.
If you want to have a link in your "event" passages which sends you back to the original "travel" passage that you were heading to, you can use the {{{$continue}}} variable there to know which passage to go to. Thus, the code you can use to exit your "event" passages may look something like this:
{{{
<<link "Continue" $continue>><</link>>
}}}
That will display a link that says "Continue", and clicking on it will send you to the original destination "travel" passage that you were attempting to go to when you were interrupted with the event.
''Note:'' As the code is currently, the {{{$continue}}} variable will be deleted once you exit any "event" passage. This is to prevent unnecessary "bloat" in the game history, which can happen if you store a bunch of unneeded story variables, since that extra data will slow down saves, loads, and passage transitions. If your random events may span multiple passages, then you'll want to change that code so that {{{$continue}}} isn't automatically deleted. (Also in that case, only the starting "event" passage should have an "''event''" tag.)
Now, to be clear, there is no reason why you would have to do things exactly as I've shown above. That's merely a simple example of one way that it could be done. There are millions of different ways you could do that, including having different odds, having different events possible depending on things such as player level or destination passage, etc... It's entirely up to your imagination and coding skills. Feel free to modify it into whatever you want.
Hopefully, though, this should give you a basic foundation for setting up random events which can trigger in your game when you want them to.
Enjoy!<h1>Loading and Saving Data Files</h1>Want to have the ability to load data from your "Part 1" game into your "Part 2" game? Want to give people the ability to create mod files for your game? Want to add a "cheat" file for your game? Or just want some code to help show you how to load or save files? Well, then the code here can help.
This code shows you how to use the <a href="http://purl.eligrey.com/github/FileSaver.js" target="_blank">FileSaver.js</a> code and how to save and load data in a way that can be loaded automatically or by using a file dialog box. It uses a ".lsm" (Load-Save Module) file, which is a JavaScript file which includes <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON" target="_blank">JSON</a> (JavaScript Object Notation) data.
You can test it out by saving and loading the below data:
<<textboxPlus "Name:" "$name" `{ default: $name }`>>
<<textboxPlus "Text:" "$text" `{ default: $text }`>>
Favorite pie:
* <label><<radiobutton "$pie" "apple" autocheck>> Apple</label>
* <label><<radiobutton "$pie" "blueberry" autocheck>> Blueberry</label>
* <label><<radiobutton "$pie" "cherry" autocheck>> Cherry</label>
<<button "Save Data">>
<<set _data = { name: $name, text: $text, pie: $pie }>>
<<run setup.saveLSMData(_data, "Save-Load.lsm")>>
<</button>> <<button "Save Data from State.variables">>
<<run setup.saveLSMData(State.variables, "Save-Load.lsm")>>
<</button>>
<<button "Load Data">>
<<script>>
function handler (data, fname, success) {
if (success) {
State.variables.name = data.name;
State.variables.text = data.text;
State.variables.pie = data.pie;
Engine.play(passage());
} else {
// error
}
}
setup.loadLSMDialog(handler);
<</script>>
<</button>> <<button "Load Data using loadHandler">>
<<script>>
setup.loadTrigger = function (success, data, fname) {
delete setup.loadTrigger;
if (success) {
Engine.play(passage());
}
};
setup.loadLSMDialog(setup.loadHandler);
<</script>>
<</button>>
<<button "Import Data from Save-Load.lsm">>
<<run setup.importLSMData("Save-Load.lsm", passage())>>
<</button>> (''Note:'' The file must be in the same directory as this HTML file.)
<<button "Clear Data">>
<<set $name = $text = $pie = "">>
<<goto `passage()`>>
<</button>>
[[Loading and Saving Data Files]] (click to reload this passage)
The code for the above is as follows:
{{{
<<textboxPlus "Name:" "$name" `{ default: $name }`>>
<<textboxPlus "Text:" "$text" `{ default: $text }`>>
Favorite pie:
* <label><<radiobutton "$pie" "apple" autocheck>> Apple</label>
* <label><<radiobutton "$pie" "blueberry" autocheck>> Blueberry</label>
* <label><<radiobutton "$pie" "cherry" autocheck>> Cherry</label>
<<button "Save Data">>
<<set _data = { name: $name, text: $text, pie: $pie }>>
<<run setup.saveLSMData(_data, "Save-Load.lsm")>>
<</button>> <<button "Save Data from State.variables">>
<<run setup.saveLSMData(State.variables, "Save-Load.lsm")>>
<</button>>
<<button "Load Data">>
<<script>>
function handler (data, fname, success) {
if (success) {
State.variables.name = data.name;
State.variables.text = data.text;
State.variables.pie = data.pie;
Engine.play(passage());
} else {
// error
}
}
setup.loadLSMDialog(handler);
<</script>>
<</button>> <<button "Load Data using loadHandler">>
<<script>>
setup.loadTrigger = function (success, data, fname) {
delete setup.loadTrigger;
if (success) {
Engine.play(passage());
}
};
setup.loadLSMDialog(setup.loadHandler);
<</script>>
<</button>>
<<button "Import Data from Save-Load.lsm">>
<<run setup.importLSMData("Save-Load.lsm", passage())>>
<</button>> (''Note:'' The file must be in the same directory as this HTML file.)
<<button "Clear Data">>
<<set $name = $text = $pie = "">>
<<goto `passage()`>>
<</button>>
}}}
The "''Save Data''" button code shows how to just save some specific data, while the "''Save Data from State.variables''" button shows how to save //all// story variables.
The "''Load Data''" button code shows how to load some specific data, while the "''Load Data using loadHandler''" button shows how to load all of the saved data into story variables. Both of these methods require that the user use a file dialog box to pick the file to load.
Finally, the "''Import Data from Save-Load.lsm''" button code shows how you can automatically load a file, without needing the user to pick the file using a file dialog box.
----
Here's an explanation of the functions the below code adds to your game:
<a onclick="$.wiki('<<ScrollTo "saveLSMData">>')">''setup.saveLSMData(data, fname)''</a>
<a onclick="$.wiki('<<ScrollTo "loadLSMDialog">>')">''setup.loadLSMDialog(handler, type)''</a>
<a onclick="$.wiki('<<ScrollTo "loadHandler">>')">''setup.loadHandler(data, fname, success)''</a>
<a onclick="$.wiki('<<ScrollTo "importLSMData">>')">''setup.importLSMData(fname, passage)''</a>
Go to the <a href="http://purl.eligrey.com/github/FileSaver.js" target="_blank">''FileSaver.js'' GitHub page</a> for an explanation of its functions.
----
<h2 id="saveLSMData">setup.saveLSMData(data, fname)</h2>Saves the data on the ''data'' object using the filename passed as the ''fname'' parameter. The file will be saved to the browser's default save directory.
----
<h2 id="loadLSMDialog">setup.loadLSMDialog(handler, type)</h2>Opens a file dialog box that only lists files of the type passed in the optional ''type'' property (which defaults to ".lsm"). It will then call the function passed as the ''handler'' parameter. See the <a onclick="$.wiki('<<ScrollTo "loadHandler">>')">''setup.loadHandler()''</a> description below as an example of how such a handler function is set up. If the file fails to load then an error message will be displayed.
----
<h2 id="loadHandler">setup.loadHandler(data, fname, success)</h2>This is an example handler function for use with the <a onclick="$.wiki('<<ScrollTo "loadLSMDialog">>')">''setup.loadLSMDialog()''</a> function. It receives the ''data'' object, which is the data from the file. The ''file'' parameter will be the filename of the file that was loaded. And ''success'' will be {{{true}}} if the file was successfully loaded, otherwise it will be {{{false}}} and the value of ''data'' will be the error message.
Once ''loadHandler'' has finished loading the data, it will attempt to call ''setup.loadTrigger(success, data, fname)'' as a function, if it's set up as a function.
----
<h2 id="importLSMData">setup.importLSMData(fname, passage)</h2>This imports the file with the filename passed using the ''fname'' parameter, and then goes to the passage named by the ''passage'' parameter.
----
To make the above code work, add the following to your game's JavaScript section:
{{{
/* Load/Save handling code - Start */
/* saveLSMData:
Saves the game data object as a JSON string wrapped inside of a
JavaScript function.
*/
setup.saveLSMData = function (data, fname) {
let blob = new Blob([
"window.getLSMData = function () { return " +
JSON.stringify(data) + " };"], { type: "text/plain;charset=utf-8" });
saveAs(blob, fname);
};
/* loadHandler:
Automatically parse out data to SugarCube story variables and
handle errors. Runs the setup.loadTrigger function upon
completion if its set to a function.
*/
setup.loadHandler = function (data, fname, success) {
if (success) {
let key = Object.keys(data), i;
for (i = 0; i < key.length; i++) {
State.variables[key[i]] = data[key[i]];
}
} else {
// Error
}
if (setup.loadTrigger && (typeof setup.loadTrigger === "function")) {
setup.loadTrigger(success, data, fname);
}
};
/* loadLSMDialog:
Loads the JavaScript picked from the "File Load" dialog box as
text, strips off the JavaScript, parses the JSON content, and
then passes it to the handler function. The optinal "type"
parameter should be a string file extension; defaults to ".lsm".
*.lsm = Load/Save Module data
*/
setup.loadLSMDialog = function (handler, type) {
function loadTrigger (event) {
let file = event.target.files[0], reader = new FileReader();
$(reader).on("load", function (ev) {
let target = ev.currentTarget;
if (!target.result) {
return;
}
try {
if (target.result.indexOf('window.getLSMData = function () { return ') === 0) {
handler(JSON.parse(target.result.slice(40, -3)), event.target.files[0], true);
} else {
alert("Error: Invalid file.");
handler("Error: Invalid file.", event.target.files[0], false);
}
} catch (ex) {
alert("Error: Unable to parse input.");
console.log("Unable to parse input. Error:");
console.log(ex);
handler("Error: Unable to parse input.", event.target.files[0], false);
}
});
// Initiate the file load.
reader.readAsText(file);
}
if (type === undefined) {
type = ".lsm";
}
if ($("#hidFileInputEl").length) {
$("#hidFileInputEl").off();
$("#hidFileInputEl").val("");
$("#hidFileInputEl").on("change", loadTrigger);
} else {
$(document.body).append($(document.createElement("input")).prop({ id: "hidFileInputEl", type: "file", accept: type }).css("display", "none").on("change", loadTrigger));
}
$("#hidFileInputEl").trigger("click");
};
/* importLSMData:
Import a .lsm file and then go to passage.
*/
setup.importLSMData = function (fname, passage) {
let lockID = LoadScreen.lock(); // Lock loading screen
importScripts(fname).then(function() {
let data = getLSMData(), key = Object.keys(data), i;
for (i = 0; i < key.length; i++) {
State.variables[key[i]] = data[key[i]];
}
Engine.play(passage);
LoadScreen.unlock(lockID); // Unlock loading screen
}).catch(function(error) {
console.log("Error: Could not find file '" + fname + "'.");
console.log(error);
alert("Error: Could not find file '" + fname + "'.");
LoadScreen.unlock(lockID); // Unlock loading screen
});
};
/*
* FileSaver.js
* A saveAs() FileSaver implementation.
*
* By Eli Grey, http://eligrey.com
* (minor code cleanup by HiEv - 12/30/2025)
*
* Ver/Date : Nov 19, 2020
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
* Source : http://purl.eligrey.com/github/FileSaver.js
*/
// The one and only way of getting global scope in all environments
// https://stackoverflow.com/q/3277182/1008999
var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : this;
function bom (blob, opts) {
if (typeof opts === 'undefined') {
opts = { autoBom: false };
} else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object');
opts = { autoBom: !opts };
}
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type });
}
return blob;
}
function download (url, name, opts) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.onload = function () {
saveAs(xhr.response, name, opts);
};
xhr.onerror = function () {
console.error('could not download file');
};
xhr.send();
}
function corsEnabled (url) {
let xhr = new XMLHttpRequest();
// use sync to avoid popup blocker
xhr.open('HEAD', url, false);
try {
xhr.send();
} catch (e) {
// noop
}
return xhr.status >= 200 && xhr.status <= 299;
}
// `a.click()` doesn't work for all browsers (#465)
function click (node) {
try {
node.dispatchEvent(new MouseEvent('click'));
} catch (e) {
let evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
node.dispatchEvent(evt);
}
}
// Detect WebView inside a native macOS app by ruling out all browsers
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent);
var saveAs = _global.saveAs || (
// Probably in some web worker
(typeof window !== 'object' || window !== _global) ? function saveAs () { /* noop */ }
// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
: ('download' in HTMLAnchorElement.prototype && !isMacOSWebView) ? function saveAs (blob, name, opts) {
let URL = _global.URL || _global.webkitURL;
let a = document.createElement('a');
name = name || blob.name || 'download';
a.download = name;
a.rel = 'noopener'; // tabnabbing
// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'
if (typeof blob === 'string') {
// Support regular links
a.href = blob;
if (a.origin !== location.origin) {
if (corsEnabled(a.href)) {
download(blob, name, opts);
} else {
a.target = '_blank';
click(a);
}
} else {
click(a);
}
} else {
// Support blobs
a.href = URL.createObjectURL(blob);
setTimeout(function () { URL.revokeObjectURL(a.href); }, 4E4); // 40s
setTimeout(function () { click(a); }, 0);
}
}
// Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator ? function saveAs (blob, name, opts) {
name = name || blob.name || 'download';
if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts);
} else {
let a = document.createElement('a');
a.href = blob;
a.target = '_blank';
setTimeout(function () { click(a); });
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name);
}
}
// Fallback to using FileReader and a popup
: function saveAs (blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank');
if (popup) {
popup.document.title = popup.document.body.innerText = 'downloading...';
}
if (typeof blob === 'string') return download(blob, name, opts);
let force = blob.type === 'application/octet-stream';
let isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
let isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari doesn't allow downloading of blob URLs
let reader = new FileReader();
reader.onloadend = function () {
let url = reader.result;
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
if (popup) popup.location.href = url;
else location = url;
popup = null; // reverse-tabnabbing #460
};
reader.readAsDataURL(blob);
} else {
let URL = _global.URL || _global.webkitURL;
let url = URL.createObjectURL(blob);
if (popup) popup.location = url;
else location.href = url;
popup = null; // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url); }, 4E4); // 40s
}
}
);
_global.saveAs = saveAs.saveAs = saveAs;
if (typeof module !== 'undefined') {
module.exports = saveAs;
}
/* FileSaver.js - End */
/* Load/Save handling code - End */
}}}
''Note:'' You can modify the code from the ''setup.saveLSMData'' function if you want to output data as something other than an LSM file. In fact, all of the sample code can be modified as needed if you want to work with files. (But, you probably already knew that.)
Enjoy!<h1>SugarCube Icons</h1>''NOTE:'' SugarCube v2.37.0 rewrote the icon structure and some icons were added, removed, and changed. The code below will not work with earlier versions of SugarCube.
SugarCube includes some built-in versions of some of the <a href="https://fontawesome.com/" target="_blank">Font Awesome</a> icons. Here's how you can easily use them in your game.
First, add the following widget to a non-story, non-special passage that has a "{{{widget}}}" tag (click the "🏷 Tags" button at the top of the passage to add tags):
{{{
<<widget "scicon">><span @class="'sc-icon sc-' + _args.raw" alt="' + _args.raw + '"></span><</widget>>
}}}
Next, select the icons you want to use below by clicking on them. Use the "Search" bar to help you find the icon you're looking for.
<<textboxPlus "''SugarCube icons:''" "_searchText" `{ default: "", placeholder: "Enter your search terms here", autofocus: true, oninput: "<<updateIcons>>" }`>> <<button "Clear">>
<<run $("#textbox--searchtext").val("").trigger("input")>>
<</button>>
<div id="unselectedicons" style="max-width: 100%"></div>''Detailed View:'' <span id="icondetail"></span>
<<if ndef $selectedIcons>><<set $selectedIcons = []>><</if>>''Selected icons:'' <<button "Add All Icons">>
<<set $selectedIcons = []>>
<<for _key, _val range _icons>>
<<set $selectedIcons.push(_key)>>
<</for>>
<<updateIcons>>
<</button>> <<button "Remove All Icons">>
<<set $selectedIcons = []>>
<<updateIcons>>
<</button>>
<div id="selectedicons" style="max-width: 100%"></div>
After you've selected all of the icons you plan on using, copy the following CSS code into your game's Stylesheet section:
<pre style="line-height: 22px"><code id="csscode">
</code></pre>
Once you've done that, you can then use those icons in a passage by doing something like this:
{{{
Rated <<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star-half-outlined>>
}}}
to get:
Rated <<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star-half-outlined>>
Alternately, to use with HTML, just do something like this:
{{{
Day <span class="sc-icon sc-sun"></span> / Night <span class="sc-icon sc-moon"></span>
}}}
to get:
Day <span class="sc-icon sc-sun"></span> / Night <span class="sc-icon sc-moon"></span>
Enjoy!
<<silently>>
<<set _icons = {}>>
<<set _icons["arrow-left"] = { id: "060", desc: ["arrow", "left"]}>>
<<set _icons["arrow-right"] = { id: "061", desc: ["arrow", "right"]}>>
<<set _icons["arrow-up"] = { id: "062", desc: ["arrow", "up"]}>>
<<set _icons["arrow-down"] = { id: "063", desc: ["arrow", "down"]}>>
<<set _icons["chevron-left"] = { id: "053", desc: ["chevron", "left", "angle", "arrow"]}>>
<<set _icons["chevron-right"] = { id: "054", desc: ["chevron", "right", "angle", "arrow"]}>>
<<set _icons["chevron-up"] = { id: "077", desc: ["chevron", "up", "angle", "arrow"]}>>
<<set _icons["chevron-down"] = { id: "078", desc: ["chevron", "down", "angle", "arrow"]}>>
<<set _icons["circle-chevron-left"] = { id: "137", desc: ["circle", "chevron", "left", "angle", "arrow"]}>>
<<set _icons["circle-chevron-right"] = { id: "138", desc: ["circle", "chevron", "right", "angle", "arrow"]}>>
<<set _icons["circle-chevron-up"] = { id: "139", desc: ["circle", "chevron", "up", "angle", "arrow"]}>>
<<set _icons["circle-chevron-down"] = { id: "13a", desc: ["circle", "chevron", "down", "angle", "arrow"]}>>
<<set _icons["caret-left"] = { id: "0d9", desc: ["caret", "left", "triangle", "arrow"]}>>
<<set _icons["caret-right"] = { id: "0da", desc: ["caret", "right", "triangle", "arrow"]}>>
<<set _icons["caret-up"] = { id: "0d8", desc: ["caret", "up", "triangle", "arrow"]}>>
<<set _icons["caret-down"] = { id: "0d7", desc: ["caret", "down", "triangle", "arrow"]}>>
<<set _icons["square-caret-left"] = { id: "191", desc: ["square", "caret", "left", "triangle", "arrow"]}>>
<<set _icons["square-caret-right"] = { id: "152", desc: ["square", "caret", "right", "triangle", "arrow"]}>>
<<set _icons["square-caret-up"] = { id: "151", desc: ["square", "caret", "up", "triangle", "arrow"]}>>
<<set _icons["square-caret-down"] = { id: "150", desc: ["square", "caret", "down", "triangle", "arrow"]}>>
<<set _icons["skip-backward"] = { id: "049", desc: ["skip", "backward", "start", "left", "arrow"]}>>
<<set _icons["step-backward"] = { id: "048", desc: ["step", "backward", "start", "left", "arrow"]}>>
<<set _icons["backward"] = { id: "04a", desc: ["backward", "fast", "rewind", "left", "arrow"]}>>
<<set _icons["play"] = { id: "04b", desc: ["play", "triangle", "right", "arrow"]}>>
<<set _icons["pause"] = { id: "04c", desc: ["pause"]}>>
<<set _icons["stop"] = { id: "04d", desc: ["stop", "square"]}>>
<<set _icons["forward"] = { id: "04e", desc: ["forward", "fast", "right", "arrow"]}>>
<<set _icons["step-forward"] = { id: "051", desc: ["step", "forward", "end", "right", "arrow"]}>>
<<set _icons["skip-forward"] = { id: "050", desc: ["skip", "forward", "end", "right", "arrow"]}>>
<<set _icons["eject"] = { id: "052", desc: ["eject", "up", "arrow"]}>>
<<set _icons["circle-play"] = { id: "144", desc: ["circle", "play", "triangle", "right", "arrow"]}>>
<<set _icons["circle-pause"] = { id: "28b", desc: ["circle", "pause"]}>>
<<set _icons["circle-stop"] = { id: "28d", desc: ["circle", "stop", "square"]}>>
<<set _icons["battery-full"] = { id: "240", desc: ["battery", "power", "full"]}>>
<<set _icons["battery-high"] = { id: "241", desc: ["battery", "power", "high"]}>>
<<set _icons["battery-medium"] = { id: "242", desc: ["battery", "power", "medium"]}>>
<<set _icons["battery-low"] = { id: "242", desc: ["battery", "power", "low"]}>>
<<set _icons["battery-empty"] = { id: "243", desc: ["battery", "power", "empty"]}>>
<<set _icons["bolt"] = { id: "0e7", desc: ["bolt", "lightning", "electricity", "zap"]}>>
<<set _icons["bookmark-solid"] = { id: "02e", desc: ["bookmark"]}>>
<<set _icons["bug"] = { id: "188", desc: ["bug", "insect", "error"]}>>
<<set _icons["check"] = { id: "00c", desc: ["check", "checkmark", "true"]}>>
<<set _icons["times"] = { id: "00d", desc: ["times", "false", "x", "*"]}>>
<<set _icons["circle-check"] = { id: "058", desc: ["circle", "check", "checkmark", "true", "yes"]}>>
<<set _icons["circle-times"] = { id: "057", desc: ["circle", "times", "false", "x", "*", "no"]}>>
<<set _icons["square-check"] = { id: "14a", desc: ["square", "check", "checkmark", "true", "yes"]}>>
<<set _icons["square-times"] = { id: "410", desc: ["square", "times", "false", "x", "*", "no"]}>>
<<set _icons["backspace"] = { id: "55a", desc: ["backspace", "delete", "erase", "times", "x", "*"]}>>
<<set _icons["circle-question"] = { id: "059", desc: ["circle", "question", "mark", "?"]}>>
<<set _icons["circle-info"] = { id: "05a", desc: ["circle", "info", "i", "information"]}>>
<<set _icons["circle-exclamation"] = { id: "06a", desc: ["circle", "exclamation", "alert", "!"]}>>
<<set _icons["triangle-exclamation"] = { id: "071", desc: ["triangle", "exclamation", "alert", "warning", "!"]}>>
<<set _icons["cog"] = { id: "013", desc: ["cog", "gear", "settings"]}>>
<<set _icons["download"] = { id: "019", desc: ["download", "import", "arrow", "computer", "desktop", "harddrive", "hdd"]}>>
<<set _icons["upload"] = { id: "093", desc: ["upload", "export", "arrow", "computer", "desktop", "harddrive", "hdd"]}>>
<<set _icons["harddrive"] = { id: "0a0", desc: ["computer", "desktop", "harddrive", "hdd"]}>>
<<set _icons["save"] = { id: "0c7", desc: ["save", "disk", "load"]}>>
<<set _icons["file-download"] = { id: "56d", desc: ["file", "download", "import", "arrow"]}>>
<<set _icons["file-upload"] = { id: "574", desc: ["file", "upload", "export", "arrow"]}>>
<<set _icons["file-import"] = { id: "56f", desc: ["file", "download", "import", "arrow"]}>>
<<set _icons["file-export"] = { id: "56e", desc: ["file", "upload", "export", "arrow"]}>>
<<set _icons["cloud"] = { id: "0c2", desc: ["cloud"]}>>
<<set _icons["cloud-download"] = { id: "381", desc: ["cloud", "download"]}>>
<<set _icons["cloud-upload"] = { id: "382", desc: ["cloud", "upload"]}>>
<<set _icons["ellipsis"] = { id: "141", desc: ["ellipsis", "three", "dots", "meatball", "menu"]}>>
<<set _icons["ellipsis-vertical"] = { id: "142", desc: ["ellipsis", "three", "dots", "vertical", "kebab", "menu"]}>>
<<set _icons["fullscreen-enter"] = { id: "065", desc: ["enter", "fullscreen", "select", "box", "rectangle", "expand"]}>>
<<set _icons["fullscreen-exit"] = { id: "066", desc: ["exit", "fullscreen", "target", "crosshairs", "compress"]}>>
<<set _icons["expand"] = { id: "31e", desc: ["expand", "enter", "fullscreen"]}>>
<<set _icons["compress"] = { id: "78c", desc: ["compress", "exit", "fullscreen"]}>>
<<set _icons["heart"] = { id: "004", desc: ["heart", "love", "favorite"]}>>
<<set _icons["home"] = { id: "015", desc: ["home", "house"]}>>
<<set _icons["language"] = { id: "1ab", desc: ["language", "font", "a", "japanese", "toggle"]}>>
<<set _icons["lightbulb"] = { id: "0eb", desc: ["lightbulb", "idea", "information"]}>>
<<set _icons["link"] = { id: "0c1", desc: ["link", "url"]}>>
<<set _icons["no-link"] = { id: "127", desc: ["link", "url", "not"]}>>
<<set _icons["external-link"] = { id: "35d", desc: ["external", "link", "url"]}>>
<<set _icons["lock"] = { id: "023", desc: ["lock", "locked"]}>>
<<set _icons["lock-open"] = { id: "09c", desc: ["lock", "unlocked"]}>>
<<set _icons["lock-open-keyhole"] = { id: "13e", desc: ["lock", "unlocked", "keyhole"]}>>
<<set _icons["not"] = { id: "05e", desc: ["not", "negation", "no"]}>>
<<set _icons["magic"] = { id: "0d0", desc: ["magic", "wand"]}>>
<<set _icons["menu"] = { id: "0c9", desc: ["menu", "burger", "options"]}>>
<<set _icons["paragraph"] = { id: "1dd", desc: ["paragraph", "p"]}>>
<<set _icons["power"] = { id: "011", desc: ["power", "shutdown"]}>>
<<set _icons["redo"] = { id: "2f9", desc: ["redo", "arrow", "forward", "advance"]}>>
<<set _icons["undo"] = { id: "2ea", desc: ["undo", "arrow", "back", "rewind"]}>>
<<set _icons["reload"] = { id: "2f1", desc: ["sync", "arrow", "reload", "refresh", "update", "recycle"]}>>
<<set _icons["show"] = { id: "06e", desc: ["show", "eye", "look", "see", "visible"]}>>
<<set _icons["hide"] = { id: "070", desc: ["hide", "eye", "look", "see", "invisible", "hidden"]}>>
<<set _icons["quote-left"] = { id: "10d", desc: ["left", "start", "quote", "mark"]}>>
<<set _icons["quote-right"] = { id: "10d", desc: ["right", "end", "quote", "mark"]}>>
<<set _icons["spinner"] = { id: "110", desc: ["spinner", "wait"]}>>
<<set _icons["share"] = { id: "1e0", desc: ["share", "three", "dots"]}>>
<<set _icons["sun"] = { id: "185", desc: ["sun", "day", "light", "mode", "star"]}>>
<<set _icons["moon"] = { id: "186", desc: ["moon", "night", "dark", "mode"]}>>
<<set _icons["search"] = { id: "002", desc: ["search", "magnify", "magnifying", "glass", "zoom"]}>>
<<set _icons["search-plus"] = { id: "00e", desc: ["search", "plus", "zoom", "in"]}>>
<<set _icons["search-minus"] = { id: "010", desc: ["search", "minus", "zoom", "out"]}>>
<<set _icons["plus"] = { id: "067", desc: ["plus", "add", "+"]}>>
<<set _icons["minus"] = { id: "068", desc: ["minus", "subtract", "remove", "-"]}>>
<<set _icons["circle-plus"] = { id: "055", desc: ["circle", "plus", "add", "+"]}>>
<<set _icons["circle-minus"] = { id: "056", desc: ["circle", "minus", "subtract", "remove", "-"]}>>
<<set _icons["square-plus"] = { id: "0fe", desc: ["square", "plus", "add", "+"]}>>
<<set _icons["square-minus"] = { id: "146", desc: ["square", "minus", "subtract", "remove", "-"]}>>
<<set _icons["settings"] = { id: "7d9", desc: ["settings", "tools", "utilities", "wrench", "screwdriver"]}>>
<<set _icons["star-solid"] = { id: "005", desc: ["star", "*"]}>>
<<set _icons["star-half"] = { id: "089", desc: ["star", "half", "*"]}>>
<<set _icons["star-half-outlined"] = { id: "5c0", desc: ["star", "half", "*"]}>>
<<set _icons["thumbtack"] = { id: "08d", desc: ["thumbtack", "pin"]}>>
<<set _icons["toggle-off"] = { id: "204", desc: ["toggle", "off", "switch", "disabled", "empty"]}>>
<<set _icons["toggle-on"] = { id: "205", desc: ["toggle", "on", "switch", "enabled"]}>>
<<set _icons["trash-solid"] = { id: "1f8", desc: ["trash", "delete"]}>>
<<set _icons["trash-solid-restore"] = { id: "829", desc: ["trash", "undelete", "restore"]}>>
<<set _icons["trash-lined"] = { id: "2ed", desc: ["trash", "delete"]}>>
<<set _icons["trash-lined-restore"] = { id: "82a", desc: ["trash", "undelete", "restore"]}>>
<<set _icons["volume"] = { id: "026", desc: ["volume", "sound", "speaker", "audio"]}>>
<<set _icons["volume-off"] = { id: "6a9", desc: ["volume", "off", "sound", "speaker", "audio"]}>>
<<set _icons["volume-down"] = { id: "027", desc: ["volume", "down", "sound", "speaker", "audio"]}>>
<<set _icons["volume-up"] = { id: "028", desc: ["volume", "up", "sound", "speaker", "audio"]}>>
<<done>>
<<updateIcons>>
<</done>>
<</silently>><h3>Example elements:</h3>[[Test link (go back to 'Day and Night Mode Setting' section)|Day and Night Mode Setting]]
<<button "Get theme state">><<run alert(settings.Theme ? "Light mode" : "Dark mode")>><</button>>
<<button "Get toggle value">><<run alert(settings.Theme)>><</button>>
<<checkbox "$testcb" false true checked>> Test checkbox
<<radiobutton "$testrb" "Test radiobutton" checked>> Test radiobutton
<<numberbox "$testnb" 100>> Test numberbox
<<textbox "$testtb" "Test textbox">>
<<textarea "$testta" "Test textarea">>
Test {{{inline code}}} example.
{{{
Test code block
}}}
<<ErrMsg "Test error message" "Some details.">>
img link:
[img[setup.ImagePath+'Example.png']]
img element:
<img @src="setup.ImagePath+'Example.png'">
background image (uses the "noinvert" class to avoid changing):
<div @style="'background-image: url(\'' + setup.ImagePath + 'Example.png\'); width: 177px; height: 107px;'" class="noinvert"></div>
<h1>Day and Night Mode Setting</h1>Want to add a setting so people can toggle between day and night mode? Well, the code here will let you do that using a slightly tweaked version of the SugarCube "Bleached" stylesheet (<a href="https://www.motoslave.net/sugarcube/2/" target="_blank">available here</a>).
First, you'll first need to copy the "Bleached" CSS at the bottom of this section into your Stylesheet, preferably somewhere near the bottom. If you need additional CSS tweaks to make it work with your styling, then you should put that below the "Bleached" CSS.
Next, add this to your game's JavaScript section:
{{{
setup.settingTheme = function () {
if (settings.Theme) {
// Delay is required during initialization.
setTimeout(function () { $("html").addClass("bleached"); }, 20);
} else {
$("html").removeClass("bleached");
}
};
Setting.addToggle("Theme", {
label : "Day Mode:",
default: false,
onInit : setup.settingTheme,
onChange : setup.settingTheme
});
}}}
That will add a "Theme" toggle to your "Settings" dialog, which can be used to toggle between "day" and "night" modes. (See the "[[Settings]]" section for more info on how to create settings.)
You can also force a passage to use "day mode" by adding a "{{{bleached}}}" tag to the passage, such as this one:
[[Bleached Passage]] (This passage also shows how different elements look in "day mode".)
Or you can do it like this:
{{{
<<run $("html").addClass("bleached")>>
}}}
You can force "night mode" by doing:
{{{
<<run $("html").removeClass("bleached")>>
}}}
And you can set the theme to match the current settings by doing:
{{{
<<run setup.settingTheme()>>
}}}
If you want to make the "Settings" dialog a little bit fancier, then you can use the [[Setting Toggles Text Changer]] code plus the [[SugarCube Icons]] "sun" and "moon" icons along with this version of the {{{Setting}}} code instead:
{{{
Setting.addToggle("Theme", {
label : "Day/Night Mode:",
default: false,
onInit : setup.settingTheme,
onChange : setup.settingTheme
});
setup.settingmods.Theme = ['<span class="sc-icon sc-moon"> Night</span>', '<span class="sc-icon sc-sun"> Day</span>'];
}}}
That will show the custom icons as you see them here when you click the "Settings" button on the UI bar.
Here's the toggleable version of "Bleached" CSS code for your game's Stylesheet section:
{{{
/**************************************************************************
BLEACHED theme (toggleable) - A largely white style for SugarCube ≥v2.25.0
**************************************************************************/
html.bleached body, body.bleached {
color: #111;
background-color: #fff;
}
html.bleached body a, body.bleached a {
color: #35c;
}
html.bleached body a:hover, body.bleached a:hover {
color: #57e;
}
html.bleached body span.link-disabled, body.bleached span.link-disabled {
color: #777;
}
html.bleached body button, body.bleached button {
color: #111;
background-color: #acf;
border-color: #8ad;
}
html.bleached body button:hover, body.bleached button:hover {
background-color: #8ad;
border-color: #68b;
}
html.bleached body button:disabled, body.bleached button:disabled {
background-color: #ccc;
border-color: #aaa;
}
html.bleached body input, body.bleached input,
html.bleached body select, body.bleached select,
html.bleached body textarea, body.bleached textarea {
color: #111;
border-color: #ccc;
}
html.bleached body select, body.bleached select {
background-color: #ccc;
}
html.bleached body input:not(:disabled):focus, body.bleached input:not(:disabled):focus,
html.bleached body select:not(:disabled):focus, body.bleached select:not(:disabled):focus,
html.bleached body textarea:not(:disabled):focus, body.bleached textarea:not(:disabled):focus,
html.bleached body input:not(:disabled):hover, body.bleached input:not(:disabled):hover,
html.bleached body select:not(:disabled):hover, body.bleached select:not(:disabled):hover,
html.bleached body textarea:not(:disabled):hover, body.bleached textarea:not(:disabled):hover {
background-color: #ddd;
border-color: #111;
}
html.bleached body hr, body.bleached hr {
border-color: #111;
}
html.bleached body pre, body.bleached pre,
html.bleached body code, body.bleached code {
background-color: #d9d9d9;
}
html.bleached body pre.error-source, body.bleached pre.error-source,
html.bleached body .error-source code, body.bleached .error-source code {
background-color: transparent;
}
html.bleached body .error-view, body.bleached .error-view {
background-color: #eaa;
border-left-color: #d77;
}
html.bleached body .error-view > .error-source:not([hidden]), body.bleached .error-view > .error-source:not([hidden]) {
background-color: rgba(255, 255, 255, 0.2);
}
html.bleached body #ui-bar, body.bleached #ui-bar {
background-color: #eee;
border-color: #ccc;
}
html.bleached body #ui-bar hr, body.bleached #ui-bar hr {
border-color: #ccc;
}
html.bleached body #ui-bar-toggle, body.bleached #ui-bar-toggle,
html.bleached body #ui-bar-history [id|="history"], body.bleached #ui-bar-history [id|="history"] {
color: #111;
border-color: #ccc;
}
html.bleached body #ui-bar-toggle:hover, body.bleached #ui-bar-toggle:hover,
html.bleached body #ui-bar-history [id|="history"]:hover, body.bleached #ui-bar-history [id|="history"]:hover {
background-color: #ddd;
border-color: #111;
}
html.bleached body #ui-bar-history [id|="history"]:disabled, body.bleached #ui-bar-history [id|="history"]:disabled {
color: #ccc;
background-color: transparent;
border-color: #ccc;
}
html.bleached body #menu ul, body.bleached #menu ul {
border-color: #ccc;
}
html.bleached body #menu li:not(:first-child), body.bleached #menu li:not(:first-child) {
border-top-color: #ccc;
}
html.bleached body #menu li a, body.bleached #menu li a {
color: #111;
}
html.bleached body #menu li a:hover, body.bleached #menu li a:hover {
background-color: #ddd;
border-color: #111;
}
/* Default dialog styling */
html.bleached body #ui-overlay, body.bleached #ui-overlay {
background-color: #777;
}
html.bleached body #ui-dialog-titlebar, body.bleached #ui-dialog-titlebar {
background-color: #ccc;
}
html.bleached body #ui-dialog-close:hover, body.bleached #ui-dialog-close:hover {
background-color: #b44;
border-color: #a33;
}
html.bleached body #ui-dialog-body, body.bleached #ui-dialog-body {
background-color: #fff;
border-color: #ccc;
}
html.bleached body #ui-dialog-body hr, body.bleached #ui-dialog-body hr {
background-color: #ccc;
}
/* List-based dialog styling */
html.bleached body #ui-dialog-body.list li:not(:first-child), body.bleached #ui-dialog-body.list li:not(:first-child) {
border-top-color: #ccc;
}
html.bleached body #ui-dialog-body.list li a, body.bleached #ui-dialog-body.list li a {
color: #111;
}
html.bleached body #ui-dialog-body.list li a:hover, body.bleached #ui-dialog-body.list li a:hover {
background-color: #ddd;
border-color: #111;
}
/* Saves dialog styling */
html.bleached body #ui-dialog-body.saves > *:not(:first-child), body.bleached #ui-dialog-body.saves > *:not(:first-child),
html.bleached body #ui-dialog-body.saves tr:not(:first-child), body.bleached #ui-dialog-body.saves tr:not(:first-child) {
border-top-color: #ccc;
}
html.bleached body #ui-dialog-body.saves .empty, body.bleached #ui-dialog-body.saves .empty {
color: #777;
}
/* Settings dialog styling */
html.bleached body #ui-dialog-body.settings button[id|="setting-control"], body.bleached #ui-dialog-body.settings button[id|="setting-control"] {
color: #111;
border-color: #ccc;
}
html.bleached body #ui-dialog-body.settings button[id|="setting-control"]:hover, body.bleached #ui-dialog-body.settings button[id|="setting-control"]:hover {
background-color: #ddd;
border-color: #111;
}
html.bleached body #ui-dialog-body.settings button[id|="setting-control"].enabled, body.bleached #ui-dialog-body.settings button[id|="setting-control"].enabled {
background-color: #9e9;
border-color: #7c7;
}
html.bleached body #ui-dialog-body.settings button[id|="setting-control"].enabled:hover, body.bleached #ui-dialog-body.settings button[id|="setting-control"].enabled:hover {
background-color: #7c7;
border-color: #5a5;
}
/* Debug bar styling */
html.bleached body #debug-bar, body.bleached #debug-bar,
html.bleached body #debug-bar-toggle, body.bleached #debug-bar-toggle,
html.bleached body #debug-bar-watch, body.bleached #debug-bar-watch {
background-color: #eee;
border-color: #ccc;
}
html.bleached body #debug-bar-watch div, body.bleached #debug-bar-watch div {
color: #777;
}
html.bleached body #debug-bar-toggle, body.bleached #debug-bar-toggle,
html.bleached body #debug-bar-watch-toggle, body.bleached #debug-bar-watch-toggle,
html.bleached body #debug-bar-views-toggle, body.bleached #debug-bar-views-toggle {
color: #111;
border-color: #ccc;
}
html.bleached body #debug-bar-toggle:hover, body.bleached #debug-bar-toggle:hover,
html.bleached body #debug-bar-watch-toggle:hover, body.bleached #debug-bar-watch-toggle:hover,
html.bleached body #debug-bar-views-toggle:hover, body.bleached #debug-bar-views-toggle:hover {
background-color: #ddd;
border-color: #111;
}
html.bleached body #debug-bar-watch:not([hidden]) ~ div #debug-bar-watch-toggle, body.bleached #debug-bar-watch:not([hidden]) ~ div #debug-bar-watch-toggle,
html.bleached body html[data-debug-view] #debug-bar-views-toggle, body.bleached html[data-debug-view] #debug-bar-views-toggle {
background-color: #9e9;
border-color: #7c7;
}
html.bleached body #debug-bar-watch:not([hidden]) ~ div #debug-bar-watch-toggle:hover, body.bleached #debug-bar-watch:not([hidden]) ~ div #debug-bar-watch-toggle:hover,
html.bleached body html[data-debug-view] #debug-bar-views-toggle:hover, body.bleached html[data-debug-view] #debug-bar-views-toggle:hover {
background-color: #7c7;
border-color: #5a5;
}
/* Debug view styling */
html.bleached body html:not([data-debug-view]) #debug-view-toggle, body.bleached html:not([data-debug-view]) #debug-view-toggle {
color: #111;
border-color: #ccc;
}
html.bleached body html:not([data-debug-view]) #debug-view-toggle:hover, body.bleached html:not([data-debug-view]) #debug-view-toggle:hover {
background-color: #eee;
border-color: #111;
}
html.bleached body html[data-debug-view] #debug-view-toggle, body.bleached html[data-debug-view] #debug-view-toggle {
background-color: #9e9;
border-color: #7c7;
}
html.bleached body html[data-debug-view] #debug-view-toggle:hover, body.bleached html[data-debug-view] #debug-view-toggle:hover {
background-color: #7c7;
border-color: #5a5;
}
html.bleached body html[data-debug-view] .debug, body.bleached html[data-debug-view] .debug {
background-color: #dc9;
}
html.bleached body html[data-debug-view] .debug.hidden, body.bleached html[data-debug-view] .debug.hidden,
html.bleached body html[data-debug-view] .debug.hidden .debug, body.bleached html[data-debug-view] .debug.hidden .debug {
background-color: #bbb;
}
/* BLEACHED theme - End */
}}}
If you're using the [[Font Size Buttons]] and/or the [[Fullscreen Button|Fullscreen]] then you'll also want to add this CSS as well:
{{{
html.bleached body .fontSizeImg, body.bleached .fontSizeImg {
filter: invert(1);
}
html.bleached body .fullscreenImg, body.bleached .fullscreenImg {
filter: invert(1);
}
}}}<h1>Setting Toggles Text Changer</h1>By default, the values of toggles in the "Settings" dialog only show "On" or "Off". But what if you want them to say something else? The following code allows you to modify the text for the toggle's two values.
First, add the following code to your game's JavaScript section:
{{{
/* SugarCube "Setting" toggle text changer v1.2.1 - Start */
setup.settingmods = {};
/* For each setting you'd like to change the toggle text for, simply
add something like the below example code, but with "example"
replaced with the setting's name. The array should have two
entries, with the "false" text in the first entry and the "true"
text in the second entry. It will look something like this:
setup.settingmods["example"] = ["Disabled", "Enabled"];
*/
// SugarCube v2's "Util.slugify()" function.
function utilSlugify (str) {
let base = String(str).trim();
let _legacy = base.replace(/[^\w\s\u2013\u2014-]+/g, "").replace(/[_\s\u2013\u2014-]+/g, "-").toLocaleLowerCase();
if (!/^-*$/.test(_legacy)) {
return _legacy;
}
return base.replace(/[\x00-\x20!-/:-@[-^`{-\x9f]+/g, "").replace(/[_\s\u2013\u2014-]+/g, "-"); // eslint-disable-line no-control-regex
}
// The "easeInQuad" easing function; similar to the CSS "ease-in" transition.
$.easing.easeInQuad = function (p) {
return Math.pow(p, 2);
};
// Modify the "Setting" dialog's toggles based on setup.settingmods.
$(document).on(":dialogopened", function () {
let prop, btn;
for (prop in setup.settingmods) {
btn = $("#setting-control-" + utilSlugify(prop));
if (btn.length > 0) {
btn.off("click.settingmod").on("click.settingmod", { prop: prop }, function (evnt) {
// Update the button text when it's toggled.
let prp = evnt.data.prop;
if ($(this).hasClass("enabled")) {
$(this).html(setup.settingmods[prp][1]);
} else {
$(this).html(setup.settingmods[prp][0]);
}
});
// Initial button text setup.
if (btn.hasClass("enabled")) {
btn.html(setup.settingmods[prop][1]);
} else {
btn.html(setup.settingmods[prop][0]);
}
}
}
// Fix dialog fade-in (modifying the dialog breaks the fade-in).
$("#ui-dialog").fadeTo(200, 0.99999, "easeInQuad");
});
$(document).on(":dialogclosed", function () {
// Reset opacity for fade-in fix.
$("#ui-dialog").css("opacity", 0);
});
/* SugarCube "Setting" toggle text changer - End */
}}}
Below that code you can then add in your own {{{Setting}}} code, something like this:
{{{
Setting.addToggle("Setting name", {
label : "Test setting:",
default: true
});
setup.settingmods["Setting name"] = ["Disabled", "Enabled"];
}}}
The {{{setup.settingmods[]}}} part is what tells the code which setting to modify and how to modify it in the "Settings" dialog. The first string in the array is the text that will be used when the setting is set to {{{false}}}, and the second string is what's used when it's set to {{{true}}}. Note that you can include HTML within those two strings.
See the <a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api" target="_blank">Setting API</a> documentation for further details on how to set up your own SugarCube settings.<h1>{{{<<cycle>>}}} Link Identifier</h1>The {{{<<cycle>>}}} macro lets you have a clickable link that changes its text each time you click it and also sets a variable based on what option is shown.
For example: <<cycle "_lang">>
<<option "Click me!" "Eng">>
<<option "¡Haz clic en mí!" "Esp">>
<<option "クリックしてください!" "Jap">>
<</cycle>>
Here's the code for that:
{{{
For example: <<cycle "_lang">>
<<option "Click me!" "Eng">>
<<option "¡Haz clic en mí!" "Esp">>
<<option "クリックしてください!" "Jap">>
<</cycle>>
}}}
The problem with {{{<<cycle>>}}} links is that they would //normally// look exactly the same as any other passage link, leaving the player to guess if any link is a cycling link or a passage link.
However, if you simply add the CSS code below to your Twine project's "Stylesheet" section, you'll get those cycle links to appear as shown above, making them visually distinct from your normal passage links:
{{{
.macro-cycle::before {
content: "\f2f1";
font-family: sc-icons;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
speak: none;
padding-right: 2px;
}
.macro-cycle {
color: #B224FF;
transition: none;
}
.macro-cycle:hover, .macro-cycle:focus {
color: #B224FF !important;
filter: contrast(0.5) brightness(1.5);
}
}}}
''NOTE:'' The above only works in SugarCube v2.37.0 or later, as the structure of the built-in icons changed in that version.
This CSS code also makes cycle links purple-ish, in addition to adding the "<<scicon reload>>" icon in front of them. If you'd like a different color, simply swap out "{{{#B224FF}}}" in both places in the CSS for the color you'd prefer. The "{{{filter:}}}" part of the CSS will make the cycle links' color lighter when the user either hovers their mouse over them or the link has focus, so you may have to swap or modify the contrast and brightness values in that filter if you have a light background instead of a dark background.
Enjoy!Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vitae euismod risus, sed auctor nunc. Donec a sodales mi, id porta mi. Suspendisse facilisis est vel urna maximus, sit amet molestie risus cursus. Integer pharetra ligula eget mauris vestibulum ultrices. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam laoreet leo nisi, non molestie velit volutpat eu. Quisque et erat sed augue fringilla fermentum nec mollis tortor. Vivamus tortor ipsum, fringilla eget eros a, vulputate venenatis dui. Vivamus sit amet massa quis enim aliquam tincidunt eu euismod risus. Ut molestie eros sit amet lectus aliquet, eu lobortis nisi porta. Curabitur id viverra leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus ornare ipsum sed elit dignissim porttitor. Nulla placerat nisl ac ipsum maximus tincidunt. Nam eget quam tincidunt, convallis odio a, vulputate justo.
Mauris lacus lorem, placerat eu vehicula sit amet, congue et nulla. Praesent suscipit a enim ut viverra. Ut facilisis dolor sit amet aliquam mollis. Sed nulla neque, finibus eget ligula nec, tristique mattis purus. Sed venenatis nunc eget pretium ultrices. Phasellus luctus eget dui in vulputate. Sed scelerisque lacus feugiat dolor elementum, ac lobortis ex placerat. Donec diam dui, porta vel libero id, consectetur blandit metus. In quis aliquet libero. Morbi commodo porta orci, non sollicitudin nisi rutrum nec. Suspendisse imperdiet convallis dictum.
Donec elementum finibus mi eu placerat. Praesent id tempor nisi, sollicitudin facilisis justo. Nulla porttitor dignissim cursus. Cras lobortis neque tortor, nec pharetra risus dapibus quis. Aliquam elementum dictum ullamcorper. Proin in finibus magna, tincidunt pretium justo. Phasellus dignissim facilisis massa a dignissim. Etiam posuere posuere metus, eu eleifend est accumsan ut. Pellentesque pretium commodo varius. Integer vel sem dui. Sed rutrum ex ac massa mattis molestie. Vivamus commodo auctor lacus, et maximus dui semper eget. Nunc mattis sagittis mollis. Vivamus eu risus tellus. Suspendisse a sollicitudin tellus.
Nunc vestibulum lorem arcu, eget hendrerit erat tristique non. Nulla risus sapien, tincidunt id aliquam sit amet, iaculis at tellus. Vivamus sit amet eros non augue venenatis scelerisque. Maecenas eleifend laoreet diam, eu volutpat tellus iaculis sit amet. Etiam luctus in justo at cursus. Pellentesque semper, mauris eu gravida rhoncus, quam lectus vehicula justo, id ornare lacus nulla a lacus. Curabitur blandit vestibulum augue ac sagittis. Proin id faucibus diam, bibendum fermentum urna. Suspendisse a aliquam neque, vitae vestibulum ex. Vestibulum eu tellus hendrerit, luctus tortor a, tempor felis. Curabitur eu elit semper, tincidunt mi in, dictum dolor.
Pellentesque malesuada quis sapien quis tincidunt. Donec condimentum nulla in tortor dictum rhoncus. Praesent nisl felis, mattis vel sagittis sed, vulputate id ligula. In quis vehicula sem. In nec cursus sem, eget feugiat ex. Nullam molestie congue tellus, id tristique mi vulputate vitae. Phasellus venenatis purus eget nibh mattis convallis. Suspendisse sagittis massa et auctor sagittis. Vivamus blandit nulla elit, non placerat ante faucibus ac. Donec commodo libero eget vehicula consectetur. Vestibulum posuere tincidunt accumsan. Phasellus consequat sagittis bibendum. Aliquam venenatis et ante ac laoreet. Pellentesque ac tincidunt orci. Cras eleifend enim ipsum, eget finibus sem luctus sed. Vestibulum rhoncus est in nunc volutpat, eu porttitor purus pellentesque.
Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, explicabo.
Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci velit, sed quia non numquam eius modi tempora incidunt, ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio, cumque nihil impedit, quo minus id, quod maxime placeat, facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.<h1>Scroll Blocks</h1>Want to make it so that an image or block of text stays visible at the top of the screen, even as the user scrolls down in the rest of the passage? Here's the {{{<<iblock>>}}} macro, which makes that easy for you. (See below for examples showing how it works.)
Once you have the code for this added to your Twine game, then then you can use the {{{<<iblock>>}}} macro in your passages simply by doing something like this:
{{{
<<iblock>>Upper block of content (typically an image).<<body>>\
Scrolling block of content (typically text).
<</iblock>>
}}}
That will make it so that the upper block remains visible, even as you scroll down through the "body" content.
If you put anything //after// the {{{<<iblock>>}}} macro, including another {{{<<iblock>>}}} macro, then then that content will be able to scroll up the upper block. Thus, by chaning this macro, with one {{{<<iblock>>}}} after another, it allows you to have a series of images which appear as the text scrolls up, and each of the images will be able to push the previous image up off the screen.
Below are a few examples of this macro in action with using random text to show how scrolling works.
''NOTE:'' This text and all of the above is above the topmost {{{<<iblock>>}}} macro in this passage. Below the line is the first {{{<<iblock>>}}} macro in this passage.
----
<<iblock>><img @src="setup.ImagePath + 'space-1.jpg'"><<body>>\
The code for this <code><<iblock>></code> macro example is just:
{{{
<<iblock>><img @src="setup.ImagePath + 'space-1.jpg'"><<body>>\
...this "body" text...
<</iblock>>
}}}
(See the "[[Displaying Images in Twine]]" section for an explanation of how the "img" element in the code above gets the correct image path.)
To use this in your own project, first simply add this code to your project's JavaScript section:
{{{
/* <<iblock>> macro - start */
Macro.add('iblock', {
skipArgs : true,
tags : ['body'],
handler : function () {
if (this.payload.length == 2) {
$(this.output).wiki('<div class="iblock_container"><div class="iblock_top">' +
'<div class="iblock_bkg"><div class="iblock_head">' + this.payload[0].contents +
'</div></div><div class="iblock_separator"></div></div>' +
'<div class="iblock_content">' + this.payload[1].contents + '</div></div>');
} else {
return this.error('IBlock macro needs one and only one <<body>> section.');
}
}
});
/* <<iblock>> macro - end */
}}}
(For details on how to write macros like this yourself, see the <a href="http://www.motoslave.net/sugarcube/2/docs/#macro-api" target="_blank">Macro API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api" target="_blank">MacroContext API</a> sections of the SugarCube documentation.)
You'll also need to add this CSS code to the Stylesheet section:
{{{
/* <<iblock>> macro styling - start */
.iblock_container {
max-width: calc(96vw - 17.5em);
transition: max-width .2s ease-in;
}
@media screen and (max-width: 768px) {
.iblock_container {
max-width: calc(93vw - 1em);
}
}
#ui-bar.stowed ~ #story .iblock_container {
max-width: calc(93vw - 1em);
}
.iblock_top {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 5;
}
.iblock_bkg {
background: #111111;
}
.iblock_head {
max-width: fit-content;
margin: auto;
}
.iblock_head * {
max-width: 54em;
max-width: -webkit-fill-available;
max-height: 55vh;
}
.iblock_separator {
height: 16px;
background-image: linear-gradient(to bottom, #111111, 75%, transparent)
}
.iblock_content {
padding: 0 2px 12px 2px;
}
/* <<iblock>> macro styling - end */
}}}
''NOTE:'' The "head" of an <code><<iblock>></code> macro uses an opaque background to keep the "body" text that may be scrolling behind it hidden, thus the <code><<iblock>></code> macro may not work well if you're displaying background images in the body of the page. Additionally, if you're using a different background color than the default, then you will need to change the two {{{#111111}}} parts above to the background color that your passages are using.
(end of "body" text; a new <code><<iblock>></code> begins below)
<</iblock>>
<<iblock>><span class="twoimages"><img @src="setup.ImagePath + 'space-1.jpg'"><img @src="setup.ImagePath + 'space-1.jpg'"></span><<body>>\
Since there is more than one image in the "head" of this <code><<iblock>></code> macro example, we have to tweak the image sizes using a custom class, like this:
{{{
<<iblock>><span class="twoimages"><img @src="setup.ImagePath + 'space-1.jpg'"><img @src="setup.ImagePath + 'space-1.jpg'"></span><<body>>\
...this "body" text...
<</iblock>>
}}}
along with this CSS for that custom class:
{{{
.twoimages img {
max-width: calc(50% - 10px);
margin: 0 5px 0 5px;
}
}}}
You can tweak that CSS if you want different margins, borders, support for more images, other media types, or other modifications.
(The text below is just nonsense text for the purpose of showing how the macro works with scrolling.)
<<include "Lorem ipsum">>
(end of "body" text)
<</iblock>>
<<iblock>>IBlock content as text.<<body>>\
The code for this is just:
{{{
<<iblock>>IBlock content as text.<<body>>\
...this "body" text...
<</iblock>>
}}}
(The text below is just more nonsense text for the purpose of showing how the macro works with scrolling.)
<<include "Lorem ipsum">>
(end of "body" text)
<</iblock>>Put your bottom bar text here.
''Passage:'' <<= passage()>>Put your top bar text here.Teach proper indentation:
For proper indentation you just need to remember to indent lines by one more unit (often a TAB character) after a "head" element (except for any "segment" elements, such as an {{{<<else>>}}} within an {{{<<if>>}}}), and then indent by one less starting at the "tail" element. Don't change the indentation for elements that don't have a "tail".
{{{
<<example>> (0) <-- head
+1 indent (1)
<<subElement>> (1) <--head
+1 more indent (2)
<</subElement>> -1 indent (1) <-- tail
<<exampleSegment>> (1-1) <-- segment
maintain indent (1)
<</example>> -1 indent (0) <-- tail
}}}
Doing that makes the code more readable and will help prevent some problems, such as if you start a "child" element within a "parent" element, and then accidentally close the "parent" element before the "child" element, which is just broken code.
----
Teach line breaks. (Perhaps mix this in with indentation?)
----
Explain primitive variables vs object variables
Link: https://www.reddit.com/r/twinegames/comments/1svf617/comment/oi9geyz/
---
Array to a series of links:
<<set _x = ["test", "xyz", "abc"]>>
<<= _x.sort().reduce((txt, val, idx) => txt + (idx > 0 ? ", " : "") + "<<link '" + val + "' '" + val + "'>><</link>>", "")>>
-- or --
<<= _x.sort().map(val => `<<link "${val}" "${val}">><</link>>`).join(", ")>>
---
/* Write up some sample code showing how to use this. */
$(document).on(":passagerender", function () {
let synth = window.speechSynthesis;
let utterThis = new SpeechSynthesisUtterance(Story.get(passage()).text);
utterThis.voice = speechSynthesis.getVoices()[0];
synth.speak(utterThis);
});
---
// closestMatch: Find the item in an array whose properties are closest to Val1 and Val2 and return the index of that item.
setup.closestMatch = function (Arr, Prop1, Prop2, Val1, Val2) {
let range1, range2, scale1 = 1, scale2 = 1;
// Find highest and lowest values for each property in the array.
let min1 = Arr[0][Prop1], min2 = Arr[0][Prop2], max1 = Arr[0][Prop1], max2 = Arr[0][Prop2];
for (let i = 1; i < Arr.length; i++) {
if (min1 > Arr[i][Prop1]) min1 = Arr[i][Prop1];
if (min2 > Arr[i][Prop2]) min2 = Arr[i][Prop2];
if (max1 < Arr[i][Prop1]) max1 = Arr[i][Prop1];
if (max2 < Arr[i][Prop2]) max2 = Arr[i][Prop2];
}
range1 = max1 - min1;
range2 = max2 - min2;
// Scale the values so that both ranges have equal weight when determining the "distance" to Val1 and Val2.
if (range1 > range2) {
scale2 = range1 / range2;
} else if (range1 < range2) {
scale1 = range2 / range1;
}
// Find the shortest "distance" from any item in the array to Val1 and Val2.
// If multiple items in the array are of the same minimum distance, choose the last one.
let curdist, closest = 0, dist = (Math.abs(Arr[0][Prop1] - Val1) * scale1) + (Math.abs(Arr[0][Prop2] - Val2) * scale2);
for (let i = 1; i < Arr.length; i++) {
curdist = (Math.abs(Arr[i][Prop1] - Val1) * scale1) + (Math.abs(Arr[i][Prop2] - Val2) * scale2);
if (curdist <= dist) {
dist = curdist;
closest = i;
}
}
return closest;
};
---
Add:
Intro to Twine and the Twine Editor (recommend against using cloud-synched folders).
UInv's addArticle() function.
UInv's Image caching. (Add MB limits?)
UInv's radial menu?
The "Verb conjugation" sample code. : https://www.reddit.com/r/twinegames/comments/1rbuxa2/help_with_verb_tenses_and_gendered_npcs/
Object.defineProperty(window, "sv", { get() { return State.variables; } }); (maybe? "How to debug" section?)
Show the order that things are executed in SugarCube. (Order of operations; i.e. why a button that changes a variable won't change a previous display of that variable. See also the "Start Order" sample code.)
How do I create NPC schedules? : https://www.reddit.com/r/twinegames/comments/1qwu2fo/how_do_i_create_npc_schedules/
Explain the different types of "variables": story variables, temporary variables, JavaScript variables (including the "setup" object), settings, save metadata (via the Save.onSave.add() handler), and game metadata (i.e. memorize()/forget()/recall() data).
Speech synthesis code from the "Voice selection" sample code.
Use "CSS URL Fix" code to update the "Displaying Images in Twin" code.
If you want this to work when launched from the Twine editor, you'll also need to change the "{{{D:/Games/MyGame/}}}" part to point to the directory where your game is located on your machine. If there is any private information in that file path, you'll want to remove that from the published versions you distribute (change it to just {{{""}}}). There are also some default paths in the code (such as {{{setup.ImagePath}}} for the image path) which you may need to change if you don't use the same directory names (e.g. change {{{"images/"}}} to {{{"Images/"}}} if your images directory is capitalized).
''IMPORTANT:''
* Do not forget to use the same capitalization for your directory paths as their //actual// capitalization. The capitalization of the paths and filenames matter on most non-Windows machines.
* Within the path strings, be sure to use forward slashes "{{{/}}}", instead of the backslashes "{{{\}}}" that Windows prefers, and don't forget to have a "{{{/}}}" at the end of each of the paths.This section has been combined into the [[Listbox Tricks]] section.This section has been combined into the [[Listbox Tricks]] section.This is just a short passage which demonstrates that the bottom bar remains at the bottom of the screen even when the passage text doesn't go down that low.
[[Bottom and Top Bars]]
[[A "tbar" Tagged Passage]]
[[A "bbar" and "tbar" Tagged Passage]]