<<section 'Macros' 'h3'>>
<<hover>>
[[Arrowbox|arrowbox-macro]]
<<tip>>A cycling input macro
<</hover>>
<<hover>>
<<a 'A macro' goto [[a-macro]]>><</a>>
<<tip>>Highly cuctomizable links and buttons
<</hover>>
<<hover>>
<<a 'Listen macro' goto [[listen-macro]]>><</a>>
<<tip>>Easy to use event listener
<</hover>>
<<hover>>
<<a 'Log macro' goto [[log-macro]] disabled true>><</a>>
<<tip>>Being re-done
<</hover>>
<<hover>>
<<a 'Either macro' goto [[either-macro]]>><</a>>
<<tip>>Weighted random and one-time events
<</hover>>
<<hover>>
<<a 'Loop macro' goto [[loop-macro]] disabled true>><</a>>
<<tip>>Still needs some polishing...
<</hover>>
<<hover>>
[[On macro|on-macro]]
<<tip>>Event-based refresh
<</hover>>
<<hover>>
[[Checkvars debugging tool|checkvars-macro]]
<<tip>>Display active variables
<</hover>>
<<hover>>
[[Drag and drop macros|drag-drop-macro]]
<<tip>>Create draggable elements and zones in which to drop them
<</hover>>
<<hover>>
<<a 'Hover macro' goto [[hover-macro]]>><</a>>
<<tip>>Bring up a tooltip or swap content on hover
<</hover>>
<<hover>>
<<a 'Input macro' goto [[input-macro]]>><</a>>
<<tip>>Customizable input elements
<</hover>>
<</section>>
<<section 'Scripts' 'h3'>>
[[Script and Style tags in Twine 2]]
<<hover>>
[[URLfixer]]
<<tip>>Give media access to game you run from the Twine app
<</hover>>
[[Custom methods|custom-methods]]
[[Update markup|update-markup]]
[[Swap markup|swap-markup]]
<<hover>>
<<a 'Container templates' goto [[container-template-markup]]>><</a>>
<<tip>>Needs a description
<</hover>>
<<hover>>
<<a 'Simple expressions' goto [[simple-expressions]] disabled true>><</a>>
<<tip>>RegExp made easy, coming soon...
<</hover>>
<<hover>>
[[Easy tab icon|tab-icon]]
<<tip>>Add a tab icon to your game
<</hover>>
<</section>>
<<section 'Tutorials' 'h3'>>
[[Primitive inventories]]
[[Object-based inventories]]
[[Array inventories, addendum]]
[[Array methods breakdown]]
[[Unicode fun]]
[[DIY save interface]]
<</section>><h2>The """<<arrowbox>>""" macro</h2>
<p>
This input macro is a fancy cycling link. You can
<<arrowbox '_inp' autofocus 'wrap'>><<option 'click the arrow buttons'>><<option 'use the scroll wheel'>><<option 'use the arrow keys'>><</arrowbox>>
to cycle in either direction.
</p>
<h3>Syntax</h3>
<p>
The syntax works exactly like that of Sugarcube's """<<cycle>>""" link.
</p>
<<code>>
<<arrowbox 'variableName'>>
[<<option label [value [selected]]>> …]
[<<optionsfrom collection>> …]
<</arrowbox>>
<</code>>
<h3>Keywords</h3>
<p>Directions : """horizontal""" (default) / """vertical"""</p>
<<code sc edit>>
<<arrowbox '_direction' horizontal>>
<<optionsfrom [1,2,3,4,5,6,7,8,9]>>
<</arrowbox>>
<<arrowbox '_direction' vertical>>
<<optionsfrom [1,2,3,4,5,6,7,8,9]>>
<</arrowbox>>
<</code>>
<p>"""autofocus""" : the element with this keyword starts in focus when the passage loads. </p>
<aside>Only one element should have this keyword per page.</aside>
<<code sc edit>>
<<arrowbox '_important' 'autofocus'>>
<<optionsfrom [1,2,3]>>
<</arrowbox>>
<</code>>
<p>"""wrap""" : options loop back to the start</p>
<<code sc edit>>
<<arrowbox '$var' 'wrap'>>
<<optionsfrom [1,2,3]>>
<</arrowbox>>
<</code>>
<p>"""type-in""" : the center display acts as a typical text input in which the player can type anything.</p>
<aside>In this case, crtl + arrow keys is needed to cycle the options.</aside>
<<code sc edit>>
<<arrowbox '$name' type-in>>
<<optionsfrom ['Greg','Sarah','Godfrey','Gwen','Aphrodite']>>
<</arrowbox>>
<</code>>
<p>"""no-animations""" : the options do not animate as they cycle.</p>
<<code sc edit>>
<<arrowbox '_sobber' 'no-animations'>>
<<optionsfrom ['No','fun','allowed','here']>>
<</arrowbox>>
<</code>>
<h2>"""[script]""" and """[style]""" tags in Twine 2</h2>
<p>The two short scripts let you create JS and CSS passages to be loaded on story startup.</p>
<p>This is my solution to the terrifying speed at which the javascript and stylesheet tabs fill up in Twine, leading to endless hours spent scrolling in search of that one line of code. Tagged passages do not benefit from the syntax highlighting of course, which makes working with them trickier. I would advise relegating code to a passage once it is considered done, leaving space for more work in progress.</p>
<aside>Unlike most code here, these scripts work in every story format, not just Sugarcube.</aside>
<h3> """[script]""" tag </h3>
<<code js clipboard>>
(()=>{//Load script passages
const tagName = 'script';// <== You can change the name of the tag here
[...document.querySelectorAll(`tw-passagedata[tags*=${tagName}]`)]
.sort((a, b) => a.getAttribute("name").localeCompare(b.getAttribute("name")))
.forEach(psg => {
try {
eval(psg.innerText);
} catch (e) {
throw new Error(`Error in script passage: ${psg.getAttribute("name")}`);
}
});
})();
<</code>>
<h3> """[style]""" tag </h3>
<<code js clipboard>>
(() => { //Load style passages
const tagName = 'style'; // <== You can change the name of the tag here
document.querySelectorAll(`tw-passagedata[tags*=${tagName}]`).forEach(psg => {
let sheet = document.createElement('style'),
inner = psg.innerText.replace(/\${.+}/gm, m => eval(m.slice(2, -1)));
sheet.setAttribute('name', psg.getAttribute("name"));
sheet.append(inner);
document.head.appendChild(sheet);
});
})();
<</code>><link rel='shortcut icon' type='image/png' href='tabicon.png'>
<header id='topTitle'></header>
<div id='wipbanner'>Work In Progress</div>
<div id='passages'></div>
<footer class='shadow'></footer>
<div id='loadScreen'>
<div class="loading-circle"></div>
<div class="loading-circle"></div>
<div class="loading-circle"></div>
</div><h2>URL fixer for Twine 2</h2>
<p>When launching a game from the Twine interface (via the Test or Play buttons), assets with relative URLs aren't loaded properly. This makes the testing and editing process slow and cumbersome, requiring the user to either publish to file or hotlink assets to test them.</p>
<p>There are currently a few solutions that enable asset loading from Twine, the most popular of which boils down to concatenating a base path to the local media path to create the final URL ("""<img @src="setup.basePath + 'images/avatars/conan.png'">"""). Still, this approach is cumbersome as it needs to be applied to every media element.</p>
<p>This short script aims to be a drop-in solution for games that use standard relative URLs ("""<img src='myFolder/pic.png'>""").</p>
<p>To do so, you need to supply either a local directory (on your computer) or a remote one (hosted online), where the assets can be found and loaded from.</p>
<p>This script is designed to only work when launching from Twine (either the desktop application, or the web version), it does nothing if the game is distributed as files or hosted online. Authors might wish to remove it from the released version but it is not necessary.</p>
<p>This code works for every story format.</p>
<h3>Online directory</h3>
<p>When using Twine from the Twinery webpage, you must use online hosting with this script (remote pages cannot automatically access local files for security reasons).<p>
<p>An online directory should look like this:</p>
<<code js>>
window.defaultFilePath = {
remote : String.raw`https://myHostingSite.com/myAssets`
};
<</code>>
<p>Be aware that hotlinking (fetching data from the web without visiting the page) is against the terms of service of most websites. You should choose the hosting platform accordingly.</p>
<h3>Local directory</h3>
<p>When launching from the Twine desktop app, it is advised that you use the local directory where the game assets are stored. The process is as follows :</p>
* Open the file explorer and navigate to the asset folder
* Copy the file path (Windows example: C:\Users\Name\Documents\firstgame\assets)
* Paste it into the script like so :
<<code js>>
window.defaultFilePath = {
local : String.raw`C:\Users\Name\Documents\firstgame\assets`
};
<</code>>
<aside>Note : An online directory can always be used (the code will default to it if no local path has been supplied), it is wasteful, however, to download remote files when they are/should be on your computer already.</aside>
<p>Thank you to TheMadExile, Hituro and Raz for helping in the development and testing!</p><h2>The """<<a>>""" macro</h2>
<p>This macro adds four interactive elements, two links and two buttons, all of which take html attributes as arguments.</p>
<h3> Syntax </h3>
<p>This macro supports HTML arguments as objects, arrays or simple pairs. [[HTML arguments]] .</p>
<<code>>
<<a "Link text" [attribute value...]>>
[...content to run silently...]
[<<rep [selector] [attributes] [t8n]>> ...new content...]
[<<prep [selector] [attributes] [t8n]>> ...content to prepend...]
[<<app [selector] [attributes] [t8n]>> ...content to append...]
[<<after [selector] [attributes] [t8n]>> ...content to add after the target selector...]
[<<before [selector] [attributes] [t8n]>> ...content to add before the target selector...]
[<<diag ['Title'] ['styles'] [t8n]>> ...content to display in dialog...]
<</a>>
<</code>>
<h4>With bracket syntax :</h4>
<<code>>
<<a [[Link text|passage]]>><</a>>
<<a [img[...url...][passage]]>><</a>>
<<a 'Text!' goto [[passage]]>><</a>>
<</code>>
<<tabs Output options>>
<<include [[a-macro-output-options]]>>
<<tab Special attributes>>
<<include [[a-macro-special-attributes]]>>
<</tabs>>
<h3> """_this""" special variable </h3>
<p>The code inside the """<<a>>""" macro can access the """_this""" special object. This variable lets you access some of the link's own context when it is clicked.</p>
<gr2>
<span>Key</span>
<span>Value</span>
"""_this.self""" <span>The link element, in a jQuery wrapper</span>
"""_this.event""" <span>The click event which triggered the code</span>
"""_this.count""" <span>The number of times the link has been clicked</span>
</gr2>
<<code sc edit>>
<<set _colors = ['green','yellow','purple','cyan']>>
<<but 'Click me!'>>
<<run _this.self.css('color', _colors.random())>>
<<rep '#data'>>
Clicked _this.count time<<= _this.count>1 ? 's':''>>
Last clicked at _this.event.timeStamp ms
<</but>>
<span id='data'/>
<</code>>/*
<<loop delay
random <number/string/function> V
silent boolean V
replace boolean V
transition boolean V
max <number> V
loopID string
>>
<<start 'startingDelay/event'>> Starting content
<</loop>>
*/
Macro.add('loop', {
tags : ['start'],
isAsync : true,
argsToObj : function(args) {
let argObject = {}, i = 0;
while(i < args.length){
const arg = args[i];
if (Array.isArray(arg)) {//An array, splice into position
args.splice(i--, 1, ...arg);
} else if (typeof arg === 'object') {//Merge objects!
Object.assign(argObject, arg);
} else {//Following pairs
const val = args[i+=1];
if (val === undefined){throw new Error('Uneven number of arguments.')};
argObject[arg.toLowerCase()] = val;
}
i++;
}
return argObject;
},
convertTime : function(t) {
return typeof t === 'string' ? Util.fromCssTime(t) : t;
},
findAndRemove : function(key,expectedType, convert) {
key = key.toLowerCase();
if (this.hasOwnProperty(key)) {
let val = this[key];
if (convert) {val = convert(val)}
if (expectedType && typeof val !== expectedType) {
throw new Error(`Improper argument type, ${key} must be a ${expectedType}.`);
}
delete this[key];
return val;
}
return null;
},
activeLoops : [],
counter : 0,
handler() {
const def = this.self,
attr = this.self.argsToObj(this.args.slice(1)),
convertTime = this.self.convertTime,
findKey = this.self.findAndRemove.bind(attr),
delay = convertTime(this.args[0]),
ID = findKey('loopid','string') || 'loop'+def.counter++,
silent = findKey('silent'),
maxIterations = findKey('max', 'number') ?? Config.macros.maxLoopIterations,
transition = findKey('transition'),
replace = findKey('replace'),
randOffset = findKey('random', 'number',convertTime) ?? 0,
timingFunc = findKey('timing','function') ?? ((d) => {return d});
if (isNaN(maxIterations)){return this.error(`Max iteration parameter is NaN.`)}
if (randOffset > delay){return this.error(`Random variation range cannot be greater than loop delay: ${randOffset}ms VS ${delay}ms.`)}
//Other special properties
if (!silent){
var wrapper = $('<div>').attr(attr).addClass(`macro-${this.name}`);
$(this.output).append(wrapper);
}
const printer = (payload) => {
if (!silent) {
if (replace){wrapper.empty()}
wrapper.wiki(payload);
} else {
$.wiki(payload);
}
};
const startParam = this.payload[1];//Start can only be second after main payload
let i = 0, startDelay = 40;
if (startParam) {
const cont = startParam.contents.trim();
if (cont) {printer(cont)}
if (startParam.args?.[0]) {startDelay = convertTime(startParam.args[0])}
}
const repeatCall = (time = 40) => {
if (def.activeLoops.includes(ID) && i < maxIterations && time > 0) {//Keep calling if wrapper is still in DOM
setTimeout(() => {
State.temporary.this = {iteration : i, delay : time, self : wrapper, id : ID};
printer(this.payload[0].contents);
i++;
repeatCall(timingFunc(delay + randomFloat(-randOffset,randOffset)));
}, time);
}
};
setTimeout(() => {
def.activeLoops.push(ID);
repeatCall(timingFunc(delay + randomFloat(-randOffset,randOffset)));
}, startDelay);
$(document).one(':passageinit', () => {//Stop on passage navigation, add extra conditions in case the loop is somewhere in static UI
def.activeLoops.delete(ID);
});
}
});
Macro.add('loopStop', {
handler () {
const acLoops = Macro.get('loop').activeLoops;
if (this.args[0]) {
acLoops.deleteWith(e => this.args.length ? e === this.args[0].trim() : e)
} else {//Clear all loops
acLoops = [];
}
}
});<h2>The """<<listen>>""" macro</h2>
<p>This macro is a container which acts as an event listener for its contents. By default, it listens for change events but the event type(s) can be supplied as a space or comma-separated list.</p>
<h3>Syntax</h3>
<p>This macro supports [[HTML arguments]].</p>
<<code>>
<<listen [elementType] [attribute value...]>>
... inner contents ...
<<when [eventType1[, eventType2]]>>
... code to run when an event of the given type is triggered ...
[<<when [eventType2]>> ... ]
<</listen>>
<</code>>
<p>If no event type is supplied to the """<<when>>""" tag, it will trigger on change events by default. See JS events for an exhaustive list.</p>
<h3>"""_event""" variable</h3>
<p>The event object is passed as the """_event""" temporary variable which can be used in the code payload.</p>
<h4>Useful properties</h4>
<gr2>
<span>Key</span>
<span>Description</span>
"""_event.target"""
<span>A reference to the element which triggered the event</span>
"""_event.target.value"""
<span>The target's value if it is an """<input>""" element</span>
"""_event.type"""
<span>The type of event ("""change""", """click""", """keypress"""...), useful if multiple types are used</span>
</gr2>
<h4>Beware!</h4>
<aside>"""<input>""" element's values are always strings! If you want a number instead, use """Number(_event.target.value)""".</aside>
<h3>Uses and examples</h3>
<li>Visually update values :</li>
<<code sc edit>>
<<listen>>
Starting number : <<numberbox '_num' `_num ?? 5`>>
Multiplier : <<numberbox '_multi' `_multi ?? 5`>>
<<when>>
<<replace '#display'>><<= _num*_multi>><</replace>>
<</listen>>
Result : <span id='display'></span>
<</code>>
<li>Color the relevant input field when enter is pressed :</li>
<<code sc edit>>
<<listen>>
<<textbox '$fname' 'John'>>
<<textbox '$name' 'Doe'>>
<<textbox '$age' '?'>>
<<when 'keypress'>>
<<if _event.code === 'Enter'>>
<<run $(_event.target).css('background-color','red')>>
<</if>>
<</listen>>
<</code>>
<li>Make an element which cannot be right-clicked (and taunts you if you do) :</li>
<<code sc edit>>
<<listen>>
You cannot right click meeee!
<<when 'contextmenu'>>
<<run _event.preventDefault(),
Dialog.setup(),
Dialog.wiki("Don't even try it!").open()>>
<<when 'click'>>
<<run Dialog.setup(),
Dialog.wiki("That's the good click!").open()>>
<</listen>>
<</code>>
<li> The world's worst numpad :</li>
<<code sc edit>>
<div id='dial'>You dialed : </div>
<<nobr>>
<<listen type 'div' style 'display: grid; grid-template-columns: 1fr 1fr 1fr'>>
<<for _i=1; _i lt 10;_i++>>
<button>_i</button>
<</for>>
<<when 'click'>>
<<append '#dial'>><<= _event.target.innerHTML>><</append>>
<</listen>>
<</nobr>>
<</code>>Easy console logging!
Create element that displays the console!
<<code sc edit>>
<<a 'test'>><<log>><</a>>
<</code>>
/*<div id='console'>
<h3>This is a pseudo-console</h3>
</div>*/<h2>HTML arguments</h2>
<p>Many of the macros on this page take HTML attributes as macro arguments.</p>
<<code sc edit>>
Standard HTML element:
<a class='myClass' style='color:green;'>Link</a>
As argument pairs:
<<a 'Link' class 'myClass' style 'color:green;'>><</a>>
As plain object:
<<a 'Link' `{class: 'myClass', style:'color:green;'}`>><</a>>
<</code>>
<p>Variables passed as arguments are evaluated by default, unlike in HTML elements where they need the attribute directive syntax. If you need to build a string however, backquotes will be needed to indicate an expression.</p>
<<code sc edit>>
<style>.tall {height: 6em}</style>
<<set _class = 'tall', _col = 'cyan'>>
Attribute directive:
<div @class='_class' @style="'color:'+_col">Content</div>
Simple evaluation:
<<drag class _class>>
Content
<</drag>>
Backquote expression:
<<drop style `'border-color:'+_col`>>
<</drop>>
<</code>><<set $inventory = []>>
<h2>The """<<either>>""" macro</h2>
<p>The """<<either>>""" macro outputs content at random, the odds of which can be optionally weighted.</p>
<h3>Syntax</h3>
<<code>>
<<either [weight] ['once']>>
First outcome...
[<<or [weight]>>
Second outcome...]
[<<after>>
This macro has already been triggered, now this shows up every time.]
<</either>>
<</code>>
<h3>Weighted random</h3>
<p>The """<<either>>""" and every other """<<or>>""" tags can receive a weight argument which describes how likely it is for the outcome to appear. This weight needs to be a number, either integer or decimal.</p>
<p>By default, every tag is given a weight of 1, meaning they are all equally likely to come up.</p>
<<code sc edit>>
<<either 90>>
Main outcome, happens 90% of times.
<<or 9>>
Uncommon outcome, 9% odds.
<<or 1>>
Rare outcome, only 1% odds!
<</either>>
<</code>>
<h4>Coin flip demo:</h4>
<<code sc edit>>
<<set _heads = _tails = _side = 0>>
<<a 'Flips the coin' style 'user-select:none' key 'f'>>
<<rep '#output'>>
<<either>>
heads
<<set _heads++>>
<<or>>
tails
<<set _tails++>>
<<or .01>>
side (ultra rare)!
<<set _side++>>
<</either>>
_heads|_tails|_side
<</a>>
<span id='output'/>
<</code>>
<aside>Since it uses """randomFloat()""" internally, this macro works with Sugarcube's seeded PRNG.</aside>
<h3>"""'once'""" keyword and """<<after>>""" tag</h3>
<p>There are some situations in which you want random events to only happen the first time it is made available. This can be achieved by using the """'once'""" keyword or by having an extra """<<after>>""" tag, in which can the contents of this tag will be displayed on subsequent navigations.</p>
<<code sc edit>>
<<either>>
There is an item on the ground, you acquire a <b>pair of rare grieves</b>.
<<set $inventory.push('Rare grieves')>>
<<after>>
...the empty room holds no further treasures...
<</either>>
<<a 'Reload passage' goto `passage()`>><</a>>
<</code>>
<aside>This kind of unique events are stateful, meaning they depend on the current <vr data-glos='sugword'>State</vr>. <i>Spent</i> events are commited to save and reverted by going backward through the history.</aside><<tabs String methods>>
<<include [[String methods]]>>
<<tab Array methods>>
<<include [[Array methods]]>>
<<tab Object methods>>
<<include [[Object methods]]>>
<<tab jQuery functions>>
<<include [[jQuery functions]]>>
<</tabs>>
//String methods
Object.defineProperty(String.prototype, 'includesAny', {
configurable : true,
writable : true,
value() {
return Array.from(arguments).some(e => this.includes(e));
}
});
Object.defineProperty(String.prototype, 'includesAll', {
configurable : true,
writable : true,
value() {
return Array.from(arguments).every(e => this.includes(e));
}
});
Object.defineProperty(String.prototype, 'startsWithAny', {
configurable : true,
writable : true,
value() {
return Array.from(arguments).some(e => this.startsWith(e));
}
});
Object.defineProperty(String.prototype, 'endsWithAny', {
configurable : true,
writable : true,
value() {
return Array.from(arguments).some(e => this.endsWith(e));
}
});
Object.defineProperty(String.prototype, 'removeEvery', {
configurable : true,
writable : true,
value() {
let output = this;
Array.from(arguments).forEach(e => output = output.replace(e,''));
return output;
}
});
Object.defineProperty(String.prototype, 'sanitize', {
configurable : true,
writable : true,
value() {
const repRef = [
['>','>'],
['<','<'],
['[','['],
[']',']'],
['$','$'],
['_','_']
];
let output = this;
repRef.forEach(p => {
output = output.replaceAll(p[0],p[1]);
})
return output;
}
});
Object.defineProperty(String.prototype, 'processText', {
configurable : true,
writable : true,
value() {
let txt = this;
const proc = Config.passages.onProcess;
if (proc){
txt = proc({tags:[], title:'', text: this});
}
if (Config.passages.nobr) {
txt = txt.replace(/^\n+|\n+$/g, '').replace(/\n+/g, ' ');
}
return txt;
}
});
Object.defineProperty(String.prototype, 'insert', {
configurable : true,
writable : true,
value(txt,at) {
if (typeof at !== 'number') {throw new Error('String insert position must be a number.')}
return this.substring(0,at)+txt+this.substring(at);
}
});
//Array methods
Object.defineProperty(Array.prototype, 'toUnique', {
configurable : true,
writable : true,
value() {
const uni = new Set(this);
this.length = 0;
uni.forEach(e => this.push(e));
return this;
}
});
Object.defineProperty(Array.prototype, 'crossFind', {
configurable : true,
writable : true,
value(...args) {
const values = args.flat();
return this.find(e => values.includes(e));
}
});
Object.defineProperty(Array.prototype, 'has', {
configurable : true,
writable : true,
value(obj) {
const str = JSON.stringify(obj);
return this.some(e => JSON.stringify(e) === str);
}
});
//Object methods
Object.defineProperty(Object.prototype, 'multiSet', {
configurable : true,
writable : true,
value(...args) {
const keys = args.slice(0,-1), val = args.at(-1);
keys.flat(Infinity).forEach(k => this[k] = val);
return this;
}
});
Object.defineProperty(Object.prototype, 'print', {
configurable : true,
writable : true,
value(lbr, colon) {
let output = '';
lbr ??= '\n', colon ??= ' : ';
for (const k in this) {
output += k + colon + this[k] + lbr;
}
return output;
}
});
Object.defineProperty(Object.prototype, 'forEach', {
configurable : true,
writable : true,
value(callback, thisArg) {
if (!callback) {return this;}
for (const k in this){
callback.call(thisArg ?? null, this[k], k, this);
}
return this;
}
});
//jQuery functions
jQuery.fn.extend({
insertAt : function(index, target) {
if (typeof target === 'number') {[index,target] = [target,index]}
const ch = target.children();
if (index < 0) {index += ch.length}
if (index >= ch.length) {
target.append(this);
} else if (ch[index]) {
$(ch[index]).before(this);
} else {
target.prepend(this);
}
return this;
},
at : function(insert, index) {
$(insert).insertAt(index, this);
return this;
}
});//EDIT PATHS HERE!
window.defaultFilePath = {
local : String.raw`...C:\Users\myName\Documents\myAssets...`,
remote : String.raw`...https://myHostingSite.com/myAssets...`
};
//EDIT PATHS HERE!
(() => {
if (!defaultFilePath.remote && !defaultFilePath.local) {return false};
let path, tempPaths = [
'AppData/Local', //Windows
'var/folders', //mac OS
'/tmp' //Linux
];
if (location.origin.includes('twinery')) {//Launched from browser Twine
if (!defaultFilePath.remote) {
return console.log(`No remote directory supplied, relative assets won't be available for testing.`);
}
path = defaultFilePath.remote.trim();
} else if (tempPaths.find(p => {return location.pathname.includes(p)})) {//Launched from desktop Twine
path = defaultFilePath.local ? 'file://' + defaultFilePath.local.trim() : defaultFilePath.remote.trim();
} else {//Local path with relative assets
return false;
}
path = path.replaceAll('\\','/');
if (path.at(-1) !== '/'){path += '/'}
const baseElem = document.createElement('base');
baseElem.setAttribute('href', path);
document.head.append(baseElem);
})();<h1>About me</h1>
<p>Hi, I'm Maliface, <<= new Date(new Date() - new Date('1996-07-04')).getFullYear() - 1970>>, French, hobbyist coder and horrible writer.</p>
<h3>Contact</h3>
<ul>
<li>Discord : maliface</li>
<li>Email : <a href='mailto:MalifaciousGames@proton.me'>MalifaciousGames@proton.me</a> </li>
<li>Ko-fi : https://ko-fi.com/malifaciousgames </li>
</ul>
<h3>Licence</h3>
<p>Every line of code provided on this page (either displayed or used internally) is covered under <<a 'MIT licence' href 'https://en.wikipedia.org/wiki/MIT_License'>><</a>>.</p>
<<adel 'See the full monty'>>
<<after '' t8n>>
<h3>MIT License</h3>
<p>Copyright (c) 2022 Maliface.</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
<p>Credit is appreciated but in no way necessary, go ham.</p>
<</adel>>
<p>Work in progress</p><h2> The """<<on>>""" and """<<trigger>>""" macros </h2>
<p>The """<<on>>""" macro generates a customizable element which refreshes its contents when the corresponding event is triggered. </p>
This macro has two main use cases:
* listen for standard events as they bubble through the DOM ("""click""", """contextmenu""", """keypress"""...) and run code accordingly
* trigger custom events that cause the """<<on>>""" block to refresh its contents
<h3> Syntax </h3>
<p>Both """<<on>>""" and """<<trigger>>""" require at least one event name to function.</p>
<<code>>
<<on 'event1[,event2,...]' [elementType] [{attribute object}] [t8n] [startHidden/hidden]>>
...content...
<</on>>
<<trigger 'event1[,envent2,...]' [target element]>>
<</code>>
<aside>By default, the """<<on>>""" block is a """<span>""", this can be changed with the second """elementType""" argument.</aside>
<h3>Demo</h3>
<<code sc edit>>
<<set _hp = 100>>
<<on 'HPLoss'>>
You have _hp health remaining.
<</on>>
<<button 'Lose 5 HP'>>
<<set _hp -= 5>>
<<trigger 'HPLoss'>>
<</button>>
<</code>>
<h3> 'hidden/startHidden' argument </h3>
<p>By default, """<<on>>""" blocks run their content when the page is loaded. The """hidden/startHidden""" special argument makes it so the code only runs when the event is triggered.</p>
<<code sc edit>>
<<set _inventory = ['Knife','Matches','Rope','Hoodie']>>
<<on 'cannotEquip' p hidden>>
You cannot equip this item!
<</on>>
<<link 'Equip item'>>
<<if _inventory.length gt 3>>
<<trigger 'cannotEquip'>>
<</if>>
<</link>>
<</code>>
<h3> 't8n/transition' argument </h3>
<p>Similarly to other sugarcube macros, this causes a fade-in effect when the """<<on>>""" container is refreshed.</p>
<h3> """_event""" special variable </h3>
<p>The """_event""" temporary variable lets you access and manipulate the event which triggered the refresh. This is mostly useful when using the macro as a """document"""-wide event listener.</p>
<<code sc edit>>
<<nobr>>
<<on 'keypress' '' hidden>>
<<append '#keys'>>_event.key <</append>>
<</on>>
<</nobr>>
Keys : <span id='keys'/>
<</code>>
<h3>"""<<on>>""" VS the update markup</h3>
<p>Both """<<on>>""" and the [[update-markup]] offer a way to refresh their content dynamically. Where they differ is in scope, the markup is designed only for variables while """<<on>>""" can run and print complex code. In an older project of mine, I used this macro and events to refresh a player's skills whenever they moved, in case they ended up in/out of range.</p>Macro.add('choices', {
separator : '<br>',
linkType : 'linkreplace',
tags : ['step','final'],
handler() {
let count = 0;
const content = [],
entries = this.payload.filter(p => p.name === this.self.tags[0]),
final = this.payload.find(p => p.name === this.self.tags[1]);
entries.forEach(p => {
content.push(`<<${this.self.linkType} '${p.args[0]}' ${this.args[0] ?? ''}>>${p.contents}<</${this.self.linkType}>>`);
});
const reqNum = final.args[0] ?? entries.length;
const wrapper = $('<span>').addClass(`macro-${this.name}`).wiki(content.join(this.self.separator));
if (final) {
wrapper.on('click', (e) => {
if ($(e.target).is(`.macro-${this.self.linkType}`)) {
count++;
if (count === reqNum) {
wrapper.wiki(this.self.separator + final.contents);
}
}
});
}
$(this.output).append(wrapper);
}
});
<h2> The """UpdateMarkup""" API </h2>
<p>This script enables you to refresh displayed variables whenever they are modified. It is done by wrapping naked variables in two sets of curly brackets : """{{$myVar}}""".</p>
<<code sc edit>>
<<set $money = 45>>
Money : {{$money}} $
<<button 'Gain 5$'>>
<<set $money += 5>>
<</button>>
<</code>>
<h3> Valid expressions </h3>
<p>The update markup can print a broad range of variables and expressions that aren't supported by Sugarcube's naked variable markup. This includes non-"""State""" variables, mathematical operations, function calls...</p>
<<code sc edit>>
<<set setup.n = 5>>
{{setup.n * 9}}
{{setup.n}}
{{setup.n.toFixed(2)}}
<<button 'Multiply'>>
<<set setup.n *= 1.25>>
<</button>>
<</code>>
<p>Be aware that function calls will run whenever update wrappers are updated.</p>
<h3> Using """<<capture>>""" </h3>
<p>This markup supports variable shadows the same way most macros do.</p>
<<code sc edit>>
<<set $array = ['One','Two','Three','Four','Five']>>
<<for _i = 0; _i lt $array.length; _i++>>
<<capture _i>>
Item _i : {{$array[_i]}}
<</capture>>
<</for>>
<<button 'To French!'>>
<<set $array = ['Un','Deux','Trois','Quatre','Cinq']>>
<</button>>
<</code>>
<p>While """$array""" changes, the shadowed """_i""" variable remains the same for each wrapper.</p>
<h3> Input-less updating </h3>
<p>The current script relies on """click""" and """change""" events to check for value changes. This handles most of the common means of variables modification (clicking a link, using an input element).
However, there may be situations in which you want variables to update without player input. In this case, you need to run the """setup.updateWrappers()""" function.</p>
<<code sc edit >>
<<set $time = 0>>
Time passes, it is now {{$time}} .
<<silently>>
<<repeat 1s>>
<<set $time++, setup.updateWrappers()>>
<</repeat>>
<</silently>>
<</code>>
<h2>The """<<checkvars>>""" macro</h2>
<p>This macro is a debugging tool which lets you browse through variables and set them to specific values for testing purposes.</p>
<p>It handles both types of <vr data-glos='sugword'>State</vr> variables as well as """setup""" and """settings""".</p>
<<code sc edit>>
<<set $num = 86,
$array = ['a','b','c'],
$obj = {prop1 : 'My property', prop2 : 56},
$boolean = false,
$null = null,
$undefined = undefined,
$function = function(arg) {return arg*56},
$set = new Set([1,2,2,3,4,5,6,6]),
$map = new Map([
[[1,2,3,4], 10],
[12, {object : true}]
])>>
<<checkvars>>
<</code>>
<aside>This was inspired by TME's <<a 'original """<<checkvars>>"""' href 'https://www.motoslave.net/sugarcube/2/'>><</a>>.</aside>
<aside> This macro does not modify values inside """Set""" or """Map""" because of the way data is accessed..</aside><h2>The drag and drop macro set</h2>
<p>This two parts macro set handles drag and drop operations in a Sugarcube context. These macros let you bind data to draggable HTML elements, run code on this data at different steps of the drop process and manage inventories in an automated fashion.</p>
<<section 'The """<<drag>>""" macro'>>
<p>This container macro is used to create the draggable element itself, it acts as a movable wrapper.</p>
<aside>Both """<<drag>>""" and """<<drop>>""" support [[HTML arguments]].</aside>
<<h 'Syntax'>>
<<code sc>>
<<drag [attribute value...]>>
...contents...
[<<data data>>]
[<<onStart>> ...code to run when dragging starts...]
[<<onEnd>> ...code to run when dragging ends...]
<</drag>><</code>>
<<code sc edit>>
<span id='target'/>
<<drag>>
Drag me!
<<onStart>>
<<replace '#target'>>
I'm being dragged!
<</replace>>
<<onEnd>>
<<replace '#target'>>
Dragging has ended...
<</replace>>
<</drag>>
<</code>>
<<h '"""<<data>>""" tag'>>
<p>The """<<data>>""" tag is used to pass data through """<<drag>>""" elements. It can be anything, from primitives to complex objects. This data can be accessed as part of the """_drag""" special object, under """_drag.data""".</p>
<<code sc edit>>
<<set _data = 95>>
<<drag>>
<<link 'See data'>>
<<replace '#data'>>_data<</replace>>
<</link>>
<<data _data>>
<<onStart>>
<<replace '#data'>>
<<= _data*3>>
<</replace>>
<</drag>>
<span id='data'/>
<</code>>
<aside>If the data is supplied as a variable ("""<<data $myData>>"""), this variable will be shadowed, both in the wikifier instance and associated <vr>onStart/onEnd</vr> callbacks.</aside>
<<h 'Special attributes'>>
* <vr>type(string)</vr> : the item's type, decides on compatibility with drop containers.
* <vr>size(number)</vr> : the item's size, decides how many <vr>slots</vr> it takes up in containers (see below for the <vr>slots</vr> attribute and demo).
* <vr>quantity(any)</vr> : the amount of times an item can be retreived, if it is a number, it will decrease with each removal, other data types make it into an infinite source.
<<code sc edit nobr wide>>
<<drop>><</drop>>
<<drop>>
<<drag quantity 3>>
Food
<</drag>>
<<drag quantity true>>
Infinite food!
<</drag>>
<</drop>>
<</code>>
<aside>Items with a <vr>quantity</vr> do not stack back (this behaviour can be coded in). At some point in the future, a <vr>stackable (stack size)</vr> attribute will likely be added.</aside>
<<h 'The """_drag""" special variable'>>
<p>Whenever a drag event starts, the """_drag""" temporary variable is set reflect the entity being dragged. This variable is used to pass informations from the """<<drag>>""" element to the """<<drop>>""" containers.</p>
<p>This variable is first set on <vr>dragstart</vr> and unset a short delay after the <vr>dragend</vr> event, it is accessible to every callback tag from this macro set.</p>
<<code js>>
{
self : the element being dragged (jQuery selection),
parent : the parent container (jQuery selection),
index : the element's position in its original parent (Number),
contents : the element's inner text content (String),
touch : whether the drag event was started on a touch device (Boolean),
dropped : set to true if the item was dropped in a container, undefined otherwise,
data : data passed through the data tag (any),
size : the item's size (Number),
type : the item's type (String),
quantity : the item's quantity (any),
}<</code>>
<</section>>
<<section 'The """<<drop>>""" macro'>>
<p>Where """<<drag>>""" creates the draggable elements, this macro creates drop zones in which they can be placed. Most of the logic happens on the """<<drop>>""" container's side, drop modes, data handling and callbacks are mostly resolved on this end of the operation.</p>
<<h 'Syntax'>>
<<code sc>>
<<drop [attribute value]>>
...contents...
[<<fromSource '$array' ['_alias'] [attributes]>>
...pattern, using _alias to print each item in $array...
]
[<<onDrop [dropMode]>> ...code to run...]
[<<onRemove [removeType]>> ...code to run...]
[<<onAny>> ...code to run...]
<</drop>>
<</code>>
<<h 'Drop modes'>>
<p>Supplied as """<<onDrop 'dropmode'>>""". Drop mode can be a callback which returns a valid mode, if so, it will be evaluated on every drop event.</p>
* <vr>anywhere</vr> (default) : """<<drag>>""" item can be placed anywhere in the container.
* <vr>append</vr> : Appends item to container.
* <vr>prepend</vr> : Prepends item to container.
* <vr>replace</vr> : """<<drag>>""" item replaces the closest item in the drop container, if any.
* <vr>replaceAll</vr> : The container is cleared, then the item it added to it.
* <vr>none</vr> : Nothing happens, but """<<onDrop>>""" runs nonetheless.
* <vr>remove</vr> : """<<drag>>""" item is destroyed.
* <vr>swap</vr> : """<<drag>>""" item can be placed anywhere, then the closest item in the container (if any) is sent back the the item's parent.
* <vr>fillswap</vr> : Place anywhere as long as the drop container has empty slots, when it is full, switch to swap mode.
<<code sc edit nobr wide>>
<<set _inv = ['Wallet','Keys','Towel','Lighter']>>
<<drop>>
<<fromSource '_inv'>>
<</drop>>
<<drop>>
<<onDrop 'swap'>>
<</drop>>
<</code>>
<<h 'Removal modes'>>
<p>Supplied as """<<onRemove 'removeMode'>>""". Containers with a remove mode cannot have elements dropped into them.</p>
* <vr>destroy</vr> : When an item is removed, its parent container is destroyed. This forces players to choose only one item for example.
<<code sc edit nobr>>
<<drop>><</drop>>
<<drop>>
<<drag>>Blue pill<</drag>>
<<drag>>Red pill<</drag>>
<<onRemove 'destroy'>>
<</drop>>
<</code>>
<<h 'Special arguments'>>
* <vr>slots</vr> : Interacts with """<<drag>>"""'s <vr>size</vr> attribute. A container with 6 <vr>slots</vr> can hold 6 <vr>size</vr> 1 items.
* <vr>type</vr> : Defines item compatibility. """<<drag>>""" items of a given <vr>type</vr> can only be dropped in a container of that same <vr>type</vr>. Type-less containers accept any item, type-less items can be dropped anywhere.
* <vr>condition</vr> : A callback, evaluated every time a """<<drag>>""" item enters the container. If it returns a <vr>falsy</vr> value, the drop is forbidden.
<<code sc edit wide nobr>>
<<drop>>
<<drag size 1>>1<</drag>>
<<drag size 2>>2<</drag>>
<<drag size 3>>3<</drag>>
<<drag size 4>>4<</drag>>
<</drop>>
<<drop slots 5>>
<<onDrop 'fillswap'>>
<</drop>>
<</code>>
<<h '"""<<fromSource>>""" tag'>>
<p>The """<<fromSource>>""" tag lets you populate a """<<drop>>""" container with the contents of an array, printed into descrete """<<drag>>""" elements.</p>
<p>"""<<drop>>""" containers printed this way are bound to their source variable. When a """<<drag>>""" container holding data is dropped, the data will be inserted in the array, according to the element's position.</p>
<<code sc>>
<<fromSource
'$variableName' // Variable name, in quotation marks
'_item' // Alias variable, used to represent the item, in quotation marks
{attributes object} // Plain object containing html attributes
>>
<</code>>
<p>The quoted variable must be a reference to an array. By default, the alias variable is """_item""".</p>
<<code sc edit wide nobr>>
<<set _players = [
{name: 'TRBRY', type: 'Popsicle', skill: 'CSS wizard'},
{name: 'GwenTastic', type: 'Vampire', skill: 'Kinky :3'},
{name: 'SloopyFeel', type: 'Coffee cup', skill: 'Magic words'},
{name: 'Hituro', type: 'Mouse', skill: 'Keen whiskers'}
]>>
<<drop>>
<<fromSource '_players' '_p'>>
<h5>_p.name</h5>
<p>Type : _p.type</p>
<p>Skill : _p.skill</p>
<</drop>>
<</code>>
<p>Here you can see how the element's positions are coupled to each array's contents, using the [[update markup|update-markup]] to refresh the results.</p>
<<code sc edit wide nobr>>
<<set _array1 = [1,2,3,4,5,6,7,8,9], _array2 = []>>
<<drop>>
<<fromSource '_array1'>>
<</drop>>
<<drop>>
<<fromSource '_array2'>>
<</drop>>
<div>{{_array1}}</div>
<div>{{_array2}}</div>
<</code>>
<aside>If the """<<fromSource>>""" tag is empty, the macro will try to print the value as a simple string.</aside>
<<h 'Events order'>>
* <vr>dragstart</vr> : An element starts being dragged, the """_drag""" variable is set to reflect this element's contents.
* <vr>dragenter</vr> : The element is dragged other a drop zone. Based on the """_drag""" object, the target drop zone decides if it can accept the element and displays the fitting overlay.
* <vr>drop</vr> : The element is released. If the drop is allowed, it is removed from its original parent and added according to the <vr>dropMode</vr>. The drop zone updates its source variable and slots parameter.
* <vr>dragend</vr> : The """<<drag>>""" element resolves the event internally. Is a drop happened, it triggers the <vr>removal</vr> event on its formet parent container.
* <vr>removal</vr> : Synthetic event called on the original parent. This parent updates its source variable and slots based on its new child count.
<</section>><h2>Primitive inventories</h2>
<p>The inventory systems below all use <vr>primitives</vr> as their main item type, hence the name, instead of being <vr>object</vr>-based.</p>
<p>There are many approaches to making inventories work, they range in complexity and versatility. One type isn't intrinsically better than any other, as long as it is done well.</p>
<p>It takes skill to code a complex, class-based inventory with extensive utility functions... but it takes wisdom to recognize that the vast majority of interactive fiction games do not need it. Like everything, you should limit complexity to what is strictly required, there is no shame in settling for simpler systems as long as they do the job.</p>
<section>
<h3>The <vr data-glos='bool'>boolean</vr> type</h3>
<p>The simplest inventories use only primitives, usually <vr data-glos='bool'>booleans</vr>. Either the player has the item, or they do not.</p>
<<code sc>>
::StoryInit
<<set $wallet = false>>
::SomePassage
<<linkreplace 'Grab the wallet.'>>
<<set $wallet = true>>
You grab the wallet, it is plain, covered in weathered leather.
<</linkreplace>>
::InventoryPassage
<<if $wallet>>
An old wallet you found. Must have belonged to someone at some point.
<</if>>
<</code>>
<b>Pros :</b>
* Extremely simple
* Everything you need to implement key items
<b>Cons :</b>
* Doesn't scale easily, each item needs its dedicated variable
* Items do not have features of their own
<aside>It is not strictly necessary to initialize the variable to <vr>false</vr> in <vr>StoryInit</vr>. By default, it will be <vr>undefined</vr> which is a <vr>falsy</vr> value. Still, it helps for organisation purpose and because checking for <vr>true/false</vr> might feel more natural for beginners.</aside>
</section>
<section>
<h3>The same but with numbers</h3>
<p>This is a variation on the primitive inventory which uses numbers instead of booleans. Most useful for implementing item quantities and a consummable system.</p>
<<code sc>>
::StoryInit
<<set $flashlight = 0>>
::SomePassage
<<linkreplace 'Scavenge a few batteries for your flashlight'>>
<<set _amount = random(2,4)>>
<<set $flashlight += _amount>>
You find _amount old batteries. Hopefully enough to power the flashlight for a bit longer.
<</linkreplace>>
::InventoryPassage
<<if $flashlight>>
An old flashlight and a handful of batteries. It is almost nothing yet it feels invaluable.
<<else>>
Without batteries, the old flashlight is useless.
<</if>>
::APassageWithInteraction
<<if not $flashlight>>
The room is too dark to explore safely.
<<else>>
<<linkreplace 'Light up the dark (-1 battery)'>>
<<set $flashlight-->>
The yellow beam reveals a decrepit bedroom, a collapsed table lies on the floor. Still, maybe you can scrounge something up...
<</linkreplace>>
<</if>>
<</code>>
<p>This is not really a replacement for boolean key items, both mechanics can work together to create different classes of items : keys and consummables. Simple resource management opens up a lot of gameplay options without needing overly complicated code.</p>
<aside>In this case, initializing the variable to a number value is essential as you can't add numbers to <vr>undefined</vr>!</aside>
</section>
<section>
<h3>Arrays to the rescue</h3>
<p>This is what I see as the intermediate inventory. While the items are still primitives, it uses an <vr>array</vr> to hold them which makes it significantly more scalable but still easy to check and handle.</p>
<p>Getting familiar with array methods such as <vr>.push()</vr>,<vr>.includes()</vr>, <vr>.delete()</vr>... is invaluable.</p>
<<code sc>>
::StoryInit
// Initialize the array, with some starting gear in it
<<set $inventory = ['Lighter','Keys']>>
::StoryCaption
Your backpack holds : $inventory
::SomePassage
<<linkreplace 'Pick up the photograph'>>
<<set $inventory.push('Photograph')>>
The old polaroid is faded but you pocket it nonetheless, who knows, the past has strange ways of getting back in touch...
<</linkreplace>>
::AnotherPassage
<<if $inventory.includes('Photograph')>>
The house bears a stricking resemblance to the one from the faded polaroid, the porch especially.
<</if>>
<</code>>
<h3>Displaying the inventory array</h3>
<p>The above code does a bit of display by printing the """$inventory""" variable in <vr>StoryCaption</vr> (the passage which populates the sidebar), but you might want something more thorough and impressive. The items being <vr>strings</vr> means they are only their own names, which limits their ability to hold data of their own.</p>
<p>Still, there a few ways to get more out of them.</p>
<h4>The simplistic way:</h4>
<<code sc>>
::InventoryPassage
<<if $inventory.includes('Lighter')>>
The trusty bic lighter, it has earned you a handful of friends along the years, all smokers.
<</if>>
<</code>>
<p>It works just fine but also foregoes the main benefit of using an array instead of loose variables. You can (and should) <vr data-glos='loop'>loop</vr> through arrays whenever possible, do not rely on handcrafted checks when the computer should be doing the work for you.</p>
<h4>The inclusive way:</h4>
<<code sc>>
::InventoryPassage
<<for _item range $inventory>>
<<include _item>>
<</for>>
::Lighter
The trusty bic lighter, it has earned you a handful of friends along the years, all smokers.
::Keys
The keys ring together nicely. In truth, you only ever use a few of those. Main gate, bike chain, tool shed...
<</code>>
<p>The """<<include>>""" trick above requires having a dedicated passage for each item name, but it lets you print any quantity of content in a way that is much less cumbersome than """<<if>>""" statements.</p>
<h4>The <vr>setup</vr> way:</h4>
<<code sc>>
::StoryInit
<<set setup.itemDescriptions = {
Lighter : "The trusty bic lighter, it has earned you a handful of friends along the years, all smokers.",
Keys : "The keys ring together nicely. In truth, you only ever use a few of those. Main gate, bike chain, tool shed...",
Photograph : "Faded and stained by the years, the outlines of a house are still visible under the right lighting."
}>>
::InventoryPassage
<<for _item range $inventory>>
<<= setup.itemDescriptions[_item]>>
<</for>>
<</code>>
<p>This is a more complex approach which requires using a <vr>setup</vr> object and the <vr>property accessor</vr> syntax. While it does have benefits I wouldn't recommend it for such a task as displaying descriptions. We will come back to this idea when tackling hybrid <vr>object</vr>-based systems...</p>
<p>...anyways, back on topic:</p>
<b>Pros :</b>
* Still fairly simple, knowing a handful of methods can get you quite far
* Endlessly scalable. Want to add a new item? You can just push it to the array.
* Looping through the array lets you display items in the order they were acquired
* Vast toolbox of array methods in case you need them
<b>Cons :</b>
* Using strings means items still can't hold much data on their own
* While you can overcome this limitation, it brings it own set of challenges
</section><h2>Object-based inventories</h2>
<p>This is the more capable but also more challenging inventory type, the one you need if you want items to have stats, prices, special effects and rarities, the whole video gamey package!</p>
<p>If you only need simple item tracking or consumable system, turn back while you can. Every extra ability comes at the cost of added complexity, <vr>object</vr>-based inventories usually require a lot of infrastructure built around them to work properly.</p>
<h3>Object basics</h3>
<p>Unlike arrays in which values can shift around, <vr>objects</vr> are collections of tightly bound pair : a key and a value. If you have a key, you can access its value like so: <vr>object.key => value</vr>.</p>
<<code sc edit>>
<<set $basicSword = {
name: 'Basic sword',
desc: 'The simplest sword money can buy.',
damage: 8,
price : 4
}>>
Item name: $basicSword.name
$basicSword.desc
$basicSword.damage damage
$basicSword.price gold
<</code>>
<p>Let's not jump into arrays just now but instead consider what life would be like for a one-armed adventurer.</p>
<<code sc>>
::StartPassage
A corpse lies there, holding on to a rusted sword.
<<linkreplace 'Steal from the dead'>>
You pry the stiff fingers off of the hilt and retrieve the sword.
From dark black to bright orange, it bears all the nuances of iron oxide.
<<set $weapon = {
name : 'Rusted sword',
damage : 5
}>>
<</linkreplace>>
::TheTavern
<<if $weapon.name is 'Rusted sword'>>
As you enter the crowded tavern, a slender man stares you down, his gaze stopping at the sword's rusty pommel.
"Hopefully it serves you better than its previous owner." he mutters.
<</if>>
::TimeToFight
A large spider bars the way, you won't get through without a fight.
<<button 'Strike it'>>
<<set $enemy.hp -= $weapon.damage>>
<<if $enemy.hp lte 0>>
<<goto 'SpiderFightVictory'>>
<</if>>
<</button>>
<</code>>
<p>In the above example, the player has a single inventory slot in the form of the """$weapon""" variable. Let's add a consumable array to the mix.</p>
<<code sc>>
::StoryInit
<<set $potionPouch = []>>
<<set setup.hpPotion = {
name : 'Health potion',
effect : '<<set $hp += 10>> The potion heals you for 10 HP.',
desc : 'The humble health potion, instrumental to staying alive!'
}>>
::FindPotion
<<set $potionPouch.push(setup.hpPotion)>>
//We now have an object in the array!
::PotionMenu
<<for _potion range $potionPouch>>
Name : _potion.name
_potion.desc
<<capture _potion>>
<<linkreplace 'Use _potion.name'>>
<<= _potion.effect>>
<<set $potionPouch.delete(_item)>>
<</linkreplace>>
<</capture>>
<</for>>
<</code>>
<p>In the code above, we use a """<<for>>""" loop to print every potion sequentially. The """_item""" temporary variable needs to be """captured""" so """<<linkreplace>>""" can reference it properly even after the loop has run. Then we use the """<<= ...>>""" (print macro) to run the effect code and display some text.</p>
<aside>Admittedly, running SC code through printing is a bit cheesy but it is also very simple and works just fine.</aside>
<p>Hopefully this illustrates the main benefit of using objects as items, the item itself carries the data. Creating a second type of potion is a simple as writing another object with a different name and effect. A bit of generic code in the <vr>PotionMenu</vr> passage handles the printing.</p>
<section>
<h3>Objects in arrays, uncut</h3>
</section><h2>More about arrays...</h2>
<p>For the sake of brevity I only tackled the main, essential functions of inventory arrays in the earlier two chapters (how to push, delete, check for item...).</p>
<p>This is enough to get it working, but it won't be enough for most games, especially those that aim for more complex mechanics. This section provides a few code examples, usually in the form of <vr>widgets</vr>, that can help you implement those extra fancy mechanics.</p>
<section>
<h3>Pickup memory</h3>
<p>If you do not implement the proper checks, a player might be able to pick up an item multiple times if they have the ability to travel back to a given passage. Lets go through the common solutions first.</p>
<h4>The auto-forward</h4>
<<code>>
<<link 'Pick up the wallet' 'NextPassage'>>
<<set $inventory.push('Wallet')>>
<</link>>
<</code>>
<p>Picking up the wallet immediately takes you to the next passage.</p>
<b>Pros:</b>
* simple to deploy, basically foolproof
* the player cannot miss out on a possibly important item
<b>Cons:</b>
* feels unsatisfying, the player isn't rewarded with a short description of the item or even an "item acquired" message
* some actions warrant passage navigation, but some do not, unnecessary passage transitions feel very clunky
* this makes this passage a "throwaway" location, never to be visited again
<aside>While this is based on personal preferences, I always advise to keep passage transitions to a minimum. """<<linkreplace>>""" is your friend!</aside>
<h4>Check the inventory!</h4>
<<code sc>>
<<if $inventory.includes('Wallet')>>
This is where you found the old wallet.
<<else>>
An old wallet lies on the dusty floor.
<<linkreplace 'Pick it up.'>>
<<set $inventory.push('Wallet')>>
You lean down and pocket the small leather object.
<</linkreplace>>
<</if>>
<</code>>
<p>Depending on the game's mechanical requirements, this might be all you ever need.</p>
<b>Pros:</b>
* No way to pick up the same item twice
* The action of picking up an item feels distinct and rewarding
* While you could display nothing if the item is in the inventory, I like idea of the player's character recognizing a passage based on what happened there
<b>Cons:</b>
* If the item ever leaves the player's inventory, they'll be able to get it back to visiting the passage again
* this does not work if the same item can be picked up multiple times in different passages
<aside>Here both issues are conditional, if your game has neither, this solution should be more than enough.</aside>
<h4>Associative tracking</h4>
<<code>>
::StoryInit
<<set $pickedUp = {}>>
::SomePassage
<<set _pickID = passage()+'Wallet'>>
<<if $pickedUp[_pickID]>>
This is where you found the old wallet.
<<else>>
An old wallet lies on the dusty floor.
<<linkreplace 'Pick it up.'>>
<<set $pickedUp[_pickID] = true>>
<<set $inventory.push('Wallet')>>
You lean down and pocket the small leather object.
<</linkreplace>>
<</if>>
<</code>>
<p>This approach associates a passage's title with the item's name, creating an entry in """$pickedUp""". The item in this passage can only be acquired if there is no entry.</p>
<b>Pros:</b>
* ensures items can be picked up once per opportunity without interfering with the rest of the story
* even if the item isn't in the player's inventory anymore, you can check """$pickedUp[ passageName + itemName]""" to know if they picked it up
<p>The above code is best turned into a """<<widget>>""":</p>
<<code sc>>
::PickupWidget [widget]
<<widget 'pickup' container>>
<<set _pickID = passage()+_args[0], _prompts = _contents.split('||')>>
<<if not $pickedUp[_pickID]>>
_prompts[0]
<<capture _pickID, _prompts>>
<<linkreplace _args[1]>>
<<set $pickedUp[_pickID] = true>>
<<set $inventory.push(_args[0])>>
_prompts[1]
<</linkreplace>>
<</capture>>
<<elseif _prompts[2]>>
_prompts[2]
<</if>>
<</widget>>
::SomePassage
<<pickup 'Wallet' 'Pick it up.'>>
An old wallet lies on the dusty floor. ||
You lean down and pocket the small leather object. ||
This is where you found the old wallet.
<</pickup>>
<</code>>
<p>This requires a few explanations... the widget requires 4 pieces of flavour text to work:</p>
* when the player sees the item
* the """<<linkreplace>>""" text
* the description of grabbing the item
* the text for already acquired items (optional)
<p>While these could all be supplied as widget arguments, this lead to jumping through syntactic hoops (don't mix the quotation marks...). In this case, I find it more convenient to """.split()""" the """_contents""" special variable on a special token (<vr>||</vr>).</p>
<p>The """<<if>>""" statement has also been rearranged so as to use """_prompts[0]""" => """_prompts[1]""" => """_prompts[2]""" in the proper order. Notice that if only one <vr>||</vr> separator is used the widget won't display anything when coming back after pick up.</p>
</section>
<section>
<h3>Inventory capacity</h3>
<p>This works with both primitive and objects items.</p>
<<code sc>>
::StoryInit
<<set $capacity = 5, $inventory = []>>
::GetItem [widget]
<<widget 'getItem'>>
<<set _id ??= 0, _id++>>
<<capture _id>>
<span @id="'get'+_id">
<<link 'Pick up item'>>
<<if $inventory.length lt $capacity>>
<<set $inventory.push(_args[0])>>
<<replace `'#get'+_id`>>
Item acquired!
<</replace>>
<<else>>
<<run UI.alert("Inventory is full, you need to make space first!");>>
<</if>>
<</link>>
<span>
<</capture>>
<</widget>>
::UseInPassage
With a primitive:
<<getItem 'Old nail clippers'>>
With an object:
<<getItem setup.legendayNailClippers>>
<</code>>
<p>Here, the widget does a few things:</p>
* It generates a wrapper with an <vr>id</vr> for itself, this makes it so multiple widgets don't replace each other's contents
* The link here is a standard """<<link>>""" instead of the """<<linkreplace>>""" I often use, that's because we only want it replaced if there is inventory space
* If there is, the wrapper gets replaced entirely, which stops players from picking up the item multiple times
* If there is not, a dialog popup appears, prompting the player to make some space
<p>Of course, this needs to be coupled to an inventory system that lets you drop items, something like that (assumes a):</p>
<<code sc>>
::DisplayItem [widget]
<<widget 'display'>>
<span @id="_args[0].replaceAll(' ','')">
Name : _args[0]
<<linkreplace 'Examine'>>
<<include _args[0]>>
<</linkreplace>>
<<link 'Drop item'>>
<<run $inventory.delete(_args[0])
<<replace `'#'+_args[0].replaceAll(' ','')`>>
Farewell _args[0]!
<</replace>>
<</link>>
</span>
<</widget>>
::InventoryPassage
<<for _item range $inventory>>
<<display _item>>
<</for>>
<</code>>
<p>Few comments about the above code:</p>
* Here, the unique <vr>id</vr> is created based on the item name. To do so, spaces must be removed!
* No """<<capture>>""" is used anywhere, this is because widget arguments ("""_args""") are <vr>captured</vr> internally.
* Similarly, the custom <vr>id</vr> is computed again in the """<<replace>>""" macro, leading to the same output
</section>
<section>
<h3>Notes about """<array>.delete()"""</h3>
<p>The [[Primitive inventories]] guide mentions the """<array>.delete()""" method as a way to remove primitives from an array. Be aware that this method removes <strong>every</strong> fitting value.</p>
<<code sc edit>>
<<= _array = [1,2,3,3,3,2,1]>>
<<run _array.delete(3)>>
_array
<</code>>
<p>This will obviously mess with inventories that allow for duplicates, in this case the item should be removed based on its index, using a combination of """<array>.deleteAt()""" and """<array>.indexOf()""".</p>
<<code sc edit>>
<<= _array = [3,3]>>
<<run _array.deleteAt(_array.indexOf(3))>>
_array
<</code>>
<aside>Only the first fitting value is removed.</aside>
</section>
<h3>"""<object>.print(break, colon)"""</h3>
<p>Returns a simple string representation of a javascript object. By default, <vr>break</vr> is """'\n'""" and <vr>colon</vr> is <vr>' : '</vr>.</p>
<<code sc edit>>
<<set _obj = {name : 'Sjoerd', type : 'spirit', specialty : 'written horror'}>>
<<= _obj.print()>>
<</code>>
<h3>"""<object>.multiSet(keys... , value)"""</h3>
<p>Sets every key to the last <vr>value</vr> argument, keys can be either strings or arrays of strings. Returns the object for chaining.</p>
<<code sc edit>>
<<set _obj = {},
_obj.multiSet('name', 'alias', 'id', 'John'),
_obj.multiSet(['size','weight'], 12)>>
<<= _obj.print()>>
<</code>>
<h3>"""<object>.forEach(callback, thisValue)"""</h3>
<p>This method lets you iterate over objects exactly like you would with arrays. The first argument is a callback, called on every entry of the object, the second argument is an optional <vr>this</vr> value.</p>
<p>Callback arguments:</p>
* value of the entry
* key of the entry
* object itself
<p>Returns the object for chaining.</p>
<<code sc edit>>
<<set _stats = {
endurance : 28,
wisdom: 3,
luck : 15,
strength : 23,
toughness : 18,
dexterity : 9
}>>
<<set _high = [], _stats.forEach((val, key) => {
if (val > 15) {
_high.push(key);
}
})>>
Your strong stats are :
_high .
<</code>>
<aside>I am aware the above result could have been achieved with <vr>Object.values(_stats)</vr>, <vr>.filter()</vr> and <vr>.map()</vr>, but code examples aren't always easy to come by...</aside><h3>"""<array>.toUnique()"""</h3>
<p>This method clears all duplicates in place, then returns a reference to the array.</p>
<<code sc edit>>
<<set _arr = [1,1,1,2,2,3,4,'a','b','c','c'],
_arr.toUnique()>>
_arr
<</code>>
<h3>"""<array>.crossFind(<array>)"""</h3>
<p>This lets you check arrays against single values or each others, returns the matching value or <vr>undefined</vr> if none was found.</p>
<<code sc edit>>
<<set _arr1 = [1,2,3,4], _arr2 = [5,6,7,8,9], _arr3 = [15,3,6,8]>>
<<= _arr1.crossFind(_arr2)>>
<<= _arr1.crossFind(_arr3)>>
<<= _arr1.crossFind(_arr2, _arr3, 'other values...')>>
<</code>>
<h3>"""<array>.has(<object>)"""</h3>
<p>Because Sugarcube's <vr>State</vr> is cloned on passage transition, objects cannot be matched based on <vr>reference</vr>. Once a transition has occurred, you cannot check that """$someArray.includes($someObject)""".</p>
<p>The """<array>.has()""" method aims to solve this issue. Returns a <vr data-glos='bool'>Boolean</vr>.</p>
<<code sc edit nobr>>
<<set $obj = {name : 'My object'}>>
<<set $inventory = [$obj]>>
<<if $inventory.has($obj)>>
This check will still work after passage transition!
<</if>>
<</code>>
<aside>This method matches items based on value, so two objects containing the exact same entries will count as being the same. This can also compare primitives.</aside><h3>"""<string>.includesAny(...values...)"""</h3>
Returns true if the test string includes any of the supplied arguments.
<<code sc edit>>
<<= 'the cat has a banana'.includesAny('apple','peach','banana')>>
<</code>>
<h3>"""<string>.includesAll(...values...)"""</h3>
Returns true if the test string includes every argument supplied.
<<code sc edit>>
<<= 'the cat has a banana'.includesAll('apple','peach','banana')>>
<<= 'the cat has a banana'.includesAll('cat','has','banana')>>
<</code>>
<h3>"""<string>.startsWithAny(...values...)"""</h3>
Returns true if the test string starts with any of the supplied arguments.
<<code sc edit>>
<<= 'the cat has a banana'.startsWithAny('a','multiple','the')>>
<</code>>
<h3>"""<string>.endsWithAny(...values...)"""</h3>
Returns true if the test string end with any of the supplied arguments.
<<code sc edit>>
<<= 'the cat has a banana'.endsWithAny('apple','peach','banana')>>
<</code>>
<h3>"""<string>.removeEvery(...values...)"""</h3>
Returns a new string with every occurence of every argument removed from it.
<<code sc edit>>
<<= 'My cat is probably smarter than the average human. It is frightening.'.removeEvery('cat','human','frightening')>>
<</code>>
<h3>"""<string>.sanitize()"""</h3>
<p>Returns a new string that cannot be parsed by sugarcube or as html. This prevents players from inputting code that would be run whenever it is printed.</p>
<aside>The <i>deactivated</i> characters are : < , > , [ , ] , $ and _ .</aside>
<<code sc edit>>
<<set _money = 0>>
Your name :
<<textbox '_name' '<<set _money += 9000>>'>>
Welcome _name!
Money : _money
Welcome <<= _name.sanitize()>>!
Money : _money
<</code>>
<h3>"""<string>.processText()"""</h3>
<p>This is a copy of the """<passage>.processText()""" method which accepts any string rather than a passage instance.</p>
<p> """<passage>.processText()""" is responsible for applying """Config.passages.nobr""" and running the """Config.passages.onProcess""" function.</p>
<h3>"""<string>.insert(text, position)"""</h3>
<p>Returns a new <vr>string</vr> with the given """text""" inserted at the given """position""".</p>
<<code sc edit>>
<<= 'I wanted this gift!'.insert('never ',2)>>
<<= '123789'.insert('-456-',3)>>
<</code>><h3>"""<jQuery>.at(insert, index)"""</h3>
<p>Insert an element at the given index. Returns the element for chaining.</p>
<h3>"""<jQuery>.insertAt(target, index)"""</h3>
<p>Insert this element at the given index. Returns the element for chaining.</p>
<p>The two methods behave exactly the same except when it comes to syntax order : <vr>target.at(insert, index)</vr> VS <vr>insert.insertAt(target, index)</vr>.</p>
<p>The order of the arguments is irrelevant, the first one to be a number is chosen as index. Negative indices are also supported.</p>
<<code sc edit nobr>>
<div id='target'>
<<for _i = 0; _i lt 9;_i++>>
<span>_i</span>
<</for>>
</div>
<<numberbox '_index' 5>>
<<set _insert = $('<span>').text('Insert').css('color','red')>>
<<button 'Insert as element n°{{_index}}'>>
<<set $('#target').at(_insert, _index)>>
<</button>>
<</code>><<section 'Simple Expressions'>>
<p>Simple Expressions (<vr>SimExp</vr>) aim to be a more accessible alternative to <vr>regular expressions</vr> (often abbreviated as <vr>RegEx</vr>).</p>
<<h 'What is it about RegEx?'>>
<p><vr>RegEx</vr> allows for matching patterns in text, it is an essential tool for processing structured strings. Do you need to fetch every digit in a text? Every uppercase letter? Every dot that is surrounded by spaces? Any text enclosed in brackets?</p>
<p><vr>RegEx</vr> is built for that, it is responsible for Sugarcube macros being identified and parsed correctly... it also looks very intimidating.</p>
<<code>>
/\#(?!\d)(?:\w|-)+/g //Detects CSS IDs
/\w+\s*\((.+?)\)(?=\s*(?:=>)*\s*{)/g //Detects variables in function declarations
<</code>>
<<h 'Disclaimer'>>
<p>Simple Expressions don't aim, or try, to be a <vr>RegEx</vr> replacement. It is a toolbox for non-coders to accomplish simple parsing tasks without having to dive into the unfamiliar syntax.</p>
<p>If you need <vr>RegEx</vr>' full potential, learn the language, it isn't terribly complicated and you can accomplish a lot even with basic expressions.</p>
<<h 'Creating expressions'>>
<p>Simple Expressions are created by using the <vr>SimExp</vr> constructor and a series of space-separated tokens.</p>
<<code sc edit>>
<<set $wordMatcher = new SimExp('word or number length 4')>>
<<= $wordMatcher>>
<</code>>
<</section>>window.SimExp = class SimExp {
constructor(...args) {
this.length = 0;
this.rawExp = [];
args.forEach(exp => this.add(exp));
}
add (...args) {
let entries = [];
args.filter(e => e).forEach(exp => {
if (exp instanceof SimExp) {//Merge them
exp.forEach(m => {
entries.push(clone(m));
});
} else if (typeof exp === 'string') {
entries.push(this.parseExp(exp));
} else {
throw new Error('SimExp.add() : Method only accepts strings or other SimExp instances.');
}
});
entries.forEach(e => {
this.rawExp.push(e.expression);
this[this.length++] = e;
})
return this;
}
parseExp (exp) {
let skip = 0, matcher = '', flags = ['g'], limit = {min : 0, max : Infinity};
const arr = exp.trim().split(' ').map(e => e.trim());
arr.forEach((t,i) => {
if (skip) {return skip--}
//A number after another selector
if (/\d/.test(t) && i) {
if (matcher.at(-1) === '+') {matcher = matcher.slice(0,-1)}
//Check if previous selector is basic or group
return matcher += '{'+t+'}';
}
//Flags
switch (t) {
case 'first': return flags.delete('g'); break;
case 'every': return flags.push('g'); break;
case 'insensitive': return flags.push('i'); break;
case 'casesensitive': return flags.delete('i'); break;
}
if (['length','shorter','longer'].includes(t)) {
const next = arr[i+1];
if (/\d/.test(next)) {
switch (t) {
case 'longer' : limit.min = Number(next); break;
case 'shorter' : limit.max = Number(next); break;
case 'length' : limit.min = Number(next) -1; limit.max = Number(next) + 1;
}
return skip++;
} else {
throw new Error(`'${exp}' : '${t}' parameter must be followed by a number.`);
}
}
matcher += this.getToken(t);
});
return {expression : exp, matcher : matcher, flags : flags.join(''), limit : limit};
}
forEach(callback, thisArg) {
for (let i = 0; i < this.length;i++) {
const entry = this[i];
callback.call(thisArg ?? null, entry, i, this);
}
return this;
}
toString() {
return `SimExp(${this.rawExp})`;
}
getToken(t) {
const tok = SimExp.token;
return tok.basic[t] ?? tok.group[t] ?? t;
}
match(str) {
const matches = [];
this.forEach(e => {
let reg = new RegExp(e.matcher, e.flags), mat = str.match(reg);
if (mat) {//Isn't null
mat
.filter(m => m && m.length > e.limit.min && m.length < e.limit.max)
.forEach(m => {
matches.push(m);
})
}
})
return matches.length ? matches : null;
}
test(str) {
return !!this.match(str);
}
replace(str,rep) {
this.forEach(e => {
let reg = new RegExp(e.matcher, e.flags);
str = str.replace(reg, m => {
if (m && m.length > e.limit.min && m.length < e.limit.max) {
return typeof rep === 'function' ? rep.call(null, m) : rep;
}
return m;
})
})
return str;
}
clone() {
return new SimExp(...this.rawExp);
}
toJSON() {//Test the JSON!
return JSON.reviveWrapper(`new SimExp(${this.rawExp})`);
}
};
SimExp.token = {
basic : {
char : '.',
any : '(?:.|\\s)',
upper : '[A-ZÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖŠÚÛÜÙÝŸŽ]',
lower : '[a-zàáâãäçèéêëìíîïñòóôõöšùúûüýÿž]',
letter : '[a-zàáâãäçèéêëìíîïñòóôõöšùúûüýÿžA-ZÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖŠÚÛÜÙÝŸŽ]',
digit : '\\d',
space : '\\s',
until : '(?:.|\\s)*?',
or : '|',
and : '\\s*'
},
group : {
word : '[a-zàáâãäçèéêëìíîïñòóôõöšùúûüýÿžA-ZÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖŠÚÛÜÙÝŸŽ]+',
number : '\\d*\\.*\\d+',
special : '[^a-zàáâãäçèéêëìíîïñòóôõöšùúûüýÿžA-ZÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖŠÚÛÜÙÝŸŽ\\d\\s]',
math : '[-+*\\/=<>()%]',
macro : '<<(?:.|\\s)*?>>',
link : '\\[\\[(?:.|\\s)*?\\]\\]',
quoted : '(\'|"|`)(?:.|\\s)*?\\1'
}
};
<h2>The """<<hover>>""" macro'</h2>
<p>
This macro creates a container which is sensitive to <vr>mouseover</vr> and <vr>focus</vr> events. Its has two display modes (which can be used in conjunction) :
</p>
* a tooltip
* swapping the container's contents with something else
<aside>On mobile, tapping the element will trigger the effect as it brings it into <vr>focus</vr>.</aside>
<aside>Elements generated by this macro can be accessed with the """tab""" key.</aside>
<h3>Syntax</h3>
<<code sc>>
<<hover [attribute object]>>
Content
[ <<swap>> Swap content ]
[ <<tip [direction]>> Tip content ]
<</hover>>
<</code>>
<<tabs Tooltip>>
<p>The """<<tip>>""" tag lets you supply the tooltip's contents. This tag takes direction arguments which can be either """up""" (default), """down""", """left""", """right""" or """over""". </p>
<p>When deployed, the tooltip will try to orient itself in the first supplied position. If it is not possible (this causes a window overflow), it will try each subsequent one until a suitable position is found.</p>
<<code sc edit nobr>>
<<hover>>
Things
<<tip>>More things!
<</hover>>
<</code>>
<div style='display: flex;justify-content: space-around;'>
<<hover>>
Left
<<tip 'left'>> To the left
<</hover>>
<<hover>>
Up
<<tip 'up'>> Above the container
<</hover>>
<<hover>>
Down
<<tip 'down'>> Under the container
<</hover>>
<<hover>>
Right
<<tip 'right'>> To the right
<</hover>>
<<hover>>
Over
<<tip 'over'>> Over the container
<</hover>>
</div>
<h3>Styling the tooltip</h3>
<p>There is one unique tooltip element, used accross all containers, it bears the """#macro-hover-tip""" id. You should supply a class to the """<<hover>>""" container in order to target a given tooltip (see code below).</p>
<<code sc edit nobr>>
<style>
.boldTip > #macro-hover-tip {
font-weight : bold;
letter-spacing: .5em;
color: #ffef00;
}
</style>
<<hover `{class : 'boldTip'}`>>
See warning
<<tip>>Beware!
<</hover>>
<</code>>
<<tab Swap>>
<p>The swap mode simply replaces the container's contents with that of the """<<swap>>""" tag.</p>
<<code sc edit nobr>>
<<hover>>Old content
<<swap>>New content
<</hover>>
<</code>>
<p>When doing so, content is wikified, meaning you can use it to run sugarcube code should you need to. If you only want to run code on hover, you should use the """<<listen>>""" macro however as it is built for it.</p>
<<code sc edit>>
<<set _i = 0>>
<<hover>>
_i
<<swap>>
<<= _i++>>
<</hover>>
<</code>>
<aside>The old and new content should be off similar sizes, otherwise this can lead to jarring effects. When the element shrinks, it might move away from the mouse pointer, thus breaking the hover.</aside>
<</tabs>>
WIP-tagged passages should display this status! V
solve the minified issue and add some automated github reference/load from. v
Fix the markups not loading from github!
Add new macros to the lexicon. ~
Go over the loop macro code, write doc.
<<section 'The tab icon function'>>
<p>This function lets you use an image as the tab's icon. The picture in question should be square and in one of those formats : <vr>.gif</vr>, <vr>.png</vr> or <vr>.ico</vr>.</p>
<<code js nobr clipboard>>
setup.setTabIcon = (url) => {
const format = url.split('.').at(-1);
$('head').append(`<link rel='icon' type='image/${format === 'ico' ? 'x-icon' : format}' href='${url}'>`);
}
<</code>>
<<h 'Using the function'>>
<p>With the code above in your JS tab, you can then call the """setup.setTabIcon(url)""" function. While this function can be called at any point, I suggest doing so in the JS tab or in <vr>StoryInit</vr>, using """<<set>>""".</p>
<</section>>Default start passage.<h2>The """swap-markup""" </h2>
<p>This very simple markup lets you output random content.</p>
<<code sc edit>>
(1|2|3|4)
(1|2|3|4)
(1|2|3|4)
(1|2|3|4)
<</code>><<section 'Container templates'>>
<style>
.player {font-weight:bold; color:orange}
</style>
<<code js `visited(passage())>1 ? '' : 'run'`>>
//Sugarcube's default template
Template.add('player', `<span class='player'>$playerName</span>`);
//Custom template container
TemplateContainer.add('playerSpeech', t => `<p>
<span class='player'>$playerName</span> : ${t}
</p>`);
<</code>>
<<code sc edit>>
<<set $playerName = 'Gwen'>>
Hello ?player.
?(playerSpeech: Hi, I'm Gwen!)
Using normal templates as containers:
?(player: Hi, I'm Gwen!)
<</code>>
<</section>><h2> All about array methods </h2>
<p>This is a short list of the most useful array methods with a quick explanation and interactive examples. Some are much more useful than others when it comes to common Sugarcube applications.</p>
<h3>Array.at(index)</h3>
<p>Returns item at a given index, negative indices start from the array's end.</p>
<p>Usefulness : situational.</p>
<<code sc edit>>
<<set _arr = ['a','b','c','d']>>
Last item : <<= _arr.at(-1)>>
Second last : <<= _arr.at(-2)>>
<</code>>
<h3>Array.concat(Array...)</h3>
<p>Returns a new array from multiple merged ones. The spread syntax makes it mostly useless.</p>
<p>Usefulness : mostly useless.</p>
<<code sc edit>>
<<set _arr1 = [1,2,3], _arr2 = [4,5,6]>>
<<= _arr1.concat(_arr2)>>
"""_arr1""" hasn't been modified : _arr1
Same result using the spread syntax:
<<= [... _arr1, ... _arr2]>>
<</code>>
<h3>Array.entries()</h3>
<p>Returns an array iterator that lets you get each array entry in this format : """[ index, value]"""</p>
<p>Usefulness : very situational</p>
<<code sc edit>>
<<for _ent range [...['a','b','c','d'].entries()]>>
_ent[0] : _ent[1]
<br>
<</for>>
<</code>>
<h3>Array.every(callback) & Array.some(callback)</h3>
<p>Checks if items in an array pass a given test.</p>
<li>Array.every() : Returns true if every item passes the test.</li>
<li>Array.some() : Returns true if any item passes the test.</li>
<p>Usefulness : useful.</p>
<<code sc edit>>
All entries are greater than 0 : <<= [0,1,2].every(e => e > 0)>>
At least one entry is greater than 0 : <<= [0,1,2].some(e => e > 0)>>
<</code>>
<h3>Array.filter(callback)</h3>
<p>Returns a new array that contains only the items that passed a given text.</p>
<p>Usefulness : very useful.</p>
<<code sc edit>>
Positive numbers only : <<= [0,1,-5,2,40,-89,0,4].filter(e => e > 0)>>
<</code>>
<h3>Array.find(callback) & Array.findLast(callback)</h3>
<p>Find and return items that pass a given test, or """undefined""" if none was found.</p>
<li>Array.find() : Returns the first item to pass the test.</li>
<li>Array.findLast() : Same but starting the search at the end.</li>
<p>Usefulness : very useful, especially when dealing with objects in arrays.</p>
<<code sc edit>>
First positive number : <<= [-5,0,6,18].find(e => e > 0)>>
Last positive number : <<= [-5,0,6,18, -8].findLast(e => e > 0)>>
<</code>>
<h3>Array.findIndex(callback) & Array.findLastIndex(callback)</h3>
<p>Very similar to the methods above, returns the item's index rather than its value. Or -1 if none was found.</p>
<p>These are commonly used in vanilla JS however Sugarcube adds very convenient methods that reduces the need to use those.</p>
<p>Usefulness : situational.</p>
<<code sc edit>>
First positive number can be found at index <<= [-5,0,6,18].findIndex(e => e > 0)>>
<</code>>
<h3>Array.flat(depth)</h3>
<p>Unpacks nested arrays to a given depth (1 by default).</p>
<p>Usefulness : very situational but when the needs arises you'll be glad it exists.</p>
<<code sc edit>>
Flattened : <<= [[1,2],[3,4], [5, [6,7]]].flat(2)>>
<</code>>
<h3>Array.forEach(callback)</h3>
<p>Calls a function on every item in the array. It is the JS equivalent of """<<for ... range ...>>""".</p>
<p>Usefulness : very useful.</p>
<<code sc edit>>
<<done>>
<<run ['Key','Wallet','Watch'].forEach(item => {
$('#items').append(item+' ');
})>>
<</done>>
<span id='items'/>
<</code>>
<h3>Array.includes(item) & Array.includesAll(item...) & Array.includesAny(item...)</h3>
<p>Checks if the array has a given item. Sugarcube adds two custom methods, Array.includesAll() and Array.includesAny().</p>
<p>Because Sugarcube clones the State on passage transition these methods are mostly useful when looking for primitives.</p>
<p>Usefulness : very useful.</p>
<<code sc edit>>
You have a watch: <<= ['Key','Wallet','Watch'].includes('Watch')>>
<</code>>
<h3>Array.indexOf(item) & Array.lastIndexOf(item)</h3>
<p>Returns an element's first or last index in the array. Unlike Array.findIndex() and Array.findLastIndex() which use a callback, this method takes the desired element as argument.</p>
<p>Very common in vanilla JS, less so in Sugarcube for two reasons : more convenient custom methods and State cloning.</p>
<p>Usefulness : situational.</p>
<<code sc edit>>
The watch is element n° <<= ['Key','Wallet','Watch'].indexOf('Watch')>>
<</code>>
<h3>Array.join(separator)</h3>
<p>Prints array items separated but an otpionnal separator (',' by default).</p>
<p>Usefulness : situational.</p>
<<code sc edit>>
<<set _inv = ['Key','Wallet','Watch']>>
<<= _inv.join()>>
<<= _inv.join(' and ')>>
<</code>>
<h3>Array.map(callback)</h3>
<p>Returns a new array containing items as they have been transformed by the callback. Especially useful for turning an array of objects into an array of a given object property. Does not modify the original.</p>
<p>Usefulness : useful.</p>
<<code sc edit>>
<<= [1,2,3,4].map(n => n*2)>>
<</code>>
<h3>Array.pop() & Array.shift()</h3>
<li>Array.pop() : Removes and returns the last element.</li>
<li>Array.shift() : Removes and returns the first element.</li>
<p>Usefulness : situational.</p>
<<code sc edit>>
<<set _arr = ['a','b','c','d']>>
First : <<= _arr.shift()>>
Last : <<= _arr.pop()>>
Remaining : _arr
<</code>>
<h3>Array.push()</h3>
<p>Adds items to the end of the array. It is one of the corner stone array methods.</p>
<p>Usefulness : very useful.</p>
<<code sc edit>>
<<set _arr = ['a','b','c']>>
Add items: <<run _arr.push('d','e')>>
_arr
Merge arrays: <<run _arr.push(...['f','g','h'])>>
_arr
<</code>><h2>Unicode, it's fun</h2>
<<code sc edit>>
<style>
.stairs::before {
content: '🠑⧙';
rotate: 45deg;
display: inline-block;
font-size: 1.2em;
margin-right: .5em;
}
</style>
<span class='stairs'>Go up.</span>
<</code>>Object.defineProperty(String.prototype, 'splitMatch', {
value(reg) {
const matchIndices = [...this.matchAll(reg)].map(m => { return { start: m.index, end: m.index + m[0].length } }),
splitMatch = { match: {}, rest: {} };
let last = 0, i = 0;
matchIndices.forEach(m => {
splitMatch.rest[i++] = this.substring(last, m.start);
splitMatch.match[i++] = this.substring(m.start, m.end);
last = m.end;
});
if (last < this.length) splitMatch.rest[i++] = this.substring(last, this.length);
splitMatch.length = i;
return splitMatch;
}
});
window.highlighter = {
esc: txt => txt.replaceAll('>', '>').replaceAll('<', '<'),
getProfileEntries() {
const prf = ['sc', 'css', 'html'], sub = ['any', 'outer', 'inner'], list = [...Object.entries(this.profiles.js)];
prf.forEach(p => {
sub.forEach(s => {
list.push(...Object.entries(this.profiles[p][s] ?? {}));
})
});
return list;
},
toElem(txt, pName) {
const $out = $(`<code class='macro-code' style='white-space: pre-wrap'>`).append(this.toFrag(txt, pName));
//add right click handler on element
//display information pop-up
$out.on('contextmenu', e => {
//e.preventDefault();
//console.log(e.target);
});
return $out;
},
toFrag(txt, pName) {
const $frag = $(new DocumentFragment()).append(this.processString(txt, pName));
//clear unwanted matches in comments and strings + number part of hexadecimals
$frag.find('.comment, .jsComment, .jsString, .htmlString, .cssHexa, .scVar').each((i, e) => e.innerHTML = this.esc(e.innerText));
//fix template literal inserts, clear classes and rerun inserts if needed
$frag.find('.jsTemplit').each((_, e) => e.innerHTML = this.esc(e.innerText).replace(/(?<=\${).*?(?=})/gs, m => `<span class=jsTemplitInsert>${this.processString(m, 'js')}</span>`));
return $frag;
},
processString(txt, pName) {
txt = this.handler(txt, pName ?? 'sc');
//sanitize elements then retrieve color spans
return this.esc(txt).replaceAll('⁅', `<span class=`).replaceAll('⁆', '</span>').replaceAll('⦙', '>');
},
handler(txt, pName) {
const profile = typeof pName === 'string' ? clone(this.profiles[pName]) : pName;
//parse block comments from the very start
txt = txt.replace(/\/\*.*?\*\//gs, m => `⁅comment⦙${m}⁆`);
//has two-parts parsing
if (profile.getInner) {
const { inner, outer } = profile, split = txt.splitMatch(profile.getInner);
//some elements need to be highlighted in both situations
if (profile.any) {
Object.assign(inner, profile.any);
Object.assign(outer, profile.any);
}
//sc macros get inner js processing
if (pName === 'sc') Object.assign(inner, this.profiles.js);
//parse inner
for (const k in split.match) split.match[k] = this.handler(split.match[k], inner);
//parse outer
for (const k in split.rest) {
split.rest[k] = this.handler(split.rest[k], outer);
if (pName === 'sc') split.rest[k] = this.handler(split.rest[k], this.profiles.html);
}
//re-assemble the text
let i = -1, output = '';
while (i++ < split.length) output += split.rest[i] ?? split.match[i] ?? '';
if (pName === 'sc') {
//parse scripts as js, styles as css
output = output.replace(/(?<=<<script>>).+?(?=<<\/script>>)/gs, m => this.handler(m, this.profiles.js));
output = output.replace(/(?<=<style>).+?(?=<\/style>)/gs, m => this.handler(m, this.profiles.css));
}
return output;
}
for (const name in profile) {
const prof = profile[name];
if (prof instanceof RegExp) {
txt = txt.replace(prof, m => `⁅${name}⦙${m}⁆`);
} else if (Array.isArray(prof)) {
txt = txt.replace(this.profiles.keyword, m => prof.includes(m) ? `⁅${name}⦙${m}⁆` : m);
} else {
//special object
const matcher = prof.matcher ?? this.profiles.keyword;
txt = txt.replace(matcher, m => `⁅${prof.class ?? name}⦙${m}⁆`);
}
}
return txt;
},
profiles: {
keyword: /(?<!\w)\w+(?!\w)/g,
sc: {
getInner: /(?<=<<(?:\w|-|=)+\s).*?(?=>>)/gs,
any: {
scVar: { matcher: /(?:\$|_)(?:\.|\w|\[.*\])+/g, style: 'color: #00e3e3;' },
scBracket: { matcher: /\[(img|)\[.+?]]/g, style: 'color: aqua;' }
},
inner: {
scOperators: ['gte', 'lte', 'gt', 'lt', 'to', 'not', 'def', 'ndef', 'is', 'isnot', 'eq', 'neq', 'and', 'or', 'range']
},
outer: {
scMacro: /<<\/*(?:\w|-|=)+(?:>>)*|>>/g,
scStyle: /@@.+?@@/g,
scTemplate: /(?<!\w)\?\w+/g,
htmlElem: /(?<!<)<\/*\w+.*?>/gs //ideally, html element should only be in html....
}
},
html: {
getInner: /(?<=(?<!<)<\w+\s).*?(?=>)/gs,
inner: {
scAttrDir: { matcher: /(?:@|sc-eval:)(?=\w)/, desc: 'Attribute directive', ref: 'https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute-directive', style: '' },
htmlAttr: /[a-zA-Z-0-9]+(?=\s*=)/gs,
htmlDigit: /-*\d*\.*\d+/g,
htmlString: /('|").*?(?:\1)/g
},
outer: {
htmlComment: /<!--.+?-->/g,
}
},
css: {
getInner: /{.*?}/gs,
any: {
cssAnim: ['to', 'from']
},
inner: {
cssProp: /(?:\w|-)+(?=\s*:)/g,
cssVal: /(?<=:).*?(?=;|})/g,
cssString: /('|").*?(?:\1)/g,
cssHexa: /#[0-9a-f]+/g,
cssUnit: /(?<=\d)(?:r*em|ch|px|s|%|v(?:h|w|min|max)|r*lh|fr)/g,
cssDigit: /(?<!\w)(?:-*\d*\.*\d+)/g,
cssFunction: /\w+\(.*?\)/g
},
outer: {
cssType: /(?<=\s|^)\w+/g,
cssAtRule: /@(?:\w|-)+/g,
cssId: /#(?:\w|-)+/g,
cssClass: /\.(?!\d)(?:\w|-)+/g,
cssPseudoClass: /(?<!:):(?:\w|-)+/g,
cssPseudoElem: /::(?:\w|-)+/g,
cssAttr: /\[.*?\]/g
}
},
js: {
//SC namespaces
jsNamespace: ['window', 'State', 'UI', 'Dialog', 'Setting', 'Engine', 'Save', 'Story', 'Template', 'Macro', 'Util', 'setup', 'SugarCube', 'Wikifier', 'Parser', 'JSON'],
jsDigit: /(?<!\w)(?:-*\d*\.*\d+)(?!\w)/g,
jsString: /('|").*?(?:\1)/g,
jsReserved: ['let', 'const', 'var',
'return', 'throw',
'function', 'yield',
'this', 'arguments',
'delete',
'for', 'in', 'of', 'while', 'do', 'continue',
'switch', 'case', 'break', 'if', 'else',
'typeof', 'instanceof',
'new', 'class', 'extends', 'static', 'get', 'set',
'try', 'catch',
'async', 'await', 'then'],
jsFunction: /\w+(?=\()|=>/g,
jsType: ['Array', 'Object', 'Boolean', 'true', 'false', 'String', 'null', 'undefined', 'Error', 'RegExp', 'Date', 'Map', 'Set', 'Number', 'NaN', 'Infinity'],
jsTemplit: /`.*?`/gs,
jsComment: /\/\/.+/g,
jsAssign: /(?<!=)(?:\+|-|\/|\*|\||\?|%|&)*=(?!=|>)/g,
jsMath: /(?<!=)(?:\+|-|\/|\*|%|<|>)+(?!=)/g,
jsEqual: /(?:>|<|!)={1,2}|={2,3}/g,
jsComparator: /(?:\||&|\?){2}(?!=)|!/g,
jqSigil: /(?:\$|jQuery)(?=\(|\.)/g,
jsRegex: /\/.+?\/[gmiyuvsd]+/g //needs flags to be recognized as regex, not great
}
}
};
highlighter.customize = {
classes: {
sc: {
scVar: 'color: #00e3e3;',
scBracket: 'color: aqua;',
scOperators: 'color: #bd0000;',
scMacro: 'color: #bd0000;',
scStyle: 'color: #ff8f00;',
scTemplate: 'color: #ff3dbc;',
scAttrDir: '',
htmlElem: 'color: #ff3dbc;',
htmlAttr: 'color: white;',
htmlDigit: '',
htmlString: 'color: #ff5334;',
htmlComment: 'color: gray;',
comment: 'color: gray;'
},
css: {
cssProp: '',
cssVal: 'color: #ff5334;',
cssStrig: 'color: #ff5334;',
cssHexa: 'color: #97ff19;',
cssUnit: 'color: #97ff19;',
cssDigit: 'color: #97ff19;',
cssFunction: 'color: #00e3e3;',
cssType: 'color: #9fff9f;',
cssAtRule: 'color: #ff8f00;',
cssAnim: 'color: #ff8f00;',
cssId: '',
cssClass: 'color: #63ffef;',
cssPseudoClass: 'color: steelblue;',
cssPseudoElem: 'color: steelblue;',
cssAttr: 'color: #ffafff;'
},
js: {
jsNamespace: 'color: yellowgreen;',
jsDigit: 'color: #97ff19;',
jsString: 'color: #ff5334;',
jsReserved: 'color: #bd0000;',
jsFunction: 'color: #00e3e3;',
jsType: '',
jsTemplit: 'color: #ff8f00;',
jsTemplitInsert: 'color: white;',
jsComment: 'color: gray;',
jsAssign: 'color: #97ff19;',
jsMath: 'color: #97ff19;',
jsEqual: 'color: #97ff19;',
jsComparator: 'color: #97ff19;',
jqSigil: '',
jsRegex: 'color: #ff8f00;'
}
},
styleSheet: $('<style id="highlightStyles">').appendTo('head'),
buildStyles(classes) {
let rules = '';
for (const k in classes) {
for (const className in classes[k]) rules += `.${className} {${classes[k][className]}}`;
}
this.styleSheet[0].innerText = rules;
},
init(withMenu) {
this.buildStyles(this.classes);
if (withMenu) this.buildMenu();
},
buildMenu() {
let classes = recall('highlighterSettings') ?? clone(this.classes);
const $menu = $(`
<div id='editorMenu' class='stowed'>
<div id='topBar'></div>
<div id='inputContainer'></div>
<span>
<button id='applyButton'>Apply changes</button>
<button id='resetButton'>Reset to default</button>
</span>
<span class='side'>
<button id='stowButton' title='Stow/unstow'>⇔</button>
<button id='loadButton'>Load pattern</button>
<button id='getButton'>Get pattern</button>
</span>
</div>
`);
Object.entries(classes).forEach(([name, v]) => {
//the select button in the topbar
const selectBut = $('<button>').text(name).on('click', _ => {
$('.active').removeClass('active');
$(`#${name}Panel`).addClass('active');
});
$menu.find('#topBar').append(selectBut);
const panel = $(`<div id='${name}Panel' class='inputPanel ${name === 'sc' ? 'active' : ''}'>`);
for (const k in v) {
const lab = $('<label>').text(k);
$(`<input value='${v[k]}'>`).on('change', function () {
classes[name][k] = this.value;
}).appendTo(lab);
panel.append(lab);
}
$menu.find('#inputContainer').append(panel);
});
//handler on apply button
$menu.find('#applyButton').on('click', _ => {
this.buildStyles(classes); //rebuild style elem
memorize('highlighterSettings', classes); //commit to memory
});
//handler on reset button
//add confirm dialog
$menu.find('#resetButton').on('click', _ => {
classes = clone(clone(this.classes));
this.buildStyles(classes); //rebuild style elem
forget('highlighterSettings'); //commit to memory
});
//stow
$menu.find('#stowButton').on('click', _ => $menu.toggleClass('stowed'));
//output current palette to a dialog, improve
$menu.find('#getButton').on('click', _ => {
Dialog.setup();
Dialog.append(JSON.stringify(classes)).open();
});
//load a stringified palette object and set it as appearance
$menu.find('#loadButton').on('click', _ => {
Dialog.setup();
Dialog.append(`<textarea id='paletteInput'>`);
const applyButton = $('<button>').text('Load palette');
applyButton.on('click', e => {
const obj = JSON.parse($('#paletteInput').val());
//the error should appear in dialog instead
if (typeof obj !== 'object') throw new Error(`Improper palette object.`);
//do better object validation!
classes = obj;
this.buildStyles(classes);
memorize('highlighterSettings', classes);
Dialog.close();
});
Dialog.append(applyButton).open();
});
$('body').append($menu);
}
};
highlighter.customize.init(true);
//simple display elems
Macro.add(['js', 'css', 'sc', 'code'], {
tags: null,
handler() {
const mode = this.name === 'code' ? this.args[0] : this.name,
wikiOutput = this.args.includes('edit');
if (wikiOutput) {
let contents = this.payload[0].contents.trim();
if (this.args.includes('nobr')) contents = contents.replace(/^\n+|\n+$/g, '').replace(/\n+/g, ' ');
const wikified = $('<div>').wiki(contents), $wrp = $(`<div class='codeWrapper' data-mode='${mode}'>`).append(highlighter.toElem(this.payload[0].contents.trim(), mode), wikified);
return $wrp.appendTo(this.output);
};
highlighter.toElem(this.payload[0].contents.trim(), mode).appendTo(this.output);
}
});
//editor part!
window.codeEditor = class editor {
constructor(txt, mode) {
this.content = txt || '';
this.subMode = this.mode = mode ?? 'sc';
this.beingTyped = '';
this.selection = null;
this.focusNode = null;
this.suggestion = {
possible: [],
dialog: $(`<div class='autocomplete'>`)
};
this.elem = $(`<code class='${this.mode} code-editor' style='white-space: pre-wrap' contenteditable='true' spellcheck='false'>`).append(highlighter.toFrag(this.content, this.mode));
this.wrapper = $(`<div class='editorWrapper'>`).append(this.elem, this.suggestion.dialog);
const toClose = {
"'": "'",
'"': '"',
'`': '`',
'{': '}',
'(': ')',
'[': ']',
};
this.elem.on('keydown', e => {
const isLetter = e.key.length === 1 && /\w/.test(e.key);
//new selection
this.selection = getSelection();
this.getSubMode();
log(this.selection);
//tab needs to run before to take into account last time's suggestions!
if (e.key === 'Tab') {
const sug = this.suggestion.possible;
this.insert(sug.length ? sug[0].slice(this.beingTyped.length) : '\t');
return false;
}
//use selection to find the word being typed!
const { focusNode, focusOffset } = this.selection;
const prevLetter = focusNode.textContent[focusOffset - 1], nextLetter = focusNode.textContent[focusOffset];
log(prevLetter, nextLetter);
if (isLetter) {//a normal character
this.beingTyped += e.key;
} else if (e.key === 'Backspace') {//backspace, trim typed
this.beingTyped = this.beingTyped.slice(0, -1);
} else {
this.beingTyped = '';
}
this.getSuggestion();
if (e.key === 'Enter' && e.ctrlKey) {
this.content = this.elem[0].innerText;
return this.refresh();
}
if (toClose[e.key]) {
this.insert(e.key + toClose[e.key], -1);
return false;
}
if (this.subMode === 'sc' && e.key === '<') {
this.insert('<>', -1);
return false;
}
});
this.elem.on('click', e => {
this.selection = getSelection();
this.getSubMode();
});
}
refresh() {
//get absolute caret position
const pos = this.absCaretPos();
this.elem.empty().append(highlighter.toFrag(this.content, this.mode));
//set caret back to the same place
this.setCaretAt(pos);
}
//count nodes, get and set absolute caret position
getFlatNodes() {
let ch = [...this.elem[0].childNodes];
while (ch.some(c => c.nodeType !== 3)) ch = ch.map(n => n.nodeType === 3 ? n : [...n.childNodes]).flat();
return ch;
}
absCaretPos() {
const { focusNode, focusOffset } = this.selection;
let count = 0, pos;
this.getFlatNodes().forEach(n => {
if (n === focusNode) pos = count + focusOffset;
count += n.length;
});
return pos;
}
setCaretAt(pos) {
let count = 0, node, nodes = this.getFlatNodes(), i = 0;
while (count < pos) {
node = nodes[i++];
count += node.length;
}
if (!node) return this.elem.focus();
getSelection().setPosition(node, Math.max(0, node.length - (count - pos)));
}
getSubMode() {
if (this.selection.focusNode === this.elem[0]) return this.subMode = this.mode;
const parent = this.selection.focusNode.parentNode;
this.subMode = parent.classList[0].substring(0, 2);
}
insert(char, offset = 0) {
const node = this.selection.focusNode, range = this.selection.getRangeAt(0);
if (node.nodeType === 3) {
const pos = this.selection.focusOffset;
if (!node.nodeValue) {
node.nodeValue = char;
} else {
node.nodeValue = node.nodeValue.substring(0, pos) + char + node.nodeValue.substring(pos);
}
range.setStart(node, pos + char.length + offset);
} else {//No text node, create one
const txtNode = document.createTextNode(char);
node.appendChild(txtNode);
range.setStart(txtNode, char.length + offset);
}
}
getSuggestion() {
this.suggestion.dialog.empty();
if (!this.beingTyped) {
this.suggestion.dialog.hide();
return this.suggestion.possible = [];
};
//handle to visual part!
this.suggestion.possible = codeEditor.autocomplete[this.subMode].filter(e => e.startsWith(this.beingTyped));
this.suggestion.dialog.show().append(this.suggestion.possible.join());
}
static autocomplete = {
//gotta figure it out...
sc: ['silently', 'set', 'link', 'textbox', 'capture', 'script', 'stop', 'range', 'for', 'textarea', 'unset', 'addclass', 'run', 'include', 'nobr', 'print', 'type', 'if', 'elseif', 'else', 'break', 'continue', 'switch', 'case', 'button', 'checkbox'],
js: [],
css: []
}
};
// <<editor mode>> stuff <</editor>>
Macro.add('editor', {
tags: null,
handler() {
const editor = new codeEditor(this.payload[0].contents, this.args[0]);
editor.wrapper.appendTo(this.output);
}
});
/* Remote macro loader */
/* Improve it for index showcase */
/* fetch both minified and standard + css if any */
window.macroDisplay = class macroDisplay {
constructor(def) {
/*def => {
name,
[type : script/macro, ] def macro
[hasCSS : bool, ] def false
[hasJS : bool] def true
[eval : bool] def true
}*/
Object.assign(this, def);
this.type ??= 'macro';
this.eval ??= true;
this.resolved = false;
const path = `https://raw.githubusercontent.com/MalifaciousGames/Mali-s-${this.type === 'macro' ? 'Macros' : 'Scripts'}/main/${this.name}/`;
this.gitLink = path.replace('raw.githubusercontent', 'github').replace('main', 'tree/main');
if (this.hasCSS ??= false) {
this.css = {
path: path + def.name + '.css',
formatted: `CSS unavailable at the moment, please check : ${this.gitLink}`
};
}
if (this.hasJS ??= true) {
this.js = {
full: {
path: path + def.name + '.js',
formatted: `Code unavailable at the moment, please check : ${this.gitLink}`,
},
min: {
path: path + def.name + '-min.js',
formatted: `Minified code unavailable at the moment.
It is possible a minified version doesn't exist for this file...
You can still check ${this.gitLink} to be sure.`
}
};
}
this.constructor[this.type + 's'][this.name] = this;
this.constructor.awaiting++;
this.fetchAndAdd();
}
fetchAndAdd() {
//try to find the <tw-passagedata> before doing the remote loading...
//full JS
if (this.js) {
this.fetchFile(this.js.full.path, t => {
if (this.eval) Scripting.evalJavaScript(t);
$('tw-storydata').append(`<tw-passagedata name="${this.name}.js" tags="script">${t}</tw-passagedata>`);
this.js.full.displayElem = highlighter.toElem(t, 'js');
});
//on load/on error listener
//fetch minified
this.fetchFile(this.js.min.path, t => {
this.js.min.displayElem = highlighter.toElem(t, 'js');
});
}
if (!this.css) return;
this.fetchFile(this.css.path, t => {
$('head').append(`<style id='${this.name}'>${t}</style>`);
this.css.displayElem = highlighter.toElem(t, 'css');
});
}
toDialog() {
Dialog.setup(this.name, 'codeDialog');
let min = false;
const codeBox = this.js.full.displayElem,
lineCounter = $(`<div class='lineCounter'>`),
sizeCounter = () => lineCounter.css('height', codeBox[0].scrollHeight + 'px');
let i = 0, lines = '';
while (i++ < 500) lines += i + '\n';
lineCounter.text(lines);
setTimeout(sizeCounter, 20);
Dialog.append(lineCounter, codeBox).open();
/*$('#ui-dialog-titlebar').append(
this.clipboardButton(this.displayed)
);*/
if (this.js.min) {
const minToggle = _ => {
this.displayed = min ? this.js : this.min;
codeBox.remove();
$('#ui-dialog-body').append(this.js.min.displayElem);
setTimeout(sizeCounter, 50);
min = !min;
};
const minButton = $(`<button>Minified code</button>`).ariaClick(minToggle);
$('#ui-dialog-titlebar').append(minButton);
}
}
clipboardButton(code) {
this.displayed = code ?? this.js;
const but = $(`<button class='clipboard' data-label='Copy to clipboard'>Copy to clipboard</button>`);
but.ariaClick(e => {
navigator.clipboard.writeText(this.displayed);
but.text('Copied!');
setTimeout(_ => but.text('Copy to clipboard'), 200);
});
return but;
}
fetchFile(path, callback) {
try {
fetch(path).then(r => {
this.constructor.resolved.add(this.name);
return r.text();
}).then(t => callback(t));
} catch (e) {
log('Remote load : failed to load :', this);
this.constructor.resolved.add(this.name);
}
}
static awaiting = 0;
static resolved = new Set();
static macros = {};
static scripts = {};
};
new macroDisplay({ name: 'a-macro' });
new macroDisplay({ name: 'arrowbox-macro', hasCSS: true });
new macroDisplay({ name: 'checkvars-macro', hasCSS: true });
new macroDisplay({ name: 'drag-drop-macro', hasCSS: true });
new macroDisplay({ name: 'either-macro' });
new macroDisplay({ name: 'hover-macro', hasCSS: true });
new macroDisplay({ name: 'input-macro' });
new macroDisplay({ name: 'listen-macro' });
new macroDisplay({ name: 'log-macro' });
new macroDisplay({ name: 'on-macro' });
new macroDisplay({ name: 'container-template-markup', type: 'script' });
new macroDisplay({ name: 'update-markup', type: 'script' });
new macroDisplay({ name: 'swap-markup', type: 'script' });<h2>DIY save interface</h2>
<p>
The following code lets user create their own save interface instead of using the built-in dialog.
</p>
<<sc>>
<<for _i = 0; _i < Save.slots.length; _i++>>
<<capture _i>>
<div class='saveSlot'>
Slot _i :
<<if Save.slots.has(_i)>>
<<button 'Load'>>
<<run Save.slots.load(_i)>>
<</button>>
<<button 'Delete'>>
<<run Save.slots.delete(_i)>>
<</button>>
<<else>>
<<button 'Save to slot _i'>>
<<run Save.slots.save(_i)>>
<</button>>
<</if>>
</div>
<</capture>>
<</for>>
<<button 'Save to disk'>>
<<run Save.export()>>
<</button>>
<<button 'Load from file'>>
<<run $('<input type=file>').on('change', Save.import).trigger('click')>>
<</button>>
<<button 'Delete all'>>
<<run Dialog.setup();
Dialog.wiki(`
Are you sure you want to delete all saves?
<<button 'Do it!'>>
<<run let i=0; while(i < Save.slots.length) Save.slots.delete(i++); Dialog.close();>>
<</button>>
`).open()>>
<</button>>
<</sc>>
<aside>
This display does not refresh like the save dialog does. This behaviour can be achieved either with """<<include>>""" + """<<replace>>""" or using somethign like the [[on-macro]].
</aside><h3> Output options </h3>
<p>The 'a' macro comes with six child tags which correspond to an output option:</p>
WIP<h3> Goto attribute </h3>
<p>The """goto""" attribute lets you specify a passage to forward the player to. It works in the exact same fashion as the default """<<link>>""" syntax.</p>
<<code sc edit>>
With strings:
<<a "Take me to some passage" goto 'a-macro'>><</a>>
With bracket syntax:
<<a "Take me to some passage" goto [[a-macro]]>><</a>>
<</code>>
<h3> Key attribute </h3>
<p>The """key""" attribute is used to bind one or more keys to an element. When one of the given keys is pressed, the element behaves as if it had been clicked.</p>
The """key""" attribute accepts both
<<a '"""e.code""" numbers' href 'https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values'>><</a>>
and
<<a '"""e.key""" values' href 'https://developer.mozilla.org/en-US/docs/web/api/ui_events/keyboard_event_key_values'>><</a>>.
<<code sc edit>>
<<a "Roll the dice." key 'r'>>
<<rep '#roll'>> <<=random(1,6)>>
<</a>>
<span id='roll'/>
<</code>>
<h4>Generating buttons bound to number keys using the `e.code` syntax:</h4>
<<code sc edit>>
<<set $inventory = ['Potion','Knife','Flint','Bandage']>>
<<for _i, _item range $inventory>>
<<capture _item>>
<<but `'Use _item (press '+(_i+1)+')'` key `'Digit'+(_i+1)`>>
<<rep '#id'>>...used _item...
<</but>>
<</capture>>
<</for>>
<span id='id'></span>
<</code>>
<h3> Condition/disabled attribute </h3>
<p>The """condition""" and """disabled""" attributes are evaluated attributes which decide if a link is shown or enabled. If they value is a quoted expression, it is evaluated after each click, if the resulting value is truthy, the element is either removed or disabled.</p>
<<code sc edit>>
<<set _hasItem = false>>
<<a 'Grab the item!' condition '!_hasItem'>>
<<set _hasItem = true>>
<</a>>
<<a 'Drop the item!' condition '_hasItem'>>
<<set _hasItem = false>>
<</a>>
<</code>>
<<code sc edit>>
<<set _choice = 1>>
<<for _i = 1; _i < 4;_i++>>
<<capture _i>>
<<a 'Option _i' disabled `'_choice ==='+_i`>>
<<set _choice = _i >>
<<rep '#val'>>_i
<</a>>
<</capture>>
<</for>>
<span id='val'/>
<</code>>
<h3> Choice attribute </h3>
<p>The """choice""" attribute creates groups of links, if one of them is clicked, all the others will be removed from the page. </p>
<<code sc edit>>
<<adel "Option 1" choice 'opt'>>
<<rep '#opt'>>You chose n°1.
<</adel>>
<<adel "Option 2" choice 'opt'>>
<<rep '#opt'>>You chose n°2.
<</adel>>
<span id='opt'/>
<</code>>
<p>This feature doesn't have its own memory so it should be used in conjunction with the condition attribute if you want choices to remain hidden after navigating back to a passage.</p>
<h3> Trigger attribute </h3>
<p>The """trigger""" attribute is used to trigger events at document level. It is meant to be used in conjunction with the <<a '"""<<on>>""" macro' goto [[on-macro]]>><</a>>. Events can be supplied as a commas separated string, a valid event object or an array of strings or objects.</p>
<<code sc edit>>
<<set $var = 45>>
<<a "Refresh contents!" trigger 'refresh'>>
<<set $var++>>
<</a>>
<<on 'refresh'>>
$var
<</on>>
<</code>>
<h3> Changer attribute </h3>
<p>Changes the link's text every time it, or another link from the macro set, is clicked.</p>
<<code sc edit>>
<<set _o = 0>>
<<a '' changer 'Increment : _o'>>
<<set _o++>>
<</a>>
<<a '' changer 'Decrement : _o'>>
<<set _o-->>
<</a>>
<</code>>
<h2>The """<<input>>""" macro set</h2>
<p>Showcase is not written yet.</p>
<p>You can find the code and full readme on GitHub (link above).</p>