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

  /**
   * @class adventurejs.Hintcard
   * @ajsinternal
   * @param {Game} game A reference to the game instance.
   * @ajsnavheading FrameworkClasses
   * @summary Manages hints for a {@link adventurejs.Game|Game} instance.
   * @classdesc
   * <p>
   * <strong>Hintcard</strong> is a repository for game hint
   * options. Hintcard is created automatically
   * by {@link adventurejs.Game|Game}. This is an internal class
   * that authors should not need to construct. However,
   * authors can set hint options from their game file as
   * shown below.
   * </p>
   * <h3 class="examples">Example:</h3>
   * <pre class="display"><code class="language-javascript">var MyGame = new adventurejs.Game( "MyGame", "GameDisplay" );
   * MyGame.Hintcard.set({
   *
   * });
   * </code></pre>
   */
  class Hintcard {
    constructor(game) {
      this.game = game;
      this.game.world._hintcard = {};
      // this.key = 0;

      this.introduction = "";
      this.hints = {};
      this.active = true;
    }

    /**
     * Activate a hint, with optional recurse.
     * @method adventurejs.Hintcard#activateHint
     * @param {String} key A name or id to search for.
     * @param {Boolean} recurse Whether to activate recursively.
     * @returns {}
     */
    activateHint(key, recurse = true) {
      return this.toggleHint(key, "active", true, recurse);
    }

    /**
     * Get a count of the number of hints in each group.
     * @method adventurejs.Hintcard#countHints
     */
    countHints(node = this) {
      if (!node.active) return [0, 0];
      if (
        "undefined" === typeof node.hints ||
        Object.keys(node.hints).length === 0
      ) {
        return [node.revealed ? 0 : 1, 1];
      }
      let count = 0;
      let unrevealed = 0;
      for (const key in node.hints) {
        let [u, t] = this.countHints(node.hints[key]);
        unrevealed += u;
        count += t;
      }
      node.unrevealed = unrevealed;
      node.count = count;
      return [unrevealed, count];
    }

    /**
     * Create a new hint.
     * @method adventurejs.Hintcard#createHint
     * @returns {String}
     */
    createHint() {
      // console.warn(`createHint`);
      let hint = {
        key: "",
        text: "",
        description: "",
        name: "",
        hints: {},
        revealed: false,
        active: true,
      };
      // return JSON.stringify(hint);
      return hint;
    }

    /**
     * Deactivate a hint, with optional recurse.
     * @method adventurejs.Hintcard#deactivateHint
     * @param {String} key A name or id to search for.
     * @param {Boolean} recurse Whether to deactivate recursively.
     * @returns {}
     */
    deactivateHint(key, recurse = true) {
      return this.toggleHint(key, "active", false, recurse);
    }

    /**
     * Find hint by key. Looks recursively.
     * @method adventurejs.Hintcard#findHintByKey
     * @param {String} key An object key to search for.
     * @param {Object} root The root object level to begin search.
     * @returns {Object|null}
     */
    findHintByKey(key, root = this) {
      if (!root || typeof root !== "object") return null;

      if (root.hints && Object.prototype.hasOwnProperty.call(root.hints, key)) {
        return root.hints[key];
      }

      if (root.hints) {
        for (const k of Object.keys(root.hints)) {
          const found = this.findHintByKey(key, root.hints[k]);
          if (found) return found;
        }
      }

      return null;
    }

    /**
     * Find hint by name. Looks recursively.
     * @method adventurejs.Hintcard#findHintByName
     * @param {String} key An object key to search for.
     * @param {Object} root The root object level to begin search.
     * @returns {Object|null}
     */
    findHintByName(name, root = this) {
      if (
        !name ||
        !root ||
        typeof root !== "object" ||
        !root.hints ||
        typeof root.hints !== "object"
      )
        return null;

      const comparator = this.game.settings.obfuscate_hints
        ? A.obfuscate(name)
        : name;

      for (let key in root.hints) {
        let hint = root.hints[key];
        if (comparator === hint.name) {
          return hint;
        }
        if (hint.hints) {
          let subhint = this.findHintByName(name, hint);
          if (subhint) return subhint;
        }
      }

      return null;
    }

    /**
     * Hide a hint, with optional recurse.
     * @method adventurejs.Hintcard#hideHint
     * @param {String} key A name or id to search for.
     * @param {Boolean} recurse Whether to activate recursively.
     * @returns {}
     */
    hideHint(key, recurse = true) {
      return this.toggleHint(key, "revealed", false, recurse);
    }

    /**
     * Reveal a hint, with optional recurse.
     * @method adventurejs.Hintcard#activateHint
     * @param {String} key A name or id to search for.
     * @param {Boolean} recurse Whether to activate recursively.
     * @returns {}
     */
    revealHint(key, recurse = true) {
      return this.toggleHint(key, "revealed", true, recurse);
    }

    /**
     * Obfuscate hint data.
     * @method adventurejs.Hintcard#obfuscateHint
     */
    obfuscateHint(hint) {
      if (!this.game.settings.obfuscate_hints) return hint;
      hint.name = A.obfuscate(hint.name);
      hint.text = A.obfuscate(hint.text);
      // hint.description = A.obfuscate(hint.description);
      return hint;
    }

    /**
     * Process hint data in longhand format.
     * @method adventurejs.Hintcard#processLonghand
     */
    processLonghand(source, dest = this.hints, prefix = "$") {
      let count = 0;
      for (let srckey in source) {
        count++;
        const srchint = source[srckey];

        // create a new empty point
        const destkey = `${prefix}${count}`;
        dest[destkey] = dest[destkey] || this.createHint();
        const hint = dest[destkey];
        hint.key = destkey;

        if ("string" === typeof srchint) {
          hint.text = srchint;
          // hint.description = srchint;
        } else if ("object" === typeof srchint && Object.keys(srchint).length) {
          for (let prop in srchint) {
            if (prop !== "hints") {
              hint[prop] = srchint[prop];
            }
          }
          if (srchint.hints && Object.keys(srchint.hints).length) {
            hint.name = srckey;
            this.processLonghand(srchint.hints, hint.hints, destkey + "|");
          }
        }
        if (this.game.settings.obfuscate_hints) {
          hint.name = A.obfuscate(hint.name);
          hint.text = A.obfuscate(hint.text);
          // hint.description = A.obfuscate(hint.description);
        }
      } // for key in hints
      return count;
    }

    /**
     * Process hint data in shorthand format.
     * @method adventurejs.Hintcard#processShorthand
     */
    processShorthand(hints) {
      const lines = String(hints)
        .trim()
        .split("\n")
        .map((line) => line.trim())
        .filter(Boolean);

      // ancestors[level] is the most recent group node for that header level.
      // ancestors[0] is the root container.
      const ancestors = [];
      ancestors[0] = this;

      let count = 0;
      for (let line of lines) {
        let id = count;
        let active = false;
        count++;

        let re = /\s*\|\s*\+$/;
        if (re.test(line)) {
          line = line.replace(re, "");
          active = true;
        }

        re = /\s*\|\s*x$/;
        if (re.test(line)) {
          line = line.replace(re, "");
          active = false;
        }

        // separate # indicators from text
        // console.warn(line);
        // console.warn(line.match(/^(#{1,6})?\s*(.*)$/));
        const line_content = line.match(/^(#{1,6})?\s*(.*)$/);
        if (!line_content) continue;

        // get level // 1 => "#", 2 => "##", 3 => "###", ...
        const level = line_content[1] ? line_content[1].length : 1;

        // get text
        let line_body = line_content[2].trim();

        // remove leading dash
        const is_hint = line_body.startsWith("-");
        if (is_hint) line_body = line_body.replace(/^-\s*/, "").trim();

        let name,
          text = "";
        if (line_body.includes("|")) {
          [name, text] = line_body.split("|").map((s) => s.trim());
        }
        let hint = null;

        // find nearest group at level-1 or above (should exist at ancestors[level-1])
        const node = ancestors[level - 1] || ancestors[0];
        node.hints = node.hints || {};

        if (is_hint) {
          // Attach item under the node at this exact header level if it exists;
          // otherwise attach to the node (level-1 (ancestors[level]
          // will exist if a node line with this level appeared)
          const groupAtLevel = ancestors[level];
          const attachTo =
            groupAtLevel && groupAtLevel.hints ? groupAtLevel : node;
          attachTo.hints = attachTo.hints || {};

          // use the count of the hint as its key
          let nodecount = Object.keys(attachTo.hints).length + 1;
          let id = `${attachTo.key ? attachTo.key + "|" : "$"}${nodecount}`;

          attachTo.hints[id] = attachTo.hints[id] || this.createHint();
          hint = attachTo.hints[id];
          // hint.description = description ? description : line_body;
          hint.name = name;
          hint.text = text || "";
          hint.key = id;
        } else {
          // create or reuse group node at this level
          let nodecount = Object.keys(node.hints).length + 1;
          let id = `${node.key ? node.key + "|" : "$"}${nodecount}`;

          node.hints[id] = node.hints[id] || this.createHint();
          hint = node.hints[id];
          hint.text = text || "";
          hint.name = name ? name : line_body;
          // hint.description = description;
          hint.key = id;
          ancestors[level] = hint;

          // clear any deeper-level ancestors (so later siblings won't incorrectly attach)
          for (let k = level + 1; k < ancestors.length; k++)
            ancestors[k] = undefined;
        }

        hint.active = active;

        if (this.game.settings.obfuscate_hints) {
          hint.name = A.obfuscate(hint.name);
          hint.text = A.obfuscate(hint.text);
          // hint.description = A.obfuscate(hint.description);
        }
      }
    }

    /**
     * Provides a chainable shortcut method for setting a number of properties on the instance.
     * @method adventurejs.Hintcard#set
     * @param {Object} props A generic object containing properties to copy to the instance.
     * @returns {adventurejs.Hintcard} Returns the instance the method is called on (useful for chaining calls.)
     * @chainable
     */
    set(hintcard) {
      if (hintcard.hints) {
        // handle shortcut format
        if ("string" === typeof hintcard.hints) {
          this.processShorthand(hintcard.hints);
        }

        // handle full format
        else {
          this.processLonghand(hintcard.hints);
        }
        delete hintcard.hints;

        this.countHints();

        // pass on any other properties - expected properties are
        for (let prop in hintcard) {
          this[prop] = hintcard[prop];
        }

        return this;
      }
    }

    test(path, delimiter = ",") {
      console.warn({ path });

      if (typeof path !== "string") return undefined;

      const parts = path
        .split(delimiter)
        .map((s) => A.serialize(s.trim()))
        .filter(Boolean);

      let current = this.hints;

      for (const part of parts) {
        if (!current) {
          console.warn("hint path not found");
          return undefined;
        }

        // Prefer nested "hints" object if it exists and contains the key
        if (
          current.hints &&
          Object.prototype.hasOwnProperty.call(current.hints, part)
        ) {
          current = current.hints[part];
        } else if (Object.prototype.hasOwnProperty.call(current, part)) {
          current = current[part];
        } else {
          // not found
          console.warn("hint path not found");
          return undefined;
        }
      }

      console.warn(current);
      return current;
    }

    /**
     * Toggle a hint property, with optional recurse.
     * @method adventurejs.Hintcard#toggleHint
     * @param {String} key A name or id to search for.
     * @param {String} property A property to toggle.
     * @param {Boolean} bool Activate or deactivate.
     * @param {Boolean} recurse Whether to activate recursively.
     * @returns {}
     */
    toggleHint(key, property, bool, recurse = true) {
      const hint = this.findHintByKey(key) || this.findHintByName(key);
      if (!hint) return false;
      hint[property] = bool;
      if (
        recurse &&
        "object" === typeof hint.hints &&
        Object.keys(hint.hints).length
      ) {
        for (let subkey in hint.hints) {
          this.toggleHint(subkey, property, bool, recurse);
        }
      }
      this.countHints();
      return true;
    }
  }
  adventurejs.Hintcard = Hintcard;
})();