Pre-release
AdventureJS Docs Downloads
Score: 0 Moves: 0
// Scorecard.js
(function () {
  /* global adventurejs A */

  /**
   * @class adventurejs.Scorecard
   * @ajsinternal
   * @param {Game} game A reference to the game instance.
   * @ajsnavheading FrameworkReference
   * @summary Manages score for a {@link adventurejs.Game|Game} instance.
   * @classdesc
   * <p>
   * <strong>Scorecard</strong> is a repository for Game score
   * options. Scorecard is created automatically
   * by {@link adventurejs.Game|Game}. This is an internal class
   * that authors should not need to construct. However,
   * authors can set scoring options from their game file as
   * shown below, and call score updates from custom scripts.
   * </p>
   * <h3 class="examples">Example:</h3>
   * <pre class="display"><code class="language-javascript">var MyGame = new adventurejs.Game( "MyGame", "GameDisplay" );
   * MyGame.scorecard.set({
   *   score_events: {
   *     "unlock door": { points: 1, complete: false, bonus: false, message: '', recorded: false },
   *     "unlock chest": { points: 1, complete: false, bonus: false, recorded: message: '', false },
   *     "drink potion": { points: 1, complete: false, bonus: false, recorded: message: '', false },
   *   }
   * });
   * </code></pre>
   */
  class Scorecard {
    constructor(game) {
      this.game = game;
      this.game.world._scorecard = {
        initialize: {
          complete: true,
          points: 0,
          recorded: true,
          message: "",
          bonus: false,
        },
      };

      /**
       * Used to keep track of player's current score.
       * @var {Boolean} adventurejs.Scorecard#score
       * @default 0
       */
      this.score = 0;

      /**
       * Used to compare old score vs new score during
       * score updates.
       * @var {Boolean} adventurejs.Scorecard#newscore
       * @default 0
       */
      this.newscore = 0;

      /**
       * Used to show the difference between old score and
       * new score during score updates.
       * @var {Boolean} adventurejs.Scorecard#diff
       * @default 0
       */
      this.diff = 0;

      /**
       * Can be set to print when player earns any score update.
       * @var {Boolean} adventurejs.Scorecard#score_message
       * @default ""
       */
      this.score_message = "";

      /**
       * Can be used to set customized score printouts in
       * the game display. <code>score_format</code> is
       * subject to <code>getStringArrayFunction()</code>
       * which means that it can be set to a string or an
       * array or a function.
       * For example, this returns a string:
       * <pre class="display"><code class="language-javascript">score_message: `{Our} score went up! `,</code></pre>
       * This returns a custom function:
       * <pre class="display"><code class="language-javascript">score_message: function(){
       *   return `Dude, you totally just got ${this.diff} points!`
       * },</code></pre>
       * Or maybe you just want to tweak the score display.
       * By default score appears in 0/0 format, but let's
       * say you'd like it to say "Score: 0 out of 0".
       * <pre class="display"><code class="language-javascript">score_format: function()
       * {
       *   return `Score: ${this.score} out of ${this.total}`;
       * }</code></pre>
       *
       * @var {Boolean} adventurejs.Scorecard#score_format
       * @default {}
       */
      this.score_format = {};

      /**
       * summarize_updates determines how score updates
       * are printed. With summarize_updates set to true,
       * if multiple score events occur in a turn, only
       * one score update will be printed, with the
       * cumulative score change. If summarize_updates are
       * false, each score update will be printed, and will
       * use unique score_message that may be provided.
       * @var {Boolean} adventurejs.Scorecard#score_format
       * @default {}
       */
      this.summarize_updates = false;

      // set scorecard to listen for end of turn,
      // then check to see if score has changed,
      // and if it has, print score updates
      this.game.reactor.addEventListener("inputParseComplete", function (e) {
        this.game.scorecard.updateScore();
      });
    }

    /**
     * Create a new event.
     * @method adventurejs.Scorecard#createEvent
     * @returns {String}
     */
    createEvent() {
      let event = {
        points: 0,
        complete: false,
        bonus: false,
        recorded: false,
        message: "",
      };
      return JSON.stringify(event);
    }

    /**
     * Mark the selected event as complete and update the score.
     * @method adventurejs.Scorecard#completeEvent
     * @param {String} event A string matching an event key.
     * @returns {Boolean}
     */
    completeEvent(event) {
      console.warn(`Scorecard.js > completeEvent(${event})`);
      if (!this.game.world._scorecard[event]) return false;
      this.game.world._scorecard[event].complete = true;
      return true;
    }

    /**
     * Update the player's score.
     * @method adventurejs.Scorecard#updateScore
     */
    updateScore() {
      let score = 0;
      let total = 0;
      let msg = "";

      for (var prop in this.game.world._scorecard) {
        //console.warn('getScore prop:',prop);
        if (!this.game.world._scorecard[prop].bonus) {
          total += this.game.world._scorecard[prop].points;
        }
        if (this.game.world._scorecard[prop].complete) {
          score += this.game.world._scorecard[prop].points;
        }
      }
      this.total = total;
      this.newscore = score;

      msg += this.summarize_updates
        ? this.aggregateUpdates()
        : this.stackUpdates();

      this.score = score;
      this.game.display.setScore(this.formatScore(score, total));
      if (msg) this.game.print(msg);
      return true;
    }

    /**
     * Print score updates in the aggregate.
     * @method adventurejs.Scorecard#aggregateUpdates
     */
    aggregateUpdates() {
      let msg = "";
      if (this.newscore !== this.score) {
        let direction = this.newscore > this.score ? "up" : "down";
        let diff = this.newscore - this.score;
        let s = Math.abs(diff) > 1 ? "s" : "";
        this.diff = diff;

        // the aggregate uses a single message setting
        msg = this.score_message
          ? A.getSAF.call(this.game, this.score_message, this)
          : `[ ** {Our} score went ${direction} by ${diff} point${s} ** ]`;

        msg = `<span class="ajs-score-msg ${direction}">${msg}</span>`;
      }
      return msg;
    }

    /**
     * Print score updates in a stack.
     * @method adventurejs.Scorecard#stackUpdates
     */
    stackUpdates() {
      let msg = "";
      for (let prop in this.game.world._scorecard) {
        if (
          this.game.world._scorecard[prop].complete &&
          !this.game.world._scorecard[prop].recorded
        ) {
          let eventmsg = "";
          let diff = this.game.world._scorecard[prop].points;
          let direction = diff > 0 ? "up" : "down";
          let s = Math.abs(diff) > 1 ? "s" : "";
          this.diff = diff;
          this.game.world._scorecard[prop].recorded = true;
          if (this.game.world._scorecard[prop].message) {
            eventmsg = A.getSAF.call(
              this.game,
              this.game.world._scorecard[prop].message,
              this
            );
          } else {
            eventmsg = this.score_message
              ? A.getSAF.call(this.game, this.score_message, this)
              : `[ ** {Our} score went ${direction} by ${diff} point${s} ** ]`;
          }
          msg += `<span class="ajs-score-msg ${direction}">${eventmsg}</span>`;
        }
      }
      return msg;
    }

    /**
     * Format the score/total before printing it to display.
     * @method adventurejs.Scorecard#setScore
     */
    formatScore(score, total) {
      if (Object.keys(this.score_format).length) {
        let results = A.getSAF.call(this.game, this.score_format, this);
        if (results) return results;
      }
      return `${score}/${total}`;
    }

    /**
     * Provides a chainable shortcut method for setting a number of properties on the instance.
     * @method adventurejs.Scorecard#set
     * @param {Object} props A generic object containing properties to copy to the instance.
     * @returns {adventurejs.Scorecard} Returns the instance the method is called on (useful for chaining calls.)
     * @chainable
     */
    set(scorecard) {
      if (scorecard.score_events) {
        for (var event in scorecard.score_events) {
          this.game.world._scorecard[event] = JSON.parse(this.createEvent());

          // we accept a number in the form of
          // "open window": 1,
          if ("number" === typeof scorecard.score_events[event]) {
            this.game.world._scorecard[event].points =
              scorecard.score_events[event];
            continue;
          }

          // or we accept an object with any subset of fields
          // "take andy": { points: 1 },
          for (var prop in scorecard.score_events[event]) {
            this.game.world._scorecard[event][prop] = JSON.parse(
              JSON.stringify(scorecard.score_events[event][prop])
            );
          }
        }
      }
      if ("undefined" !== typeof scorecard.summarize_updates)
        this.summarize_updates = scorecard.summarize_updates;
      if (scorecard.score_message) this.score_message = scorecard.score_message;
      if (scorecard.score_format) this.score_format = scorecard.score_format;
      return this;
    }
  }
  adventurejs.Scorecard = Scorecard;
})();