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

/**
 * <strong>handlePlaceholders</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 substitutions:
 *
 * <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_GameVars.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 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">{[myclass]text}</code> is a
 * shortcut to &lt;span class="myclass"&gt;text&lt;/span&gt;,
 * 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" }` }
 *   }
 * })
 *
 * @TODO update classdesc
 * @memberOf adventurejs
 * @method adventurejs#handlePlaceholders
 * @param {String} msg A string on which to perform substitutions.
 * @returns {String}
 */

adventurejs.handlePlaceholders = 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 processClasses = (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>`;
  }; // processClasses

  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.handlePlaceholders(`{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.handlePlaceholders(`{east [is] open}`)
    // ex MyGame.handlePlaceholders(`{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.handlePlaceholders(`{[foo] bar}`)
    else if (token.search(/\[([^\]]*)\]/) > -1) {
      new_string = processClasses(token, this);
    }

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

  return msg;
}; // handlePlaceholders