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

  /**
   * @ajspath adventurejs.Atom.Asset.Matter.Tangible.Character
   * @augments adventurejs.Tangible
   * @class adventurejs.Character
   * @ajsconstruct MyGame.createAsset({ "class":"Character", "name":"foo", [...] })
   * @ajsconstructedby adventurejs.Game#createAsset
   * @ajsnavheading CharacterClasses
   * @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.
   * @summary Base class for all character types.
   * @tutorial Characters_NPCs
   * @tutorial GetStarted_CreateAPlayer
   * @ajstangiblecontainer in
   * @todo more methods, better example
   * @classdesc
   * <p>
   * <strong>Character</strong> is anything that moves or speaks
   * or takes or gives. {@link adventurejs.Player|Player} and
   * {@link adventurejs.NPC|NPCs} are both Characters.
   * Character can sense things, hold things, be asked things,
   * be told things...
   * </p>
   * <h3 class="examples">Example:</h3>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   "class": "Character",
   *   "name": "Yurtle",
   *   "synonyms": ["turtle"],
   *   "place": {in: "Pond"},
   * })
   * </code></pre>
   */
  class Character extends adventurejs.Tangible {
    constructor(name, game_name) {
      super(name, game_name);
      this.class = "Character";

      this.is = new adventurejs.Character_Is("is", this.game_name, this.id);

      this.can = new adventurejs.Character_Can("can", this.game_name, this.id);

      this.setDOVs(["feed"]);
      this.setIOVs(["throw", "take", "give", "hold", "tie"]);

      this.posture = "stand";

      this.aspects.in = new adventurejs.Aspect("in", this.game_name, this.id);
      this.default_aspect = "in";

      this.dont_use_articles = true;
      this.name_is_proper = true;

      /**
       * We can track the user's y position. This option sets the maximum distance that the
       * player can reach vertically, ie when climbing an object like a tree. 1 is considered
       * to be about the height of a person.
       * @var {Boolean} adventurejs.Character#reach_height
       * @default 1
       */
      this.reach_height = 1;

      /**
       * We can track the user's x/z position. This option sets the maximum distance that the
       * player can reach horizontally within a room. 0.33 is considered to be about arm's length.
       * @var {Boolean} adventurejs.Character#reach_length
       * @default .33
       */
      this.reach_length = 0.33;

      /**
       * We can track the user's x/z position. This option sets the default distance that the
       * player travels horizontally when moving within a room. 1 is considered to be about
       * the height of a person.
       * @var {Boolean} adventurejs.Character#stride_length
       * @default .33
       */
      this.stride_length = 0.33;

      /**
       * We can track the user's y position. This option sets the maximum distance that the
       * player can travel vertically. 1 is considered to be about
       * the height of a person.
       * @var {Boolean} adventurejs.Character#jump_height
       * @default 1
       */
      this.jump_height = 1;

      /**
       * We can track the user's x/z position. This option sets the maximum distance that the
       * player travels horizontally when moving within a room. 1 is considered to be about
       * the height of a person.
       * @var {Boolean} adventurejs.Character#jump_length
       * @default 1
       */
      this.jump_length = 1;

      /**
       * Generic message to return when an NPC is given
       * a command that they can't act on. Can be left blank
       * to use a default message.
       * @var {String} adventurejs.Character#ignore_msg
       * @default ""
       */
      this.ignore_msg = "";

      /**
       * Object to keep track of assets known by the character.
       * @var {String} adventurejs.Character#knows_about
       * @default {}
       */
      this.knows_about = {}; // ok

      /**
       * Object to keep track of assets seen by the character.
       * @var {String} adventurejs.Character#has_seen
       * @default {}
       */
      this.has_seen = {}; // ok

      /**
       * Object to keep track of room assets visited by the character.
       * @var {String} adventurejs.Character#has_been
       * @default {}
       */
      this.has_been = {}; // ok

      /**
       * Object to keep track of room assets visited by the character.
       * @var {String} adventurejs.Character#nest
       * @default {}
       */
      this.nest = {};
    }

    /**
     * Get / set nest. Nest is related to but differs from place.
     * Non-character classes can be placed in any other asset.
     * Character classes can only be placed in rooms, and nest
     * in other assets. This is so they can be, for example,
     * nested on a bicycle while in a room.
     * The private var _nest is an
     * object with two properties: aspect and asset.
     * For example: { aspect:"in", asset:"room" }
     * However, the public var place appears in the form
     * { in: "room" }. This is to make it easier and more
     * intuitive for authors to set asset nests.
     * @var {Object} adventurejs.Character#nest
     */
    get nest() {
      return this._nest;
    }
    set nest(nest) {
      if (Object(nest) !== nest) {
        let msg = `${this.id}.nest received a malformed value `;
        this.game.log("L1428", "error", "critical", msg, "Tangible");

        nest = {};
      } else {
        var keys = Object.keys(nest);

        if (keys.length > 1) {
          let msg = `${this.id}.nest received more than one location. Using the first. `;
          for (let i = 0; i < keys.length; i++) {
            msg += keys[i] + ", ";
          }
          nest = { [keys[0]]: nest[keys[0]] };
          this.game.log("L1429", "error", "critical", msg, "Character");
        }

        if (keys.length === 1) {
          // serialize asset name
          nest[keys[0]] = A.serialize(nest[keys[0]]);

          // verify asset
          if ("string" !== typeof nest[keys[0]]) {
            const msg = `${this.id}.nest set to an invalid asset `;
            this.game.log("L1430", "error", "critical", msg, "Tangible");
          }
        }

        if (keys.length === 0) {
        }
      }

      this._nest = nest;
    }

    /**
     * Character knowledge is stored such that it is written
     * with saved games. This utility allows an author to back up
     * a character's knowledge in case they should want
     * to temporarily delete it, such as giving a character
     * temporary amnesia, and then later restore it.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#saveKnowledge
     */
    saveKnowledge() {
      this.game.world._vars[`${this.id}_knows_about`] = this.knows_about;
      this.game.world._vars[`${this.id}_has_seen`] = this.has_seen;
    }

    /**
     * Character knowledge is stored such that it is written
     * with saved games. Should an author want
     * to temporarily delete it, such as by giving a character
     * temporary amnesia, this allows them to later restore it.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#restoreKnowledge
     */
    restoreKnowledge() {
      if (this.game.world._vars[`${this.id}_knows_about`]) {
        this.knows_about = this.game.world._vars[`${this.id}_knows_about`];
        delete this.game.world._vars[`${this.id}_knows_about`];
      }
      if (this.game.world._vars[`${this.id}_has_seen`]) {
        this.has_seen = this.game.world._vars[`${this.id}_has_seen`];
        delete this.game.world._vars[`${this.id}_has_seen`];
      }
    }

    /**
     * Clear all of a character's knowledge.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#clearKnowledge
     */
    clearKnowledge() {
      this.knows_about = {};
      this.has_seen = {};
    }

    /**
     * Set whether character knows about an asset.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#knowAsset
     * @param {*} asset
     * @param {Boolean} recurse If true, will make content of aspects known.
     */
    knowAsset(asset, recurse = true) {
      if ("string" === typeof asset) {
        asset = this.game.getAsset(asset);
      }
      if (!asset || !asset.id) return;

      this.knows_about[asset.id] = true;

      if (asset.linked_asset) {
        let linked_asset = this.game.getAsset(asset.linked_asset);
        if (linked_asset && linked_asset.id) {
          this.knows_about[linked_asset.id] = true;
        }
      }

      if (recurse) {
        for (let aspect in asset.aspects) {
          if (asset.aspects[aspect].know_contents_with_parent) {
            this.knowAspect(asset, asset.aspects[aspect], recurse);
          }
        }
      }
    }

    /**
     * Set whether character knows about an aspect of an asset
     * and all of its contents.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#knowAspect
     * @param {*} asset
     * @param {*} aspect
     * @param {Boolean} recurse If true, will make content of aspects known.
     */
    knowAspect(asset, aspect, recurse = true) {
      if ("string" === typeof asset) {
        asset = this.game.getAsset(asset);
      }
      if (!asset || !asset.id) return;

      if ("string" === typeof aspect) {
        aspect = asset.aspects[aspect];
      }
      if (!aspect) return;

      if (aspect.context_id !== asset.id) return;

      if (
        aspect.name === "in" &&
        asset.isDOV("open") &&
        asset.is.closed &&
        asset.appearance.opacity >= 1
      ) {
        return;
      }

      if (aspect.vessel) {
        this.knows_about[aspect.vessel.id] = true;
      }

      var contents = aspect.contents;
      for (let i = 0; i < contents.length; i++) {
        let nested_asset = this.game.getAsset(contents[i]);
        this.knowAsset(nested_asset, recurse);
      }
    }

    /**
     * Ask whether character knows about an asset.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#knowsAbout
     * @param {*} object
     * @returns {Boolean}
     */
    knowsAbout(object) {
      let target, asset, parts, vessel;
      if (object.id) asset = object;
      // just to complicate things, asset can also be a vessel
      if ("string" === typeof object) {
        if (object.includes("|")) {
          // it's a nested object
          parts = object.split("|");
          // error if it has more than 3 parts
          if (parts.length !== 3) return false;
          // error if middle piece is something other than "in"
          if (parts[1] !== "in") return false;
          // error if last piece is something other than vessel
          if (parts[2] !== "vessel") return false;
          asset = this.game.getAsset(parts[0]);
          vessel = asset.getVessel();
        } else asset = this.game.getAsset(object);
      }
      target = vessel ? vessel : asset;
      return true === this.knows_about[target.id] || target.is?.known
        ? true
        : false;
    }

    /**
     * Set whether character has seen an asset.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#see
     * @param {*} asset
     */
    seeAsset(asset, recurse = true) {
      if ("string" === typeof asset) {
        asset = this.game.getAsset(asset);
      }
      if (!asset || !asset.id) return;

      this.has_seen[asset.id] = true;

      if (asset.linked_asset) {
        let linked_asset = this.game.getAsset(asset.linked_asset);
        if (linked_asset && linked_asset.id) {
          this.has_seen[linked_asset.id] = true;
        }
      }

      if (recurse) {
        for (let aspect in asset.aspects) {
          if (aspect.see_contents_with_parent) {
            this.seeAspect(asset, aspect, recurse);
          }
        }
      }
    }

    /**
     * Set whether character has seen an aspect of an asset
     * and all of its contents.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#seeAspect
     * @param {*} asset
     * @param {*} aspect
     * @param {Boolean} recurse If true, will make content of aspects seen.
     */
    seeAspect(asset, aspect, recurse = true) {
      if ("string" === typeof asset) {
        asset = this.game.getAsset(asset);
      }
      if (!asset || !asset.id) return;

      if ("string" === typeof aspect) {
        aspect = asset.aspects[aspect];
      }
      if (!aspect) return;

      if (aspect.context_id !== asset.id) return;

      if (
        aspect.name === "in" &&
        asset.is.closed &&
        asset.appearance.opacity >= 1
      ) {
        return;
      }

      if (aspect.vessel?.see_contents_with_parent) {
        this.has_seen[aspect.vessel.id] = true;
      }
      if (aspect.see_contents_with_parent) {
        var contents = aspect.contents;
        for (let i = 0; i < contents.length; i++) {
          let nested_asset = this.game.getAsset(contents[i]);
          this.seeAsset(nested_asset, recurse);
        }
      }
    }

    /**
     * Ask whether character has seen an asset.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#hasSeen
     * @param {*} object
     * @returns {Boolean}
     */
    hasSeen(object) {
      let target, asset, parts, vessel;
      if (object.id) asset = object;
      // just to complicate things, asset can also be a vessel
      if ("string" === typeof object) {
        if (object.includes("|")) {
          // it's a nested object
          parts = object.split("|");
          // error if it has more than 3 parts
          if (parts.length !== 3) return false;
          // error if middle piece is something other than vessel
          if (parts[1] !== "vessel") return false;
          // error if third piece is something other than "in"
          if (parts[2] !== "in") return false;
          asset = this.game.getAsset(parts[0]);
          vessel = asset.getVessel();
        } else asset = this.game.getAsset(object);
      }
      target = vessel ? vessel : asset;
      return true === this.has_seen[target.id] || target.is?.seen
        ? true
        : false;
    }

    /**
     * Set whether character has been in a room asset.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#been
     * @param {*} room
     */
    visitRoom(room) {
      if ("string" === typeof room) {
        room = this.game.getAsset(room);
      }
      if (!room || !room.id) return;
      this.knowAsset(room, true);
      this.seeAsset(room, true);
      this.has_been[room.id] = true;

      // player recognizes exits
      // @TODO is this handled by knowRecursively?
      var keys = Object.keys(room.exits);
      for (var i = 0; i < keys.length; i++) {
        var exitID = room.exits[keys[i]];
        var exit = room.game.world[exitID];
        if (exit.is.hidden || this.is.blind) {
          /* || TODO dark / no visibility */
        } else {
          this.knowAsset(exit);
          this.seeAsset(exit);
        }
      }
    }

    /**
     * Ask whether character has been to a room.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#hasVisitedRoom
     * @param {*} room
     * @returns {Boolean}
     */
    hasVisitedRoom(room) {
      if ("string" === typeof room) {
        room = this.game.getAsset(room);
      }
      if (!room) return false;
      return true === this.has_been[room.id] ? true : false;
    }

    /**
     * Handle asset initialization for Character.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#initialize
     * @param {Object} game
     * @returns {Boolean}
     */
    initialize(game) {
      super.initialize(game);

      let room = this.getRoomAsset();
      if (room) {
        this.knowAsset(room, true);
        this.seeAsset(room, true);
        this.has_been[room.id] = true;
      }
      let place = this.getPlaceAsset();
      if (place && !place.hasClass("Room")) {
        // set nest to place and move player to room
        this.nest = { [this._place.aspect]: this._place.asset };
        this.place = { in: room.id };
      }
      let nest = this.getNestAsset();
      if (nest && nest.hasClass("Room")) {
        this.place = { in: nest.id };
        this.nest = {};
      }
      return true;
    } // p.initialize

    /**
     * Ask whether character is current player.
     * @memberof adventurejs.Character
     * @method adventurejs.Character#isPlayer
     * @returns {Boolean}
     */
    isPlayer() {
      return this.id === this.game.getPlayer().id;
    }
  }
  adventurejs.Character = Character;
})();