"use strict"
/**
*
* @version 0.9
* @author {@link https://github.com/KVonGit|KV}
* @fileoverview Add hyperlink functionality to {@link https://github.com/ThePix/QuestJS|QuestJS}.
*
* ---
* ###### **NOTE**
*
* This library is now included in QuestJS as of QuestJS version 0.4, but the library is not
* loaded by default.
*
* View [the instructions](https://github.com/ThePix/QuestJS/wiki/Hyperlinks "https://github.com/ThePix/QuestJS/wiki/Hyperlinks") on the [QuestJS Wiki](https://github.com/ThePix/QuestJS/wiki).
*
* This code is sometimes more up to date than the file which is included in Quest.
*
* ---
* #### These mods are not necessary, but I like to use them.
*
* ```
* lang.contentsForData.surface.prefix = 'on which you see ';
* lang.contentsForData.surface.suffix = '';
* lang.open_successful = "Done.";
* lang.close_successful = "Done.";
* lang.inside_container = "{nv:item:be:true} inside {sb:container}.";
* lang.look_inside = "Inside, {nv:char:can} see {param:list}.";
* lang.take_successful = "Taken.";
* lang.drop_successful = "Dropped.";
* ```
*/
/**
* @namespace
* @property {object} settings
* @property {boolean} settings.linksEnabled - Enable item links and exit links
* @example settings.linksEnabled = true;
*/
settings.linksEnabled = true;
/**
* @namespace
* @property {object} itemLinks
* @property {function} itemLinks.update - keeps the verb links and exit links updated after each turn.
* @see {@link updateAllItemLinkVerbs} & {@link updateExitLinks}
*/
const itemLinks = {};
io.modulesToUpdate.push(itemLinks);
itemLinks.update = function() {
if(settings.linksEnabled){
if(settings.debugItemLinks) {
console.info("running itemLinks.update() to update verbs . . .");
}
updateAllItemLinkVerbs();
updateExitLinks();
}
};
/**
* @function updateAllItemLinkVerbs
* @description Updates all verb links in items' dropdown menus
* @see {@link updateItemLinkVerbs}
*/
function updateAllItemLinkVerbs(){
let verbEls = $("[link-verb]");
Object.keys(verbEls).forEach(i => {
let el = verbEls[i];
let objName = $(el).attr("obj");
if (!objName) return;
let obj = w[objName];
updateItemLinkVerbs(obj);
})
}
/**
* @function updateItemLinkVerbs
* @param {object} obj - The in-game item
* @description Sets the current available verbs in the item link dropdown menu
* @see {@link updateAllItemLinkVerbs} & {@link itemLinks}
*/
function updateItemLinkVerbs(obj){
let oName = obj.name;
if (!obj.scopeStatus) {
disableItemLink($(`[obj="${oName}"]`));
return;
}
enableItemLinks($(`[obj="${oName}"]`));
let id = obj.alias || obj.name;
let el = $(`[obj='${oName}-verbs-list-holder'`);
let endangered = el.hasClass("endangered-link") ? "endangered-link" : "";
let newVerbsEl = getVerbsLinks(obj, endangered);
el.html(newVerbsEl);
}
/**
* @function getArticle
* @description Returns 'a', 'an', or 'the' when type is set to INDEFINITE or DEFINITE.
*
* Returns <code>false</code> otherwise.
* @param {object} item - The in-game item
* @param {number} type - DEFINITE or INDEFINITE
* @returns {string} 'a', 'an', or 'the' (or <code>false</code>)
* @todo Should this simply <code>return</code> rather than <code>return false</code> if no type is requested?
*/
function getArticle(item, type){
if (!type) return false;
return type === DEFINITE ? lang.addDefiniteArticle(item) : lang.addIndefiniteArticle(item);
}
/**
* @function getDisplayAliasLink
* @param {object} item - The in-game item
* @param {object} [options] - Includes options such as 'article' (@see {@link getArticle})
* @param {boolean} [cap] - If <code>true</code>, first letter of string will be capitallized
* @return {string} - The item's link
*/
function getDisplayAliasLink(item, options, cap){
let art = false;
if (options) art = options.article
let article = getArticle(item, art)
if (!article) {
article = '';
}
let s = article + getItemLink(item);
s = s.trim();
return s;
}
/**
* @function handleExamineHolder
* @description Used by npcs and containers to print a list of contents.
*
* Must be manually added to an item's <code>examine</code> attribute.
* @param {object} params - Actually, this function does nothing with <code>params</code>
* @todo Learn about name modifiers, because this code may be reinventing the wheel.
*/
function handleExamineHolder(params){
let obj = parser.currentCommand.objects[0][0];
if (!obj) return;
if (!obj.container && !obj.npc) return;
if (obj.container) {
if (!obj.closed || obj.transparent) {
let contents = obj.getContents();
contents = contents.filter(o => !o.scenery)
if (contents.length <= 0){
return;
}
let pre = obj.contentsType === 'surface' ? lang.on_top : lang.inside;
pre = sentenceCase(pre);
let subjVerb = processText("{pv:pov:see}", {pov:game.player});
pre += `, ${subjVerb} `;
contents = settings.linksEnabled ? getContentsLink(obj) : contents;
msg(`${pre}${contents}.`);
}
} else {
let contents = getAllChildrenLinks(obj)
if (contents == 'nothing') return;
let pre = processText('{pv:char:be:true} ' + lang.carrying, {char:obj});
msg(`${pre} ${contents}.`);
}
}
/**
* @function getContentsLink
* @description Used for containers. (NPCs use {@link getAllChildrenLinks}.)
* @param {object} o - The in-game item
* @returns {string} A string, which contains item links of the item's contents
*/
function getContentsLink(o) {
let s = '';
const contents = o.getContents(world.LOOK);
if (contents.length > 0 && (!o.closed || o.transparent)) {
if (!o.listContents) {
return getAllChildrenLinks(o);
}
s = o.listContents(world.LOOK);
}
return s
}
/**
* @function canHold
* @description Returns true if the item may have contents.
* @param {object} obj - The in-game item
* @returns {boolean} <code>true</code> if the item is a container, surface, or NPC
*/
function canHold(obj){
return ( obj.container && ( !obj.closed || obj.transparent ) ) || obj.npc;
}
/**
* @function getDirectChildren
* @param {object} item - The in-game item
* @description Returns an array of the item's direct children.
*
* To return a recursive list, use {@link getAllChildren}.
* @returns {array} Array of items
*/
function getDirectChildren(item){
if (!item.getContents) return [];
return item.getContents(item);
}
/**
* @function hasChildren
* @param {object} item - The in-game item
* @returns {boolean} <code>true</code> if the item is containing or carrying items.
*/
function hasChildren(item){
return item.getContents(item).length > 0;
}
/**
* @function getAllChildren
* @param {object} item - The in-game item
* @param {boolean} [isRoom] - Set to <code>false</code> by default. If set to <code>true</code>, excludes the player and the player's inventory.
* @returns {array} An array of items
*/
function getAllChildren(item, isRoom=false){
let result = [];
let children = getDirectChildren(item);
if (isRoom){
children = children.filter(o =>o != game.player);
}
if (children.length < 1) return [];
children.forEach(child => {
result.push(child);
let grandchildren = child.getContents ? child.getContents(child) : [];
if (grandchildren.length > 0){
result.push(getAllChildren(child));
}
})
return result;
}
/**
* @function getRoomContents
* @param {object} room - The room item
* @returns {array} An array of items in the room
* @see {@link getAllChildren}
*/
function getRoomContents(room){
let result = [];
let children = getAllChildren(room, true);
if (children.length < 1) return [];
children.forEach(child => {
result.push(child);
let grandchildren = child.getContents ? child.getContents(child) : [];
if (grandchildren.length > 0){
result.push(getAllChildren(child));
}
})
return result;
}
/**
* @function getAllChildrenLinks
* @description Used for NPCs. (Containers use {@link getContentsLink}.)
* @param {object} item - The in-game item
* @returns {string} - String to display list of item's contents' links
*/
function getAllChildrenLinks(item){
let kids = getDirectChildren(item);
kids = kids.map(o => lang.getName(o,{modified:true,article:INDEFINITE}));
return formatList(kids,{doNotSort:true, lastJoiner:lang.list_and, nothing:lang.list_nothing});
}
/**
* @function getItemLink
* @description Uses <code>{@link getName}</code> to return a link for the item.
* @param {object} obj - The in-game item
* @param {string} [id] - Optional display alias. This parameter is not required.
* @param {boolean} [capitalise] - Option to capitalize the first letter in the string. Not required. Default is <code>false</code>.
* @returns {string} The item's link
*/
function getItemLink(obj, id='_DEFAULT_', capitalise=false){
if(!settings.linksEnabled){
let s = lang.getNameOG(obj,{capitalise:capitalise});
return s;
}
let oName = obj.name;
if (id === '_DEFAULT_'){
id = obj.alias || obj.name;
}
id = capitalise ? sentenceCase(id) : id;
let s = `<span class="object-link dropdown">`;
s +=`<span onclick="toggleDropdown($(this).next())" obj="${oName}" `+
`class="droplink" name="${oName}-link">${id}</span>`;
s += `<span obj="${oName}" class="dropdown-content">`;
s += `<span obj="${oName}-verbs-list-holder">`;
s += getVerbsLinks(obj);
s += `</span></span></span>`;
return s;
}
/**
* @function getVerbsLinks
* @description Returns a string containing all available verbs for the item.
* @param {object} obj - The in-game item
* @returns {string} String list of verb links available for the item.
* @see {@link getItemLink}
*/
function getVerbsLinks(obj){
let verbArr = obj.getVerbs();
let oName = obj.name;
let id = obj.alias || obj.name;
let s = ``;
if (verbArr.length>0){
verbArr.forEach (o=>{
o = sentenceCase(o);
s += `<span class="list-link-verb" `+
`onclick="$(this).parent().parent().toggle();handleObjLnkClick('${o} '+$(this).attr('obj-alias'));" `+
`link-verb="${o}" obj-alias="${id}" obj="${oName}">${o}</span>`;
})
}
return s;
}
/**
* @function toggleDropdown
* @param {object} element - The HTML element
* @description Toggles the display of the element
*/
function toggleDropdown(element) {
$(element).toggle();
let disp = $(element).css('display');
let newDisp = disp === 'none' ? 'block' : 'block';
$(element).css('display', newDisp);
}
/**
* @function handleObjLnkClick
* @description Handles item link actions passed via clicking
* @param {string} cmd The command to be parsed
*/
function handleObjLnkClick(cmd){
runCmd(cmd);
}
/**
* @function disableItemLink
* @param {object} el - The item link class to be disabled
* @description Disables the item link class. (Used when an item is out of scope.)
*/
function disableItemLink(el){
let type = ''
if ($(el).hasClass("dropdown")) type = 'dropdown'
if ($(el).hasClass("droplink")) type = 'droplink'
$(el).addClass(`disabled disabled-${type}`).attr("name","dead-droplink").removeClass(type).css('cursor','default');
}
/**
* @function enableItemLinks
* @param {object} el - The item link class to enable
* @description Enables the item link class. (Used when an item is in scope.)
*/
function enableItemLinks(el){
let type = '';
if ($(el).hasClass("disabled-dropdown")) type = 'dropdown'
if ($(el).hasClass("disabled-droplink")) type = 'droplink'
$(el).removeClass("disabled").removeClass(`disabled-${type}`).addClass(type).attr("name",$(el).attr("obj")).css("cursor","pointer");
}
/**
* @function updateExitLinks
* @description Updates all the exit links, making sure only available exits have enabled links.
* @see {@link itemLinks|itemLinks.update}
*/
function updateExitLinks(){
const exits = util.exitList();
let link = $(`.exit-link`);
if (link.length > 0){
Object.values(link).forEach(el => {
let dir = $(el).attr('exit');
if (!dir) return
let ind = exits.indexOf(dir);
if (ind < 0) {
$(el).addClass("disabled")
el.innerHTML = dir;
} else {
$(el).removeClass("disabled");
el.innerHTML = processText(`{cmd:${dir}}`);
}
})
}
}
function disableAllLinks(){
let elArr = $('.exit-link');
Object.values(elArr).forEach(el => {
if ($(el).hasClass("exit-link")){
let dir = $(el).attr('exit');
$(el).addClass("disabled");
el.innerHTML = dir;
}
})
elArr = $(".dropdown");
Object.values(elArr).forEach(el => {
if ($(el).attr("name")){
disableItemLink(el);
}
})
elArr = $(".droplink");
Object.values(elArr).forEach(el => {
if ($(el).attr("name")){
disableItemLink(el);
}
})
}
//------
// MODS
//------
/**
* @namespace
* @property {object} util
* @property {function} util.listContents
* @param {object} situation - I'm honestly not sure what this is for.
* @param {boolean} [modified] - Not required. Set to true by default, to invoke the item's name modifier functions.
* @description ##### MODDED to return an array of strings containing item links
*
* NOTE: <code>this</code> targets the in-game item
* @see {@link getAllChildrenLinks}
* @returns {array} Array of strings of item links
*/
util.listContents = function(situation, modified = true) {
let objArr = getAllChildrenLinks(this);
return objArr
};
/**
* @namespace
* @property {object} io
* @property {function} io.finishBak - A backup of <code>io.finish</code>
* @since 0.9
*/
io.finishBak = io.finish;
/**
* @namespace
* @property {object} io
* @property {function} finish - Ends the story. Modified to disable all item and object links beforehand.
* @since 0.9
*/
io.finish = () => {
disableAllLinks();
io.finishBak();
};
/**
* @namespace
* @property {object} Inv
* @property {function} Inv.script - Modified to return recursive contents links.
* @returns {number}
*/
findCmd('Inv').script = function() {
if (settings.linksEnabled) {
msg(lang.inventoryPreamble + " " + getAllChildrenLinks(game.player) + ".");
return settings.lookCountsAsTurn ? world.SUCCESS : world.SUCCESS_NO_TURNSCRIPTS;
}
let listOfOjects = game.player.getContents(world.INVENTORY);
msg(lang.inventoryPreamble + " " + formatList(listOfOjects, {lastJoiner:lang.list_and, modified:true, nothing:lang.list_nothing, loc:game.player.name}) + ".");
return settings.lookCountsAsTurn ? world.SUCCESS : world.SUCCESS_NO_TURNSCRIPTS;
};
/**
* @namespace
* @property {object} lang
* @property {function} lang.getNameOG - The original <code>lang.getName</code>, with a new name
* @param {object} item - The in-game item
* @param {object} options - The options can include indefinite or definite article, possessive, pronoun, count, or pluralAlias.
* @returns {string} String with the item's item link or a pronoun with no link
*/
lang.getNameOG = lang.getName;
/**
* @namespace
* @property {object} lang
* @property {function} lang.getName - Modified for this library to return an item link.
* @param {object} item - The in-game item
* @param {object} [options] - The options can include indefinite or definite article, possessive, pronoun, count, or pluralAlias.
* @returns {string} String with the item's item link or a pronoun with no link
*/
lang.getName = (item, options) => {
if (!settings.linksEnabled) {
return lang.getNameOG(item, options);
}
if (!options) options = {}
if (!item.alias) item.alias = item.name
let s = ''
let count = options[item.name + '_count'] ? options[item.name + '_count'] : false
if (!count && options.loc && item.countable) count = item.countAtLoc(options.loc)
if (item.pronouns === lang.pronouns.firstperson || item.pronouns === lang.pronouns.secondperson) {
s = options.possessive ? item.pronouns.poss_adj : item.pronouns.subjective;
s += util.getNameModifiers(item, options); // ADDED by KV
return s; // ADDED by KV
}
else {
if (count && count > 1) {
s += lang.toWords(count) + ' '
}
if (item.getAdjective) {
s += item.getAdjective()
}
if (!count || count === 1) {
s += item.alias
}
else if (item.pluralAlias) {
s += item.pluralAlias
}
else {
s += item.alias + "s"
}
if (options.possessive) {
if (s.endsWith('s')) {
s += "'"
}
else {
s += "'s"
}
}
}
let art = getArticle(item, options.article);
if (!art) art = '';
let cap = options && options.capital;
if (!item.room) s = getItemLink(item, s, cap);
s = art + s;
s += util.getNameModifiers(item, options);
return s;
};
/**
* @namespace
* @property {object} tp
* @property {array} tp.text_processors
* @property {function} tp.text_processors.exits
* @description MODIFIED to return a string containing a list of exit links.
* @param {array} arr
* @param {object} params
* @returns A string containing a list of exit links
*/
tp.text_processors.exits = function(arr, params) {
let elClass = settings.linksEnabled ? `-link` : ``;
const list = [];
util.exitList().forEach(exit => {
let s = settings.linksEnabled ? `{cmd:${exit}}` : `${exit}`;
let el = processText(`<span class="exit${elClass}" exit="${exit}">${s}</span>`);
list.push(el);
})
return formatList(list, {lastJoiner:lang.list_or, nothing:lang.list_nowhere});
}
//----------------
// END OF MODS
//----------------
//Capture clicks for the objects links
/**
* @namespace
* @property {object} settings
* @property {array} settings.clickEvents
* @description Keeps track of clicked events in order to close one dropdown when another dropdown is clicked.
*/
settings.clickEvents = [{one0:`<span>_PLACEHOLDER_</span>`}];
/**
* @namespace
* @property {object} window
* @property {function} window.onclick
* @param {object} event
* @description Handles item link clicks.
* @see {@link clickEvents|settings.clickEvents}
*/
window.onclick = function(event) {
if (!event.target.matches('.droplink')) {
$(".dropdown-content").hide();
}else{
settings.clickEvents.unshift(event.target);
if (typeof(settings.clickEvents[1].nextSibling)!=='undefined' && settings.clickEvents[1].nextSibling!==null){
if (settings.clickEvents[1] !== event.target && settings.clickEvents[1].nextSibling.style.display==="block" && event.target.matches('.droplink')){
$(".dropdown-content").hide();
event.target.nextSibling.style.display="block";
}
}
}
}