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 = /\{(.*?)\}/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
})();