Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
//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 &lt;span class="tag"&gt;text&lt;/span&gt;,
 * 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