Pre-release
AdventureJS Docs Downloads
Score: 0 Moves: 0
//substituteCustomTemplates.js
/* global adventurejs A */

/**
 * <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 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 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 processSpans = (token) => {
      // look for [foo]bar
      const regex = /\[([^\]]*)\]/;
      let split_token;
      if (token.search(regex) > -1) {
        split_token = token.trim().split(regex);
      }
      let tag = split_token[1].trim();
      let content = split_token[2].trim();

      // is it an image?
      if (tag === "image") {
        if (this.game.image_lookup[content]) {
          return `<img src="${this.game.image_lookup[content]}"/>`;
        }
      }

      // otherwise assume it's a css class
      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

    /**
     *
     * @param {string} token The original template such as {our}.
     * @param {string} pronoun The corresponding pronoun for the player character such as "your".
     * @returns string;
     */
    const processPronoun = (token, pronoun) => {
      const player = this.game.getPlayer();
      const subject = this.game.getInput().getSubject();
      const upper_regex = new RegExp(/[A-Z]/);
      const lower_regex = new RegExp(/[a-z]/);
      const leading_cap = token.substring(0, 1).search(upper_regex) > -1;
      const all_cap = token.search(lower_regex) === -1;

      if (subject && subject.id !== player.id) {
        if (leading_cap && !all_cap) {
          // use proper name
          pronoun = subject.propername || subject.Name;
        } else {
          // subject is not the player so lookup the original token
          // rather than use the pronoun we arrived at for player
          pronoun = this.game.dictionary.pronouns[subject.pronouns][token];
        }
      } else {
        // respect case of the original
        if (leading_cap) {
          // capitalize first letter
          pronoun = A.propercase(pronoun);
        }
      }
      if (all_cap) {
        // capitalize
        pronoun = pronoun.toUpperCase();
      }
      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, dont;
      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 & contractions
      // {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
      // {[image] url}

      // 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]);
      }

      // look for "[is]" and "[then]" as in `sink drain [is] open [then] "string"`
      // ex MyGame.substituteCustomTemplates(`{door [is] open [then] "string" [else] "other string"}`)
      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}`)
      else if (-1 !== token.indexOf("[is]") || -1 !== token.indexOf(" is ")) {
        new_string = processAssetIsOr(token);
      } // is

      // look for "[class] content"
      // ex MyGame.substituteCustomTemplates(`{[foo] bar}`)
      else if (token.search(/\[([^\]]*)\]/) > -1) {
        new_string = processSpans(token, this);
      }

      // do replacement
      msg =
        msg.substring(0, first) + new_string + msg.substring(last, msg.length);
      tokens.pop();
    }

    return msg;
  }; // substituteCustomTemplates