Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// Scorecard.js
(function () {
  /*global adventurejs A*/
  "use strict";

  /**
   * @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,
        },
      };
      this.score = 0;
      this.newscore = 0;
      this.diff = 0;
      this.aggregate_updates = true;
      this.score_message = "";
      this.score_format = {};
      this.aggregate_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) {
      if (!this.game.world._scorecard[event]) return false;
      this.game.world._scorecard[event].complete = true;
      return true;
    }

    /**
     * Get the current 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.aggregate_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 pole = 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 ${pole} by ${diff} point${s} ** ]`;

        msg = `<span class="score_msg ${pole}">${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 pole = 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 ${pole} by ${diff} point${s} ** ]`;
          }
          msg += `<span class="score_msg ${pole}">${eventmsg}</span>`;
        }
      }
      return msg;
    }

    /**
     * Format the score/total before printing it to display.
     * @method adventurejs.Scorecard#setScore
     */
    formatScore(score, total) {
      // @TODO add a more robust check for score_format
      if (Object.keys(this.score_format).length)
        return A.getSAF.call(this.game, this.score_format, this);
      else 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" !== scorecard.aggregate_updates)
        this.aggregate_updates = scorecard.aggregate_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;
})();