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, message: '', recorded: false },
   *     "drink potion": { points: 1, complete: false, bonus: false, message: '', recorded: 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.game.world._scorecard = {};

      /**
       * 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 = {};

      /**
       * Used to store the game's score events.
       * @var {Boolean} adventurejs.Scorecard#score_events
       * @default {}
       */
      this.score_events = {};

      /**
       * 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;

      this.score_events[event].complete = true;
      // this.score_events[event].recorded = true;

      if ("undefined" === typeof this.game.world._scorecard[event]) {
        this.game.world._scorecard[event] = {};
      }
      this.game.world._scorecard[event].complete = true;
      // this.game.world._scorecard[event].recorded = true;

      return true;
    }

    /**
     * Mark the selected event as recorded after updating the score.
     * @method adventurejs.Scorecard#recordEvent
     * @param {String} event A string matching an event key.
     * @returns {Boolean}
     */
    recordEvent(event) {
      // console.warn(`Scorecard.js > recordEvent(${event})`);

      this.score_events[event].recorded = true;

      if ("undefined" === typeof this.game.world._scorecard[event]) {
        this.game.world._scorecard[event] = {};
      }
      this.game.world._scorecard[event].recorded = 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.score_events) {
        if (!this.score_events[prop].bonus) {
          total += this.score_events[prop].points;
        }
        if (this.score_events[prop].complete) {
          score += this.score_events[prop].points;
        }
      }

      this.total = total;
      this.newscore = score;

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

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

    /**
     * Restore the player's score from a saved file or undo.
     * @method adventurejs.Scorecard#restoreScore
     */
    restoreScore() {
      for (var prop in this.score_events) {
        // cycle through scorecard
        if ("undefined" === typeof this.game.world._scorecard[prop]) {
          // not found in saved game so make sure it's unset
          this.score_events[prop].complete = false;
          this.score_events[prop].recorded = false;
        } else {
          this.score_events[prop].complete =
            this.game.world._scorecard[prop].complete;
          this.score_events[prop].recorded =
            this.game.world._scorecard[prop].recorded;
        }
      }
    }

    /**
     * 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>`;
      //   }
      // }
      for (let prop in this.score_events) {
        if (
          this.score_events[prop].complete &&
          !this.score_events[prop].recorded
        ) {
          let eventmsg = "";
          let diff = this.score_events[prop].points;
          let direction = diff > 0 ? "up" : "down";
          let s = Math.abs(diff) > 1 ? "s" : "";
          this.diff = diff;

          this.recordEvent(prop);

          if (this.score_events[prop].message) {
            eventmsg = A.getSAF.call(
              this.game,
              this.score_events[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#formatScore
     */
    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])
        //     );
        //   }
        // }
        for (var event in scorecard.score_events) {
          // create a new empty event
          this.score_events[event] = JSON.parse(this.createEvent());

          // we accept a number in the form of
          // "open window": 1,
          if ("number" === typeof scorecard.score_events[event]) {
            this.score_events[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.score_events[event][prop] = JSON.parse(
              JSON.stringify(scorecard.score_events[event][prop])
            );
          }

          // if it's already completed or recorded, ensure that's stored
          if (this.score_events[event].complete) {
            this.completeEvent(event);
          }
          if (this.score_events[event].recorded) {
            this.recordEvent(event);
          }
        }
      }

      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;
})();