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

  /**
   * @class adventurejs.Verb
   * @param {adventurejs.Game} game A reference to the game instance.
   * @ajsnavheading FrameworkReference
   * @summary Framework class that all verbs are instanced from.
   * @classdesc
   * <p>
   * <strong>Verb</strong> is the base class for all Verb
   * instances. {@link adventurejs} comes with over 100
   * predefined Verbs, each with logic to support
   * all of the predefined {@link adventurejs.Asset|Asset}
   * classes. You can get pretty far using the predefined
   * Verbs and Assets. Naturally, you're going to have
   * ideas that don't fit the predefined logic. adventurejs
   * provides several methods to modify Verbs, which range
   * in complexity from making small edits all the way up
   * to writing new Verbs from scratch.
   * </p>
   * <ul>
   *
   * <li>{@link adventurejs.Dictionary#patchVerb|patchVerb()}
   * lets an author replace only selected properties
   * and methods of any predefined Verb.</li>
   *
   * <li>{@link adventurejs.Dictionary#replaceVerb|replaceVerb()}
   * lets an author completely replace any of the predefined
   * Verbs.</li>
   *
   * <li>{@link adventurejs.Dictionary#combineVerbs|combineVerbs()}
   * lets an author consolidate predefined Verbs. Some of
   * the predefined Verbs exist to catch subtle distinctions
   * that may not be necessary for your game. For instance,
   * <code class="property">twist</code> and
   * <code class="property">turn</code> are predefined
   * as distinct Verbs. If you don't need that level of
   * distinction, you can use combineVerbs to
   * consolidate them.</li>
   *
   * <li>{@link adventurejs.Dictionary#disableVerbs|disableVerbs()}
   * lets an author delete specified Verbs from their game's
   * {@link adventurejs.Dictionary|Dictionary}. Useful when
   * you don't want to support certain Verbs.</li>
   *
   * <li>{@link adventurejs.Dictionary#enableVerbs|enableVerbs()}
   * lets an author re-enable Verbs that have been disabled.</li>
   *
   * <li>{@link adventurejs.Dictionary#disableAllVerbsBut|disableAllVerbsBut()}
   * lets an author disable all but specified Verbs. Useful
   * for creating a game with limited language. </li>
   *
   * <li>{@link adventurejs.Dictionary#createVerb|createVerb()}
   * lets an author create a new Verb from scratch.</li>
   *
   * <li>{@link adventurejs.Dictionary#doVerb|doVerb()}
   * lets an author call a Verb from custom code
   * during runtime.</li>
   *
   * </ul>
   * <h3 class="examples">Examples:</h3>
   * <pre class="display"><code class="language-javascript"><h4>// patchVerb()</h4>
   * MyGame.patchVerb({
   *   name: "crawl",
   *   prettyname: "wriggle", // change prettyname
   * });
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>//replaceVerb()</h4>
   * MyGame.replaceVerb( "xyzzy", {
   *   name: "xyzzy",
   *   prettyname: "xyzzy",
   *   synonyms: [],
   *   do: function( params )
   *   {
   *     console.log( "verbs.do" );
   *     var msg = "Clever xyzzy response!";
   *     if(msg) this.game.print( msg, MyGame.input.output_class );
   *     return params
   *   },
   * });
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// combineVerbs()</h4>
   * MyGame.combineVerbs( [ "twist" ], "turn" );
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// disableVerbs()</h4>
   * MyGame.disableVerbs( [ "lick", "eat" ] );
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// enableVerbs()</h4>
   * MyGame.enableVerbs( [ "lick", "eat" ] );
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// disableAllVerbsBut()</h4>
   * MyGame.disableAllVerbsBut( [ "look", "examine" ] );
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// createVerb()</h4>
   * MyGame.createVerb({
   *   name: "blab_about_noun1",
   *   prettyname: "blab about",
   *   verb_prep_noun: [ "blab about" ], // blab about thing
   *   verb_noun_prep_noun: [ "blab about" ], // blab person about thing
   *   doTry: function( input )
   *   {
   *     [insert logic here]
   *     return true;
   *   },
   *   doSuccess: function( input )
   *   {
   *     [insert logic here]
   *     return true;
   *   },
   * });
   * </code></pre>
   * <br>
   * <pre class="display"><code class="language-javascript"><h4>// doVerb() (call during runtime)</h4>
   * this.game.dictionary.doVerb("xyzzy");
   * </code></pre>
   * <p>
   * For more information about creating Verbs or modifying Verbs, see
   * <a href="/doc/Scripting_VerbSubscriptions.html">Verb Subscriptions</a>,
   * <a href="/doc/Scripting_VerbPhases.html">Verb Phases</a>,
   * <a href="/doc/Scripting_VerbActions.html">Verb Actions</a>,
   * <a href="/doc/Verbs_VerbAnatomy.html">Verb Anatomy</a>,
   * <a href="/doc/Verbs_VerbProcess.html">Verb Process</a>, or
   * <a href="/doc/Verbs_ModifyVerbs.html">Modify Verbs</a>.
   * </p>
   */
  class Verb {
    constructor(game) {
      /**
       * A reference back to the main {@link adventurejs.Game|Game} object.
       * @var {Object} adventurejs.Verb#game
       * @default {}
       */
      this.game = game;

      /**
       * A shortcut to the main {@link adventurejs.Game|Game}
       * {@link adventurejs.Dictionary|Dictionary}.
       * @var {Object} adventurejs.Verb#dictionary
       * @default {}
       */
      this.dictionary = game.dictionary;

      /**
       * May be used to help narrow verb selections in ambiguous situations.
       * @var {String} adventurejs.Verb#type
       * @default ""
       *
       */
      this.type = {
        travel: false,
        manipulation: false,
        direction: false,
        locomotion: false,
      };

      /**
       * Extension verbs may perform some contextual logic before forwarding
       * to another verb for the bulk of logic, such as "crawl" -> "go".
       * @var {String} adventurejs.Verb#extends
       * @default ""
       *
       */
      this.extends = "";

      /**
       * String provided in Verb definition file (aka preverb).
       * @var {String} adventurejs.Verb#name
       * @default ""
       *
       */
      this.name = "";

      /**
       * String provided in verb definition file. The prettyname
       * is used for printing, and can include spaces,
       * ie ask prints as "ask about".
       * @var {String} adventurejs.Verb#prettyname
       *
       */
      this.prettyname = "";

      /**
       * The past tense of the verb. May be used in output strings.
       * @var {String} adventurejs.Verb#past_tense
       *
       */
      this.past_tense = "";

      /**
       * <code>state</code> is an optional property for verbs that apply
       * state to assets, such as close and lock. For example, "close door"
       * will set door.is.closed to true. When used, state will contain the
       * state to be set true on an asset. In the case of close, its state
       * would be "closed".
       * @var {String} adventurejs.Verb#state
       */
      this.state = "";

      /**
       * <code>unstate</code> is an optional property for verbs that unset
       * state from assets, such as open and unlock. For example, "open door"
       * will set door.is.closed to false. When used, unstate will contain the
       * state to be set false on an asset. In the case of open, its unstate
       * would be "closed".
       * @var {String} adventurejs.Verb#unstate
       */
      this.unstate = "";

      /**
       * <code>state_strings</code> is an optional property for verbs that is
       * used to provide string substitutions for authors using the string
       * substitution form of $(sink drain is| plugged or| unplugged).
       * Because "unplugged" isn't a proper verb state, we'll use this as a
       * reverse lookup to test whether the asset, sink_drain in this case,
       * is subscribed to the relevant verb and has the specified state.
       * state_strings only apply to direct objects.
       * @var {String} adventurejs.Verb#state_strings
       */
      this.state_strings = { state: "", unstate: "" };

      /**
       * <code>with_params</code> can contain properties specific to this verb
       * that may be applied to assets which are objects of the verb. For example:
       * the verb <code class="property">plugIn</code> has
       * <code class="property">with_params.max_connections</code>,
       * and in the case of something like a power cable that can be plugged in at
       * both ends, an author could set its
       * <code>asset.dov.plugIn.with_params.max_connections</code>
       * to 2.
       */
      this.with_params = {};

      /**
       * <code>linked_params</code> are params in with_params that are used
       * to store the IDs of assets that share state as a result of being
       * acted upon by this verb. For example: the verb
       * <code class="property">plugIn</code> connects a direct object with
       * an indirect object, and both assets need a reference back to the other.
       */
      this.linked_params = [];

      /**
       * Verb.adjectives are for direction verbs so that, for example,
       * 'south' can be associated with 'southern' and 'southernly'.
       * @var {String} adventurejs.Verb#adjectives
       *
       */
      this.adjectives = [];

      /**
       * <strong>player_must_be</strong> sets conditions that the
       * Player Character must meet in order for the Verb to act.
       * @var {Object} adventurejs.Verb#player_must_be
       * @default {}
       */
      this.player_must_be = {
        not_constrained: true,
        not_on_floor: false,
        not_under: false,
        not_behind: false,
        not_nested_elsewhere: false,
        //not_on_target: false, // used for from/to
        //on_origin: false, // used for from/to
      };

      /**
       * Setting this to true allows you to write your own
       * disambiguation script. Warning: going off road!
       * Recommended for experienced Javascript users.
       * @var {Boolean} adventurejs.Verb#let_verb_handle_disambiguation
       * @default false
       */
      this.let_verb_handle_disambiguation = false;

      /**
       * When input is parsed, parse the verb and then pass the
       * remainder of the input to the verb as a string, for the
       * verb to act on. Chief example is: "oops xxx" where we don't
       * want to parse xxx, we just want to let oops use it as a
       * substitute for last turn's unknown input.
       * @var {Boolean} adventurejs.Verb#let_verb_handle_remaining_input
       * @default false
       */
      this.let_verb_handle_remaining_input = false;

      /**
       * Some types of objects can accept 'in' for 'on'
       * interchangeably, such as 'sit in chair' / 'sit on chair',
       * or 'lie in bed' / 'lie on bed'.
       * @var {Boolean} adventurejs.Verb#in_can_mean_on
       * @default false
       */
      this.in_can_mean_on = false;

      /**
       * Set whether verb is a direction verb.
       * @var {Boolean} adventurejs.Verb#is_direction
       * @default false
       */
      this.is_direction = false;

      /**
       * Set whether direction verb is a compass direction,
       * meaning, it can be found on a compass rose.
       * @var {Boolean} adventurejs.Verb#is_compass_direction
       * @default false
       */
      this.is_compass_direction = false;

      /**
       * Set whether direction verb is a relative direction
       * such as those used on ships: port, starboard, etc.
       * Also applies to left, right, forward, back, etc.
       * @var {Boolean} adventurejs.Verb#is_spatial_direction
       * @default false
       */
      this.is_spatial_direction = false;

      /**
       * Set whether a direction can be referred to with an
       * article, as in "there is a door to the north" vs
       * "there is a door to starboard". This is a bit of mixed
       * purpose because this property doesn't apply to the verb,
       * but is stored in directionLookup for reference with
       * directions.
       * @var {Boolean} adventurejs.Verb#article
       * @default false
       */
      this.article = "";

      /**
       * When player travels, this string may be prepended before
       * the verb name, such as "you walk to the north"
       * @var {Boolean} adventurejs.Verb#direction_preposition
       * @default ""
       */
      this.direction_preposition = "";

      this.phrase1 = new adventurejs.Phrase();
      this.phrase2 = new adventurejs.Phrase();
      this.phrase3 = new adventurejs.Phrase();

      this.accepts_structures = [];

      this.accepts_adverb = false;
      this.accepts_adverbs = [];

      /**
       * To simplify identifying verbs in input,
       * specifically with regards to adverbs & prepositions,
       * we can provide a list of synonyms for the verb.
       * The parser will look for these synonyms in the input
       * and replace them with the verb name. Then, the verb
       * can handle the adverb/preposition as it sees fit.
       * @var {Object} adventurejs.Verb#input_substitutions
       * @default {}
       */
      this.input_substitutions = {};

      /**
       * Provides a simple method for an author to override all failure messages
       * for a verb with one generic string.
       * @var {String} adventurejs.Verb#override_verb_failure_msg
       * @default undefined
       *
       */
      this.override_verb_failure_msg = "";

      /**
       * Provides a simple method for an author to override success messages
       * for a verb with one generic string.
       * @var {String} adventurejs.Verb#override_verb_success_msg
       * @default undefined
       *
       */
      this.override_verb_success_msg = "";

      this.msgNoObject = "";
      this.msgNoAspect = "";

      /**
       * <code>related</code> is an array of related verbs, intended for use
       * with tryToInferIndirectObject. Depending on a game's settings, some
       * verbs may be applied automatically, but only when a user has already
       * applied the verb themselves. For example, an author might not want a
       * door to be opened automatically because the player should bring
       * intention to the action of opening it, in order to appreciate the
       * consequences. tryToInferIndirectObject considers whether a verb has
       * previously been applied to an asset. In some cases, the verb's
       * opposing verb may also be considered. For example, whether the player
       * unlocked a door, or picked it, or locked it themselves, we consider
       * them to have interacted with it.
       * @var {Array} adventurejs.Verb#related
       */
      this.related = [];

      /**
       * <code>enqueue_collections</code> if true allows a verb to
       * unbundle the members of a collection in order to queue up
       * separate actions for each. For example, "gems" is a collection
       * that refers to three unique assets; "diamond", "emerald"
       * and "ruby". If take.enqueue_collections is true, "take gems"
       * will act individually on the diamond, the emerald and the ruby.
       * Only applies to direct object.
       * @var {Array} adventurejs.Verb#enqueue_collections
       * @default false
       */
      this.enqueue_collections = false;

      return this;
    }

    /**
     * Return uppercase name of the verb.
     * @var {Getter} adventurejs.Verb#Name
     * @default []
     */
    get Name() {
      return A.propercase(this.name);
    }

    /**
     * Returns "try[Verb]This" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbThis
     */
    get tryVerbThis() {
      return "try" + this.Name + "This";
    }

    /**
     * Returns "try[Verb]WithThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbWithThis
     */
    get tryVerbWithThis() {
      return "try" + this.Name + "WithThis";
    }

    /**
     * Returns "try[Verb]FromThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbFromThis
     */
    get tryVerbFromThis() {
      return "try" + this.Name + "FromThis";
    }

    /**
     * Returns "try[Verb]ThisWithThat" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbThisWithThat
     */
    get tryVerbThisWithThat() {
      return "try" + this.Name + "ThisWithThat";
    }

    /**
     * Returns "try[Verb]ThatWithThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbThatWithThis
     */
    get tryVerbThatWithThis() {
      return "try" + this.Name + "ThatWithThis";
    }

    /**
     * Returns "try[Verb]ThisFromThat" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbThisFromThat
     */
    get tryVerbThisFromThat() {
      return "try" + this.Name + "ThisFromThat";
    }

    /**
     * Returns "try[Verb]ThatFromThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#tryVerbThatFromThis
     */
    get tryVerbThatFromThis() {
      return "try" + this.Name + "ThatFromThis";
    }

    /**
     * Returns "do[Verb]This" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerb
     */
    get doVerb() {
      return `do${this.Name}`;
    }

    /**
     * Returns "do[Verb]This" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbThis
     */
    get doVerbThis() {
      return `do${this.Name}This`;
    }

    /**
     * Returns "do[Verb]FromThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbFromThis
     */
    get doVerbFromThis() {
      return `do${this.Name}FromThis`;
    }

    /**
     * Returns "do[Verb]WithThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbWithThis
     */
    get doVerbWithThis() {
      return `do${this.Name}WithThis`;
    }

    /**
     * Returns "do[Verb]ThisWithThat" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbThisWithThat
     */
    get doVerbThisWithThat() {
      return `do${this.Name}ThisWithThat`;
    }

    /**
     * Returns "do[Verb]ThatWithThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbThatWithThis
     */
    get doVerbThatWithThis() {
      return `do${this.Name}ThatWithThis`;
    }

    /**
     * Returns "do[Verb]ThisFromThat" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbThisFromThat
     */
    get doVerbThisFromThat() {
      return `do${this.Name}ThisFromThat`;
    }

    /**
     * Returns "do[Verb]ThatFromThis" for consistency with callAction()
     * @var {Getter} adventurejs.Verb#doVerbThatFromThis
     */
    get doVerbThatFromThis() {
      return `do${this.Name}ThatFromThis`;
    }

    /**
     * <strong>synonyms</strong> provide alternate words for verbs,
     * such as "get" for "take".
     * @var {Getter/Setter} adventurejs.Verb#synonyms
     * @default []
     */
    get synonyms() {
      return this._synonyms;
    }
    set synonyms(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._synonyms)) {
        this._synonyms = [];
      }

      // apply new words
      this._synonyms = arr;

      // ensure verb name is in synonyms
      if (-1 === this._synonyms.indexOf(this.name)) {
        this._synonyms.push(this.name);
      }
    }

    /**
     * For phrases like "jump from branch to vine" or
     * "look at sun with glasses", where we have a verb + preposition
     * followed by a noun and then another preposition
     * @var {Array} adventurejs.Verb#verb_prep_noun_prep_noun
     * @default []
     */
    get verb_prep_noun_prep_noun() {
      return this._verb_prep_noun_prep_noun;
    }
    set verb_prep_noun_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (!Array.isArray(this._verb_prep_noun_prep_noun)) {
        this._verb_prep_noun_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_prep_noun_prep_noun.length) {
        for (var i = this._verb_prep_noun_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_prep_noun_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_prep_noun_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_prep_noun_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_prep_noun_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_prep_noun_prep_noun = arr;
    }

    /**
     * For verb/noun pairs with a trailing preposition,
     * or more likely a direction, such as "push bed north".
     *
     * When player input is parsed, they'll be concatenated,
     * eg to "pushnorth bed".
     * @var {Array} adventurejs.Verb#verb_noun_prep
     * @default []
     */
    get verb_noun_prep() {
      return this._verb_noun_prep;
    }
    set verb_noun_prep(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_noun_prep)) {
        this._verb_noun_prep = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_noun_prep.length) {
        for (var i = this._verb_noun_prep.length; i > -1; i--) {
          var pair = [this._verb_noun_prep[i], this.name];
          var index = A.indexOfSubarray(pair, this.dictionary.verb_noun_preps);
          if (-1 !== index) {
            this.dictionary.verb_noun_preps.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_noun_preps.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_noun_prep = arr;
    }

    /**
     * For verb/preposition pairs separated by a space,
     * such as "go to" or "look at".
     *
     * When player input is parsed, they'll be concatenated,
     * eg "go to" to "goTo".
     * @var {Array} adventurejs.Verb#verb_prep_noun
     * @default []
     */
    get verb_prep_noun() {
      return this._verb_prep_noun;
    }
    set verb_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_prep_noun)) {
        this._verb_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_prep_noun.length) {
        for (var i = this._verb_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_prep_noun[i], this.name];
          var index = A.indexOfSubarray(pair, this.dictionary.verb_prep_nouns);
          if (-1 !== index) {
            this.dictionary.verb_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_prep_noun = arr;
    }

    /**
     * For compound preps separated by spaces, verb/prep/prep,
     * such as "get out of"
     * @var {Array} adventurejs.Verb#verb_prep_prep_noun
     * @default []
     */
    get verb_prep_prep_noun() {
      return this._verb_prep_prep_noun;
    }
    set verb_prep_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_prep_prep_noun)) {
        this._verb_prep_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_prep_prep_noun.length) {
        for (var i = this._verb_prep_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_prep_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_prep_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_prep_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_prep_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_prep_prep_noun = arr;
    }

    /**
     * For three part compound preps, verb/prep/prep/prep,
     * such as "get out from behind"
     * @var {Array} adventurejs.Verb#verb_prep_prep_prep_noun
     * @default []
     */
    get verb_prep_prep_prep_noun() {
      return this._verb_prep_prep_prep_noun;
    }
    set verb_prep_prep_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_prep_prep_prep_noun)) {
        this._verb_prep_prep_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_prep_prep_prep_noun.length) {
        for (var i = this._verb_prep_prep_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_prep_prep_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_prep_prep_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_prep_prep_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_prep_prep_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_prep_prep_prep_noun = arr;
    }

    /**
     * For verb/preposition pairs separated by another word,
     * usually a noun,
     * such as "lock door with key" or "take sword from stone".
     * When player input is parsed, they'll be concatenated,
     * eg to "lockwith door key" or "takefrom sword stone".
     * <br><br>
     * Though verb_prep_noun and verb_noun_prep_noun look similar, the reason
     * they are separate fields is because we have to use
     * different regex patterns to find each type in user input.
     * @var {Array} adventurejs.Verb#verb_noun_prep_noun
     * @default []
     */
    get verb_noun_prep_noun() {
      return this._verb_noun_prep;
    }
    set verb_noun_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_noun_prep_noun)) {
        this._verb_noun_prep_noun = [];
      }
      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_noun_prep_noun.length) {
        for (var i = this._verb_noun_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_noun_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_noun_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_noun_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_noun_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_noun_prep_noun = arr;
    }

    /**
     * For a verb phrase with two nouns and two prepositions.
     * For example, in the phrase "take skateboard from under bed",
     * we're looking for "take" and "from" and "under",
     * and we would parse the phrase as "takefromunder skateboard bed"
     * @var {Array} adventurejs.Verb#verb_noun_prep_prep_noun
     * @default []
     */
    get verb_noun_prep_prep_noun() {
      return this._verb_noun_prep;
    }
    set verb_noun_prep_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_noun_prep_prep_noun)) {
        this._verb_noun_prep_prep_noun = [];
      }
      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_noun_prep_prep_noun.length) {
        for (var i = this._verb_noun_prep_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_noun_prep_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_noun_prep_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_noun_prep_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_noun_prep_prep_nouns.push([arr[i], this.name]);
        }
      }

      // apply new words
      this._verb_noun_prep_prep_noun = arr;
    }

    /**
     * For a verb phrase with three nouns and two prepositions.
     * For example, in the phrase "tie boat to pier with rope",
     * we're looking for "tie" and "to" and "with",
     * and we would parse the phrase as "tietowith boat pier rope"
     * @var {Array} adventurejs.Verb#verb_noun_prep_noun_prep_noun
     * @default []
     */
    get verb_noun_prep_noun_prep_noun() {
      return this._verb_noun_prep_noun_prep_noun;
    }
    set verb_noun_prep_noun_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_noun_prep_noun_prep_noun)) {
        this._verb_noun_prep_noun_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_noun_prep_noun_prep_noun.length) {
        for (var i = this._verb_noun_prep_noun_prep_noun.length; i > -1; i--) {
          var pair = [this._verb_noun_prep_noun_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_noun_prep_noun_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_noun_prep_noun_prep_nouns.splice(index, 1);
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_noun_prep_noun_prep_nouns.push([
            arr[i],
            this.name,
          ]);
        }
      }

      // apply new words
      this._verb_noun_prep_noun_prep_noun = arr;
    }

    /**
     * For a verb phrase with three nouns and three prepositions.
     * For example, in the phrase
     * "swing from branch to tree on vine", we're looking for "swing from with on".
     * @var {Array} adventurejs.Verb#verb_prep_noun_prep_noun_prep_noun
     * @default []
     */
    get verb_prep_noun_prep_noun_prep_noun() {
      return this._verb_prep_noun_prep_noun_prep_noun;
    }
    set verb_prep_noun_prep_noun_prep_noun(arr) {
      // don't know how to initialize Object.defineProperty as an array
      if (false === Array.isArray(this._verb_prep_noun_prep_noun_prep_noun)) {
        this._verb_prep_noun_prep_noun_prep_noun = [];
      }

      // remove all prior words from dictionary,
      // in case this verb is being redefined
      if (0 < this._verb_prep_noun_prep_noun_prep_noun.length) {
        for (
          var i = this._verb_prep_noun_prep_noun_prep_noun.length;
          i > -1;
          i--
        ) {
          var pair = [this._verb_prep_noun_prep_noun_prep_noun[i], this.name];
          var index = A.indexOfSubarray(
            pair,
            this.dictionary.verb_prep_noun_prep_noun_prep_nouns
          );
          if (-1 !== index) {
            this.dictionary.verb_prep_noun_prep_noun_prep_nouns.splice(
              index,
              1
            );
          }
        }
      }

      // add new words to lookup
      if (arr.length > 0) {
        for (var i = 0; i < arr.length; i++) {
          this.dictionary.verb_prep_noun_prep_noun_prep_nouns.push([
            arr[i],
            this.name,
          ]);
        }
      }

      // apply new words
      this._verb_prep_noun_prep_noun_prep_noun = arr;
    }

    /**
     * <strong>Verb.do</strong> is a coordinating method that
     * sequences six other submethods in a series. In the case of
     * Verb instances that can act on a collection of
     * {@link adventurejs.Asset|Assets} in a single turn, Verb.do
     * only fires once, but it loops through the Asset collection
     * and calls each submethod for every Asset in the collection.
     * The sequence is:
     * <br><br>
     * do ->
     * <ul>
     * <li><a href="#doBeforeTry">doBeforeTry</a> (hook for authors)</li>
     * <li><a href="#doTry">doTry</a></li>
     * <li><a href="#doAfterTry">doAfterTry</a> (hook for authors)</li>
     * <li><a href="#doBeforeSuccess">doBeforeSuccess</a> (hook for authors)</li>
     * <li><a href="#doSuccess">doSuccess</a></li>
     * <li><a href="#doAfterSuccess">doAfterSuccess</a> (hook for authors)</li>
     * </ul>
     * The two key submethods are Verb.doTry and Verb.doSuccess.
     * For most Verb instances, these two methods contain the bulk
     * of the logic particular to this Verb. Verb.doTry determines
     * whether a Verb can act on an Asset, and if it can't,
     * prints an error message
     * to {@link adventurejs.Display|Display}.
     * Verb.doSuccess applies the Verb to the Asset: updates the game
     * state, assembles dynamic output, and prints the results to Display.
     * <br><br>
     * A Verb instance isn't required to use all of these methods.
     * Some Verbs may bypass Verb.doTry
     * because no special conditions are required to apply the Verb.
     * Some specialized Verbs such as {@link oops} and {@link undo}
     * override Verb.do entirely and don't use any submethods.
     * <br><br>
     * The other four submethods – Verb.doBeforeTry, Verb.doAfterTry,
     * Verb.doBeforeSuccess, and Verb.doAfterSuccess – exist to
     * provide optional hooks for authors to add custom interactions
     * with individual Assets.
     * For more information about Verb Actions and Verb Phases, see
     * <a href="/doc/Scripting_VerbActions.html">Verb Actions</a>
     * and
     * <a href="/doc/Scripting_VerbPhases.html">Verb Phases</a>.
     * <br><br>
     * And so, the first thing Verb.do does is to verify that each
     * method exists on the Verb instance. If the submethod exists,
     * it is called. Each submethod sends a return to Verb.do.
     * <br><br>
     * If the Verb is acting on a collection,
     * a false return means that the Asset currently being acted
     * on has responded in a way that blocks further parsing, and
     * brings this turn to a halt.
     * A null return means that the Asset currently being acted
     * on has concluded its own parsing, but not in such a way as
     * to block further parsing, and Verb.do moves on to the next Asset.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#do
     */
    do() {
      this.game.log("log", "high", "Verb.js > do " + this.name, "Verb");

      var input = this.game.getInput();
      var input_copy;
      var msg = "";

      if (
        -1 < input.verb_chain.indexOf(this.name) &&
        !input.allow_circular_verb
      ) {
        msg = "Error: Circular verb call! See console for more information. ";
        if (msg) this.game.print(msg, "error");
        msg = msg + "Circular verb call:";
        for (var i = 0; i < input.verb_chain.length; i++) {
          msg += "\n - " + input.verb_chain[i];
        }
        this.game.log("warn", "critical", msg, "Verb");
        return false;
      } else {
        input.allow_circular_verb = false;
        input.verb_chain.push(this.name);
      }

      var qualifiedCount = 1;
      if (input.parsedNoun1) {
        qualifiedCount = input.parsedNoun1.matches.qualified.length;
      }

      // "take all" for example will iterate through all available assets
      // because verbs can mutate input, if we're iterating,
      // make a copy of the original input
      // to use as a base for each iteration
      if (qualifiedCount > 1) {
        input_copy = new adventurejs.Input({ game_name: this.game.game_name });
        input_copy = Object.assign(input_copy, input);
      }

      for (var i = 0; i < qualifiedCount; i++) {
        if (i > 0) {
          // push a new input to the input history...
          this.game.parser.input_history.unshift(
            new adventurejs.Input({ game_name: this.game.game_name })
          );
          // and copy the original input onto it
          this.game.parser.input_history[0] = Object.assign(
            this.game.parser.input_history[0],
            input_copy
          );
        }

        let results;
        if (input.parsedNoun1) {
          input.parsedNoun1.matches.qualifiedIndex = i;
        }

        msg = this.name + ".js > doBeforeTry. ";
        this.game.log("log", "high", msg, "Verb");
        results = this.tryPhaseHook("doBeforeTry");
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // advance to next step

        msg = this.name + ".js > handleActions('try'). ";
        this.game.log("log", "high", msg, "Verb");
        results = this.handleActions("try");
        //if ("undefined" !== typeof results) return results;
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object

        msg = this.name + ".js > Try. ";
        this.game.log("log", "high", msg, "Verb");
        results = this.doTry();
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // advance to next step

        msg = this.name + ".js > doAfterTry. ";
        this.game.log("log", "high", msg, "Verb");
        results = this.tryPhaseHook("doAfterTry");
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // advance to next step

        msg = this.name + ".js > doBeforeSuccess. ";
        this.game.log("log", "high", msg, "Verb");
        results = this.tryPhaseHook("doBeforeSuccess");
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // advance to next step

        msg = this.name + ".js > handleActions('do'). ";
        this.game.log("log", "high", msg, "Verb");
        results = this.handleActions("do");
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object

        msg = this.name + ".js > Success. ";
        this.game.log("log", "high", msg, "Verb");
        results = this.doSuccess();
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // advance to next step

        msg = this.name + ".js > doAfterSuccess. ";
        this.game.log("log", "high", msg, "Verb");
        // results = this.doAfterSuccess();
        results = this.tryPhaseHook("doAfterSuccess");
        if (false === results) return false; // end turn
        if (null === results) continue; // advance to next object
        // complete handling for this object
      } // forloop qualifiedCount
    } // do

    /**
     * <strong>doTry</strong> typically contains all the specific
     * logic needed to determine if this Verb can act on the specified
     * {@link adventurejs.Asset|Asset}. (We already applied
     * some general logic supplied by
     * {@link adventurejs.NounMustBe|NounMustBe} before arriving here.)
     * For information about modifying verbs, see
     * <a href="/doc/Verbs_ModifyVerbs.html">Modify Verbs</a>.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#doTry
     */
    doTry() {
      return true;
    }

    /**
     * <strong>handleActions</strong> attempts to call any
     * <a href="Scripting_VerbActions.html">verb actions</a>
     * that match the current assets and sentence structure.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#handleActions
     */
    handleActions(dotry) {
      let input = this.game.getInput();
      let Input_verb = A.propercase(input.input_verb);
      let player = this.game.getPlayer();
      let direct_object = input.getAsset(1);
      let direct_preposition = input.getPreposition(1);
      let Direct_preposition = A.propercase(direct_preposition);
      let indirect_object = input.getAsset(2);
      let indirect_preposition = input.getPreposition(2);
      let Indirect_preposition = A.propercase(indirect_preposition);
      let indirect_object2 = input.getAsset(3);
      let indirect_preposition2 = A.propercase(input.getPreposition(3));
      let Indirect_preposition2 = A.propercase(indirect_preposition2);
      let hook = "";
      let results;
      switch (input.getStructure()) {
        case "verb":
          // ex player.tryTest
          results = player.callAction(`${dotry}${Input_verb}`);
          break;
        case "verb preposition":
          // ex: player.tryTestIn
          results = player.callAction(
            `${dotry}${Input_verb}${Direct_preposition}`
          );
          break;
        case "verb noun":
          // ex: asset.tryTestThis
          results = direct_object.callAction(`${dotry}${Input_verb}This`);
          break;
        case "verb preposition noun":
          // ex: asset1.tryTestToThis asset2
          results = direct_object.callAction(
            `${dotry}${Input_verb}${Direct_preposition}This`
          );
          break;
        case "verb noun noun":
          // ex: asset1.tryTestThisThat asset2
          results = direct_object.callAction(`${dotry}${Input_verb}ThisThat`);
          if ("undefined" !== typeof results) return results;
          // ex: asset2.tryTestThatThis asset2
          results = indirect_object.callAction(`${dotry}${Input_verb}ThatThis`);
          break;
        case "verb noun preposition noun":
          // ex: asset1.tryTestThisToThat asset2
          results = direct_object.callAction(
            `${dotry}${Input_verb}This${Indirect_preposition}That`
          );
          if ("undefined" !== typeof results) return results;
          break;
        case "verb preposition noun preposition noun":
          // ex: asset1.tryTestThisToThat asset2
          results = direct_object.callAction(
            `${dotry}${Input_verb}${Direct_preposition}This${Indirect_preposition}That`
          );
          if ("undefined" !== typeof results) return results;
          break;
        case "verb noun preposition noun preposition noun":
          // ex: asset1.tryTestThisFromThatToThat asset2
          hook = `${dotry}${Input_verb}This${Indirect_preposition}That${Indirect_preposition2}Other`;
          results = direct_object.callAction(hook);
          if ("undefined" !== typeof results) return results;
          break;
        case "verb preposition noun preposition noun preposition noun":
          // ex: asset1.tryTestFromThisToThatWithThat asset2
          hook = `${dotry}${Input_verb}${Direct_preposition}This${Indirect_preposition}That${Indirect_preposition2}Other`;
          results = direct_object.callAction(hook, indirect_object);
          if ("undefined" !== typeof results) return results;
          break;
      }
      return results;
    }

    /**
     * <strong>doSuccess</strong> typically contains all the code
     * needed to apply this Verb to the specified
     * {@link adventurejs.Asset|Asset} once it has successfully
     * passed through all of our conditional logic. doBeforeSuccess
     * and doAfterSuccess are provided so that authors can
     * apply custom success code on an item-by-item basis,
     * but it is also possible to globally modify doSuccess.
     * For information about modifying verbs, see
     * <a href="/doc/Verbs_ModifyVerbs.html">Modify Verbs</a>.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#doSuccess
     */
    doSuccess() {
      return true;
    }

    tryPhaseHook(phase) {
      //console.warn("tryPhaseHook", phase);
      var input = this.game.getInput();

      for (var i = 1; i <= 3; i++) {
        var asset = input.getAsset(i);
        if (!asset) continue;
        if (phase === "doBeforeTry") asset.incrementTryVerbCount(this.name, i);

        let fx;

        // does this asset have a verb phase?
        if (
          (i === 2 || i === 3) &&
          asset.iov[this.name] &&
          "function" === typeof asset.iov[this.name][phase]
        ) {
          fx = asset.iov[this.name][phase];
        } else if (
          (i === 2 || i === 1) &&
          asset.dov[this.name] &&
          "function" === typeof asset.dov[this.name][phase]
        ) {
          fx = asset.dov[this.name][phase];
        }

        // no verb phase?
        if (!fx) continue;

        var results = fx.call(asset, { index: i, verb: this.name });
        if ("undefined" !== typeof results) {
          var msg = `Verb.js > ${asset.name} ${this.name} ${[
            phase,
          ]} returned ${results}. `;
          this.game.log("log", "critical", msg, "Verb");
          return results;
        }
      } // for

      return true;
    }

    /**
     * <strong>tryToInferIndirectObject</strong> is called by some verbs
     * when they receive a direct object with no indirect object, to
     * test whether an indirect object can be inferred. In order to be
     * inferred, indirect object must be in player inventory.
     * If player hasn't already interacted with direct object and
     * game.settings.infer_indirect_objects_only_after_interaction
     * is true, tryToInferIndirectObject will fail regardless of other
     * circumstances.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#tryToInferIndirectObject
     * @param {Object} direct_object
     * @param {Boolean} handle_input If true, updates the global input object
     * per standard specs used by most (but not all) of the verb instances
     * that call this method.
     * @returns {Object}
     */
    tryToInferIndirectObject(direct_object, handle_input) {
      if (
        !this.game.settings.try_to_infer_indirect_objects ||
        !direct_object ||
        !(direct_object instanceof adventurejs.Matter)
      ) {
        return { fail: true };
      }
      var input = this.game.getInput();
      var related_verbs = this.related;
      related_verbs.push(this.name);

      var player = this.game.getPlayer();
      var found;
      var prompt = false;

      // player might have an asset, but has never interacted with this
      // in which case game settings determine whether to infer asset
      if (
        this.game.settings.infer_indirect_objects_only_after_interaction &&
        !direct_object.didDoVerbs(related_verbs)
      ) {
        prompt = true;
      }

      // ok to try to infer asset
      // is player carrying a relevant asset?
      found = player.getIOVkeys(this.name, direct_object);

      // player isn't carrying a tool for this
      if (!found || !found.length) {
        prompt = true;
      }

      if (
        found.length > 1 &&
        this.game.settings.if_inferred_multiple_indirect_objects_then_prompt
      ) {
        prompt = true;
      }

      if (prompt && handle_input) {
        input.setPreposition(2, "with");
        input.setSoftPrompt({
          noun2: true,
          structure: "verb noun preposition noun",
        });
        return { prompt: true };
      }

      if (handle_input) {
        input.setNewPhrase({
          asset: found[0],
          preposition: "with",
        });
        input.setStructure("verb noun preposition noun");
      }
      return { success: true, indirect_object: found[0] };
    }

    /**
     * <strong>tryToPutThisInThatAspect</strong>
     * checks to see if a asset can be placed within
     * the specified aspect of another specified asset.
     * For example, "put sword in stone" and
     * "push stone into depression" would both be
     * tested with this function.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#tryToPutThisInThatAspect
     * @param {Object} direct_object
     * @param {String} preposition
     * @param {Object} indirect_object
     * @returns {Object}
     */
    tryToPutThisInThatAspect(direct_object, preposition, indirect_object) {
      var response = { fail: false, msg: "", status: "", end_turn: false };
      var asset_aspect;
      var msg = "";
      var can = true;

      if (
        !direct_object ||
        !(direct_object instanceof adventurejs.Tangible) ||
        !indirect_object ||
        !(indirect_object instanceof adventurejs.Tangible) ||
        !preposition ||
        !indirect_object.hasAspectAt(preposition)
      ) {
        this.game.debug(
          `F1670 | Verb.js via ${this.name}.js | received bad request `
        );
        if (!indirect_object.hasAspectAt(preposition)) {
          msg += `$(We) can't put anything ${preposition} ${indirect_object.articlename}. `;
        } else {
          msg += this.game.parser.getUnparsedMessage(
            this.game.getInput().input
          );
        }
        response = {
          fail: true,
          msg: msg,
          status: "bad_request",
          end_turn: false,
        };
        return response;
      }

      // is indirect_object closed?
      if (
        "in" === preposition &&
        indirect_object.isDOV("close") &&
        indirect_object.is.closed
      ) {
        // @TODO automatically open the thing, if context allows it
        this.game.debug(
          `F1671 | Verb.js via ${this.name}.js | indirect_object.id.is.closed `
        );
        msg += indirect_object.Articlename + " is closed.";
        this.handleFailure(msg);
        return false;
      }

      // does indirect object aspect limit what assets can be put in it?
      var with_classes = indirect_object.aspects[preposition].with_classes;
      if (with_classes.length > 0) {
        can = false;
        for (var i = 0; i < with_classes.length; i++) {
          var clas = with_classes[i];
          if (direct_object instanceof adventurejs[clas]) {
            can = true;
            break;
          }
        }
        if (!can) {
          this.game.debug(
            `F1661 | Verb.js via ${this.name}.js | ${direct_object.id}.class ${direct_object.class} is not among ${indirect_object.id}.aspects.${preposition}.with_classes `
          );
          msg += `${direct_object.Articlename} can't be placed ${preposition} ${indirect_object.articlename}. `;
          response = {
            fail: true,
            msg: msg,
            status: "with_classes",
            end_turn: false,
          };
          return response;
        }
      }

      // does indirect object aspect limit what classes can be put in it?
      var with_assets = indirect_object.aspects[preposition].with_assets;
      if (with_assets.length > 0) {
        can = false;
        for (var i = 0; i < with_assets.length; i++) {
          if (direct_object.id === with_assets[i]) {
            can = true;
            break;
          }
        }
        if (!can) {
          this.game.debug(
            `F1662 | Verb.js via ${this.name}.js | ${direct_object.id} is not among ${indirect_object.id}.aspects.${preposition}.with_assets `
          );
          msg += `${direct_object.Articlename} can't be placed ${preposition} ${indirect_object.articlename}. `;
          response = {
            fail: true,
            msg: msg,
            status: "with_assets",
            end_turn: false,
          };
          return response;
        }
      }

      asset_aspect = indirect_object.getAspectAt(preposition);

      if (
        asset_aspect.maxsize > -1 &&
        direct_object.dimensions.size > asset_aspect.maxsize
      ) {
        this.game.debug(
          `F1667 | Verb.js via ${this.name}.js | ${direct_object.id}.dimensions.size > ${indirect_object.id}.aspects.${preposition}.maxsize `
        );
        msg += `${direct_object.Articlename} doesn't fit ${preposition} ${indirect_object.articlename}. `;
        response = {
          fail: true,
          msg: msg,
          status: "maxsize",
          end_turn: false,
        };
        return response;
      }

      if (
        asset_aspect.maxweight > -1 &&
        direct_object.dimensions.weight > asset_aspect.maxweight
      ) {
        this.game.debug(
          `F1668 | Verb.js via ${this.name}.js | ${direct_object.id}.dimensions.weight > ${indirect_object.id}.aspects.${preposition}.maxweight `
        );
        msg += `${direct_object.Articlename} is too heavy to ${this.name} ${preposition} ${indirect_object.articlename}. `;
        response = {
          fail: true,
          msg: msg,
          status: "maxweight",
          end_turn: false,
        };
        return response;
      }

      if (
        asset_aspect.maxcount > -1 &&
        asset_aspect.contents.length >= asset_aspect.maxcount
      ) {
        this.game.debug(
          `F1669 | Verb.js via ${this.name}.js | ${indirect_object.id}.aspects.${preposition}.contents.length >= ${indirect_object.id}.aspects.${preposition}.maxcount `
        );
        msg += `Nothing ${asset_aspect.maxcount > 0 ? "more " : ""} can be ${
          this.past_tense
        } ${preposition} ${indirect_object.articlename}. `;
        response = {
          fail: true,
          msg: msg,
          status: "maxcount",
          end_turn: true,
        };
        return response;
      }

      return response;
    }

    /**
     * Unused.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#validate
     */
    validate() {}

    /**
     * If Verb is a direction, <strong>initialize</strong> adds
     * it to game.dictionary.directionLookup.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#initialize
     * @todo How does patchVerb handle initialization?
     */
    initialize() {
      if (!this.prettyname) {
        this.prettyname = this.name;
      }

      // TODO how does patch handle this?
      // if(-1 === this.synonyms.indexOf( this.name ) ) {
      // 	this.synonyms.push( this.name );
      // }

      // TODO how does patch handle this?
      if (this.is_direction) {
        // update lookup
        this.dictionary.directionLookup[this.name] = {
          synonyms: this.synonyms,
          adjectives: this.adjectives,
          article: this.article,
        };
      }

      if (this.state_string) {
        this.game.dictionary.verb_state_lookup[this.state_string] = this.name;
      }
      if (this.unstate_string) {
        this.game.dictionary.verb_state_lookup[this.unstate_string] = this.name;
      }
    }

    /**
     * <strong>enqueueCollection</strong> takes a collection of
     * Assets and enqueues them to game.parser for sequential
     * handling.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#enqueueCollection
     */
    enqueueCollection(object) {
      this.game.log("log", "high", "Verb.js.enqueueCollection(, 'Verb' ) ");

      for (var i = 0; i < object.collection.length; i++) {
        var linefeed = i === object.collection.length ? true : undefined;
        this.game.parser.input_queue.push({
          input: this.name + " " + object.collection[i],
          output_class: "concatenate_output",
          linefeed: linefeed,
        });
      }
      return false;

      // alternately
      //var msg = "You'll have to look in " + object.articlename + " individually.";
      //if(msg) this.game.print( msg, output_class );
      //return null;
    }

    /**
     * Provides a chainable shortcut method for setting a number of properties on the instance.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#set
     * @param {Object} props A generic object containing properties to copy to the DisplayObject instance.
     * @returns {adventurejs.Verb} Returns the instance the method is called on (useful for chaining calls.)
     * @chainable
     */
    set(props) {
      return A.deepSet.call(this.game, props, this);
    }

    /**
     * <strong>handleFailure</strong> prints either a given fail message
     * or a generic fail msg if one is specified.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#handleFailure
     */
    handleFailure(msg) {
      var input = this.game.getInput();
      var noun1 = input.getAsset(1);
      var noun2 = input.getAsset(2);
      var noun3 = input.getAsset(3);
      var results;

      msg = A.getSAF.call(this.game, this.override_verb_failure_msg) || msg;

      if (noun1?.dov[this.name]) {
        if (
          1 >= noun1.DOVdidTryCount(this.name) &&
          noun1.dov[this.name].on_first_failure
        ) {
          results = A.getSAF.call(
            this.game,
            noun1.dov[this.name].on_first_failure,
            noun1
          );
        } else {
          results = A.getSAF.call(
            this.game,
            noun1.dov[this.name].on_failure,
            noun1
          );
        }
        if (results && "string" === typeof results) msg += results;
      }

      if (noun2?.iov[this.name]) {
        if (
          1 >= noun2.IOVdidTryCount(this.name) &&
          noun2.iov[this.name].on_first_failure
        ) {
          results = A.getSAF.call(
            this.game,
            noun2.iov[this.name].on_first_failure,
            noun2
          );
        } else {
          results = A.getSAF.call(
            this.game,
            noun2.iov[this.name].on_failure,
            noun2
          );
        }
        if (results && "string" === typeof results) msg += results;
      } else if (noun2?.dov[this.name]) {
        if (
          1 >= noun2.DOVdidTryCount(this.name) &&
          noun2.dov[this.name].on_first_failure
        ) {
          results = A.getSAF.call(
            this.game,
            noun2.dov[this.name].on_first_failure,
            noun2
          );
        } else {
          results = A.getSAF.call(
            this.game,
            noun2.dov[this.name].on_failure,
            noun2
          );
        }
        if (results && "string" === typeof results) msg += results;
      }

      if (noun3?.iov[this.name]) {
        if (
          1 >= noun3.IOVdidTryCount(this.name, 3) &&
          noun3.iov[this.name].on_first_failure
        ) {
          results = A.getSAF.call(
            this.game,
            noun3.iov[this.name].on_first_failure,
            noun3
          );
        } else {
          results = A.getSAF.call(
            this.game,
            noun3.iov[this.name].on_failure,
            noun3
          );
        }
        if (results && "string" === typeof results) msg += results;
      }

      if (msg) {
        this.game.print(msg, this.game.getInput().output_class);
      }
    }

    /**
     * <strong>handleSuccess</strong> prints the provided
     * success message or a generic one that has been
     * defined by author. It also checks direct and indirect
     * objects for custom verb subscription on_success
     * results and tryDestroy results.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#handleSuccess
     */
    handleSuccess(msg, object) {
      var input = this.game.getInput();
      var noun1 = input.getAsset(1);
      var noun2 = input.getAsset(2);
      var noun3 = input.getAsset(3);
      var results;

      msg = A.getSAF.call(this.game, this.override_verb_success_msg) || msg;

      if (noun1) {
        noun1.incrementDoVerbCount(this.name, 1);
        if (noun1.dov[this.name]) {
          // if( !noun1.DOVdidDo(this.name)
          if (
            1 === noun1.DOVdidDoCount(this.name) &&
            noun1.dov[this.name].on_first_success
          ) {
            results = A.getSAF.call(
              this.game,
              noun1.dov[this.name].on_first_success,
              noun1
            );
          } else {
            results = A.getSAF.call(
              this.game,
              noun1.dov[this.name].on_success,
              noun1
            );
          }
          if (results && "string" === typeof results) msg += results;

          // then disable?
          if (noun1.dov[this.name].then_disable) {
            noun1.dov[this.name].enabled = false;
          }

          // destroy after using?
          results = noun1.tryDestroyDirectObjectAfterUsing(this.name);
          if (results.destroy)
            msg += results.msg
              ? results.msg
              : `${noun1.Articlename} snaps into pieces. `;
        } else {
          // warn because we probably shouldn't have got here without this
          this.game.log(
            "warn",
            0,
            `${noun1.id} is not direct subscribed to ${this.name}`,
            "Verb"
          );
        }
      } // noun1

      if (noun2) {
        noun2.incrementDoVerbCount(this.name, 2);
        // usually noun2 is indirect object
        if (noun2.iov[this.name]) {
          // if( !noun2.IOVdidDo(this.name)
          if (
            1 === noun2.IOVdidDoCount(this.name) &&
            noun2.iov[this.name].on_first_success
          ) {
            results = A.getSAF.call(
              this.game,
              noun2.iov[this.name].on_first_success,
              noun2
            );
          } else {
            results = A.getSAF.call(
              this.game,
              noun2.iov[this.name].on_success,
              noun2
            );
          }
          if (results && "string" === typeof results) msg += results;

          // then disable?
          if (noun2.iov[this.name].then_disable) {
            noun2.iov[this.name].enabled = false;
          }

          // destroy after using?
          results = noun2.tryDestroyIndirectObjectAfterUsing(this.name);
          if (results.destroy)
            msg += results.msg
              ? results.msg
              : `${noun2.Articlename} crumbles to pieces. `;
        }
        // but some verbs, like attach, treat noun1 && noun2 as direct
        else if (noun2.dov[this.name]) {
          // if( !noun2.DOVdidDo(this.name)
          if (
            1 === noun2.DOVdidDoCount(this.name) &&
            noun2.dov[this.name].on_first_success
          ) {
            results = A.getSAF.call(
              this.game,
              noun2.dov[this.name].on_first_success,
              noun2
            );
          } else {
            results = A.getSAF.call(
              this.game,
              noun2.dov[this.name].on_success,
              noun2
            );
          }
          if (results && "string" === typeof results) msg += results;

          // then disable?
          if (noun2.dov[this.name].then_disable) {
            noun2.dov[this.name].enabled = false;
          }

          // destroy after using?
          results = noun2.tryDestroyDirectObjectAfterUsing(this.name);
          if (results.destroy)
            msg += results.msg
              ? results.msg
              : `${noun2.Articlename} crumbles to pieces. `;
        } else {
          // warn because we probably shouldn't have got here without this
          this.game.log(
            "warn",
            0,
            `${noun2.id} is not indirect or direct subscribed to ${this.name}`,
            "Verb"
          );
        }
      } // noun2

      if (noun3) {
        noun3.incrementDoVerbCount(this.name, 3);
        if (noun3.iov[this.name]) {
          // if( !noun3.IOVdidDo(this.name)
          if (
            1 === noun3.IOVdidDoCount(this.name) &&
            noun3.iov[this.name].on_first_success
          ) {
            results = A.getSAF.call(
              this.game,
              noun3.iov[this.name].on_first_success,
              noun3
            );
          } else {
            results = A.getSAF.call(
              this.game,
              noun3.iov[this.name].on_success,
              noun3
            );
          }
          if (results && "string" === typeof results) msg += results;

          // then disable?
          if (noun3.iov[this.name].then_disable) {
            noun3.iov[this.name].enabled = false;
          }

          // destroy after using?
          results = noun3.tryDestroyIndirectObjectAfterUsing(this.name);
          if (results.destroy)
            msg += results.msg
              ? results.msg
              : `${noun3.Articlename} crumbles to pieces. `;
        } else {
          // warn because we probably shouldn't have got here without this
          this.game.log(
            "warn",
            0,
            `${noun3.id} is not indirect subscribed to ${this.name}`,
            "Verb"
          );
        }
      } // noun3

      if (msg) this.game.print(msg, this.game.getInput().output_class);
    } // handleSuccess

    /**
     * Verb can be intransitive if it doesn't require a noun.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#canBeIntransitive
     */
    canBeIntransitive() {
      return !this.phrase1.requires_noun;
    }

    /**
     * Does this verb have state or unstate?
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#hasState
     */
    hasState() {
      return this.state || this.unstate;
    }

    /**
     * Get this verb's state or unstate.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#getState
     */
    getState() {
      return this.state || this.unstate;
    }

    /**
     * Apply this verb's state or unstate to an asset.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#setState
     */
    setState(asset, bool) {
      if (this.getState()) asset.setState(this.getState(), bool);
    }

    /**
     * Connect two assets that share a connection when acted upon by this verb.
     * For example, in the case of 'plug computer into socket',
     * each asset has the other asset's ID saved to its verb subscription like this:
     * <br><br>
     * <code class="property">computer.dov.plugIn.with_params.connections = ['socket']</code>
     * <br>
     * <code class="property">socket.iov.plugIn.with_params.connections = ['computer']</code>
     * <br><br>
     * This is one of two verb subscription properties that are related and very similar,
     * and it's important to understand the distinction between them.
     * <code class="property">...with_assets</code>
     * defines which assets <strong>CAN BE</strong> connected.
     * <code class="property">...with_params.connections</code>
     * stores which assets <strong>ARE</strong> connected.
     * <br><br>
     * <strong>with_assets</strong>:
     * <code class="property">computer.dov.plugIn.with_assets = ['socket']</code>
     * <br>
     * <strong>connections</strong>:
     * <code class="property">computer.dov.plugIn.with_params.connections = ['socket']</code>
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#setVerbSubscriptionConnection
     */
    setVerbSubscriptionConnection(direct_object, indirect_object) {
      //console.warn(this.name,'setVerbSubscriptionConnection',direct_object.id,indirect_object.id);

      if (
        direct_object &&
        direct_object.dov[this.name] &&
        direct_object.dov[this.name].with_params.connections
      ) {
        // some verbs allow direct objects to connect to nothing
        let indirect_value =
          indirect_object && indirect_object.id ? indirect_object.id : null;
        if (
          -1 ===
          direct_object.dov[this.name].with_params.connections.indexOf(
            indirect_value
          )
        ) {
          direct_object.dov[this.name].with_params.connections.push(
            indirect_value
          );
        }
      }

      if (
        indirect_object &&
        indirect_object.iov[this.name] &&
        indirect_object.iov[this.name].with_params.connections
      ) {
        // I don't think indirect objects can connect to nothing, but for completionism
        let direct_value =
          direct_object && direct_object.id ? direct_object.id : null;
        if (
          -1 ===
          indirect_object.iov[this.name].with_params.connections.indexOf(
            direct_value
          )
        ) {
          indirect_object.iov[this.name].with_params.connections.push(
            direct_value
          );
        }
      }
    }

    /**
     * Disconnect two assets that share a connection when acted upon by this verb.
     * For example, in the case of 'plug computer into socket',
     * each asset has the other asset's ID saved to its verb subscription like this:
     * <br><br>
     * <code class="property">computer.dov.plugIn.with_params.connections = ['socket']</code>
     * <br>
     * <code class="property">socket.iov.plugIn.with_params.connections = ['computer']</code>
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#unsetVerbSubscriptionConnection
     */
    unsetVerbSubscriptionConnection(direct_object, indirect_object) {
      if (
        direct_object &&
        direct_object.dov[this.name] &&
        direct_object.dov[this.name].with_params.connections
      ) {
        // some verbs allow direct objects to connect to nothing
        let indirect_value =
          indirect_object && indirect_object.id ? indirect_object.id : null;
        if (
          -1 !==
          direct_object.dov[this.name].with_params.connections.indexOf(
            indirect_value
          )
        ) {
          direct_object.dov[this.name].with_params.connections =
            direct_object.dov[this.name].with_params.connections.filter(
              (item) => item !== indirect_value
            );
        }
      }

      if (
        indirect_object &&
        indirect_object.iov[this.name] &&
        indirect_object.iov[this.name].with_params.connections
      ) {
        // I don't think indirect objects can connect to nothing, but for completionism
        let direct_value =
          direct_object && direct_object.id ? direct_object.id : null;
        if (
          -1 !==
          indirect_object.iov[this.name].with_params.connections.indexOf(
            direct_value
          )
        ) {
          indirect_object.iov[this.name].with_params.connections =
            indirect_object.iov[this.name].with_params.connections.filter(
              (item) => item !== direct_value
            );
        }
      }
    }

    /**
     * Test whether two assets are connected by this verb, for example
     * a rope tied to a tree, or a computer plugged into a socket.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#hasVerbSubscriptionConnection
     */
    hasVerbSubscriptionConnection(direct_object, indirect_object) {
      let dconnect = true;
      let iconnect = true;

      if (
        !direct_object.dov[this.name] ||
        !direct_object.dov[this.name].with_params.connections ||
        -1 ===
          direct_object.dov[this.name].with_params.connections.indexOf(
            indirect_object.id
          )
      ) {
        dconnect = false;
      }

      if (
        !indirect_object.iov[this.name] ||
        !indirect_object.iov[this.name].with_params.connections ||
        -1 ===
          indirect_object.iov[this.name].with_params.connections.indexOf(
            direct_object.id
          )
      ) {
        iconnect = false;
      }

      // we should never have a one-way connection, and if we do, something is broken
      if (dconnect && !iconnect) {
        this.game.log(
          "warn",
          "high",
          `${direct_object.id}.dov.${this.name}.with_params.connections contains ${indirect_object.id} but ${indirect_object.id}.iov${this.name}.with_params.connections does not contain ${direct_object.id}`,
          "Verb"
        );
        return false;
      }
      if (!dconnect && iconnect) {
        this.game.log(
          "warn",
          "high",
          `${indirect_object.id}.iov.${this.name}.with_params.connections contains ${direct_object.id} but ${direct_object.id}.dov${this.name}.with_params.connections does not contain ${indirect_object.id}`,
          "Verb"
        );
        return false;
      }

      return true;
    }

    /* *
     * Test whether player can reach an asset from position on nest asset.
     * @memberOf adventurejs.Verb
     * @method adventurejs.Verb#canPlayerGoThereFromNest
     */
    canPlayerGoThereFromNest(direct_preposition, direct_object) {
      let response = { failure: false, return: null, msg: "" };
      let input = this.game.getInput();
      let player = this.game.getPlayer();
      let nest_asset = player.getNestAsset();
      let nest_preposition = player.getNestPreposition();

      let reachable = true; // default = reachable

      if (this.game.settings.xz_determines_reachability) {
        // is distance between assets greater than jump_length?
        let distance = A.getHorizontalDistance(
          nest_asset.position,
          direct_object.position
        );
        reachable = distance <= this.game.settings.jump_length;
        // @TODO doSuccess handling for this
      }
      if (!reachable) {
        this.game.debug(
          `F1027 | ${this.name}.js | ${player.id} is nested ${nest_preposition} ${nest_asset.id} `
        );
        response.msg += `$(We) can't ${this.name} ${direct_preposition} ${
          direct_object.articlename
        } while ${player.getPostureGerund()} ${nest_preposition} ${
          nest_asset.articlename
        }. `;
        response.failure = true;
        return response;
      }

      return response;
    }
  } // class Verb

  adventurejs.Verb = Verb;
})();