Pre-release
AdventureJS Docs Downloads Bundler Devlog Score: 0 Moves: 0
//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 &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" }` }
   *   }
   * })
   *
   * @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 = /\{(.*?)\}/gs;
    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];

      token.trim();

      // lowercase everything inside [] square brackets
      // we never need to save case for those
      token = token.replace(
        /\[([^\]]*)\]/g,
        (match, content) => `[${content.toLowerCase()}]`
      );

      // save a record of the original case for return
      // this mostly only works for pronoun replacements
      let text_case = A.FX.getCase(token);

      // Save the original string (apart from chars in [] that we've
      // already lowercased) before setting everything toLowerCase.
      // This is for cases like "[if a is b] A is b." where we might
      // want to print an entire sentence if condition is met.
      let Token = token;

      // lowercase everything to help with comparisons
      token = token.toLowerCase();

      // we want to know if the string starts with a []
      // as those instances have specific handling
      let leading_bracket = "";
      if (token.startsWith("[")) {
        leading_bracket = token.substring(0, token.indexOf("]") + 1);
      }

      // 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}
      // {[if player is x] " some string "}
      // {[if player is not x] " some string "}
      // {[if player has x] " some string "}
      // {[if player has not x] " some string "}
      // {[if asset is x] " some string "}
      // {[if asset is not x] " some string "}
      // {[if asset has x] " some string "}
      // {[if asset has not x] " some string "}

      // is it a pronoun other than first person plural?
      // if so convert to first person plural
      if (this.dictionary.inflections_reverse_lookup[token]) {
        token = this.dictionary.inflections_reverse_lookup[token];
        Token = this.setCase(token, text_case);
      }

      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 "[if player is x]" as in `The heavy crash couch {[if player
      // is on crash couch] that you are strapped to} dominates the room. `
      else if (token.includes("[if player is")) {
        token_type = "if player is";
        new_string = this.renderIfPlayerIs(Token);
      }

      // look for "[if player is not x]" as in `The heavy crash couch {[if player
      // is not on crash couch] that you should be strapped to} dominates the room. `
      else if (token.includes("[if player is not")) {
        token_type = "if player is not";
        new_string = this.renderIfPlayerIsNot(Token);
      }

      // look for "[if player has x]" as in `... {[if player
      // has glasses] that you can see through the glasses} ... `
      else if (token.includes("[if player has")) {
        token_type = "if player has";
        new_string = this.renderIfPlayerHas(Token);
      }

      // look for "[if player has x]" as in `... {[if player
      // has glasses] that you can see through the glasses} ... `
      else if (token.includes("[if player has not")) {
        token_type = "if player has not";
        new_string = this.renderIfPlayerHasNot(Token);
      }

      // look for "[if asset is x]" as in `... {[if treasure chest
      // is open], the chest is open} ... `
      else if (token.startsWith("[if ") && leading_bracket.includes(" is ")) {
        token_type = "if asset is";
        new_string = this.renderIfAssetIs(Token);
      }

      // look for "[if asset is not x]" as in `... {[if treasure chest
      // is not open], the chest is closed} ... `
      else if (
        token.startsWith("[if ") &&
        leading_bracket.includes(" is not ")
      ) {
        token_type = "if asset is not";
        new_string = this.renderIfAssetIsNot(Token);
      }

      // look for "[if asset has x]" as in `... {[if treasure chest
      // has ruby], the chest is full of rubies} ... `
      else if (token.startsWith("[if ") && leading_bracket.includes(" has ")) {
        token_type = "if asset has";
        new_string = this.renderIfAssetHas(Token);
      }

      // look for "[if asset has x]" as in `... {[if treasure chest
      // has not ruby], the chest is empty} ... `
      else if (
        token.startsWith("[if ") &&
        leading_bracket.includes(" has not ")
      ) {
        token_type = "if asset has not";
        new_string = this.renderIfAssetHasNot(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}`)
      //  || token.includes(" is ")
      else if (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",
          "if player is",
          "if player has",
        ].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
})();