//renderPlaceholders.js
(function () {
/* global AdventureJS A */
var p = AdventureJS.Game.prototype;
/**
* <strong>renderPlaceholders</strong> acts on strings prior to
* printing them to {@link AdventureJS.Display|Display}.
* Placeholder substitution is the last step of
* {@link AdventureJS.Game#print|Game.print()}.
* It replaces placeholders, aka substrings inside
* {squiggly brackets}, 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 no $dollar sign: {foo}. </li>
* <li>Substrings with {no dollar signs} are evaluated by
* AdventureJS, rather than native Javascript, so they have
* limited scope.</li>
* <br><br>
*
* There are several types of valid placeholders:
*
* <li><code class="property">{ author_variables }</code>
* refers to author-created game variables
* that are stored within the game scope so that they
* can be written out to saved game files. (See
* <a href="/doc/BasicScripting_WorldVariables.html">Basic Scripting: Game Variables</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 rendering, 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">{[myclass]text}</code> is a
* shortcut to <span class="myclass">text</span>,
* to make it easier to add custom CSS styles to text.
* <br><br>
*
* AdventureJS placeholders can be mixed & matched with
* template literals. Placeholders 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" }` }
* }
* })
*
* @memberOf AdventureJS
* @method AdventureJS#renderPlaceholders
* @param {String} msg A string on which to perform placeholder substitutions.
* @returns {String}
* @TODO placeholders for "any key to continue" and wipe screen
*/
p.renderPlaceholders = p.render = function Game_renderPlaceholders(
msg,
params = {}
) {
const token_regex = /\{(.*?)\}/g;
let exec_results = [];
let tokens = [];
// 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) {
// we have to work backwords because
// we'll be changing the string length
let token = tokens[tokens.length - 1][0];
let token_parts, prefix, parser;
let token_type = "";
let new_string = "<span class='system error'>{" + token + "}</span>";
let asset;
token_parts = token.split(":").map((item) => {
return item.trim();
});
switch (token_parts.length) {
case 1:
// no prefix
break;
case 2:
// found prefix
token = token_parts[1];
prefix = token_parts[0];
if (["p", "parser"].includes(prefix)) {
parser = true;
} else {
asset = this.game.getAsset(prefix);
}
break;
default:
// too many colons
continue;
}
let first = tokens[tokens.length - 1][1];
let last = tokens[tokens.length - 1][2];
// save the original case for return
let text_case = A.FX.getCase(token);
token = token.toLowerCase();
// default to an error message for author
// SEARCH TYPES
// {author} the game's author
// {title} the game's title
// {version} the game's version number
// {ifid} the game's ifid
// {description} the game's description
// {we} // pronouns
// {p:we} // parser pronouns
// {is} or {was} // auxiliary verbs & contractions
// {success_adverb} // randomizer
// {fail_adverb} // randomizer
// {var} // game vars
// {verb} // this turn's verb
// {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
// {[image] url}
// is it a pronoun other than first person plural?
// if so convert to first person plural
token = this.dictionary.inflections_reverse_lookup[token] || token;
if (token === "title") {
token_type = "title";
new_string = this.game.settings.title;
} else if (token === "author") {
token_type = "author";
new_string = this.game.settings.author;
} else if (token === "version") {
token_type = "version";
new_string = this.game.settings.version;
} else if (token === "ifid") {
token_type = "ifid";
new_string = this.game.settings.ifid;
} else if (token === "description") {
token_type = "description";
new_string = this.game.settings.description;
} else if (token === "verb") {
token_type = "verb";
new_string = this.renderVerb(this.game.getInput().getVerb());
}
// // is it a parser verb?
// else if (parser && this.dictionary.verb_form_lookup[token]) {
// // new_string = this.renderParserVerb(token);
// new_string = this.renderVerb(token, this.game.settings.game_pronouns);
// }
//
// is it a verb?
else if (this.dictionary.verb_form_lookup[token]) {
token_type = "verb_form";
if (parser) {
new_string = this.renderVerb(token, this.game.settings.game_pronouns);
} else if (asset) {
new_string = this.renderVerb(token, asset.pronouns, asset);
} else {
new_string = this.renderVerb(token);
}
}
// is it an auxiliary verb or contraction?
else if (this.dictionary.agreements_lookup[token]) {
token_type = "agreement";
new_string = this.renderAgreement(token);
}
// is it a first person plural pronoun?
else if (this.dictionary.inflect(token)) {
token_type = "inflection";
if (parser) {
new_string =
this.dictionary.inflections[this.game.settings.game_pronouns][
token
];
} else if (asset) {
new_string = this.renderPronoun(token, asset.pronouns, asset);
} else {
new_string = this.renderPronoun(token);
}
}
// is it a success adverb?
else if (token === "success_adverb") {
token_type = "success";
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") {
token_type = "fail";
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]) {
token_type = "undefined";
new_string = A.FX.getSAF.call(this, this.world._vars[token]);
}
// look for "[is]" and "[then]" as in `sink drain [is] open [then] "string"`
// ex MyGame.fx.renderPlaceholders(`{door [is] open [then] "string" [else] "other string"}`)
else if (token.includes("[is]") && token.includes("[then]")) {
token_type = "is then";
new_string = this.renderAssetIsThen(token);
// is
}
// look for "[is]" as in "east [is] open" or "door [is] open [or] closed"
// ex MyGame.fx.renderPlaceholders(`{east [is] open}`)
// ex MyGame.fx.renderPlaceholders(`{door [is] open [or] closed}`)
else if (token.includes("[is]") || token.includes(" is ")) {
token_type = "is";
new_string = this.renderAssetIsOr(token);
// is
}
// look for "[class] content"
// ex MyGame.fx.renderPlaceholders(`{[foo] bar}`)
else if (token.search(/\[([^\]]*)\]/) > -1) {
// @TODO can't remember why we're passing 'this'?
// method has no second param
token_type = "class";
new_string = this.renderClasses(token, this);
}
// respect the original case for certain types
if (["inflection", "agreement", "verb_form"].includes(token_type)) {
new_string = A.FX.setCase(new_string, text_case);
}
// do replacement
msg =
msg.substring(0, first) + new_string + msg.substring(last, msg.length);
tokens.pop();
}
return msg;
}; // renderPlaceholders
})();