//substituteCustomTemplates.js
/*global adventurejs A*/
"use strict";
/**
* <strong>substituteCustomTemplates</strong> acts on strings prior to
* printing them to {@link adventurejs.Display|Display}.
* Substitution is the last step of
* {@link adventurejs.Game#print|Game.print()}.
* It replaces custom templates, aka substrings bracketed
* inside parentheses, like $(door is| open or| closed).
* <br><br>
*
* For example:
* <pre class="display">descriptions: { look: "The drawer is $( drawer is| open or| closed )." }</pre>
* <br><br>
*
* This method is similar to Javascript ES6
* template literals but with important distinctions.
* <li>The adventurejs version uses different symbols: $(parentheses)
* instead of Javascript's native ${curly braces}. </li>
* <li>Substrings inside $(parens) are evaluated by adventurejs, rather
* than native Javascript, so they have limited scope.</li>
* <br><br>
*
* There are several types of valid substitutions:
*
* <li><code class="property">$( author_variables )</code>
* refers to author-created custom variables
* that are stored within the game scope so that they
* can be written out to saved game files. (See
* <a href="/doc/Scripting_CustomVars.html">How to Use Custom Vars</a>
* for more info.)
* </li>
*
* <li><code class="property">$( asset is| state or| unstate )</code>
* allows authors to refer to a game asset by name or id
* and print certain verb states. Names are serialized
* during the substitution process, meaning that, for example:
* <code class="property">$( brown jar is| open or| closed )</code>
* will be interpreted to check for
* <code class="property">MyGame.world.brown_jar.is.closed</code>.</li>
*
* <li><code class="property">$(tag|text)</code> is a
* shortcut to <span class="tag">text</span>,
* to make it easier to add custom CSS styles to text.
* <br><br>
*
* Adventurejs custom templates can be mixed & matched with
* template literals. Custom templates can be used in any
* string that outputs to the game display. However, because
* template literals in strings are evaluated when the
* properties containing them are created, they will cause
* errors on game startup. In order to use native Javascript
* template literals, they must be returned by functions.
*
* MyGame.createAsset({
* class: "Room",
* name: "Standing Room",
* descriptions: {
* brief: "The north door is $(north is| open or| closed). ",
* through: "Through the door you see a $(northcolor) light. ",
* verbose: return function(){ `The north door
* ${MyGame.world.aurora_door.is.closed ?
* "is closed, hiding the aurora. " :
* "is open, revealing the $(northcolor) aurora light" }` }
* }
* })
*
* @TODO update classdesc
* @memberOf adventurejs
* @method adventurejs#substituteCustomTemplates
* @param {String} msg A string on which to perform substitutions.
* @returns {String}
*/
adventurejs.substituteCustomTemplates =
function Adventurejs_substituteCustomTemplates(msg) {
var token_regex = /\$\((.*?)\)/g;
var exec_results = [];
var tokens = [];
const getVerbState = (asset, state) => {
let verb =
this.dictionary.verbs[this.dictionary.verb_state_lookup[state]];
if (verb && asset.dov[verb.name]) {
if (verb.state && asset.is[verb.state]) {
return verb.state_string;
}
if (false === asset.is[verb.state]) {
// undefined doesn't qualify
return verb.unstate_string;
}
}
}; // getVerbState
// const processDebug = (token) => {
// token = token.substring(6);
// let token_array = token.split("|");
// if (token_array.length > 1) {
// for (var i = 0; i < token_array.length; i++) {
// token_array[i] =
// "<span class='debug_" + i + "'>" + token_array[i] + "</span>";
// }
// token = token_array.join("");
// }
// return '<em class="debug">' + token + "</em>";
// }; // processDebug
const processSpans = (token) => {
let tag = token.split("|")[0];
let content = token.split("|")[1];
return `<span class="${tag}">${content}</span>`;
}; // processSpans
const getAssetFromTokenId = (token_id) => {
let asset;
let direction;
direction = this.dictionary.getDirection(token_id);
if (direction) {
asset = this.getExitFromDirection(direction);
if (asset && asset.aperture) {
asset = this.getAsset(asset.aperture);
}
} else {
asset = this.getAsset(token_id);
}
return asset;
};
const processAssetIsOr = (token) => {
let token_array = token.split(" is|").map((e) => e.trim());
let token_id = token_array[0];
let token_state = token_array[1]; // everything after 'is'
let new_string = "in an unknown state";
let asset = getAssetFromTokenId(token_id);
let verb_states = token_state.split(" or|").map((e) => e.trim());
if (verb_states.length) {
// we expect something like verb_states=[ "plugged", "unplugged" ]
// but we can handle one or more than two
let found = false;
for (let i = 0; i < verb_states.length; i++) {
let state = verb_states[i];
let state_string;
if (state) {
state_string = getVerbState(asset, state);
}
if (state_string) {
new_string = state_string;
found = true;
break;
}
}
if (!found) {
// we didn't find a clear verb state
// is there a state property we can get?
for (let i = 0; i < verb_states.length; i++) {
if (asset.is[verb_states[i]]) {
new_string = verb_states[i];
break;
}
}
}
} // verb_states
return new_string;
}; // processAssetIsOr
const processAssetIsThen = (token) => {
let token_array = token.split(" is|").map((e) => e.trim());
let token_id = token_array[0];
let isThen = token_array[1];
let is, then, ells;
let isState = false;
let hasElse = -1 !== isThen.indexOf(" else|");
if (hasElse) {
ells = isThen.split(" else|")[1];
isThen = isThen.split(" else|")[0];
}
then = isThen.split(" then|")[1];
is = isThen.split(" then|")[0].split("is|")[0].trim();
//console.warn( 'processAssetIsThen token:', token, ', is:',is,', then:',then,', ells:',ells );
let asset = getAssetFromTokenId(token_id);
if (asset.is[is]) {
isState = true;
}
if (isState && then) return then;
if (!isState && ells) return ells;
return "";
}; // processAssetIsThen
const processPronoun = (token, pronoun) => {
// respect case of the original
let upper = new RegExp(/[A-Z]/);
let lower = new RegExp(/[a-z]/);
if (token.search(lower) === -1) {
pronoun = pronoun.toUpperCase();
} else if (token.substring(0, 1).search(upper) > -1) {
pronoun = A.propercase(pronoun);
}
return pronoun;
}; // processPronoun
// specifically using exec() here rather than replace() or match()
// because replace() can't take a scope arg
// and match() doesn't return an index for groups
while ((exec_results = token_regex.exec(msg)) !== null) {
// exec() returns each found token with its first/last indices
tokens.push([exec_results[1], exec_results.index, token_regex.lastIndex]);
}
while (tokens.length > 0) {
let token, first, last, pronoun;
let new_string = "unknown";
// we have to work backwords because we'll be changing the string length
token = tokens[tokens.length - 1][0];
first = tokens[tokens.length - 1][1];
last = tokens[tokens.length - 1][2];
// default to an error message for author
new_string =
"<span class='system error'>No substitute found for $(" +
token +
")</span>";
// SEARCH TYPES
// $(we) // pronouns
// $(success_adverb) // randomizer
// $(fail_adverb) // randomizer
// $(var) // game vars
// $(debug:message) // debug message - moved to debug function
// $(north is| open or| closed) // direction + state
// $(sink is| plugged or| unplugged) // asset + state
// $(sink is| plugged then| " some string ") // asset + state + string
// $(sink is| plugged then| " some string " else| " other string ") // asset + state + string + string
// is it a pronoun?
pronoun = this.dictionary.getPronoun(token.toLowerCase());
if (pronoun) {
new_string = processPronoun(token, pronoun);
}
// is it a success adverb?
else if (token === "success_adverb") {
new_string =
this.dictionary.success_adverbs[
Math.floor(Math.random() * this.dictionary.success_adverbs.length)
];
}
// is it a fail adverb?
else if (token === "fail_adverb") {
new_string =
this.dictionary.fail_adverbs[
Math.floor(Math.random() * this.dictionary.fail_adverbs.length)
];
}
// is it an author's game var?
else if ("undefined" !== typeof this.world._vars[token]) {
new_string = A.getSAF.call(this, this.world._vars[token]);
}
// if ("debug:" === token.substring(0, 6)) {
// new_string = this.settings.print_debug_messages
// ? processDebug(token, this)
// : "";
// }
// look for ' is ' and ' then ' as in 'sink drain is open then "output"'
// ex MyGame.substituteCustomTemplates(`$(door is| open then| "string" else| "other string")`)
// @TODO changing this to is|
else if (-1 !== token.indexOf(" is|") && -1 !== token.indexOf(" then|")) {
new_string = processAssetIsThen(token);
} // is
// look for ' is| ' as in 'east is| open' or 'door is| open or| closed'
// ex MyGame.substituteCustomTemplates(`$(east is| open)`)
// ex MyGame.substituteCustomTemplates(`$(door is| open or| closed)`)
// @TODO changing this to is|
else if (-1 !== token.indexOf(" is|") || -1 !== token.indexOf(" is ")) {
new_string = processAssetIsOr(token);
} // is
// look for cssclass|content
// ex MyGame.substituteCustomTemplates(`$(foo|bar)`)
else if (-1 !== token.indexOf("|")) {
new_string = processSpans(token, this);
}
// do replacement
msg =
msg.substring(0, first) + new_string + msg.substring(last, msg.length);
tokens.pop();
}
return msg;
}; // substituteCustomTemplates