Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// Asset.js
(function () {
  /*global adventurejs A*/
  "use strict";
  /**
   * @augments adventurejs.Atom
   * @class adventurejs.Asset
   * @ajsconstruct MyGame.createAsset({ "class":"Asset", "name":"foo" })
   * @ajsconstructedby adventurejs.Game#createAsset
   * @param {String} game_name The name of the top level game object.
   * @param {String} name A name for the object, to be serialized and used as ID.
   * @ajsnavheading BaseClasses
   * @summary Framework class that is the ancestor for all classes in the game world.
   * @tutorial AssetReference__Overview
   * @classdesc
   * <p>
   * <strong>Asset</strong> is subclassed from the
   * foundational class {@link adventurejs.Atom|Atom},
   * and is the most basic game world class
   * from which all other asset classes,
   * {@link adventurejs.Tangible|Tangible},
   * {@link adventurejs.Substance|Substance}, and
   * {@link adventurejs.Intangible|Intangible},
   * are derived. Besides setting
   * the prototypal validation and initialization functions,
   * it also defines many common properties used to determine
   * how assets appear in printed statements. It's unlikely
   * that authors would want to subclass Asset directly as
   * it has few properties, unless it's to create a whole new
   * low-level Asset type.
   * </p>
   **/
  class Asset extends adventurejs.Atom {
    constructor(name, game_name) {
      super(name, game_name);
      this.class = "Asset";

      /**
       * <strong>is</strong> is a generalized container for
       * asset states. Many states are / can be stored here.
       * A chief benefit of this is being able to get specific
       * states by testing <code>asset.is.state</code>.
       * Note that there is also an asset.$is() method which
       * is related to this, but is a distinct function.
       * @var {Object} adventurejs.Asset#is
       */
      this.is = new adventurejs.Asset_Is("is", this.game_name, this.id).set({
        parent_id: this.id,
      });

      /**
       * <strong>can</strong> is a generalized container for
       * asset boolean properties.
       * @var {Object} adventurejs.Asset#can
       */
      this.can = new adventurejs.Asset_Can("can", this.game_name, this.id).set({
        parent_id: this.id,
      });

      /**
       * <strong>must</strong> is a generalized container for
       * asset boolean properties.
       * @var {Object} adventurejs.Asset#must
       */
      this.must = new adventurejs.Asset_Must(
        "must",
        this.game_name,
        this.id
      ).set({
        parent_id: this.id,
      });

      /**
       * <strong>dov</strong> is a container for direct object
       * verb subscriptions.
       * @var {Boolean} adventurejs.Asset#dov
       * @default {}
       */
      this.dov = {};

      this.setDOV("think");

      /**
       * <strong>iov</strong> is a container for direct object
       * verb subscriptions.
       * @var {Boolean} adventurejs.Asset#iov
       * @default {}
       */
      this.iov = {};

      this.setIOV("ask");
      this.setIOV("tell");

      /**
       * <strong>quirks</strong> is a container for setting how
       * to handle ambiguous verbs based on context. For example,
       * if player inputs "stand" while sitting in a chair with
       * <code>quirks.stand_means_get_off</code> set to true, player
       * will get off the chair, as opposed to trying to stand in
       * place on the chair.
       * @var {Object} adventurejs.Asset#quirks
       */
      this.quirks = {};

      /**
       * Set to true to have an object's name be printed bold.
       * @var {Boolean} adventurejs.Asset#print_bold
       * @default false
       */
      this.print_bold = false;

      /**
       * Set to true to have an object's name be printed italic.
       * @var {Boolean} adventurejs.Asset#print_italic
       * @default false
       */
      this.print_italic = false;

      /**
       * Almost all assets will be listed in the world_lookup table
       * that is used to find assets from player input ,
       * with some exceptions, such as SceneryZones.
       * @var {Boolean} adventurejs.Asset#exclude_from_lookup
       * @default false
       */
      this.exclude_from_lookup = false;

      /**
       * Object names are split into individual words for the world_lookup table.
       * For example, given an object named "crystal sword", adventurejs creates
       * lookup table entries for "crystal" and "sword". This lets the game
       * recognize either word as player input, such as "get crystal" or
       * "swing sword".<br><br>
       * But, an author might want to name a thing, eg, "hole in the ground",
       * in which case we wind up with lookup table entries for "hole" and
       * "in" and "the" and "ground", which is likely to lead to bad input parsing.
       * To avoid name splitting, set split_name_for_world_lookup to false.
       * The object's full name will still be added to the lookup.
       * @var {Boolean} adventurejs.Asset#split_name_for_world_lookup
       * @default true
       *
       */
      this.split_name_for_world_lookup = true;

      /**
       * A list of descriptions for a variety of contexts. Only
       * <code class="property">description</code> is required, all others
       * are optional. Most of these apply only to Tangible Asset.
       * <ul>
       * <li><code class="property">descriptions.description</code> - </li>
       * <li><code class="property">descriptions.brief</code> - used for room descriptions if player has typed "brief"</li>
       * <li><code class="property">descriptions.verbose</code> - used for room descriptions if player has typed "verbose"</li>
       * <li><code class="property">descriptions.listen</code> - used if player types "listen" or "listen to thing"</li>
       * <li><code class="property">descriptions.in</code> - used if player types "look in thing"</li>
       * <li><code class="property">descriptions.through</code> - used if player types "look through thing"</li>
       * <li><code class="property">descriptions.smell</code> - used if player types "smell thing"</li>
       * <li><code class="property">descriptions.taste</code> - used if player types "taste thing"</li>
       * <li><code class="property">descriptions.touch</code> - used if player types "touch thing"</li>
       * <li><code class="property">descriptions.careful</code> - used if player types "carefully examine thing"</li>
       * </ul>
       * @var {Object} adventurejs.Asset#descriptions
       */
      this.descriptions = {
        look: "",
        brief: "",
        verbose: "",
      };

      /**
       * Collection is a special property that lets one object
       * stand in for a group of objects.
       * For example, object desk has three drawers:
       * top_drawer, middle_drawer, bottom_drawer.
       * desk_drawers is a scenery object attached to desk which
       * intercepts player input like "examine drawers", so that author
       * can return a response like "You see three drawers."
       * @var {Getter/Setter} adventurejs.Asset#collection
       *
       */
      this.collection = [];

      this.name = "";

      /**
       * Used to determine whether to display a name or proper name.
       * @var {Boolean} adventurejs.Asset#name_is_proper
       * @default false
       */
      this.name_is_proper = false;

      /**
       * The asset's proper name, as provided in the game file. Assets
       * can have both a name and a proper name. Used chiefly for
       * characters, so that, for example, an asset can be both 'cat' and
       * 'Mister Whiskers'.
       * @var {String} adventurejs.Asset#propername
       */
      this.propername = "";

      /**
       * The asset's preferred definite article.
       * Can be overridden in game file.
       * @var {String} adventurejs.Asset#definite_article
       * @default 'the'
       */
      this.definite_article = "the"; // TODO but maybe not? honorific? "Mr." "Mrs."

      /**
       * The asset's preferred indefinite article.
       * Can be overridden in game file.
       * @var {String} adventurejs.Asset#indefinite_article
       * @default 'a'
       */
      this.indefinite_article = "a"; // "a" or "an", or "the" if the object is singular

      /**
       * A preference for how an asset's name appears when it is printed
       * in certain contexts. Chiefly used for room names.
       * For example, you might name a room 'Sitting Room', but want it
       * to appear as 'the Sitting Room' in some contexts for narrative clarity.
       * Can be overridden in game file.
       * @var {Boolean} adventurejs.Asset#use_definite_article_in_lists
       * @default false
       */
      this.use_definite_article_in_lists = false;

      /**
       * An option to prevent definite/indefinite articles from
       * appearing before an asset name. Used chiefly for characters,
       * to prevent phrases such as "the Mrs. Beasley".
       * Can be overridden in game file.
       * @var {Boolean} adventurejs.Asset#dont_use_articles
       * @default false
       */
      this.dont_use_articles = false;

      /**
       * <strong>noun</strong> is a string representing the type of asset
       * this is, for example 'mug' or 'rock'. Players can input this word
       * to refer to any asset of its type.
       * @var {String} adventurejs.Asset#noun
       *
       */
      this.noun = "";

      /**
       * <strong>pronoun</strong> is a string representing the generic
       * pronoun.
       * @var {String} adventurejs.Asset#pronoun
       *
       */
      this.pronoun = "it";

      /**
       * <strong>singlePluralPairs</strong> is a collection of strings
       * representing the singular and plural forms of this asset, for
       * example ['mug', 'mugs']. Players can use the singular to refer
       * to a single asset or the plural to refer to a group of assets.
       * These are stored in pairs so we can check for correspondence
       * between the forms.
       * @var {Array} adventurejs.Asset#singlePluralPairs
       *
       */
      this.singlePluralPairs = [];

      /**
       * <strong>plural</strong> is a string representing multiple assets
       * of this type, for example 'swords'. Players can input this word
       * to refer to groups of assets.
       * @var {String} adventurejs.Asset#plural
       *
       */
      this.plural = "";

      /**
       * <strong>group</strong> is a string representing a collective
       * category. For example, chair, bed, and desk all share a group
       * of furniture. Players can input this word
       * to refer to any asset of its type.
       * @var {String} adventurejs.Asset#noun
       *
       */
      this.group = [];

      /**
       * <strong>synonyms</strong> is a collection of alternate words that
       * can be used to refer to this asset. For example, an object called
       * 'rock' might have synonyms of ['pebble', 'stone'].
       * Players can input synonyms to refer to this asset.
       * @var {String} adventurejs.Asset#noun
       *
       */
      this.synonyms = [];

      /**
       * A list of adjectives defined for this asset in the game file.
       * Some asset classes may have predefined adjectives.
       * If a class has predefined adjectives, and an author creates
       * an instance of the class with custom adjectives, the custom
       * adjectives will be appended to the predefined ones.
       * @var {Getter/Setter} adventurejs.Asset#adjectives
       */
      this.adjectives = [];

      /**
       * <strong>did_do_verb</strong> is used to track
       * which verbs have acted upon this object.
       * This is  a backup of the verb counter used
       * in verb subscriptions.
       * @var {Object} adventurejs.Asset#did_do_verb
       */
      this.did_do_verb = {};

      /**
       * <strong>did_do_verb_count</strong> is used to track
       * the number of times that given verbs have acted
       * upon this object.
       * This is  a backup of the verb counter used
       * in verb subscriptions.
       * @var {Object} adventurejs.Asset#did_do_verb_count
       */
      this.did_do_verb_count = {};

      /**
       * <strong>did_try_verb</strong> is used to track
       * which verbs have been tried upon this object.
       * This is  a backup of the verb counter used
       * in verb subscriptions.
       * @var {Object} adventurejs.Asset#did_try_verb
       */
      this.did_try_verb = {};

      /**
       * <strong>did_try_verb_count</strong> is used to track
       * the number of times that given verbs have been tried
       * upon this object.
       * This is  a backup of the verb counter used
       * in verb subscriptions.
       * @var {Object} adventurejs.Asset#did_try_verb_count
       */
      this.did_try_verb_count = {};

      /**
       * <strong>image</strong> is used to store the id
       * of an image in game.image_lookup that is
       * associated with this asset.
       * @var {String} adventurejs.Asset#image
       */
      this.image = "";

      /**
       * <strong>redirected_verbs</strong> is an object used
       * for storing verb redirections via redirectVerb().
       * @var {Boolean} adventurejs.Tangible#redirected_verbs
       * @default {}
       */
      this.redirected_verbs = {};
    }

    /**
     * Getter function returns asset name preceded by its definite article,
     * example: 'the asset'.
     * @var {Getter} adventurejs.Asset#definite_name
     * @returns {String}
     */
    get definite_name() {
      //return this.definite_article + " " + this.name;
      var name = this.definite_article + " " + this.name;
      if (this.print_bold) name = "<strong>" + name + "</strong>";
      if (this.print_italic) name = "<em>" + name + "</em>";
      return name;
    }

    /**
     * Getter function returns asset name preceded by its indefinite article,
     * example: 'an asset'.
     * @var {Getter} adventurejs.Asset#indefinite_name
     * @returns {String}
     */
    get indefinite_name() {
      //return this.indefinite_article + " " + this.name;
      var name = this.indefinite_article + " " + this.name;
      if (this.print_bold) name = "<strong>" + name + "</strong>";
      if (this.print_italic) name = "<em>" + name + "</em>";
      return name;
    }

    /**
     * Getter function returns asset name preceded either by definite
     * article or indefinite article, depending on what's set
     * for the asset's use_definite_article_in_lists property.
     * @var {Getter} adventurejs.Asset#article_name
     * @returns {String}
     */
    get article_name() {
      var article = this.use_definite_article_in_lists
        ? this.definite_article
        : this.indefinite_article;
      return article + " " + this.name;
    }

    /**
     * Getter function returns asset name or proper name, depending
     * on settings.
     * @var {Getter} adventurejs.Asset#articlename
     * @returns {String}
     */
    get articlename() {
      var name;
      if (this.propername) {
        name = this.propername;
      } else if (this.name_is_proper) {
        name = this.name;
      } else {
        name = this.definite_article + " " + this.name;
      }
      if (this.print_bold) name = "<strong>" + name + "</strong>";
      if (this.print_italic) name = "<em>" + name + "</em>";
      return name;
    }

    /**
     * Returns this.articlename with propercase.
     * @var {Getter} adventurejs.Asset#Articlename
     * @returns {String}
     */
    get Articlename() {
      return A.propercase(this.articlename);
    }

    get adjectives() {
      return this.__adjectives;
    }
    set adjectives(arr) {
      if (false === Array.isArray(this.__adjectives)) {
        this.__adjectives = [];
      }
      this.__adjectives = arr;
    }

    /**
     * An alias to descriptions.look.
     * @var {*} adventurejs.Asset#description
     */
    get description() {
      if (
        Array.isArray(this.descriptions.look) ||
        "string" === typeof this.descriptions.look ||
        "function" === typeof this.descriptions.look
      ) {
        return this.descriptions.look;
      }
      if (
        "object" === typeof this.descriptions.look &&
        this.descriptions.look.default
      ) {
        return this.descriptions.look.default;
      }
      return `${this.Articlename} is undescribed. `;
    }
    set description(look) {
      this.descriptions.look = look;
    }

    get collection() {
      return this.__collection;
    }
    set collection(arr) {
      if (false === Array.isArray(this.__collection)) {
        this.__collection = [];
      }
      this.__collection = A.validateAssetList(arr);
    }

    /**
     * <strong>validate()</strong> provides an opportunity to
     * check author-made Assets for errors before initializing them.
     * @method adventurejs.Asset#validate
     * @memberOf adventurejs.Asset
     */
    validate(game) {
      this.game.log("log", 3, ["Validating " + this.id], "Asset");

      // has it got an id?
      if ("undefined" === typeof this.name) {
        var msg = "This " + this.constructor.name + " has no id: ";
        //console.error(msg, this);
        this.game.log("warn", 0, msg, "Asset");
        return false;
      }

      this.setVerbSubscriptionsWithAssets();
      this.setVerbSubscriptionsWithConnection();

      return true;
    }

    /**
     * <strong>initialize()</strong> is the final step of
     * creating a new game world asset, following
     * construction and validation. The chief job of
     * initialization is to add all of
     * this asset's words to
     * {@link adventurejs.Game#world_lookup | MyGame.world_lookup}.
     * <br><br>
     * Because we're not doing natural language processing
     * and everything is indexed, we want to increase the chances
     * that the parser understands player input that is partial or
     * has words jumbled. We add a lookup entry for every
     * combination of an object's adjectives with its noun(s).
     * For example we have a "green colored pencil", and we want
     * the parser to understand "green pencil" or "colored pencil".
     * If there are other colored pencils, we want the parser to
     * understand "colored pencils". If there are colored pencils
     * in three shades of green, we want the parser to understand
     * "green pencils". We store all of these combinations to the lookup.
     * @method adventurejs.Asset#initialize
     * @memberOf adventurejs.Asset
     */
    initialize(game) {
      if (this.is.initialized) return this;
      this.game.log("log", 3, "Initializing " + this.id);
      var id;
      var name;
      var names = [];
      var pairs = [];
      var synonyms = [];
      var adjectives = [];
      var group = [];
      var serialized_group = [];

      for (var a = 0; a < this.adjectives.length; a++) {
        var adjective = this.adjectives[a].trim();
        for (var i = 0; i < this.singlePluralPairs.length; i++) {
          if ("" === adjective || " " === adjective) {
            continue;
          }
          pairs.push([
            // example: brass lantern
            A.deserialize(adjective + " " + this.singlePluralPairs[i][0]),
            // example: brass lanterns
            A.deserialize(adjective + " " + this.singlePluralPairs[i][1]),
          ]);

          // TODO SERIALIZED LOOKUP?
          // does world_lookup really need serialized entries?
          // pairs.push(
          //   [
          //     // example: brass_lantern
          //     A.serialize( adjective + "_" + this.singlePluralPairs[i][0] ),
          //     // example: brass_lanterns
          //     A.serialize( adjective + "_" + this.singlePluralPairs[i][1] )
          //   ]
          // );
        }
      }
      this.singlePluralPairs = this.singlePluralPairs.concat(pairs);

      // Most objects will be listed in world_lookup.
      // zones are excluded
      if (this.exclude_from_lookup) {
      } else {
        // There shouldn't be any spaces by this point, but just in case.
        id = this.id.toLowerCase().split(" ");
        name = [this.name].slice(0);

        /**
         * By default we add the name, each word of the name
         * and each pair of words in the name.
         * Possibly excessive, but helps catch
         * typos in long names for use with "oops".
         * Can be disabled per asset by setting
         * asset.split_name_for_world_lookup = false.
         */
        if (this.split_name_for_world_lookup) {
          // add each word of the name
          names = this.name.toLowerCase().split(" ");

          if (2 <= names.length) {
            for (let i = names.length - 1; i > 0; i--) {
              // add each pair of words in the name
              // ie for "tiny silver key" we add "tiny silver" and "silver key"
              let wordPair = `${names[i - 1]} ${names[i]}`;
              if (-1 === names.indexOf(wordPair)) {
                names.push(wordPair);
              }
            }
          }
        }

        /**
         * We're going to pass these properties through addWordsToLookup which
         * is destructive, so we use copies in order to retain the originals.
         */
        synonyms = this.synonyms.slice(0);
        adjectives = this.adjectives.slice(0);
        group = this.group.slice(0);

        // added this because non-serialized group words
        // were being omitted from lookup
        serialized_group = this.group.slice(0);
        serialized_group = A.serializeArray(serialized_group);

        this.addWordsToLookup(["all"], "plural");
        this.addWordsToLookup(id, "id");
        this.addWordsToLookup(name, "name");
        this.addWordsToLookup(names, "name");
        this.addWordsToLookup(synonyms, "synonym");
        this.addWordsToLookup(adjectives, "adjective");
        this.addWordsToLookup(group, "group");
        this.addWordsToLookup(serialized_group, "group");
        //this.addWordsToLookup( serialized_adjectives, "group" );

        if (this.singlePluralPairs.length > 0) {
          for (var i = 0; i < this.singlePluralPairs.length; i++) {
            var singular = this.singlePluralPairs[i][0];
            var plural = this.singlePluralPairs[i][1];

            // is this unneccessarily indirect?
            this.addWordsToLookup([singular], "singular");
            this.addWordsToLookup([plural], "plural");

            /*
             * Add lookup entry for "all [plural]"
             * which is really the same as just "[plural]"
             * Example:
             *  take keys
             *  take all keys
             * Are the same.
             * And don't add "all all".
             */
            if (false === this instanceof adventurejs.All) {
              this.addWordsToLookup([A.deserialize("all " + plural)], "plural");
              // TODO SERIALIZED LOOKUP?
              //this.addWordsToLookup( [ A.serialize( "all_" + plural ) ], "plural" );
            }

            // i'm not sure the singular needs a reference to the plural
            this.game.world_lookup[singular].plural = plural;

            // the plural word def needs a reference to the singular
            this.game.world_lookup[plural].singular = singular;

            if (false === this instanceof adventurejs.All) {
              // all plural
              this.game.world_lookup[A.deserialize("all " + plural)].singular =
                singular;
              // TODO SERIALIZED LOOKUP?
              //this.game.world_lookup[ A.serialize( "all_" + plural ) ].singular = singular;
            }
          }
        }
      }

      return this;
    }
  }

  adventurejs.Asset = Asset;
  var p = Asset.prototype;

  /**
   * <strong>aliases()</strong> is a collection of method
   * names that are meant for authors to use. Since they
   * don't exist on all classes, we set up these aliases
   * so that, if authors call them on classes they're not
   * applicable to, they will politely return null instead
   * of throwing a "not a function" error.
   * @method adventurejs.Asset#aliases
   * @memberOf adventurejs.Asset
   */
  p.aliases =
    p.$isIn =
    p.$is =
    p.$has =
    p.$put =
    p.$moveTo =
    p.$moveFrom =
    p.$get =
    p.$room =
    p.$exits =
    p.$directions =
    p.$player =
    p.$inventory =
    p.$nest =
    p.$nested =
    p.$getKeys =
    p.$getKey =
    p.$hasKey =
    p.$parent =
    p.$ =
      function Asset_aliases() {
        return null;
      };
})();