<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]]'' Nov. 27, '24</span>)</span>
Here's an ever-expanding collection of sample code, tips, and tricks for use with <a href="http://twinery.org/">Twine 2</a> and the <a href="http://www.motoslave.net/sugarcube/2/">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">Twine_Sample_Code.zip</a> (alternately <a href="https://drive.google.com/file/d/1bK6-QZOf2i8cdV710iiaDvPwooIQxV88/view?usp=sharing">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">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. When you do that, you should edit the line of the JavaScript that says {{{setup.Path = "D:/Games/Twine_Sample_Code/";}}} to match your file path to the HTML.
<span class="dumb_terminal"> </span><<run forget("countdown")>>Background 1!
The long June twilight faded into night. Dublin lay enveloped in darkness but for the dim light of the moon that shone through fleecy clouds, casting a pale light as of approaching dawn over the streets and the dark waters of the Liffey. Around the beleaguered Four Courts the heavy guns roared. Here and there through the city, machine guns and rifles broke the silence of the night, spasmodically, like dogs barking on lone farms. Republicans and Free Staters were waging civil war.
[[Backgrounds]]
[[Background 2]]
[[Background 3]]
[[Background 4]]
Background 2!
The long June twilight faded into night. Dublin lay enveloped in darkness but for the dim light of the moon that shone through fleecy clouds, casting a pale light as of approaching dawn over the streets and the dark waters of the Liffey. Around the beleaguered Four Courts the heavy guns roared. Here and there through the city, machine guns and rifles broke the silence of the night, spasmodically, like dogs barking on lone farms. Republicans and Free Staters were waging civil war.
[[Backgrounds]]
[[Background 2]]
[[Background 3]]
[[Background 4]]
Background 3!
The long June twilight faded into night. Dublin lay enveloped in darkness but for the dim light of the moon that shone through fleecy clouds, casting a pale light as of approaching dawn over the streets and the dark waters of the Liffey. Around the beleaguered Four Courts the heavy guns roared. Here and there through the city, machine guns and rifles broke the silence of the night, spasmodically, like dogs barking on lone farms. Republicans and Free Staters were waging civil war.
[[Backgrounds]]
[[Background 2]]
[[Background 3]]
[[Background 4]]
Background 4!
The long June twilight faded into night. Dublin lay enveloped in darkness but for the dim light of the moon that shone through fleecy clouds, casting a pale light as of approaching dawn over the streets and the dark waters of the Liffey. Around the beleaguered Four Courts the heavy guns roared. Here and there through the city, machine guns and rifles broke the silence of the night, spasmodically, like dogs barking on lone farms. Republicans and Free Staters were waging civil war.
[[Backgrounds]]
[[Background 2]]
[[Background 3]]
[[Background 4]]
<<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">>[[Jump to Start|Main Menu]]
Volume: <<volume>><div id="verticalhealthbarbkg" class="vertbarbkg"><div id="verticalhealthbar" class="vertbar"></div></div><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 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,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3C/svg%3E" alt="Larger font" title="Larger font" class="fullscreenImg nohide" style="top: 70px;" onclick="fontSize(1)"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3C/svg%3E" alt="Smaller font" title="Smaller font" class="fullscreenImg nohide" style="top: 100px;" onclick="fontSize(-1)">
<<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/thank me:
<a href="https://www.patreon.com/HiEv"><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"><img @src="setup.ImagePath+'PayPal-donate.png'" class="nohide"></a><</if>><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">Setting API</a> and the <a href="https://www.motoslave.net/sugarcube/2/docs/#setting-api-object-settings">''settings'' object</a>. This section is just to show you an example of how that works and what the code might look like.
For example: <<timed settings.textDelay t8n>>The appearance of this text was delayed by <<= settings.textDelay>>, based on the settings.<</timed>>
Source:
{{{
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 using the "Settings" button in the UI bar. Try changing the "text delay" setting and then [[reload this page|Settings]] to see the new display delay. ({{{<<=>>}}} is shorthand for the {{{<<print>>}}} macro.)
The next example shows you a possible way to have a setting to mute/unmute 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.)
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)
To see how the settings were set up, here's the relevant code in the JavaScript section that does it:
{{{
/* Settings code - Start */
function videoMuteHandler() {
// (Un)Mute the videos
$("video").prop("muted", settings.videoMute); // (Un)Mute all videos
}
Setting.addToggle("videoMute", {
label : "Video Mute",
default : false,
onInit : videoMuteHandler,
onChange : videoMuteHandler
});
Setting.addList("textDelay", {
label : "Choose how long it takes for text to appear.",
list : ["No delay", "1s", "2s", "3s"],
default : "2s"
});
Setting.addToggle("images", { label: "Hide images?", onChange: toggleImages });
/* Settings code - End */
}}}
That code adds the two true/false "toggles" and a dropdown list to the dialog window which is displayed when you click the "Settings" button on the UI bar.
The "Video Mute" toggle also triggers the {{{videoMuteHandler()}}} function when the program first starts (due to the {{{onInit}}}) and whenever the "Video Mute" toggle is changed (due to the {{{onChange}}}). If you want changing a setting to take effect immediately, like this one does, then you'll need to create your own "handler" function of some sort to make whatever changes are necessary happen at that point. Otherwise the player will likely only see any changes after the next passage transition.
Within the passage it checks the value of {{{settings.videoMute}}} to determine whether to initially play the video muted or not, like this:
{{{
<<if settings.videoMute>>
\<video @src="_vid" autoplay loop controls muted height="240px"></video>
\<<else>>
\<video @src="_vid" autoplay loop controls height="240px"></video>
\<</if>>
}}}
See the [[Image Toggle]] section for details on the "images" toggle and the rest of the code it uses.
<<silently>>
<<repeat 0.5s>>
<<replace "#mute">><<= settings.videoMute>><</replace>>
<</repeat>>
<</silently>><h1>Music Example</h1><<audio "night_bgm" volume 0.05 play loop>>
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>>
{{{
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 = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.SoundPath = setup.Path + "sounds/";
// Volume Slider, by Chapel; for SugarCube 2
// version 1.2.0 (modified by HiEv)
// For custom CSS for slider use: http://danielstern.ca/range.css/#/
/*
Changelog:
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.
var options = {
current : 50, // Default volume level.
rangeMax : 100,
step : 1,
setting : true
};
Setting.load();
if (options.setting && settings.volume) {
options.current = parseInt(settings.volume);
}
var 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() {
var change = parseInt($('input[name=volume]').val());
setVolume(change / options.rangeMax);
vol.last = change;
});
// Create the <<volume>> macro.
Macro.add('volume', {
handler : function () {
var wrapper = $(document.createElement('span'));
var slider = $(document.createElement('input'));
var 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>>
{{{
if (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 = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.SoundPath = setup.Path + "sounds/";
// 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
var volRef = setup.vol.options;
var change = $('input[name=volume]').val();
var 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
var $wrapper = $(document.createElement('span'));
var $slider = $(document.createElement('input'));
var className = 'macro-' + this.name;
var 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>
You'll need to change the {{{setup.Path = "C:/Games/Twine_Sample_Code/";}}} line to be your file path. The <a href="sounds/Peach Girl OST - 15 Oyasumi Romantic Night.mp3">sound file</a> should be in a subdirectory called "sounds".
--
(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:
{{{
if (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 = "C:/Games/MyGameDirectory/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
setup.SoundPath = setup.Path + "sounds/";
}}}
You'll want to change "''C:/Games/MyGameDirectory/''" above to the directory where your HTML will go. 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">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">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) {
var 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) {
var 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) {
var tracks = Macro.get("cacheaudio").tracks;
if (tracks.hasOwnProperty(BGM)) {
var 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">>
<<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>>
<</widget>>
}}}
Put that code into a non-story passage with "''widget''" and "''nobr''" tags. (See <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-widget">the {{{<<widget>>}}} macro</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.
{{{
// 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>For more information on the JavaScript {{{Date}}} object, see:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
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">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">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.)<h1>Dropdown Selection Example</h1>If you would like a certain item to be selected in a dropdown listbox when you enter a passage, based on the value of a certain variable, there are a couple of ways to do that.
''Note:'' Examples 1 and 3 use the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-listbox"><<listbox>> macro</a>, and thus require SugarCube v2.26.0 or later.
__''Example 1''__
This dropdown adds the "selected" parameter to the matching option within a {{{<<listbox>>}}} macro:
<<listbox "$variable">>
<<option "LabelA" "A" `$variable == "A" ? "selected" : ""`>>
<<option "LabelB" "B" `$variable == "B" ? "selected" : ""`>>
<<option "LabelC" "C" `$variable == "C" ? "selected" : ""`>>
<</listbox>>
Source:
{{{
<<listbox "$variable">>
<<option "LabelA" "A" `$variable == "A" ? "selected" : ""`>>
<<option "LabelB" "B" `$variable == "B" ? "selected" : ""`>>
<<option "LabelC" "C" `$variable == "C" ? "selected" : ""`>>
<</listbox>>
}}}
The above uses the backtick ({{{`}}}; usually found on the tilde {{{~}}} key) to evaluate the contents within the backtics. The {{{conditional ? ifTrue : ifFalse}}} code will do the "''ifTrue''" part if the conditional evaluates to 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>>" value, or it will do the "''ifFalse''" part if the conditional evaluates to a "<<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.
So, in this case, if {{{$variable}}} was set to "''B''" when you entered this passage, then "''LabelB''" would be selected in the listbox.
Select a different option above and [[try it out|Next Passage][$FontName = $("#FontName").val()]]
__''Example 2''__
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>
<<script>>
$(document).one(':passagerender', function (ev) {
if ($(ev.content).find("#FontName") && State.variables.hasOwnProperty("FontName")) {
$(ev.content).find("#FontName").val(State.variables.FontName);
} else {
$(ev.content).find("#FontName").val("Arial");
}
});
<</script>>(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.
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 [[try it out|Next Passage][$FontName = $("#FontName").val()]]
Source:
{{{
''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>
<<script>>
$(document).one(':passagerender', function (ev) {
if ($(ev.content).find("#FontName") && State.variables.hasOwnProperty("FontName")) {
$(ev.content).find("#FontName").val(State.variables.FontName);
} else {
$(ev.content).find("#FontName").val("Arial");
}
});
<</script>>
}}}
__''Example 3''__
This example shows how to have a default value ("''Cherry''") and a button to select a particular option in a dropdown:
''Pie:'' <<if ndef $pie>><<set $pie = "cherry">><</if>><<listbox "$pie">>
<<option "Blueberry" "blueberry" `$pie == "blueberry" ? "selected" : ""`>>
<<option "Cherry" "cherry" `$pie == "cherry" ? "selected" : ""`>>
<<option "Coconut cream" "coconut cream" `$pie == "coconut cream" ? "selected" : ""`>>
<</listbox>>
<<button "Set Pie to Blueberry by index">>
<<run $("#listbox-pie").val("0").trigger("change")>>
<</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 [[try it out|Next Passage][$FontName = $("#FontName").val()]]
Source:
{{{
''Pie:'' <<if ndef $pie>><<set $pie = "cherry">><</if>><<listbox "$pie">>
<<option "Blueberry" "blueberry" `$pie == "blueberry" ? "selected" : ""`>>
<<option "Cherry" "cherry" `$pie == "cherry" ? "selected" : ""`>>
<<option "Coconut cream" "coconut cream" `$pie == "coconut cream" ? "selected" : ""`>>
<</listbox>>
<<button "Set Pie to Blueberry by index">>
<<run $("#listbox-pie").val("0").trigger("change")>>
<</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: {{{<<if ndef $pie>><<set $pie = "cherry">><</if>>}}} will set the value of {{{$pie}}} to "''cherry''" if the variable isn't defined yet ("ndef" = "not defined"). That causes it to be the "default" value which is selected in the dropdown. You have to set that variable before the {{{<<listbox>>}}} macro is called for this to work.
For the {{{"#listbox-pie"}}} part in your own code, you'll need to change it to {{{"#listbox-yourVariableName"}}} if you're using the {{{<<listbox>>}}} macro to set a story variable (one which starts with a {{{$}}}), or {{{"#listbox--yourVariableName"}}} (two dashes) if you're setting a temporary variable (one which starts with an {{{_}}}).
It should remember your selections of Label$variable, $FontName, and $pie when you return to the [[Dropdown Select]] section now.
<h1>{{{<<capture>>}}} Example</h1>Honestly, in SugarCube the <a href="http://www.motoslave.net/sugarcube/2/docs/#macros-macro-capture">{{{<<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 = 1>>
<<button "Button 1">>
<<run alert(_n)>>
<</button>><br><br>
<<capture _n>>
<<button "Button 2">>
<<run alert(_n)>>
<</button>><br>
<</capture>>
<<set _n = 4>>
<</nobr>>
And here's the code for those two buttons:
{{{
<<nobr>>
<<set _n = 1>>
<<button "Button 1">>
<<run alert(_n)>>
<</button>><br><br>
<<capture _n>>
<<button "Button 2">>
<<run alert(_n)>>
<</button>><br>
<</capture>>
<<set _n = 4>>
<</nobr>>
}}}
"Button 1" //did NOT// capture the value of {{{_n}}}, so it will display "4", because that's what the second to last line sets it to.
"Button 2" //did// capture the value of {{{_n}}}, so it will display "1", because that's what the value of {{{_n}}} was at the time the button was created.
That example shows how the {{{<<capture>>}}} macro is needed in cases where you have a variable that may have a different value at the time the user interacts with that element.
So, in cases where you have something like a loop variable, and the loop creates something player interacts with later on that needs to know what value some loop variable had, and not what that value is currently, then that would be an example of a variable that you would need to {{{<<capture>>}}}.
Hope that helps!<h1>Table of Links Example</h1>This code 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.
<<nobr>>
<<set $inv = []>>
/* Get the list of story passages' names. */
<<set _passages = Story.lookupWith(function () { return true; })>>
<<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>>
<<set $inv = $inv.sort()>>
<<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 $inv[_i] == "Main Menu">>
/* Bold "Main Menu" 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.)
Source code for the above:
{{{
<<nobr>>
<<set $inv = []>>
/* Get the list of story passages' names. */
<<set _passages = Story.lookupWith(function () { return true; })>>
<<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>>
<<set $inv = $inv.sort()>>
<<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 $inv[_i] == "Main Menu">>
/* Bold "Main Menu" 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>>
}}}<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>><<script>>
$(document).one(':passageend', function (ev) {
Health(State.variables.CurHP, State.variables.MaxHP, "horizontalhealthbar", true, ev.content);
Health(State.variables.CurHP, State.variables.MaxHP, "verticalhealthbar", false);
});<</script>>
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;
}
var HP = parseInt((CurHP / MaxHP) * 100).clamp(0, 100);
var 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>
<<script>>$(document).one(':passageend', function (ev) {
Health(State.variables.CurHP, State.variables.MaxHP, "horizontalhealthbar", true, ev.content);
});<</script>>
}}}
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>
<<script>>$(document).one(':passageend', function (ev) {
Health2(100, 100, "hzhealthbar", true, ev.content);
});<</script>><<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>
<<script>>$(document).one(':passageend', function (ev) {
Health2(100, 100, "hzhealthbar", true, ev.content);
});<</script>><<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;
}
var HP = parseInt((CurHP / MaxHP) * 100).clamp(0, 100);
var 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)
var 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">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">{{{<<replace>>}}} macro</a>. Here's some sample code showing how that works.
<<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>>
<<nobr>><<set _repeatText = "press it and watch as">>
<span id="test1">There is a button here.
<<link "Press it">>
<<set _press to true>>
<<replace "#test1">>You _repeatText the door opens.<br>
<br>
[[Reload|Replace]]<</replace>>
<</link>> or
<<link "don't press it">>
<<set _press to false>>
<<replace "#test1">>You do not _repeatText nothing happens.<br>
<br>
<<link "Reload passage" `passage()`>><</link>><</replace>>
<</link>>?
</span><</nobr>>
Source 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>>
<<nobr>><<set _repeatText = "press it and watch as">>
<span id="test1">There is a button here.
<<link "Press it">>
<<set _press to true>>
<<replace "#test1">>You _repeatText the door opens.<br>
<br>
[[Reload|Replace]]<</replace>>
<</link>> or
<<link "don't press it">>
<<set _press to false>>
<<replace "#test1">>You do not _repeatText nothing happens.<br>
<br>
<<link "Reload passage" `passage()`>><</link>><</replace>>
<</link>>?
</span><</nobr>>
}}}<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.
----
<<button "Show Alert">>
<<run alert("Alert text.")>>
<</button>>
{{{
<<button "Show Alert">>
<<run alert("Alert text.")>>
<</button>>
}}}
----
<<button "Show Dialog">>
<<run Dialog.setup("Dialog title")>>
<<run Dialog.wiki("Dialog text.")>>
<<run Dialog.open()>>
<</button>>
{{{
<<button "Show Dialog">>
<<run Dialog.setup("Dialog title")>>
<<run Dialog.wiki("Dialog text.")>>
<<run Dialog.open()>>
<</button>>
}}}
----
<<button "Show Input Dialog">>
<<script>>
Dialog.setup("Please enter your name:");
Dialog.wiki('<<textbox "$name" "Steve" autofocus>> <<button "Done">><<run $("#name").empty().wiki("Hello there, $name.")>><<run Dialog.close()>><</button>>');
Dialog.open({top: "calc(50vh - 51px)"}, setup.done);
<</script>>
<</button>>
<span id="name"></span>
{{{
<<button "Show Input Dialog">>
<<script>>
Dialog.setup("Please enter your name:");
Dialog.wiki('<<textbox "$name" "Steve" autofocus>> <<button "Done">><<run $("#name").empty().wiki("Hello there, $name.")>><<run Dialog.close()>><</button>>');
Dialog.open({top: "calc(50vh - 51px)"}, setup.done);
<</script>>
<</button>>
<span id="name"></span>
}}}<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.
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.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.
Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien.
<<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>>
}}}
<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.
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>First paragraph (non-delayed).
@@.delayed;Second paragraph.@@
@@.delayed;Third paragraph.@@
@@.delayed;Fourth paragraph.@@
Blinky text: @@#blinky;Blinky text@@
[[Reload|Delayed and Blinky Text]]
<<script>>
$(document).one(':passagerender', function (ev) {
$(ev.content).find("#blinky").fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1);
$(ev.content).find("#blinky").css("color", "red");
setTimeout( function () { $(ev.content).find("#blinky").css("color", "green"); }, 1000);
setTimeout( function () { $(ev.content).find("#blinky").css("color", "blue"); }, 3200);
setTimeout( function () { $(ev.content).find("#blinky").css("color", "white"); }, 5400);
});
<</script>>
For Delayed Text you'd do this in your passage:
{{{
First paragraph (non-delayed).
@@.delayed;Second paragraph.@@
@@.delayed;Third paragraph.@@
@@.delayed;Fourth paragraph.@@
}}}
Delayed Text Stylesheet section:
{{{
.delayed {
opacity: 0;
}
}}}
Delayed Text JavaScript section:
{{{
/* Delayed Text code - Start */
$(document).on(':passagerender', function (ev) {
// Find all elements containing the delayed class.
var elems = $(ev.content).find('.delayed');
// Appearance delay (in milliseconds) between each delayed text block.
var delay = 1000; // 1 second fade-in
if (elems.length > 0) {
elems.each(function (i) {
$(this)
.delay(delay * (i + 1))
.fadeTo(delay, 1);
});
}
});
/* Delayed Text code - End */
}}}
For Blinky Text you'd just do this in your passage:
{{{
Blinky text: @@#blinky;Blinky text@@
<<script>>
$(document).one(':passagerender', function (ev) {
$(ev.content).find("#blinky").fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1).delay(200).fadeTo(1000, 0).fadeTo(1000, 1);
$(ev.content).find("#blinky").css("color", "red");
setTimeout( function () { $(ev.content).find("#blinky").css("color", "green"); }, 1000);
setTimeout( function () { $(ev.content).find("#blinky").css("color", "blue"); }, 3200);
setTimeout( function () { $(ev.content).find("#blinky").css("color", "white"); }, 5400);
});
<</script>>
}}}
You can remove the four lines with {{{.css("color"}}} if you don't want it to change colors (which means it will just blink in white, by default).<h1>Embedded Image Sample Code</h1>PNG image encoded as base64 data: <img id="imgElem">
See: https://www.base64-image.de/
(Note: This is not recommended for large images and/or lots of images due to file bloat and page file size limitations in some browsers.)
Open this HTML in Twine and take a look at the "Embed Image" passage to see the code.
<<script>>
$(document).one(':passagerender', function (ev) {
var img = $(ev.content).find('#imgElem');
var baseStr64 = "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=";
img.attr('src', baseStr64);
})
<</script>><h1>Smart Checkboxes and Radio Buttons</h1>''NOTE:'' For SugarCube v2.32.0 and higher, the sample code below is obsolete. 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.
----
<<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>How to throw a custom error with Twine code</h1>Doing:
{{{
@@#error;@@
<<run $(document).one(':passagerender',
function (ev) { throwError(
$(ev.content).find('#error'),
'No player character.',
'You appear to be kind of mostly dead.\nSorry you had to find out like this.'
)
}
)>>
}}}
results in this:
@@#error;@@<<run $(document).one(':passagerender', function (ev) { throwError($(ev.content).find('#error'), 'No player character.', 'You appear to be kind of mostly dead.\nSorry you had to find out like this.') })>>
You can turn that into a widget if, for example, you'd like your own widgets to be able to have custom error messages. To do this, just create a passage with "widget" and "nobr" tags and put this code in it:
{{{
<<widget "ErrMsg">>
<<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>>
<</widget>>
}}}
And now 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.">>
<h1>How to throw an error from JavaScript code</h1>If you have the following code in your JavaScript section:
{{{
window.ErrTest = function () {
throw new Error("This returns an error message. Click the arrow on the far left.");
};
}}}
then doing:
{{{
<<run ErrTest()>>
}}}
results in this:
<<run ErrTest()>>
You can use that if you want to write your own custom JavaScript functions, and have them be able to properly throw an error when needed.
<h1>Fullscreen Button Code</h1><<button "Fullscreen (direct toggleFullscreen() call)">><<run toggleFullscreen()>><</button>>
This second button usually won't work because the browser doesn't see the toggleFullscreen() call as initiated by a user gesture after that much delay:
<<button "Fullscreen (delayed toggleFullscreen() call)">><<run window.setTimeout(window.toggleFullscreen, 3000)>><</button>>
''Real-time status:''
<<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>>
Needs files:
- images/<a href="images/FullScreenExit_white.png">FullScreenExit_white.png</a>
- images/<a href="images/FullScreenGo_white.png">FullScreenGo_white.png</a>
''StoryCaption'' passage:
{{{
<input type="checkbox" id="fullscreen"><label for="fullscreen" class="gofullscreen"><img @src="setup.ImagePath+'FullScreenGo_white.png'" alt="Go full screen" title="Go full screen" class="fullscreenImg"></label><label for="fullscreen" class="exitfullscreen"><img @src="setup.ImagePath+'FullScreenExit_white.png'" alt="Exit full screen" title="Exit full screen" class="fullscreenImg"></label>
}}}
Or use this in your ''StoryCaption'' passage if you want to use an embedded image, instead of an external file, for the fullscreen button:
{{{
<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>
}}}
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")) {
setup.Path = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
/* 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 icon/button support - Start */
/* You can remove this part if you're not using the on-screen button. */
$(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 */
/* Fullscreen toggle code - End */
}}}
Set the default path for your HTML on the second line of the above JavaScript.
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 */
}}}
''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 Line Sample Code</h1>Random link: <<set _lnk = ["Main Menu", "Backgrounds", "Settings", "Music", "Time", "Dropdown Select", "Capture"].random()>><<link _lnk _lnk>><</link>>
[[Reload|Random Link]]
The top link is generated using this code:
{{{
Random link: <<set _lnk = ["Main Menu", "Backgrounds", "Settings", "Music", "Time", "Dropdown Select", "Capture"].random()>><<link _lnk _lnk>><</link>>
}}}<<widget "mom">>
dad
<</widget>>
/* SugarCube Icons passage widgets - Start */
<<widget "scicon">><span @class="'sc-icon sc-' + $args.raw" alt="' + $args.raw + '"></span><</widget>>
<<widget "updateIcons">>
<<set _txt = "", _unselectedIcons = []>>
<<set _csstxt = '/* SugarCube\'s built-in Font Awesome icons - Start */\n.sc-fa {\n font-family: tme-fa-icons;\n font-style: normal;\n font-weight: 400;\n font-variant: normal;\n text-transform: none;\n line-height: 1;\n speak: none;\n}\n.sc-icon {\n padding-left: 4px;\n}\n.sc-icon::before {\n font-family: tme-fa-icons;\n font-style: normal;\n font-weight: 400;\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: "\\e8' + _val.id + '\\00a0";\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
*/
<</widget>>
/* SugarCube Icons widgets - End */
/* <<ErrMsg>> Widget - Start */
<<widget "ErrMsg">>
<<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>>
<</widget>>
/* <<ErrMsg>> Widget - End */
/* <<PrintType>> Widget - Start */
<<widget "PrintType">>
<<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>>
<</widget>>
/* <<PrintType>> Widget - End */
/* <<HighlightTrue>> Widget - Start */
<<widget "HighlightTrue">>
<<if $args[0]>>
@@.invert;true@@
<<else>>
false
<</if>>
<</widget>>
/* <<HighlightTrue>> 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. */
/* EXAMPLE: <<HoverTxt 200 "text">> */
/* EXAMPLE: <<HoverTxt 300 `someFunction()`>> */
<<widget "HoverTxt">>
<<if !Number.isInteger($args[0])>>
<<set _width = 200>> /* Default to a width of 200 if an invalid width is passed. */
<<else>>
<<set _width = $args[0]>>
<</if>>
<<set _left = Math.trunc(_width / 2) - 11>>
<<if ndef _HoverTxtCount>>
<<set _HoverTxtCount = 1>>
<<else>>
<<set _HoverTxtCount += 1>>
<</if>>
<a class="hoverTxt" style="text-decoration: none;"><img class="hoverIco" @src="setup.ImagePath+'NoteIcon.png'">
<span @id="'hoverTxt' + _HoverTxtCount" class="hoverBox" @style="'left: -' + _left + 'px; width: ' + _width + 'px;'">
<<print $args[1]>>
</span>
</a>
<</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">>
*/
<<widget "SetFlag">>
<<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 */
<</widget>>
/* <<SetFlag>> 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">>
<<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>>
<</widget>>
/* <<SetPronouns>> widget - End */
/* <<SetGender>> widget - Start */
<<widget "SetGender">>
/* 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 */
<<switch $args[0]>>
<<case "f">>
<<set $pgen = 1>>
<<case "b">>
<<set $pgen = 2>>
<<case "n">>
<<set $pgen = 3>>
<<default>>
<<set $pgen = 0>>
<</switch>>
<</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">>
*/
<<widget "checkboxPlus">>
/* 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>
<</widget>>
/* <<checkboxPlus>> Widget - End */
/* <<toggleLink>> Widget - Start */
<<widget "toggleLink">>
<<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>
<</widget>>
/* <<toggleLink>> Widget - End */
/* <<countdownTimer>> Widget - Start */
<<widget "countdownTimer">>
<<set _seconds = $args[0]>>
<<set _minutes = Math.floor(_seconds / 60)>>
<<set _replacementPassage = $args[1]>>
<div id="timer" class="timergreen">Time remaining _minutes:<<= (_seconds - (_minutes * 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 {
var curtime = new Date(), str, seconds = State.temporary.seconds;
var 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>>
<</widget>>
/* <<countdownTimer>> Widget - End */
/* <<textboxPlus>> widget v1.3 - 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
*/
<<widget "textboxPlus">>
<<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][$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>>
<</widget>>
<<script>>
$(document).on(":passagerender", function (event) {
/* Update textboxPlus input boxes. */
$(event.content).find(".textboxplus").each(function () {
var options = {}, props = {};
var 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);
}
});
var changeCode = $(this).data("onchange");
if (changeCode) {
$(this).find("input").on("change", function (event) {
$.wiki(changeCode);
});
}
var inputCode = $(this).data("oninput"), parent = this;
if (inputCode) {
$(this).find("input").on("input", function (event) {
State.setVar($(parent).data("variable"), $(this).val());
$.wiki(inputCode);
});
}
var returnCode = $(this).data("onreturn");
if (returnCode) {
$(this).on("keyup", function (event) {
if (event.key === "Enter") {
$.wiki(returnCode);
}
});
}
});
});
<</script>>
/* <<textboxPlus>> widget - End */
<<widget "showLinks">>
<<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>>
<</widget>>
<<widget "catInfo">>
<<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>>
<<if _updated>>
<<set _txt += ", " + _updated + " Updated">>
<</if>>
<<if _new>>
<<set _txt += ", " + _new + " New">>
<</if>>
<<if _txt>>
<<set _txt += ")">>
<</if>>
_txt
<</widget>><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">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>>">>}}} (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>">>}}}
{{{$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" }>>}}}
{{{$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"]>>}}}
{{{$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.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">>}}}
{{{$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.">>}}}
{{{$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 */
}}}
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 HTML elements.
For example, this (note the space before the {{{>>}}} at the end, which is required to prevent the macro from being ended prematurely):
{{{
<<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 to show the raw value of the {{{$test}}} variable, then you'd have to write something 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>Click the 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)">
Needs files:
- images/GrowF.png
- images/ShrinkF.png
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)"><img @src="setup.ImagePath+'ShrinkF.png'" alt="Smaller font" title="Smaller font" class="fullscreenImg" style="top: 100px;" onclick="fontSize(-1)">
}}}
The above code assumes "setup.ImagePath" is set to your image path (e.g. ''setup.ImagePath = "images/"''). See the JavaScript below.
Alternately, instead of using external images files like in the above code, you could use SVG images embedded directly in the code itself.
<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="fullscreenImg" style="position: inherit;" onclick="fontSize(1)"> <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="fullscreenImg" style="position: inherit;" onclick="fontSize(-1)">
StoryCaption passage using embedded SVG images:
{{{
<img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3C/svg%3E" alt="Larger font" title="Larger font" class="fullscreenImg" style="top: 70px;" onclick="fontSize(1)"><img src="data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3Cpath 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='%23FFF'/%3E%3C/svg%3E" alt="Smaller font" title="Smaller font" class="fullscreenImg" style="top: 100px;" onclick="fontSize(-1)">
}}}
Either way, you'll also need this in 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")) {
setup.Path = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
// fontSize: Increase or decrease passage font size by "value" pixels.
window.fontSize = function(value) {
$("#story").css("font-size", (parseInt($("#story").css("font-size")) + value) + "px");
};
}}}
Set the default path for your HTML on the second line of the above JavaScript.
You'll also need this in your Stylesheet section:
{{{
/* Font Size Buttons - Start */
.fullscreenImg {
cursor: pointer;
transition: 0.3s;
border-radius: 3px;
position: absolute;
top: 40px;
left: 252px;
vertical-align: text-bottom;
height: 25px;
visibility: visible;
}
.fullscreenImg:hover {
background: #444444;
}
#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)),
// Internet Explorer 6-11
isIE : /*@cc_on!@*/false || !!document.documentMode,
// 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("/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>Examples of different types of objects and how to tell them apart using the JavaScript functions below:
<<set $test = {}>>{}: (a generic object)
isGenericObject = <<- isGenericObject($test)>>
isObject = <<- isObject($test)>>
type = <<- typeof $test>>
<<set $test = []>>
[]: (an array)
isGenericObject = <<- isGenericObject($test)>>
isObject = <<- isObject($test)>>
type = <<- typeof $test>>
<<set $test = window.isGenericObject>>
window.isGenericObject: (a function)
isGenericObject = <<- isGenericObject($test)>>
isObject = <<- isObject($test)>>
type = <<- typeof $test>>
<<set $test = new Date('August 19, 1975 23:15:30')>>
new Date(): (a Date object)
isGenericObject = <<- isGenericObject($test)>>
isObject = <<- isObject($test)>>
type = <<- typeof $test>>
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';
}
}}}<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">Google</a>}}} which displays as: <a data-nokeys="true" href="http://google.com">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 - Start */
var 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")) {
var 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");
}
var 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();
var 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 take a look at it using the {{{getObjectProperties()}}} function. For example, this:
{{{
SugarCube.Browser = <<= getObjectProperties(SugarCube.Browser)>>
}}}
shows this:
SugarCube.Browser = <<= getObjectProperties(SugarCube.Browser)>>
For the {{{getObjectProperties()}}} function to work you'll need the following in your 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 += ".";
}
var 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)) {
var 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 */
}}}<h1>{{{<<HoverTxt>>}}} Widget</h1>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="setup.ImagePath+'Prism64-2.png'">
}}}
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'">
(Hold your mouse over the image.)
With HoverTxt you would do this:
{{{
<<HoverTxt 300 "This text will appear as a tooltip if you hover your mouse over the image.">>
}}}
Which appears like this: <<HoverTxt 300 "This text will appear as a tooltip if you hover your mouse over the image.">>
The first parameter is the width in pixels, which I recommend you not go above 600 to avoid going wider than most screens. The second parameter is the text that you want 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.
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 300 'This is a prism: <img style="position: relative; vertical-align: text-bottom; height: 20px;" @src="setup.ImagePath+\'Prism64-2.png\'">'>>
}}}
(Note: The apostrophes in the text parameter are escaped using a backslash in the example above.)
That will display an image inside the text, like this: <<HoverTxt 300 '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' })`>>
''NOTE:'' This code requires ''SugarCube v2.23.5 or later''. Grab the latest version of <a href="http://www.motoslave.net/sugarcube/2/#downloads">SugarCube v2</a>, if you haven't already, and add the updated format into Twine. Then do "Change Story Format" in your story and set it to the updated version of SugarCube.
''Needs file:''
* images/NoteIcon.png
In a non-special story passage with a "''widget''" and "''nobr''" tags:
{{{
/* HoverTxt : Show notepad icon shows some wikified text in a window of width X pixels above an icon when it's hovered over. */
/* EXAMPLE: <<HoverTxt 200 "text">> */
/* EXAMPLE: <<HoverTxt 300 `someFunction()`>> */
<<widget "HoverTxt">>
<<if !Number.isInteger($args[0])>>
<<set _width = 200>> /* Default to a width of 200 if an invalid width is passed. */
<<else>>
<<set _width = $args[0]>>
<</if>>
<<set _left = Math.trunc(_width / 2) - 11>>
<<if ndef _HoverTxtCount>>
<<set _HoverTxtCount = 1>>
<<else>>
<<set _HoverTxtCount += 1>>
<</if>>
<a class="hoverTxt" style="text-decoration: none;"><img class="hoverIco" @src="setup.ImagePath+'NoteIcon.png'">
<span @id="'hoverTxt' + _HoverTxtCount" class="hoverBox" @style="'left: -' + _left + 'px; width: ' + _width + 'px;'">
<<print $args[1]>>
</span>
</a>
<</widget>>
}}}
JavaScript section:
{{{
// Make it so that paths can work properly when launched from Twine.
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 = "C:/Games/MyGame/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
/* HoverTxt - Start */
var 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);
var 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 */
}}}
(Change "C:/Games/MyGame/" to your own file path if you want this to work within Twine.)
Stylesheet section:
{{{
/* HoverTxt code - Start */
.hoverTxt {
position: relative;
top: 5px;
vertical-align: text-bottom;
height: 30px;
}
.hoverTxt:hover span {
visibility: visible;
}
.hoverTxt span {
position: absolute;
background-color: #e5e5f5;
padding: 3px;
border: 1px solid #666;
border-radius: 10px;
visibility: hidden;
color: black;
text-decoration: none;
}
.hoverBox {
top: -100px;
text-align: left;
box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.5);
z-index: 100;
}
/* HoverTxt code - End */
}}}<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 - Start */
Macro.add('chunkText', {
tags: [ 'next' ],
handler: function() {
var tmp = '<<if ndef $textGroup>><<set $textGroup = 1>><</if>><<switch $textGroup>>', linkText, n, cls, txt, psg;
for (var 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 */
}}}
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>><<if tags().includes("tbar")>>\
<div id="topbar"><div id="bbblock"><div id="bbtext">Put your top bar text here.</div></div></div><</if>>\
<<script>>
$(document).one(":passagedisplay", function (event) {
if ($("#topbar").length) {
$("#passages").css("margin-top", $("#topbar").outerHeight() + 10);
} else {
$("#passages").css("margin-top", 0);
}
});
<</script>>\<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, then you can use the following code. It causes Twine to look at the <a href="https://en.wikipedia.org/wiki/Fragment_identifier">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.
This Twine file uses this technique itself.
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;
var 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 "#<span class="sc-fa"> </span>" 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><<set _a = 0>><<set _b = 0>><div id="event"><<include "Event Passage">></div>
This passage has the following code above:
{{{
<<set _a = 0>><<set _b = 0>>
<div id="event"><<include "Event Passage">></div>
}}}
The {{{<<include>>}}} macro causes the contents of the named passage (in this case it's the {{{Event Passage}}} passage) to be displayed inside the current passage. Here the contents of {{{Event Passage}}} are displayed inside of a {{{<div>}}} element with an ID of "event".
The {{{Event Passage}}} passage has the following code in it: (and a {{{nobr}}} tag)
{{{
<<button "Add 1 to a">>
<<set _a++>>
<<replace "#event">>
<<include "Event Passage">>
<</replace>>
<</button>>
a = _a<br>
<<button "Add 1 to b">>
<<set _b++>>
<<replace "#event">>
<<include "Event Passage">>
<</replace>>
<</button>>
b = _b<br>
}}}
Clicking the buttons displayed from the {{{Event Passage}}} passage in this passage will first modify the variable's value, and then replace the contents of the "event" {{{<div>}}} element with {{{Event Passage}}} itself. This causes everything from {{{Event Passage}}} to redisplayed in the current passage within that {{{<div>}}} element, including the variables' updated values.
__''NOTE:''__ Any changes made to any variables within the passage will //''not''// get saved unless you go to another passage first. You //can// work around that issue by creating a custom {{{Config.saves.onSave}}} function, where you manually update the relevant variables in the current moment before saving.
The {{{"#event"}}} inside the {{{<<replace>>}}} macro refers to the {{{<div>}}} element with {{{id="event"}}} in it. Element IDs you use should be unique.
If you instead wanted to refer to a class, which does not need to be unique, you would use {{{".className"}}}, which would refer to all elements with {{{class="className"}}} in them.
Just remember that the {{{#}}} in {{{"#name"}}} refers to an element's {{{id}}}, and the {{{.}}} in {{{".name"}}} refers to all elements with that {{{class}}}.
Also, you can use a {{{<span>}}} element, instead of a {{{<div>}}} element, if you want to keep the replaced contents inline with other elements.<<button "Add 1 to a">>
<<set _a++>>
<<replace "#event">>
<<include "Event Passage">>
<</replace>>
<</button>>
a = _a<br>
<<button "Add 1 to b">>
<<set _b++>>
<<replace "#event">>
<<include "Event Passage">>
<</replace>>
<</button>>
b = _b<br><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. The "images" directory should be in the same 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> */
Once you've done that, to make sure you can see your images both while running inside Twine and also when opening the published versions, you can put something like this at the top of 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")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
setup.SoundPath = setup.Path + "sounds/";
}}}
It's important that this code be above any other JavaScript code which tries to use those paths, otherwise it won't work.
You'll also need to change {{{C:/Games/Twine_Sample_Code/}}} to the directory where your game is located on your machine (and be sure to use forward slashes "/", instead of the backslashes "\" that Windows prefers) if you want that to work for you inside of Twine. (Also, if there's any personal information in that path, you'll want to remove that from the published versions you distribute.)
If you're not working on Windows, and you're using Twine v2.3.2+, then you'll likely need to change {{{"/temp/"}}} to part of the default URL path that Twine uses to open your story.
''NOTE:'' This assumes you're using the offline version of Twine. If you're using the online version of Twine, then you will have to use online resources, due to browser security measures.
Once you've done that you can now load images two different ways...
Using Twine code:
{{{
[img[setup.ImagePath+'image.jpg']]
}}}
or 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.
''NOTE:'' Using the HTML method may require updating to ''SugarCube v2.23.5 or later'' due to a bug in attribute directives (using the {{{@}}} in HTML code to reference variables) in earlier versions of SugarCube. Download the latest version of <a href="http://www.motoslave.net/sugarcube/2/#downloads">SugarCube v2</a>, if you haven't already, and <a href="http://www.motoslave.net/sugarcube/2/docs/#guide-installation">install the updated format into Twine.</a> Then do "Change Story Format" in the Twine editor in your story, and set it to the updated version of SugarCube. If you aren't sure what version you're using, do "Change Story Format" to see the current version number.
If you want to make 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 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 (with added linebreaks for readability). 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">>
}}}
--
To add this functionality to your Twine game, first, create a passage for the ''SetFlag'' widget and add the "widget" and "nobr" tags to it. Then put this code in that passage:
{{{
/* SetFlag : Set Flag X to value Y (Y defaults to True). Flag names are NOT case sensitive. */
/* EXAMPLE: <<SetFlag "Mentor" "Bob">> */
/* EXAMPLE: <<SetFlag "TrialMed">> = sets TrialMed flag to True */
<<widget "SetFlag">>
<<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 */
<</widget>>
}}}
Next, for the ''Flag'' function, add the following code to your 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:\
<<capture _i>><<for _i = 0; _i < $weapons.length; _i++>>\
<<set _inv = "inv" + (_i + 1)>>\
<span @id="_inv" title="Click to make this the primary weapon"> <<link $weapons[_i].name>>
<<set _temp = $weapons[_i]>>
<<set $weapons[_i] = $weapons[0]>>
<<set $weapons[0] = _temp>>
<<set _inv = "#inv" + (_i + 1)>>
<<run $("#inv1 a").empty().wiki(" " + $weapons[0].name)>>
<<run $(_inv + " a").empty().wiki(" " + $weapons[_i].name)>>
<</link>><<if _i == 0>> (primary)<</if>></span>\
<<if _i < $weapons.length - 1>> |<</if>>\
<</for>><</capture>>
The code for the above line:
{{{
<<set $weapons = [ {name: "sword"}, {name: "axe"}, {name: "dagger"}, {name: "fist"}, {name: "rubber chicken"}]>>
Weapons:\
<<capture _i>><<for _i = 0; _i < $weapons.length; _i++>>\
<<set _inv = "inv" + (_i + 1)>>\
<span @id="_inv" title="Click to make this the primary weapon"> <<link $weapons[_i].name>>
<<set _temp = $weapons[_i]>>
<<set $weapons[_i] = $weapons[0]>>
<<set $weapons[0] = _temp>>
<<set _inv = "#inv" + (_i + 1)>>
<<run $("#inv1 a").empty().wiki(" " + $weapons[0].name)>>
<<run $(_inv + " a").empty().wiki(" " + $weapons[_i].name)>>
<</link>><<if _i == 0>> (primary)<</if>></span>\
<<if _i < $weapons.length - 1>> |<</if>>\
<</for>><</capture>>
}}}
<h1>Sample Code Updates</h1>''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">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">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|Smart Checkboxes]] code is no longer needed for SugarCube v2.32.0 or higher.
''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|Smart Checkboxes]].
''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, adding explanations and visible source code. Also, fixed example 3 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.
''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.
<h1>Maximum Checked Checkboxes</h1>This code limits the number of checkboxes that can be checked (2 in this case).
<label><input type="checkbox" id="cb1" value="Good" class="chkGroup"> Good</label>
<label><input type="checkbox" id="cb2" value="Fast" class="chkGroup"> Fast</label>
<label><input type="checkbox" id="cb3" value="Cheap" class="chkGroup"> Cheap</label>
<<script>>
$(document).one(':passagerender', function (ev) {
function selectiveCheck (event) {
var i, checkedChecks = document.querySelectorAll(".chkGroup:checked");
$("#result").empty();
for (i = 0; i < checkedChecks.length; i++) {
if (checkedChecks.length < max + 1) {
$("#result").wiki($(checkedChecks[i]).attr("value") + " ");
} else if (event.target != checkedChecks[i]) {
$("#result").wiki($(checkedChecks[i]).attr("value") + " ");
}
}
if (checkedChecks.length >= max + 1)
return false;
}
var checks = $(ev.content).find(".chkGroup");
var max = 2;
for (var i = 0; i < checks.length; i++) {
checks[i].onclick = selectiveCheck;
}
});
<</script>>
Results: <span id="result"></span>
The above code is:
{{{
<label><input type="checkbox" id="cb1" value="Good" class="chkGroup"> Good</label>
<label><input type="checkbox" id="cb2" value="Fast" class="chkGroup"> Fast</label>
<label><input type="checkbox" id="cb3" value="Cheap" class="chkGroup"> Cheap</label>
<<script>>
$(document).one(':passagerender', function (ev) {
function selectiveCheck (event) {
var i, checkedChecks = document.querySelectorAll(".chkGroup:checked");
$("#result").empty();
for (i = 0; i < checkedChecks.length; i++) {
if (checkedChecks.length < max + 1) {
$("#result").wiki($(checkedChecks[i]).attr("value") + " ");
} else if (event.target != checkedChecks[i]) {
$("#result").wiki($(checkedChecks[i]).attr("value") + " ");
}
}
if (checkedChecks.length >= max + 1)
return false;
}
var checks = $(ev.content).find(".chkGroup");
var max = 2;
for (var i = 0; i < checks.length; i++) {
checks[i].onclick = selectiveCheck;
}
});
<</script>>
Results: <span id="result"></span>
}}}
Changing {{{max = 2}}} changes the maximum number of checkboxes.
You can access the value of a checkbox using:
{{{
$("#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" value="Good" class="chkGroup">
}}}
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/">''.prop()'' method</a> for details.
For a usage example:
<<button "Get values">>
<<run $("#values").empty()>>
<<set _cbox = $(".chkGroup")>>
<<for _i = 0; _i < _cbox.length; _i++>>
<<run $("#values").wiki("<br>" + $(_cbox[_i]).val() + " = " + $(_cbox[_i]).prop("checked"))>>
<</for>>
<</button>>
Values: <span id="values"></span>
The above code is:
{{{
<<button "Get values">>
<<run $("#values").empty()>>
<<set _cbox = $(".chkGroup")>>
<<for _i = 0; _i < _cbox.length; _i++>>
<<run $("#values").wiki("<br>" + $(_cbox[_i]).val() + " = " + $(_cbox[_i]).prop("checked"))>>
<</for>>
<</button>>
Values: <span id="values"></span>
}}}<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.
<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) {
var Value = this.args[0];
if (typeof Value === "string" || Value instanceof String) {
var element = null, params = undefined;
if (this.args.length > 1) {
params = this.args[1];
}
// wait for element
var 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 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 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, just add the following to your story's JavaScript section:
{{{
/* hashStr - Start */
window.hashStr = function(txt) {
var 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>Multiselect Listboxes</h1>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, 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 and their Value">>
<<set _values = []>>
<<set _list = $("#testList").prop("options")>>
<<set _len = _list.length>>
<<for _i = _len - 1; _i >= 0; _i-->>
<<if _list[_i].selected>>
<<set _values.push(_list[_i].value)>>
<<run alert(_list[_i].label + ": " + _list[_i].value)>>
<</if>>
<</for>>
<</button>>
Note that the selected items displayed are shown in reverse order. This is done intentionally so that if you wanted to use this code to remove items from an array before going to another passage, you could do that without having to worry about the indexes changing as you removed them.
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 and their Value">>
<<set _values = []>>
<<set _list = $("#testList").prop("options")>>
<<set _len = _list.length>>
<<for _i = _len - 1; _i >= 0; _i-->>
<<if _list[_i].selected>>
<<set _values.push(_list[_i].value)>>
<<run alert(_list[_i].label + ": " + _list[_i].value)>>
<</if>>
<</for>>
<</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 above code uses the <a href="http://api.jquery.com/prop/">jQuery .prop() method</a> to get the properties from the listbox.
Also, as an example, the above code fills in the {{{_values}}} array with the value of each selected item. You can remove that code if you don't need to use those values in your code.
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:
{{{
<<script>>
$(document).one(':passagerender', function (ev) {
var lst = $(ev.content).find("#testList").prop("options"), i;
for (i = 0; i < lst.length; i++) {
lst[i].selected = false;
}
});
<</script>>
}}}
(''NOTE:'' The code inside of the {{{<<script>>}}} tags is JavaScript code.)
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.
If you want the "{{{[Ctrl]}}}" and other keys to display as seen here, then you'll need to add this to your Stylesheet section as well:
{{{
code {
background-color: #333;
padding: 0px 3px 2px 3px;
}
}}}<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>>}}} 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 passage with a "widget" tag:
{{{
<<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">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">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"><<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. */
var 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:
{{{
var 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">{{{variables()}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-temporary">{{{temporary()}}}</a> functions or the <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-method-getvar">{{{State.getVar()}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-method-setvar">{{{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">{{{State.variables}}}</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#state-api-getter-temporary">{{{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. For example, <<hovertip 'And now you can see the "hovertip" text.'>>hover your mouse over this text.<</hovertip>> Note that the tip will also appear if it's clicked on, so this works on mobile devices as well.
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 above example:
{{{
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.
By default the maximum width of the tip window is 330 pixels, except in the UI bar where it defaults to a maximum width of 230 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, except in the UI bar where it defaults to a maximum width of 230 pixels.">>Default 330px width<</hovertip>>
<<hovertip "By default the maximum width of the tip window is 330 pixels, except in the UI bar where it defaults to a maximum width of 230 pixels." 500>>Custom 500px width<</hovertip>>
The above code looks like this:
{{{
<<hovertip "By default the maximum width of the tip window is 330 pixels, except in the UI bar where it defaults to a maximum width of 230 pixels.">>Default 330px width<</hovertip>>
<<hovertip "By default the maximum width of the tip window is 330 pixels, except in the UI bar where it defaults to a maximum width of 230 pixels." 500>>Custom 500px width<</hovertip>>
}}}
To add the {{{<<hovertip>>}}} macro to your project, just add the following code to your JavaScript section:
{{{
/* hovertip v2.0 - Start */
window.UpdateHoverTipTxt = function (container) {
if (Engine.isIdle()) {
clearInterval(HTTIntervalID);
if (container === undefined) {
container = $(document);
} else {
container = $(container);
}
var i, id, top, left, parent, elementList, element, hoverPos, boxPos, zindex;
elementList = container.find('span[id^="hoverTipTxt"]');
for (i = 0; i < elementList.length; i++) {
element = $(elementList[i]);
id = elementList[i].id.substring(11);
/* Find parent hoverTip item on the page. */
parent = $(container).find("#hoverTip" + (id));
/* Position bottom of hoverTipTxt just above the parent. */
top = Math.round(-element.outerHeight() - 6);
/* Center hoverTipTxt horizontally over parent. */
left = Math.round((parent.outerWidth() - element.outerWidth()) / 2);
/* See if the hoverTip is contained by something with a higher z-index. */
zindex = element.css("z-index");
if (zindex === "auto") {
zindex = 0;
} else {
zindex = parseInt(zindex, 10);
}
while (parent.parent()[0] !== document) {
if ((parent.parent().css("z-index") !== "auto") && (parseInt(parent.parent().css("z-index"), 10) > zindex)) {
/* Get container rect. */
boxPos = parent[0].getBoundingClientRect();
break;
}
parent = parent.parent();
}
/* Update position. */
element.css({ top: top, left: left });
hoverPos = element[0].getBoundingClientRect();
/* Make sure the text isn't outside the bottom of the screen. */
if (hoverPos.top > window.innerHeight - hoverPos.height - 10) {
top -= hoverPos.top - (window.innerHeight - hoverPos.height - 10);
}
/* Make sure the text isn't outside the top of the screen. */
if (hoverPos.top < 4) {
top -= hoverPos.top - 4;
}
/* Make sure the text isn't outside the right of the screen. */
if (hoverPos.left > window.innerWidth - hoverPos.width - 26) {
left -= hoverPos.left - (window.innerWidth - hoverPos.width - 26);
}
/* Make sure the text isn't outside the left of the screen. */
if (hoverPos.left < 4) {
left -= hoverPos.left - 4;
}
/* Update position. */
element.css({ top: Math.round(top), left: Math.round(left) });
hoverPos = element[0].getBoundingClientRect();
if (boxPos) { /* Fit within dialog boxes and the like. */
/* Make sure the text isn't outside the bottom of the box. */
if (hoverPos.top > boxPos.bottom - hoverPos.height - 10) {
top -= hoverPos.top - (boxPos.bottom - hoverPos.height - 10);
}
/* Make sure the text isn't outside the top of the box. */
if (hoverPos.top < boxPos.top + 4) {
top -= hoverPos.top - (boxPos.top + 4);
}
/* Make sure the text isn't outside the right of the box. */
if (hoverPos.left > boxPos.right - hoverPos.width - 26) {
left -= hoverPos.left - (boxPos.right - hoverPos.width - 26);
}
/* Make sure the text isn't outside the left of the box. */
if (hoverPos.left < boxPos.left + 4) {
left -= hoverPos.left - boxPos.left - 4;
}
/* Update position. */
element.css({ top: Math.round(top), left: Math.round(left) });
}
}
} else {
clearInterval(HTTIntervalID);
HTTIntervalID = setInterval(UpdateHoverTipTxt, 300);
}
};
/* Waits for passage to be fully rendered before doing anything. */
var HTTIntervalID = 0;
$(document).on(":passageend", function (ev) {
UpdateHoverTipTxt();
});
$(window).on("resize scroll", function (ev) {
clearInterval(HTTIntervalID);
HTTIntervalID = setInterval(UpdateHoverTipTxt, 300);
});
$("#ui-bar-toggle").on("click", function (ev) {
clearInterval(HTTIntervalID);
HTTIntervalID = setInterval(UpdateHoverTipTxt, 300);
});
/* <<hovertip>> macro */
Macro.add("hovertip", {
tags : null,
handler : function () {
if (this.args.length > 0) {
var mw = "";
if ((this.args.length > 1) && (!isNaN(parseInt(this.args[1], 10)))) {
mw = ' style="max-width: ' + parseInt(this.args[1], 10) + 'px;"';
}
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++;
}
var output = '<span id="hoverTip' + State.temporary.HoverTipCount +
'" class="hoverTipTxt hoverTip" tabindex="0" ' +
'onmouseenter="UpdateHoverTipTxt();">' +
this.payload[0].contents + '<span id="hoverTipTxt' +
State.temporary.HoverTipCount + '" class="hoverBox hoverTail"' +
mw + '>' + this.args[0] + '</span></span>';
$(this.output).wiki(output);
} else {
$(this.output).wiki(this.payload[0].contents);
}
}
});
/* hovertip v2.0 - End */
}}}
And this CSS code to your Stylesheet section:
{{{
/* hovertip v2.0 - Start */
.hoverTip {
display: inline-block;
top: 4px;
border-width: 0px;
border-bottom: 2px dotted white; /* If you want dots under the hoverable text */
}
.hoverTipTxt {
position: relative;
vertical-align: text-bottom;
line-height: initial;
}
.hoverTipTxt .hoverBox {
position: absolute;
visibility: hidden;
}
.hoverTipTxt:hover .hoverBox, .hoverTipTxt:focus .hoverBox {
visibility: visible;
}
.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;
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;
}
.hoverTail::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;
}
#ui-bar .hoverBox {
max-width: 230px;
}
/* hovertip v2.0 - 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 your text inside of a span with the "drunk" class, like this:
{{{
<span class="drunk">Drunk text goes here.</span>
}}}
And that would look 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.
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 for comparison:
<<checkbox "$test" false true>> Separate text
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 for comparison:
<<checkbox "$test" false true>> Separate text
}}}
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 passage with "widget" and "nobr" 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">>
*/
<<widget "checkboxPlus">>
/* 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>
<</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: 2px;
top: -1px;
}
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: 150%;
position: absolute;
text-align: center;
transform: translate(-130%, 20%);
}
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.<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>><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.)
You'll also need something like this in your Stylesheet section:
{{{
/* Multicolor Links - Start */
.redlink {
color: red;
}
a:hover .redlink {
color: pink;
}
.orangelink {
color: orange;
}
a:hover .orangelink {
color: #FFE1AB;
}
.yellowlink {
color: yellow;
}
a:hover .yellowlink {
color: #FFFBD8;
}
.greenlink {
color: green;
}
a:hover .greenlink {
color: lightgreen;
}
.bluelink {
color: blue;
}
a:hover .bluelink {
color: lightblue;
}
.purplelink {
color: purple;
}
a:hover .purplelink {
color: orchid;
}
.magentalink {
color: magenta;
}
a:hover .magentalink {
color: #FFC0FF;
}
/* Multicolor Links - End */
}}}
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>
{{{
<a data-passage="Main Menu" class="link-internal" tabindex="0">Normal text <span class="redlink">red text</span> normal text.</a>
}}}
The value of the "data-passage" attribute tells Twine which passage the link will send you to. In this case it's the "Main Menu" passage.
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 the "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 {
color: pink;
}
}}}
The first part determines what color font the text with the "redlink" class will have. The second part does the same thing, but gives a different color for when the user hovers their mouse over the link. This "hover" color should be different from the non-hover color so that the user can tell this is a clickable link when they hover their mouse over it.
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">color names</a> for each color, you can use <a href="https://www.w3schools.com/colors/colors_rgb.asp">RGB color values</a> instead. For example:
{{{
.magentalink {
color: #FF00FF;
}
a:hover .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.
''Note:'' This uses the {{{.findIndex()}}} method, which is not supported in Internet Explorer. If you want to use this code in IE you'll need to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex#Polyfill">this polyfill</a> for the {{{.findIndex()}}} method.
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}}}.
''You can try out the above here:''
EXP = <<textbox "$exp" "0" autofocus>><<button "Get Level">><<run alert("level = " + getLevel(parseInt($exp)))>><</button>>
<<script>>
$(document).one(":passagerender", function (ev) {
$(ev.content).find("#textbox-exp").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-exp + button").trigger("click");
}
});
});
<</script>>
Here's the above code:
{{{
EXP = <<textbox "$exp" "0" autofocus>><<button "Get Level">><<run alert("level = " + getLevel(parseInt($exp)))>><</button>>
<<script>>
$(document).one(":passagerender", function (ev) {
$(ev.content).find("#textbox-exp").on("keyup", function (e) {
if (e.keyCode === 13) {
$("#textbox-exp + button").trigger("click");
}
});
});
<</script>>
}}}
To make this work, in addition to the above ''StoryInit'' code, put this in your JavaScript section:
{{{
/* getLevel - Start */
window.getLevel = function(exp) {
return setup.levels.findIndex(function(element) {
return element > exp;
});
};
/* getLevel - End */
}}}<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 passage with "{{{widget}}}" and "{{{nobr}}}" tags:
{{{
<<widget "toggleLink">>
<<if ndef _tlink>>
<<set _tlink = 1>>
<<else>>
<<set _tlink++>>
<</if>>
<<set _linkStr = "$('.togglelink[id!=\\'tlink" + _tlink + "\\']')">>
<span @id="'tlink' + _tlink" class="togglelink"
@onmouseenter="_linkStr + '.addClass(\'disabled\')'"
@onmouseleave="_linkStr + '.removeClass(\'disabled\')'">
<<link $args[0] $args[1]>><</link>>
</span>
<</widget>>
}}}
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:
{{{
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 = "C:/Games/MyGame/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.SoundPath = setup.Path + "sounds/";
setup.ImagePath = setup.Path + "images/";
setup.JSLoaded = false;
var 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);
}
);
}}}
You'll need to change {{{"C:/Games/MyGame/"}}} to the directory path of the game's HTML on the machine you're developing on (make sure you use slashes {{{/}}} in the file path). You can also modify the sound and image paths as needed so it will work properly on your machine. Also, you may need to change {{{/temp/}}} if that's not the default temporary directory that Twine uses on your computer.
The sample code above loads the <a href="https://api.jqueryui.com/category/all/">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">''importStyles()''</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#functions-function-importscripts">''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">LoadScreen API</a> and <a href="http://www.motoslave.net/sugarcube/2/docs/#engine-api-method-play">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">''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">{{{<<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 unless no "between" range is matched.
<<between 0 2>>
#1) This text appears if $X is >= 0 and $X <= 2.
<<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 "between" range is matched.
<</selectRange>>
}}}
It's possible to trigger more than one "between" at a time. In the above example, if {{{$X}}} = 5 then both lines #2 and #3 will be shown.
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, just add the following code to your JavaScript section:
{{{
/* <<selectRange>> macro - Start */
Macro.add('selectRange', {
skipArgs : false,
tags : ["between", "selectDefault"],
handler : function () {
if (this.args.length < 1) {
throw new Error("macro needs a value parameter to test.");
}
var val = parseInt(this.args[0], 10);
if (isNaN(val)) {
throw new Error("macro's value parameter must be an integer.");
}
var noMatch = true, i, hi, lo, header = "", shownHeader = true;
for (i = 0; i < this.payload.length; ++i) {
switch (this.payload[i].name) {
case "between":
if (this.payload[i].args.length < 2) {
throw new Error("<<between>> tag needs two value parameters to test the range.");
}
lo = parseInt(this.payload[i].args[0]);
hi = parseInt(this.payload[i].args[1]);
if (isNaN(lo) || isNaN(hi)) {
throw new Error("<<between>> tag's value parameters must be integers.");
}
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":
if (noMatch) {
$(this.output).wiki(this.payload[i].contents);
}
break;
default:
if (i == 0) {
header = this.payload[0].contents;
shownHeader = false;
}
}
}
}
});
/* <<selectRange>> macro - End */
}}}<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 it 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="http://sensi.org/~svo/glasstty/">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">VT323 font</a> from <a href="https://fonts.google.com/?category=Sans+Serif">Google Fonts</a>.
</div>
To use this style, put this in your Stylesheet section:
{{{
@import url("https://fonts.googleapis.com/css?family=VT323&display=swap");
@font-face {
/* Font source: http://sensi.org/~svo/glasstty/ */
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://C:/Games/MyGame/Glass_TTY_VT220.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
.passage {
color: #a3ff99;
text-shadow: 0px 0px 20px #5dff4b;
font-family: Glass, VT323, monospace;
font-size: 20px;
line-height: 22px;
}
.passage a, .passage a:hover, .passage a:active {
color: #a3ff99;
text-shadow: 0px 0px 20px #5dff4b;
text-decoration: underline;
}
}}}
You'll need to change {{{"C:/Games/MyGame/"}}} above to the path to the directory where you're testing your game on your computer in if you want it to display correctly within Twine.
If you want the links to remain the normal blue color, then you can replace that last part with this:
{{{
.passage a, .passage a:hover, .passage a:active {
text-shadow: 0px 0px 20px #618eff;
}
}}}
If you only want text to appear like that in part of the game, change all of the {{{.passage}}} parts of the CSS above to {{{.dumb_terminal}}} instead, and 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>
}}}
<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 (see the bar at the bottom of this screen). 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.
Test links:
[[A "tbar" Tagged Passage]]
[[A "bbar" and "tbar" Tagged Passage]]
To add this to your game, first, create a passage named ''PassageFooter'' with this code inside of it:
{{{
<<if tags().includes("bbar")>>
<div id="bottombar"><div id="bbblock"><div id="bbtext">Put your bottom bar text here.</div></div></div><</if>>
}}}
Just put your text, links, or whatever you want where it says, "Put your text here." It will automatically make the bar the correct height to contain the content. (In most cases you'll want to make sure you include the blank line to give a bit of space at the bottom of your passages as well.)
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}}}".)
Next, you'll need to put this in your ''Stylesheet'' section:
{{{
/* Bottom Bar styling - Start */
#bottombar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #333;
border-top: 1px solid #444;
box-shadow: 0 0 10px 0 #333;
z-index: 40;
}
#bbblock {
margin-left: 20em;
margin-right: 2.5em;
-o-transition: margin .2s ease-in;
transition: margin .2s ease-in;
}
#bbtext {
text-align: center;
max-width: 54em;
margin: 0 auto;
}
#ui-bar.stowed~#story #bbblock {
margin-left: 4.5em;
}
@media screen and (max-width: 1136px) {
#bbblock {
margin-left: 19em;
margin-right: 1.5em;
}
#ui-bar.stowed~#story #bbblock {
margin-left: 3.5em;
}
}
@media screen and (max-width: 768px) {
#bbblock {
margin-left: 3.5em;
}
}
/* Bottom Bar styling - End */
}}}
Now, in any passage which has a "''bbar''" tag, you'll see a bar at the bottom of the passage with your content in it.
If you wanted a input textbox in the bottom bar, then you could add this CSS:
{{{
#bbblock input#textbox-input {
width: 100%;
background-color: black;
}
}}}
and change the ''PassageFooter'' passage to this:
{{{
<<if !tags().includes("NoBBar")>>
<div id="bottombar"><div id="bbblock"><<textbox "$input" "" autofocus>></div></div><</if>><<script>>
$(document).one(":passagerender", function (event) {
$(event.content).find("#textbox-input").attr("placeholder", "Enter text here");
});
<</script>>
}}}
If you change {{{$input}}} to some other variable, like {{{$test}}}, then you'll have to change "#textbox-input" to match, like "#textbox-test". For a temporary variable, like {{{_test}}}, you'd use two dashes, like "#textbox--test". Also, you can change "Enter text here" to whatever placeholder text you want inside the input textbox.
If the bottom bar is too tall and cuts off the bottom of your text, then you'll need to add something like this:
{{{
body.bbar #passages {
margin-bottom: 140px;
}
}}}
That will add a margin of 140 pixels to the bottom of any passage with a "''bbar''" tag. You'll have to adjust that height appropriately to the height of your bottom bar.
If you wanted a top bar instead, you only need to change {{{bottom: 0;}}} to {{{top: 0;}}} in the CSS, put the ''PassageFooter'' code into the ''PassageHeader'' passage instead, and add a little code (shown below) to adjust the top margin.
If you want to have the option of both top and bottom bars, then add this to your ''Stylesheet'' section in addition to the earlier "Bottom Bar" CSS:
{{{
#topbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #333;
border-top: 1px solid #444;
box-shadow: 0 0 10px 0 #333;
z-index: 40;
}
}}}
And put this in the ''PassageHeader'' passage:
{{{
<<if tags().includes("tbar")>>\
<div id="topbar"><div id="bbblock"><div id="bbtext">Put your top bar text here.</div></div></div><</if>>\
<<script>>
$(document).one(":passagedisplay", function (event) {
if ($("#topbar").length) {
$("#passages").css("margin-top", $("#topbar").outerHeight() + 10);
} else {
$("#passages").css("margin-top", 0);
}
});
<</script>>\
}}}
And now a "''tbar''" tag on a passage would make it show that top bar.
Enjoy!
[[Bottom and Top Bars]]
[[A "tbar" Tagged Passage]]
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?[[Bottom and Top Bars]]
[[A "bbar" and "tbar" Tagged Passage]]
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.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.
Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien.<<if tags().includes("bbar")>><div id="bottombar"><div id="bbblock"><div id="bbtext">Put your bottom bar text here.</div></div></div><</if>><h1>Using jQuery UI in Twine</h1><a href="https://jqueryui.com/demos/">''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. So, let me save you the trouble of figuring it all out on your own like I did. ;-P
To use jQuery UI in Twine with the SugarCube story format, first you'll first want to <a href="https://jqueryui.com/themeroller/">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:
{{{
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 this HTML file is
// located if you want to run this from inside Twine.
setup.Path = "C:/Games/MyGame/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
setup.SoundPath = setup.Path + "sounds/";
/* 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 */
}}}
You'll need to change {{{"C:/Games/MyGame/"}}} to the path to your game's directory, and that should make sure that your code works when run both from within Twine and from the published HTML version.
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/">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.
var 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/">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.
var 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:
{{{
<<script>>
$(document).one(":passagerender", function(event) {
// Passage is about to be displayed.
$(event.content).find(".draggable").draggable(); // Make "draggable" elements draggable.
$(event.content).find(".droppable").droppable(); // Make "droppable" elements droppable.
});
<</script>>
}}}
''NOTE:'' If you're using jQuery UI in your project, then when distributing your project you'll need to make sure you include 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">Geolocation API</a> built into most (but not all) browsers, to do that yourself.
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 */
setup.geoFindMe = function() {
function success(position) {
var latitude = position.coords.latitude;
var 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.)
Also note that the code will only work within a browser that supports it. For example, if you're running this within the offline version of Twine prior to v2.3.2, then it will fail. However, the published version should work fine in most browsers, provided that you allow it access to your geolocation data.
<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">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
var 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>
}}}
and the Stylesheet code:
{{{
/* 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);
}
}}}
This button also pauses/plays the video.
<span id="btn"><<button "Pause">>
<<script>>
// Get the video
var 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><h1>Lined Paper and Handwriting</h1><<set $font = 0>>If you want something that looks like handwriting on lined paper, you can use this.
<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.
</div>\
</section>
''Font:'' <label><<radiobutton "$font" 0 checked>> "Waiting for the Sunrise"</label> / <label><<radiobutton "$font" 1>> "Reenie Beanie"</label> / <label><<radiobutton "$font" 2>> "Dancing Script"</label> / <label><<radiobutton "$font" 3>> generic "cursive"</label>
Here's the above code:
{{{
<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.
</div>\
</section>
}}}
To use that, you'll need to add this 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 cursive;
font: bold 24px/20px "Dancing Script";
font: bold italic 25px "Reenie Beanie";
font: bold italic 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 */
}}}
If you would prefer that a different font besides "Waiting for the Sunrise" be the primary font, simply put the preferred font at the bottom of the list of fonts found near the top of the {{{.paper}}} styling. Basically, the last font listed there that actually works will be the one that's applied.
If you're putting your game online, then you'll need to add this to the top of your Stylesheet section for the fonts to work:
{{{
/* Handwriting - 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=Dancing+Script&display=swap');
/* Handwriting - End */
}}}
That loads the "Waiting for the Sunrise", "Reenie Beanie", and "Dancing Script" fonts from <a href="https://fonts.google.com/?category=Handwriting">Google Fonts</a>, which means that the user will have to be online for those fonts to work.
It the game is being run locally, instead of online, then instead of the above method, you'll want to put the <a href="https://raw.githubusercontent.com/typekit/webfontloader/master/webfontloader.js">"Web Font Loader" code</a> at the bottom of your JavaScript section, followed by something like this:
{{{
WebFont.load({
google: {
families: ["Waiting for the Sunrise", "Reenie Beanie", "Dancing Script"]
}
});
}}}
That will load those fonts from the "Google Fonts" site, so the computer will need to be online for that code to work. (''Note:'' This method and the following one also work when the game is accessed online.)
If the game is run locally, but 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). (''Note:'' While WOFF is suppored in the IE browser, WOFF2 is not.) 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:
{{{
@font-face {
/* Font source: http://sensi.org/~svo/glasstty/ */
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://C:/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">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-family: "Reenie Beanie";
src: url("fonts/ReenieBeanie-Regular.ttf") format("truetype"),
url("file://C:/Games/MyGame/fonts/ReenieBeanie-Regular.ttf") format("truetype");
font-weight: bold;
font-style: italic;
}
}}}
Make sure you change the {{{C:/Games/MyGame}}} part to the path you're using on your development computer. (''Note:'' You will likely need to change any spaces in the path to {{{%20}}}, which represents a space.)
Also, be aware that the capitalization of the paths and filenames matter on most non-Windows machines, so make sure that the capitalization of the paths and filenames used in your code exactly match the actual capitalization of the paths and filenames.
Enjoy!<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 passage with "widget" and "nobr" tags, put this:
{{{
/* <<countdownTimer>> Widget - Start */
<<widget "countdownTimer">>
<<set _seconds = $args[0]>>
<<set _minutes = Math.floor(_seconds / 60)>>
<<set _replacementPassage = $args[1]>>
<div id="timer" class="timergreen">Time remaining _minutes:<<= (_seconds - (_minutes * 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 {
var curtime = new Date(), str, seconds = State.temporary.seconds;
var 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>>
<</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 {{{_seconds}}} variable. For example:
<<button "Add 1 min">>
<<set _seconds += 60>>
<</button>>
The code for that button:
{{{
<<button "Add 1 min">>
<<set _seconds += 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.seconds += 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.seconds += 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 = _seconds]]
<<countdownTimer 60 "Done">>
}}}
or, if you need to, you can combine that with clearing the pause/unpause interval like this:
{{{
[[Next][$sec = _seconds; clearInterval(_int)]]
<<countdownTimer 60 "Done">>
}}}
Either way, that keeps track of the number of seconds (by keeping track of the value of {{{_seconds}}} in a story variable) so that you can use it in subsequent passages like this:
{{{
[[Repeat][$sec = _seconds]]
<<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 {{{_seconds}}}, 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.
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 {
display: none !important;
}
}}}
and add this to your JavaScript section:
{{{
/* ToggleImages JavaScript - Start */
function allowHide(el) {
return (!$(el).hasClass("nohide")) && (!$(el).parent().hasClass("nohide"));
}
window.imageRecheck = function () {
if (settings.images) {
$("img").each(function () {
if (allowHide(this)) {
$(this).addClass("hidden");
}
});
}
};
var toggleImages = function () {
if (settings.images) {
// Hide images
$(".toggle-wrapper").removeClass("pushed");
window.imageRecheck();
} else {
// Show images
$(".toggle-wrapper").addClass("pushed");
$("img").removeClass("hidden");
$("img").each(function () {
if (allowHide(this)) {
$(this).attr("src", $(this).data("src"));
}
});
}
};
Setting.addToggle("images", { label: "Hide images?", onChange: toggleImages });
/* You can do <<run toggleImg()>> to toggle the display of images,
<<run toggleImg(true)>> to hide images,
and <<run toggleImg(false)>> to show images. */
window.toggleImg = function (val) {
if (val === undefined) {
settings.images = !settings.images;
} else {
settings.images = !!val;
}
Setting.save();
toggleImages();
return settings.images;
};
$(document).on("click", ".toggle_handler", function (event) {
window.toggleImg();
});
$(document).on(":passagerender", function (event) {
$(event.content).find("img").each(function () {
if (allowHide(this)) {
$(this).data("src", $(this).attr("src"));
if (settings.images) {
$(this).addClass("hidden").removeAttr("src");
}
}
});
});
$(document).on(":passageend", function (event) {
window.imageRecheck();
});
/* ToggleImages JavaScript - End */
}}}
That should //probably// be all you need.
However, if you're adding images after the passage is initially displayed, such as when a user clicks on something, then you'll need to add the following after adding any images:
{{{
<<run imageRecheck()>>
}}}
For example:
<<button "Add Image">>
<<append "#tstimg">>[img[setup.ImagePath+"Prism64-2.png"]]<</append>>
<<run imageRecheck()>>
<</button>>: <span id="tstimg"></span>
Source:
{{{
<<button "Add Image">>
<<append "#tstimg">>[img[setup.ImagePath+"Prism64-2.png"]]<</append>>
<<run imageRecheck()>>
<</button>>: <span id="tstimg"></span>
}}}
Using the {{{imageRecheck()}}} function, after adding the image, prevents the appended image from displaying if images are disabled.
Also, 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 name for the save, it's actually pretty simple to do that.
Simply add the following code to your JavaScript section:
{{{
Config.saves.onSave = function (save, details) {
if (details.type === "slot") {
var title = prompt("Enter Save Slot Title:", save.title);
if (title !== null) {
save.title = title;
}
}
};
}}}
Now, when the user chooses to save in a save slot, it will ask them to enter in their own save slot title, 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.33.0 or later, as the "details" parameter was added at that point. See the <a href="https://www.motoslave.net/sugarcube/2/docs/#config-api-property-saves-onsave">''Config.saves.onSave'' documentation</a> for details.
Enjoy!<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, 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.
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 the 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).
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">{{{<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">{{{<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">{{{<input type="text">}}}</a>" and "<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text">{{{<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">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..." }`>>}}}
<<textboxPlus "No spellcheck or autocomplete: " "_text" `{ spellcheck: false, autocomplete: "off" }`>>
Source: {{{<<textboxPlus "No spellcheck or autocomplete: " "_text" `{ spellcheck: false, autocomplete: "off" }`>>}}}
<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to go to the Main Menu: " "$menu" `{ passage: "Main Menu", default: $menu }`>>
Source: {{{<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to go to the Main Menu: " "$menu" `{ passage: "Main Menu", default: $menu }`>>}}}
<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to have that text displayed: " "$input1" `{ onreturn: "<<run alert('You entered: ' + $input1)>>", default: $input1 }`>>
Source: {{{<<textboxPlus "Hit ''RETURN'' or ''ENTER'' within to have that text displayed: " "$input1" `{ onreturn: "<<run alert('You entered: ' + $input1)>>", default: $input1 }`>>}}}
<<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: {{{<<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 "{{{nobr}}}" and "{{{widget}}}" tags, and put the following code inside it:
{{{
/* <<textboxPlus>> widget v1.3 - 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
*/
<<widget "textboxPlus">>
<<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][$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>>
<</widget>>
<<script>>
$(document).on(":passagerender", function (event) {
/* Update textboxPlus input boxes. */
$(event.content).find(".textboxplus").each(function () {
var options = {}, props = {};
var 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);
}
});
var changeCode = $(this).data("onchange");
if (changeCode) {
$(this).find("input").on("change", function (event) {
$.wiki(changeCode);
});
}
var inputCode = $(this).data("oninput"), parent = this;
if (inputCode) {
$(this).find("input").on("input", function (event) {
State.setVar($(parent).data("variable"), $(this).val());
$.wiki(inputCode);
});
}
var 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 when an image is loaded or if it fails, you can use the code for the {{{imageExists()}}} function below.
To use this function, simply add this 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")) {
// Change this to the path where the HTML file is
// located if you want to run this from inside Twine.
setup.Path = "C:/Games/Twine_Sample_Code/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
setup.SoundPath = setup.Path + "sounds/";
/* imageExists() function - Start */
window.imageExists = function(image_url, selector, failcode) {
function Loaded() {
ret.status = true; /* ending status */
if (selector) {
$(selector).attr("src", image.src);
}
}
function Failure() {
ret.status = false; /* ending status */
if (failcode) {
$.wiki(failcode);
}
}
var image = new Image();
/* Return an object to indicate the image's loading state. */
var ret = { status: "waiting", src: setup.ImagePath + image_url, url: 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; /* load image */
return ret;
};
/* imageExists() function - End */
}}}
See the [[Displaying Images in Twine]] section for details on the first part of the above code, which makes sure that the images can load when you launch your game from Twine.
Here are some examples of how to use the {{{imageExists()}}} function:
<<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>>
<<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>>
<img id="imag">
The above code 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>>
<<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>>
<<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>>
<img id="imag">
}}}
The "Test 1" buttons use the {{{.status}}} property of the returned value to watch the status of the image loading. It can have the values of {{{"waiting"}}} while it's waiting to load, then it will either be {{{true}}} or {{{false}}} depending on whether or not the image loaded correctly, respectively.
The "Test 2" buttons pass the selector of the image 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.
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 (2020) v1.0
*/
(function() {
function scaleImageMap() {
function resizeMap() {
function resizeAreaTag(cachedAreaCoords, idx) {
function scale(coord) {
var dimension = 1 === (isWidth = 1 - isWidth) ? "width" : "height";
return (padding[dimension] + Math.floor(Number(coord) * scalingFactor[dimension]));
}
var isWidth = 0;
areas[idx].coords = cachedAreaCoords.split(",").map(scale).join(",");
}
if (image) {
var scalingFactor = {
width: $(image).width() / image.naturalWidth,
height: $(image).height() / image.naturalHeight,
};
var 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);
}
}
}
var tries = 0;
if (image) {
setupMap();
var 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
}
var /* jshint validthis:true */
map = this, areas = null,
cachedAreaCoordsArray = null,
image = null, timer = null;
if (!beenHere()) {
setup();
addEventListeners();
start();
} else {
var 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);
}
}
var 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;
};
}
var 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 <a onclick="$.wiki('<<ScrollTo "code">>')">the "FlagBit" code</a> 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 number.
For example, you could use the first five (5) bits (#0 to #4) to store a number with a range from 0 to 31 (2^5 - 1 = 31) representing the up to 32 room types, and then use the remaining 27 bits 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>To make working with bits like that a lot easier, use the following five FlagBit functions: <a onclick="$.wiki('<<ScrollTo "setFlagName">>')">''setFlagName()''</a>, <a onclick="$.wiki('<<ScrollTo "setFlag">>')">''setFlag()''</a>, <a onclick="$.wiki('<<ScrollTo "hasFlag">>')">''hasFlag()''</a>, <a onclick="$.wiki('<<ScrollTo "setVal">>')">''setVal()''</a>, and <a onclick="$.wiki('<<ScrollTo "getVal">>')">''getVal()''</a>.
----
<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 should only be used within 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).
''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>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 "<<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 value after the flag is set.
''Example:''
To add a north exit (flag = 11) to a room you could do:
{{{
<<set $room[1][5] = setup.setFlag($room[1][5], 11, enabled)>>
}}}
or:
{{{
<<set $room[1][5] = setup.setFlag($room[1][5], "North", enabled)>>
}}}
----
<h2 id="hasFlag">setup.hasFlag(value, flag) ═► flagSet</h2>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.
''Example:''
To see if a room 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:
{{{
<<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.
* ''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 oldValue, but with the bit range set to 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 use this shorter method:
{{{
<<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.
''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 use this shorter method:
{{{
<<set _roomType = setup.getVal($room[1][5], 4)>>
}}}
----
<span id="code"></span>
''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 () {
var 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)) {
var 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)) {
var 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.
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 (!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.");
}
var 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.");
}
var 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.
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 (!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.");
}
var 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>><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 making the image you want displayed there. It's best to either create it as a square transparent PNG image that's at least 260x260 pixels or an SVG image. It's also recommended that it isn't too elaborate, since it will usually be displayed //much// smaller.
Next, use that image to generate a favicon at a site like the <a href="https://realfavicongenerator.net/">Favicon Generator</a> site. If you use that particular site, in the "''Favicon Generator Options''" section at the bottom, set it to "I cannot or I do not want to place favicon files at the root of my web site. Instead I will place them here:" and put your path as something like "{{{images/favicon}}}".
That site will then generate some HTML code which it will tell you to put in the header of your HTML file. Unfortunately, that's not so easy if you're using the Twine 2 editor. Here's how you work around that.
The HTML it will generate will look something like this:
{{{
<link rel="apple-touch-icon" sizes="180x180" href="images/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon/favicon-16x16.png">
<link rel="manifest" href="images/favicon/site.webmanifest">
<link rel="shortcut icon" href="images/favicon/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="images/favicon/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
}}}
You'll then translate that into something like the following code in your JavaScript section:
{{{
// Make it so that paths can work properly when launched from Twine.
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 = "C:/Games/MyGameDirectory/"; // Running inside Twine application
} else {
setup.Path = ""; // Running in a browser
}
setup.ImagePath = setup.Path + "images/";
// Game icon in tab. Generated at: https://realfavicongenerator.net
var favIco = setup.ImagePath + "favicon/";
$(document.head).append('<link rel="apple-touch-icon" sizes="180x180" href="' + favIco + 'apple-touch-icon.png">');
$(document.head).append('<link rel="icon" type="image/png" sizes="32x32" href="' + favIco + 'favicon-32x32.png">');
$(document.head).append('<link rel="icon" type="image/png" sizes="16x16" href="' + favIco + 'favicon-16x16.png">');
$(document.head).append('<link rel="manifest" href="' + favIco + 'site.webmanifest">');
$(document.head).append('<link rel="shortcut icon" href="' + favIco + 'favicon.ico">');
$(document.head).append('<meta name="msapplication-TileColor" content="#da532c">');
$(document.head).append('<meta name="msapplication-config" content="' + favIco + 'browserconfig.xml">');
$(document.head).append('<meta name="theme-color" content="#ffffff">');
}}}
You'll need to change {{{"C:/Games/MyGameDirectory/"}}} to the path to your game's directory to make the code work when launched from from Twine.
Then just put the favicon images into the "{{{images/favicon}}}" directory with your HTML file and that should be it!
Enjoy!<h1>weightedEither() Function</h1>The SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#functions-function-either">{{{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">>
<<replace "#wwname">><<= weightedEither(_WWName)>><</replace>>
<</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <span id="wwname"><<= weightedEither(_WWName)>></span> attacks you!''
The above code looks like this:
{{{
Try the following example: <<button "Retry function">>
<<replace "#wwname">><<= weightedEither(_WWName)>><</replace>>
<</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <span id="wwname"><<= weightedEither(_WWName)>></span> 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">>
<<replace "#wwname2">><<set _lastName = weightedEither(_WWName, _lastName)>>_lastName<</replace>>
<</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <span id="wwname2"><<set _lastName = weightedEither(_WWName)>>_lastName</span> attacks you!''
The code for that looks like this:
{{{
For example: <<button "Retry function">>
<<replace "#wwname2">><<set _lastName = weightedEither(_WWName, _lastName)>>_lastName<</replace>>
<</button>>
<<set _WWName = {
werewolf: 80,
beast: 40,
monster: 60,
lycanthrope: 20,
"dire beast": 30
}>>
''The <span id="wwname2"><<set _lastName = weightedEither(_WWName)>>_lastName</span> 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);
var 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. */
var 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 (var 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.
''WARNING:'' The {{{<<seen>>}}} macro makes use of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a>, which does not exist in Internet Explorer. This means that this macro will not work in Internet Explorer.
''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 indicate that those parts are optional.)
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 "visibile" 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 page the following code is used:
{{{
<<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 */
setup.seenNewObservers = [];
setup.seenOldObservers = [];
Macro.add("seen", {
tags: [ "content", "hidden" ],
handler: function() {
var vis = 0;
if (this.args.length) {
vis = parseFloat(this.args[0]);
if (isNaN(vis)) {
vis = 0;
} else {
vis = vis.clamp(0, 99) / 100; // Fixes problems with 99% visibility when totally visible.
}
}
if (State.temporary.SeenElementIndex == undefined) {
State.temporary.SeenElementIndex = 0;
}
var n = ++State.temporary.SeenElementIndex;
var txt = '<span id="seen-macro-' + n + '">', hid = "";
if (this.payload.length > 1) {
for (var 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();
}
var 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 */
}}}
<<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" (<span class="sc-fa"></span>) and "forward" (<span class="sc-fa"></span>) buttons, then just add the following code to your game's JavaScript section:
{{{
/* TrackScroll code - Start */
setup.currTurn = 1;
State.variables.WinPosX = 0;
State.variables.WinPosY = 0;
$(document).on(":passageinit", function (event) {
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 (event) {
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.
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">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>>}}} 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 */
/* $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:
{{{
/* <<SetGender>> widget - Start */
<<widget "SetGender">><<nobr>>
/* 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 */
<<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 () {
var id = this.args[0], name = id;
if (this.args.length > 1) name = this.args[1];
var 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 that, see the [[Macro API|http://www.motoslave.net/sugarcube/2/docs/#macro-api]] and [[MacroContext API|http://www.motoslave.net/sugarcube/2/docs/#macrocontext-api]] sections of the SugarCube documentation.)
Then put this CSS code into your Stylesheet section:
{{{
/* speech macro - Start */
.speech {
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.
''Note:'' Because the CSS above uses relative paths, you won't be able to see those images in Twine. If you want to see them when playing it in Twine 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).one(":passagerender", function (ev) {
setTimeout(function () { /* Delay needed so CSS values are set. */
$(".avatar").each(function () {
var url = $(this).css("background-image");
url = url.replace(/\"/gi, "").replace(')','');
url = url.slice(url.lastIndexOf("/") + 1);
$(this).css("background-image", "url('file:///C:/Games/My%20Game/" + url + "')");
});
});
});
}
}}}
Just replace {{{C:/Games/My%20Game}}} with the path to your game's directory (FYI, the {{{%20}}} represents a space). 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.
<<script>>
if (document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
/* Detect if you're running in Twine. */
$(document).one(":passagerender", function (ev) {
setTimeout(function () { /* Delay needed so CSS values are set. */
$(".avatar").each(function () {
var url = $(this).css("background-image");
url = url.replace(/\"/gi, "").replace(')','');
url = url.slice(url.lastIndexOf("/") + 1);
$(this).css("background-image", "url('file:///C:/Games/Twine_Sample_Code/images/" + url + "')");
});
});
});
}
<</script>><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">{{{<<listbox>>}}} macro</a>.
----
If you want to trigger an action to occur when the user changes the option selected listbox, such as updating some text that's displayed, like this...
<blockquote><<set _classes = ["Warrior", "Archer", "Sorcerer"]>>''Select class:'' <<listbox "$class">>
<<optionsfrom _classes>>
<</listbox>>
''Your class:'' <span id="class-info">(nothing)</span><<script>>
$(document).one(":passagerender", function (event) {
/* Initial display of text pulled from the "Class Info X" passages. */
$(event.content).find("#class-info").empty().wiki("<<include 'Class Info " + State.variables.class + "'>>");
/* Trigger text display upon listbox change. */
$(event.content).find("#listbox-class").on("change", function (event) {
/* Fade out text. */
$("#class-info").fadeOut(500, function () {
/* Update text and then fade it back in. */
$("#class-info").empty().wiki("<<include 'Class Info " + State.variables.class + "'>>").fadeIn(500);
});
});
});
<</script>></blockquote>...then you can use some code like this:
{{{
<<set _classes = ["Warrior", "Archer", "Sorcerer"]>>''Select class:'' <<listbox "$class">>
<<optionsfrom _classes>>
<</listbox>>
''Your class:'' <span id="class-info">(nothing)</span><<script>>
$(document).one(":passagerender", function (event) {
/* Initial display of text pulled from the "Class Info X" passages. */
$(event.content).find("#class-info").empty().wiki("<<include 'Class Info " + State.variables.class + "'>>");
/* Trigger text display upon listbox change. */
$(event.content).find("#listbox-class").on("change", function (event) {
/* Fade out text. */
$("#class-info").fadeOut(500, function () {
/* Update text and then fade it back in. */
$("#class-info").empty().wiki("<<include 'Class Info " + State.variables.class + "'>>").fadeIn(500);
});
});
});
<</script>>
}}}
The above uses the {{{_classes}}} array to fill in the options given by the {{{<<listbox>>}}} macro. The values in that array are also used to get generate the "Class Info X" passage names which contain the text displayed for each class (where "X" is 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.
See the jQuery <a href="https://api.jquery.com/one/">.one()</a>, <a href="https://api.jquery.com/on/">.on()</a>, <a href="https://api.jquery.com/find/">.find()</a>, <a href="https://api.jquery.com/empty/">.empty()</a>, <a href="https://api.jquery.com/fadeOut/">.fadeOut()</a>, and <a href="https://api.jquery.com/fadeIn/">.fadeIn()</a> methods and the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#methods-jquery-method-wiki">.wiki()</a> method for details on how they work.
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 {
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 {
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">{{{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.
<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 main body image, with separate shirt and leggings images overlaid on top of it.
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, right-click on the image you want to modify, choose "Inspect" (or your browser's equivalent), and then (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 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 - Start */
// Add the SlideWin window to the page.
var 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"
);
var 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 */
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);
}
}
}}}
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)">
''Value:'' <span id="slideval"></span>
<<button "Show $$test value">>
<<run alert("$test = " + $test)>>
<</button>>
If you want to make your own slider, you just need to use the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range">{{{<input type="range">}}} element</a>. You can then use the "change" event on it to determine when the user has finished changing its value, or use the "input" event to detect any changes as they happen.
For an example of how to do that, you could first put this code into 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 replaces 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) {
var varname = $(element).data("var");
if ((varname.indexOf("$") === 0) || (varname.indexOf(".") === 0)) {
State.setVar(varname, $(element).val());
}
};
/*
Make 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 () {
var 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">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">see here</a>), so don't replace the word "this" with something else.
You could then use that code to do what we did at the top of the page by doing 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)">
''Value:'' <span id="slideval"></span>
<<button "Show $$test value">>
<<run alert("$test = " + $test)>>
<</button>>
<<script>>
$(document).one(":passagerender", function (event) {
// Display initial value.
$(event.content).find("#slideval").text(State.variables.test);
// Update display when the value changes.
$(event.content).find("#slidetest").on("input", function () {
$("#slideval").empty().text(State.variables.test);
});
});
<</script>>
}}}
The first three lines create a label for the slider and the slider itself, including the range it covers and its default value. The {{{data-var}}} attribute and the {{{oninput}}} event let it trigger the ''SugarCubeInput()'' function so that the SugarCube variable named in the {{{data-var}}} attribute will get updated with the slider's value as it changes.
By adding {{{class="slider"}}} to your sliders like that, you can then style how the 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 style your own sliders, see the "<a href="http://danielstern.ca/range.css/?ref=css-tricks#/">range.css - CSS Style Generator for Range Inputs</a>" page.
The {{{<<script>>}}} section at the end just makes sure that the value displayed in the "''slideval''" {{{<span>}}} element is set initially and updated by the slider's changes as they happen.
''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 just do 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 in the first line there. (See the SugarCube <a href="https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute-directive">Attribute Directive</a> for details on how that works.)
Enjoy!
<<script>>
$(document).one(":passagerender", function (event) {
// Display initial value.
$(event.content).find("#slideval").text(State.variables.test);
// Update display when the value changes.
$(event.content).find("#slidetest").on("input", function () {
$("#slideval").empty().text(State.variables.test);
});
});
<</script>><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">''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">''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">''.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">''random()'' function</a> to get a random number, the <a href="https://www.motoslave.net/sugarcube/2/docs/#state-api-getter-variables">''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">''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">''.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">''.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">''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">{{{<<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">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">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">''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) {
var 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) {
var 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) {
var file = event.target.files[0], reader = new FileReader();
$(reader).on("load", function (ev) {
var 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) {
var lockID = LoadScreen.lock(); // Lock loading screen
importScripts(fname).then(function() {
var 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)
*
* 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) {
var 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) {
var 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) {
var 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) {
var URL = _global.URL || _global.webkitURL;
var 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 {
var 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);
var force = blob.type === 'application/octet-stream';
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader();
reader.onloadend = function () {
var 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 {
var URL = _global.URL || _global.webkitURL;
var 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:'' This code was broken in SugarCube v2.37.0 and still needs to be rewritten.
SugarCube includes some built-in versions of some of the <a href="https://fontawesome.com/">Font Awesome</a> icons. Here's how you can easily use them in your game.
First, add the following widget to a non-special passage that has both "''widget''" and "''nobr''" 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>>
}}}
to get:
Rated <<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star-solid>><<scicon star>>
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["star-solid"] = { id: "00", desc: ["star", "*"]}>>
<<set _icons["star"] = { id: "01", desc: ["star", "empty", "*"]}>>
<<set _icons["image"] = { id: "02", desc: ["image", "picture"]}>>
<<set _icons["check"] = { id: "03", desc: ["check", "true"]}>>
<<set _icons["times"] = { id: "04", desc: ["times", "false", "x", "*"]}>>
<<set _icons["plus"] = { id: "05", desc: ["plus", "add", "+"]}>>
<<set _icons["minus"] = { id: "06", desc: ["minus", "subtract", "remove", "-"]}>>
<<set _icons["question"] = { id: "07", desc: ["question", "mark", "?"]}>>
<<set _icons["question-circle"] = { id: "08", desc: ["question", "mark", "?"]}>>
<<set _icons["info"] = { id: "09", desc: ["info", "i", "information"]}>>
<<set _icons["info-circle"] = { id: "0a", desc: ["info", "i", "information"]}>>
<<set _icons["exclamation"] = { id: "0b", desc: ["exclamation", "alert", "!"]}>>
<<set _icons["exclamation-circle"] = { id: "0c", desc: ["exclamation", "alert", "!"]}>>
<<set _icons["exclamation-triangle"] = { id: "0d", desc: ["exclamation", "alert", "warning", "!"]}>>
<<set _icons["external-link"] = { id: "0e", desc: ["external", "link"]}>>
<<set _icons["lock"] = { id: "0f", desc: ["lock", "locked"]}>>
<<set _icons["lock-open"] = { id: "10", desc: ["lock", "unlocked"]}>>
<<set _icons["lightbulb"] = { id: "11", desc: ["lightbulb", "idea", "information"]}>>
<<set _icons["tag"] = { id: "12", desc: ["tag"]}>>
<<set _icons["tags"] = { id: "13", desc: ["tags"]}>>
<<set _icons["bookmark-solid"] = { id: "14", desc: ["bookmark"]}>>
<<set _icons["bookmark"] = { id: "15", desc: ["bookmark"]}>>
<<set _icons["reply"] = { id: "16", desc: ["reply", "back", "undo", "rewind"]}>>
<<set _icons["share"] = { id: "17", desc: ["share", "forward", "redo", "advance"]}>>
<<set _icons["caret-down"] = { id: "18", desc: ["caret", "down", "triangle", "arrow"]}>>
<<set _icons["caret-left"] = { id: "19", desc: ["caret", "left", "triangle", "arrow"]}>>
<<set _icons["caret-right"] = { id: "1a", desc: ["caret", "right", "triangle", "arrow"]}>>
<<set _icons["caret-up"] = { id: "1b", desc: ["caret", "up", "triangle", "arrow"]}>>
<<set _icons["chevron-down"] = { id: "1c", desc: ["chevron", "down", "angle", "arrow"]}>>
<<set _icons["chevron-left"] = { id: "1d", desc: ["chevron", "left", "angle", "arrow"]}>>
<<set _icons["chevron-right"] = { id: "1e", desc: ["chevron", "right", "angle", "arrow"]}>>
<<set _icons["chevron-up"] = { id: "1f", desc: ["chevron", "up", "angle", "arrow"]}>>
<<set _icons["arrow-down"] = { id: "20", desc: ["arrow", "down"]}>>
<<set _icons["arrow-left"] = { id: "21", desc: ["arrow", "left"]}>>
<<set _icons["arrow-right"] = { id: "22", desc: ["arrow", "right"]}>>
<<set _icons["arrow-up"] = { id: "23", desc: ["arrow", "up"]}>>
<<set _icons["redo"] = { id: "24", desc: ["redo", "arrow", "forward", "advance"]}>>
<<set _icons["undo"] = { id: "25", desc: ["undo", "arrow", "back", "rewind"]}>>
<<set _icons["sync"] = { id: "26", desc: ["sync", "arrow", "reload", "refresh", "update", "recycle"]}>>
<<set _icons["trash-solid"] = { id: "27", desc: ["trash", "delete", "full"]}>>
<<set _icons["trash"] = { id: "28", desc: ["trash", "delete", "empty"]}>>
<<set _icons["download"] = { id: "29", desc: ["download", "import", "arrow"]}>>
<<set _icons["upload"] = { id: "2a", desc: ["upload", "export", "arrow"]}>>
<<set _icons["save"] = { id: "2b", desc: ["save", "disk", "load"]}>>
<<set _icons["power"] = { id: "2c", desc: ["power", "shutdown"]}>>
<<set _icons["cog"] = { id: "2d", desc: ["cog", "gear", "settings"]}>>
<<set _icons["menu"] = { id: "2e", desc: ["menu", "burger", "options"]}>>
<<set _icons["share"] = { id: "2f", desc: ["share"]}>>
<<set _icons["toggle-off"] = { id: "30", desc: ["toggle", "off", "switch", "disabled", "empty"]}>>
<<set _icons["toggle-on"] = { id: "31", desc: ["toggle", "on", "switch", "enabled"]}>>
<<set _icons["checkbox-on"] = { id: "32", desc: ["checkbox", "on", "checked", "enabled"]}>>
<<set _icons["checkbox-off"] = { id: "33", desc: ["checkbox", "off", "unchecked", "disabled", "empty"]}>>
<<set _icons["radiobutton-off"] = { id: "34", desc: ["radiobutton", "off", "disabled", "empty"]}>>
<<set _icons["radiobutton-on"] = { id: "35", desc: ["radiobutton", "on", "enabled"]}>>
<<set _icons["ellipsis-h"] = { id: "36", desc: ["ellipsis", "more"]}>>
<<set _icons["ellipsis-v"] = { id: "37", desc: ["ellipsis", "more", "settings"]}>>
<<set _icons["bug"] = { id: "38", desc: ["bug", "insect", "error"]}>>
<<set _icons["bolt"] = { id: "39", desc: ["bolt", "lightning", "electricity", "zap"]}>>
<<set _icons["magic"] = { id: "3a", desc: ["magic", "wand"]}>>
<<set _icons["asterisk"] = { id: "3b", desc: ["asterisk", "star", "*"]}>>
<<set _icons["search-plus"] = { id: "3c", desc: ["search", "plus", "zoom", "in"]}>>
<<set _icons["search-minus"] = { id: "3d", desc: ["search", "minus", "zoom", "out"]}>>
<<set _icons["font"] = { id: "3e", desc: ["font", "a"]}>>
<<set _icons["sort"] = { id: "3f", desc: ["sort", "arrow"]}>>
<<set _icons["sort-down"] = { id: "40", desc: ["sort", "down", "arrow"]}>>
<<set _icons["sort-up"] = { id: "41", desc: ["sort", "up", "arrow"]}>>
<<set _icons["play"] = { id: "42", desc: ["play", "triangle", "right", "arrow"]}>>
<<set _icons["pause"] = { id: "43", desc: ["pause", "square"]}>>
<<set _icons["stop"] = { id: "44", desc: ["stop"]}>>
<<set _icons["backward"] = { id: "45", desc: ["backward", "left", "arrow"]}>>
<<set _icons["forward"] = { id: "46", desc: ["forward", "right", "arrow"]}>>
<<set _icons["step-backward"] = { id: "47", desc: ["step", "backward", "start", "left", "arrow"]}>>
<<set _icons["step-forward"] = { id: "48", desc: ["step", "forward", "end", "right", "arrow"]}>>
<<set _icons["eject"] = { id: "49", desc: ["eject", "up", "arrow"]}>>
<<set _icons["volume-off"] = { id: "4a", desc: ["volume", "off", "sound", "speaker", "audio"]}>>
<<set _icons["volume-down"] = { id: "4b", desc: ["volume", "down", "sound", "speaker", "audio"]}>>
<<set _icons["volume-up"] = { id: "4c", desc: ["volume", "up", "sound", "speaker", "audio"]}>>
<<set _icons["sun"] = { id: "4d", desc: ["sun", "day", "light", "star"]}>>
<<set _icons["moon"] = { id: "4e", desc: ["moon", "night", "dark"]}>>
<<set _icons["eye"] = { id: "4f", desc: ["eye", "look", "see", "visible"]}>>
<<set _icons["search"] = { id: "50", desc: ["search", "magnify", "magnifying", "glass", "zoom"]}>>
<<set _icons["thumbtack"] = { id: "51", desc: ["thumbtack", "pin"]}>>
<<script>>
$(document).one(":passageend", function (event) {
$.wiki("<<updateIcons>>");
});
<</script>>
<</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/">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.
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 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 .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 - 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) {
var base = String(str).trim();
var _legacy = base.replace(/[^\w\s\u2013\u2014-]+/g, "").replace(/[_\s\u2013\u2014-]+/g, "-").toLocaleLowerCase(); // eslint-disable-line no-control-regex
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 (event) {
var 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.
var 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 (event) {
// 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">Setting API</a> documentation for further details on how to set up your own SugarCube settings.