Pre-release
AdventureJS Docs Downloads
Score: 0 Moves: 0
// handleSentence.js

(function () {
  /* global AdventureJS A */

  var p = AdventureJS.Parser.prototype;

  /**
   * Handle multi-word input.
   * @memberOf AdventureJS.Parser
   * @method AdventureJS.Parser#handleSentence
   */
  p.handleSentence = function Parser_handleSentence() {
    const fx = `handleSentence.js`;
    this.game.log(
      "L1198",
      "log",
      "high",
      `[${fx}] handleSentence() begin`,
      "Parser"
    );

    var this_turn = this.input_history[0];
    var last_turn = this.input_history[1];
    var subject = this_turn.getSubject();
    var parsed_verb_name = "",
      dictionary_verb = null,
      dictionary_verb_name = "",
      nouns = [],
      containers = [],
      exclusions = [],
      prepositions = [],
      parsedNouns = [],
      count = { phrase: 0 },
      msg = "";
    const phrase1 = this_turn.verified_sentence.phrase1;

    // --------------------------------------------------
    // parsed one word
    // --------------------------------------------------

    if (this_turn.parsed_word.enabled) {
      // we've arrived here via handleWord and
      // we're probably amending last turn's input
      // so copy that to start with
      this_turn.verified_sentence = A.FX.clone.call(
        this.game,
        last_turn.verified_sentence
      );

      // get verb + nouns from last turn prompt,
      // which may differ from last turn's input
      if (this_turn.parsed_word.verb) {
        this_turn.setVerb(1, this_turn.parsed_word.verb);
      }

      for (let i = 1; i <= 3; i++) {
        if (this_turn.parsed_word[`noun${i}`]) {
          // @TODO re review this - I've gone back and forth between setNoun and setAsset
          // the thing is that the word received may not have been parsed into a full name
          // which breaks setAsset
          // haven't we already derived a parsednoun from this word in an earlier step?
          this_turn.setNoun(i, this_turn.parsed_word[`noun${i}`]);
          //this_turn.setAsset(i, this_turn.parsed_word[`noun${i}`]);
        }
        if (this_turn.parsed_word[`parsedNoun${i}`]) {
          this_turn.setParsedNoun(i, this_turn.parsed_word[`parsedNoun${i}`]);
        }
        if (this_turn.parsed_word[`container${i}`]) {
          this_turn.setContainer(i, this_turn.parsed_word[`container${i}`]);
        }
        if (this_turn.parsed_word[`preposition${i}`]) {
          this_turn.setPreposition(i, this_turn.parsed_word[`preposition${i}`]);
        }
      }
    } // parsed_word

    // --------------------------------------------------
    // get verb and nouns
    // --------------------------------------------------

    if (this_turn.hasInput()) {
      // we've got fresh input from parseInput
      if (this_turn.hasVerb()) {
        parsed_verb_name = this_turn.getVerb();
      }

      for (let i = 1; i <= 3; i++) {
        if (this_turn.hasPhrase(i)) {
          prepositions[i] = this_turn.getPreposition(i);
          nouns[i] = this_turn.getNoun(i);
          containers[i] = this_turn.getContainer(i);
          exclusions[i] = this_turn.getExclusion(i);
        }
      }
    } else {
      // we shouldn't arrive here but may need an error message
      this.game.debug(`D1207`, `${fx} `, ` didn't receive words `);
      msg += this.game.settings.getUnparsedMessage(this_turn.input);
      this.game.print(msg, this_turn.output_class);
    }

    // --------------------------------------------------
    // oops
    // --------------------------------------------------

    if ("oops " === this_turn.input.substring(0, 5)) {
      // this.game.display.printInput(this_turn.input);
      return this.game.dictionary.doVerb("oops");
    }

    // --------------------------------------------------
    // verify verb
    // --------------------------------------------------

    if (!parsed_verb_name || !this.dictionary.verbs[parsed_verb_name]) {
      // if no verb, did we get a two-word response to a soft prompt?
      if (
        this_turn.verified_sentence_structure === "preposition noun" &&
        last_turn.soft_prompt.enabled
      ) {
        for (let i = 1; i <= 3; i++) {
          if (
            last_turn.soft_prompt[`noun${i}`] &&
            last_turn.soft_prompt[`preposition${i}`]
          ) {
            let new_phrase = Object.assign(
              {},
              this_turn.verified_sentence.phrase1
            );
            this_turn.verified_sentence = A.FX.clone.call(
              this.game,
              last_turn.verified_sentence
            );
            this_turn.setPhrase(i, new_phrase);
            this_turn.verified_sentence_structure =
              last_turn.soft_prompt.structure ||
              last_turn.verified_sentence_structure;
            parsed_verb_name = this_turn.getVerb();
          }
        }
      }
    }

    // check for a verb again
    if (!parsed_verb_name || !this.dictionary.verbs[parsed_verb_name]) {
      let err = `parser.handleSentence > Verb not found. This may happen if a compound phrase is improperly found. Original input: ${this_turn.input} Parsed input: ${this_turn.parsed_input}`;
      this.game.log("L1199", "warn", "high", err, "Parser");
      this.game.debug(
        `D1721`,
        `${fx} `,
        ` verb not found. Check the console for more info. `
      );
      msg += this.game.settings.getUnparsedMessage(this_turn.input);
      this.game.print(msg, this_turn.output_class);
      return false;
    }

    // if verb.let_verb_handle_remaining_input, bypass handleSentence
    // originally used for oops and left in for future cases

    if (
      this.dictionary.verbs[parsed_verb_name].let_verb_handle_remaining_input
    ) {
      this.dictionary.doVerb(parsed_verb_name);
      return false;
    }

    // --------------------------------------------------
    // qualify verb
    // --------------------------------------------------

    // Check verb against circumstances that prevent its use,
    // such as if player is constrained, lying on the floor, etc.

    dictionary_verb = this.qualifyParsedVerb({
      parsed_verb_name: parsed_verb_name,
    });
    // If qualifyParsedVerb returned false, then it already
    // printed an error msg to the player, so we can just...
    if (!dictionary_verb) return false;

    // --------------------------------------------------
    // get phrase count
    // --------------------------------------------------

    for (const key in this_turn.verified_sentence) {
      if (key.startsWith("phrase")) {
        count.phrase++;
      }
    }

    // ==================================================
    // for each phrase
    // this is the first of two passes
    // ==================================================

    for (let n = 1; n <= count.phrase; n++) {
      const phrase = this_turn.verified_sentence[`phrase${n}`];
      let noun = phrase.noun;
      let preposition = phrase.preposition;
      let exclusion = phrase.exclusion;
      let noun_is_plural = false;
      let noun_is_all = false;

      this.game.log(
        "L1200",
        "log",
        "high",
        `[${fx}] handle phrase ${n}`,
        "Parser"
      );

      // this shouldn't happen
      if (!noun && !preposition) {
        this.game.debug(
          `D1034`,
          `${fx} `,
          ` ${dictionary_verb.name} didn't receive a noun or preposition`
        );
        msg += `How did {we} want to ${this_turn.input}? `;
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // unsupported noun?
      // --------------------------------------------------

      if (noun && !dictionary_verb[`phrase${n}`].accepts_noun) {
        this.game.debug(
          `D2018`,
          `${fx} `,
          ` ${dictionary_verb.name} received a noun it can't handle`
        );
        msg += this.game.settings.getUnparsedMessage(this_turn.input);
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // unsupported preposition without noun?
      // --------------------------------------------------

      if (
        preposition &&
        !noun &&
        !dictionary_verb[`phrase${n}`].accepts_preposition_without_noun
      ) {
        this.game.debug(
          `D1033`,
          `${fx} `,
          ` ${dictionary_verb.name} received preposition without noun, soft prompt noun${n}`
        );
        msg += `What would {we} like to ${this_turn.input}? `;
        this_turn.setSoftPrompt({
          index: n,
          type: "noun",
          [`noun${n}`]: true,
          verb: this_turn.input_verb,
          verb_phrase: this_turn.verb_phrase,
        });
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // noun
      // --------------------------------------------------

      if (noun) {
        // --------------------------------------------------
        // noun and global_string
        // --------------------------------------------------

        if (noun === "global_string") {
          // if there's a string, ensure that we have the value
          // it should be on global_string.values
          // but if we're following up on a soft prompt,
          // global_string.values will have been reset
          // and we'll have to get it from this_turn.strings
          // or this_turn.verified_sentence.phraseX.strings
          // which will have been copied from last turn
          const authoritative = this.game.world.global_string.values.length
            ? this.game.world.global_string.values
            : phrase.strings?.length
              ? phrase.strings
              : this_turn.strings?.length
                ? this_turn.strings
                : [];
          phrase.strings =
            this.game.world.global_string.values =
            this_turn.strings =
              authoritative;
        }
        // similarly...
        if (noun === "global_number") {
          const authoritative = this.game.world.global_number.values.length
            ? this.game.world.global_number.values
            : phrase.strings?.length
              ? phrase.strings
              : this_turn.strings?.length
                ? this_turn.strings
                : [];
          phrase.strings =
            this.game.world.global_number.values =
            this_turn.strings =
              authoritative;
        }

        // --------------------------------------------------
        // plural
        // --------------------------------------------------

        // "this and that" is parsed as "this&that"
        let split_noun = noun.split("&");

        // "all keys" might be parsed as "key=keya,keyb,keyc"
        // we deliberately do not handle that here as
        // it conflicts with a later disambiguation handler

        noun_is_plural = "all" === noun || split_noun.length > 1;
        noun_is_all = "all" === noun;

        if (
          noun_is_plural &&
          !dictionary_verb[`phrase${n}`].accepts_plural_noun
        ) {
          this.game.debug(
            `D1032`,
            `${fx} `,
            ` ${dictionary_verb.name}.phrase${n}.accepts_plural_noun is false`
          );
          msg += `{We} can't ${dictionary_verb.prettyname} more than one thing at a time. `;
          this.game.print(msg, this_turn.output_class);
          return false;
        }

        // Split noun string and queue verb-noun inputs for each item
        if (split_noun.length > 1) {
          for (let i = 1; i < split_noun.length; i++) {
            this.input_queue.push({
              input: this_turn.parsed_input.replace(noun, split_noun[i]),
              printInput: false,
            });
          }
          noun = split_noun[0];
        } // plural

        // write a parsedNoun back to the phrase
        // it's possible that one was already set by a disambiguation
        // so check for existing
        if (!phrase.parsedNoun) {
          let context = null;
          // if (noun === "all") {
          if (noun_is_plural) {
            // if player is putting/dropping/giving all,
            // set context to player's inventory
            if (["put", "drop", "give"].includes(dictionary_verb.name)) {
              context = subject;
            }
          }
          phrase.parsedNoun = this.parseNoun(noun, context);
        }
      } // noun

      // --------------------------------------------------
      // unsupported noun without preposition?
      // --------------------------------------------------

      if (
        noun &&
        !preposition &&
        dictionary_verb[`phrase${n}`].requires_preposition
      ) {
        // console.warn(`verified_sentence`, this_turn.verified_sentence);
        this.game.debug(
          `D1041`,
          `${fx} `,
          ` ${dictionary_verb.name} received noun without preposition, soft prompt preposition${n}`
        );
        // msg += `How did {we} want to ${this_turn.input_verb} ${phrase.noun}? `;
        switch (n) {
          case 1:
            msg += `Where in relation to ${
              this.game.getAsset(
                this_turn.verified_sentence.phrase1.parsedNoun.asset_id
              ).article_name
            } did {we} want to ${dictionary_verb.prettyname}? `;
            break;
          case 2:
            msg += `Where in relation to ${
              this.game.getAsset(
                this_turn.verified_sentence.phrase2.parsedNoun.asset_id
              ).article_name
            } did {we} want to ${dictionary_verb.prettyname} ${
              this.game.getAsset(
                this_turn.verified_sentence.phrase1.parsedNoun.asset_id
              ).article_name
            }? `;
            break;
          case 3:
            msg += `That seems to be missing a preposition. Can {we} try phrasing that another way? `;
            break;
        }

        this_turn.setSoftPrompt({
          index: 2,
          type: "preposition",
          [`preposition${n}`]: true,
          verb: this_turn.input_verb,
          verb_phrase: this_turn.verb_phrase,
        });
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // unsupported preposition?
      // --------------------------------------------------

      if (
        preposition &&
        dictionary_verb[`phrase${n}`].preposition_must_be.length &&
        -1 ===
          dictionary_verb[`phrase${n}`].preposition_must_be.indexOf(preposition)
      ) {
        this.game.debug(
          `D1222`,
          `${fx} `,
          ` ${
            dictionary_verb.name
          }.phrase${n}.preposition_must_be: ${dictionary_verb[
            `phrase${n}`
          ].preposition_must_be.join(", ")}`
        );
        let asset = this.game.getAsset(
          this_turn.verified_sentence[`phrase${n}`].parsedNoun.asset_id
        );
        msg += `{We} {don't} seem to know how to ${
          dictionary_verb.name
        } anything ${preposition} ${
          asset && this.game.getPlayer().knowsAbout(asset)
            ? asset.article_name
            : noun
              ? noun
              : ""
        }. `;
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // unknown noun?
      // --------------------------------------------------

      if (phrase.parsedNoun && !phrase.parsedNoun.matches.all.length) {
        this.game.log(
          "L1201",
          "log",
          "high",
          [
            `[${fx}] input: ${this_turn.input}, phrase${n}.parsedNoun: `,
            phrase.parsedNoun,
          ],
          "Parser"
        );

        // save record for "oops"
        this_turn.unknown_word = noun;

        this.game.debug(`D1035`, `${fx} `, ` ${noun} isn't recognized `);

        // can we return the original string?
        if (this_turn.tokens[noun]) {
          msg += `{We} {don't} know of any ${this_turn.tokens[noun].source}. `;
        }
        // does the unrecognized input match a class name?
        else if (
          AdventureJS[A.FX.propercase(noun)] ||
          AdventureJS.Assets[A.FX.propercase(noun)]
        ) {
          msg += `{We} {don't} know of any ${noun}. `;
        }
        // as last resort use response_to_no_response
        else {
          msg += this.game.settings.getUnparsedMessage(
            this_turn.normalized_input
          );
        }

        this.game.print(msg, this_turn.output_class);
        return false;
      } // parsedNoun

      this.game.log(
        "L1202",
        "log",
        "high",
        [`[${fx}] phrase${n}.parsedNoun:`, phrase.parsedNoun.matches.qualified],
        "Parser"
      );

      // --------------------------------------------------
      // verb phrase accepts parsedNoun?
      // is this redundant to D2018?
      // --------------------------------------------------

      if (phrase.parsedNoun && !dictionary_verb[`phrase${n}`].accepts_noun) {
        this.game.debug(
          `D1036`,
          `${fx} `,
          ` ${dictionary_verb.name} received a noun it can't handle`
        );
        msg += this.game.settings.getUnparsedMessage(this_turn.input);
        this.game.print(msg, this_turn.output_class);
        return false;
      }

      // --------------------------------------------------
      // in means on?
      // --------------------------------------------------

      if (
        phrase.parsedNoun &&
        "in" === preposition &&
        this.game.getAsset(phrase.parsedNoun.asset_id).quirks.in_means_on &&
        dictionary_verb.in_can_mean_on
      ) {
        // Replace 'in' with 'on' where applicable such as 'sit in chair'
        this_turn.setInPhrase(n, "preposition", "on");
      }

      // --------------------------------------------------
      // exclusion (aka but)
      // supported syntaxes:
      //  "take all but green gem"
      //  "take all gems but green gem"
      //  "take all gems but green gem and blue gem"
      //  "take all but gems from table"
      //  "take all from table but gems"
      // --------------------------------------------------

      if (exclusion && "string" === typeof exclusion) {
        const exclusions = exclusion.split("&");

        // if this isn't phrase1, move exclusions there
        // using this target_phrase var because it seems
        // like we might need to make this more flexible
        const target_phrase = phrase1; // n > 1 ? this_turn.getPhrase(1) : phrase;

        for (let x = 0; x < exclusions.length; x++) {
          const excluded_noun = exclusions[x];
          const excluded_parsed_noun = this.parseNoun(excluded_noun);

          // copy exclusions to phrase1 if necessary
          if ("undefined" === typeof target_phrase.exclusions)
            target_phrase.exclusions = [];
          if (!target_phrase.exclusions.includes(excluded_noun))
            target_phrase.exclusions.push(excluded_noun);

          this.game.log(
            "L1203",
            "log",
            "high",
            [
              `[${fx}] phrase${n} excludes:`,
              excluded_parsed_noun.matches.qualified,
            ],
            "Parser"
          );

          if (excluded_parsed_noun) {
            for (
              let exp = 0;
              exp < excluded_parsed_noun.matches.all.length;
              exp++
            ) {
              let excluded_id = excluded_parsed_noun.matches.all[exp];
              // remove excluded_id from parsedNoun.matches.all
              if (target_phrase.parsedNoun.matches.all.includes(excluded_id)) {
                target_phrase.parsedNoun.matches.all.splice(
                  target_phrase.parsedNoun.matches.all.indexOf(excluded_id),
                  1
                );
                // remove excluded_id from parsedNoun.matches.qualified
                target_phrase.parsedNoun.matches.qualified.splice(
                  target_phrase.parsedNoun.matches.qualified.indexOf(
                    excluded_id
                  ),
                  1
                );
                // remove excluded_id from parsedNoun.is_unambiguous
                // @NOTE I think this is the only place we compare
                // is_unambiguous, which is a precursor to is_qualified
                if (target_phrase.parsedNoun.is_unambiguous === excluded_id)
                  target_phrase.parsedNoun.is_unambiguous = "";
              } // parsedNoun includes excluded
            } // for excluded_parsed_noun.matches.all
          } // excluded_parsed_noun
        } // for exclusions

        this.game.log(
          "L1204",
          "log",
          "high",
          [`[${fx}] removed phrase${n} exclusions from phrase1: `, exclusions],
          "Parser"
        );
      } // exclusion

      // --------------------------------------------------
      // exclude noun from first phrase's qualified assets
      // --------------------------------------------------

      if (n > 1) {
        //let phrase1 = this_turn.getPhrase(1);
        let last_split_noun = [];
        last_split_noun = phrase1.noun && phrase1.noun.split("&");
        let last_noun_is_plural =
          phrase1.noun === "all" || last_split_noun.length > 1;
        let exclude_from_plural =
          dictionary_verb[`phrase${n}`].not_in_prior_plural;
        if (last_noun_is_plural && exclude_from_plural) {
          // was there an unambiguous match for this?
          if (phrase.parsedNoun.is_unambiguous) {
            // remove unambiguous from prior noun's matches
            let excluded_id = phrase.parsedNoun.is_unambiguous;
            let allindex = phrase1.parsedNoun.matches.all.indexOf(excluded_id);
            let qualindex =
              phrase1.parsedNoun.matches.qualified.indexOf(excluded_id);
            // remove excluded_id from parsedNoun.matches.all
            phrase1.parsedNoun.matches.all.splice(allindex, 1);
            // remove excluded_id from parsedNoun.matches.qualified
            phrase1.parsedNoun.matches.qualified.splice(qualindex, 1);
            // remove excluded_id from parsedNoun.is_unambiguous
            if (phrase1.parsedNoun.is_unambiguous === excluded_id)
              phrase1.parsedNoun.is_unambiguous = "";
            if (allindex > -1 || qualindex > -1) {
              // we might need to re-qualify the last noun
              if (phrase1.parsedNoun) {
                phrase1.parsedNoun = this.qualifyParsedNoun({
                  parsedNoun: phrase1.parsedNoun,
                  parsedVerb: dictionary_verb.name,
                  nounIndex: n - 1,
                });
                // If qualifyParsedNoun returned false, then it already
                // printed an error msg to the player, so we can just...
                if (!phrase.parsedNoun) return false;
              }
            }
          }
        }

        // --------------------------------------------------
        // remove iobj from dobj collection (may be redundant)
        // --------------------------------------------------
        if (
          phrase.parsedNoun &&
          phrase.parsedNoun.asset_id &&
          phrase1.parsedNoun.asset_id &&
          phrase1.parsedNoun.matches.qualified.length > 1
        ) {
          for (
            var i = phrase1.parsedNoun.matches.qualified.length - 1;
            i > -1;
            i--
          ) {
            if (
              phrase.parsedNoun.is_qualified ===
              phrase1.parsedNoun.matches.qualified[i]
            ) {
              phrase1.parsedNoun.matches.qualified.splice(i, 1);
            }
          }
        } // remove iobj from dobj collection
      } // n>1 / exclude from prior

      // --------------------------------------------------
      // qualify noun
      // --------------------------------------------------

      let context = null;

      // if (phrase.noun === "all") {
      if (noun_is_plural) {
        if (["put", "drop", "give"].includes(dictionary_verb.name)) {
          // if player is putting/dropping/giving all,
          // set context to player's inventory
          context = subject;
        }

        if (["take"].includes(dictionary_verb.name)) {
          // if player input "take all" we can handle that as is
          // but if they input "take all from thing" we don't yet
          // have the context of thing, so we'll have to do a
          // followup pass after we've handled phrase2
        }
      }

      if (phrase.parsedNoun) {
        phrase.parsedNoun = this.qualifyParsedNoun({
          parsedNoun: phrase.parsedNoun,
          parsedVerb: dictionary_verb.name,
          nounIndex: n,
          context: context,
        });
        // If qualifyParsedNoun returned false, then it already
        // printed an error msg to the player, so we can just...
        if (!phrase.parsedNoun) return false;
      }

      // --------------------------------------------------
      // qualify possessive
      // --------------------------------------------------

      if (phrase.possessive) {
        const asset = this.game.getAsset(phrase.parsedNoun.asset_id);

        if (!phrase.possessive.has(asset)) {
          console.warn(`phrase.possessive`, phrase.possessive);
          let err = `parser.handleSentence > ${phrase.possessive.id} doesn't contain ${asset.id}`;
          this.game.log("L1569", "warn", "high", err, "Parser");
          this.game.debug(`D1634`, `${fx} `, `${err}`);
          let player = this.game.getPlayer();
          let name = "";
          if (phrase.possessive.id === player.id) {
            name = A.FX.sentencecase(player.inflect("we"));
          }
          msg += `${name || phrase.possessive.proper_name || phrase.possessive.name} ${phrase.possessive.inflect("don't")} appear to be carrying any ${asset.name}. `;
          this.game.print(msg, this_turn.output_class);
          return false;
        }
      } // phrase.possessive

      // --------------------------------------------------
      // redirectVerb
      // --------------------------------------------------

      // Has author used redirectVerb for this verb on direct_object?
      if (n === 1 && phrase.parsedNoun && phrase.parsedNoun.is_qualified) {
        let direct_object = this.game.getAsset(phrase.parsedNoun.is_qualified);
        if (direct_object.redirected_verbs[dictionary_verb.name]) {
          // requalify verb
          dictionary_verb = this.qualifyParsedVerb({
            parsed_verb_name:
              direct_object.redirected_verbs[dictionary_verb_name],
          });
          if (!dictionary_verb) return false;
        }
      } // redirectVerb
    } // for each phrase

    // ==================================================
    // END first pass for each phrase
    // BEGIN second pass cleanup
    // ==================================================

    // --------------------------------------------------
    // handle "take all from asset"
    // we'll have already set
    // phrase1.parsedNoun.matches.qualified
    // in the first loop, and now we need
    // to reset it to asset scope.
    // --------------------------------------------------
    // let phrase1 = this_turn.verified_sentence[`phrase1`];
    const phrase2 = this_turn.verified_sentence[`phrase2`];
    // if (phrase1?.parsedNoun?.input === "all" && phrase2?.noun) {
    if (phrase1?.parsedNoun?.is_plural && phrase2?.noun) {
      let context,
        context_contents,
        shift_exclusions = null;

      // does phrase2 have exclusions?
      // if so those should apply to phrase 1
      console.warn(`phrase2.exclusions`, phrase2.exclusions);
      if (phrase2.exclusions?.length) {
        console.warn(`phrase 2 has exclusions`);
        // phrase2 exclusions probably belong with phrase 1
        if ("undefined" === typeof phrase1.exclusions) phrase1.exclusions = [];
        // phrase1.exclusions = phrase1.exclusions.concat(phrase2.exclusions);
        for (let x = 0; x < phrase2.exclusions.length; x++) {
          if (!phrase1.exclusions.includes(phrase2.exclusions[x]))
            phrase1.exclusions.push(phrase2.exclusions[x]);
        }
        // phrase2.exclusion = "";
        phrase2.exclusions = [];
        // shift_exclusions = true;
      }

      if (["put", "give"].includes(dictionary_verb.name) && phrase2.noun) {
        context = subject;

        // exclude phrase 2 target asset from phrase1 qualified list
        if (phrase1.parsedNoun.matches.qualified.includes(context.id)) {
          let index = phrase1.parsedNoun.matches.qualified.indexOf(context.id);
          phrase1.parsedNoun.matches.qualified.splice(index, 1);
          if (phrase1.parsedNoun.matches.qindex >= index) {
            phrase1.parsedNoun.matches.qindex = index - 1;
          }
        }
      }

      // take / get
      if (
        ["take"].includes(dictionary_verb.name) &&
        phrase2.preposition === "from"
      ) {
        context = this.game.getAsset(phrase2.parsedNoun.asset_id);
      }

      if (context) {
        context_contents = context.getAllContents(); // getAllNestedContents ?

        // did we have an exclusion?
        // something like: "take all but a from b"
        if (phrase1.exclusions?.length) {
          // we already split .exclusion into .exclusions in the first pass
          const exclusions = phrase1.exclusions;

          for (let x = 0; x < exclusions.length; x++) {
            const excluded_noun = exclusions[x];
            const excluded_parsed_noun = this.parseNoun(excluded_noun, context);

            this.game.log(
              "L1052",
              "log",
              "high",
              ["[${fx}] exclude:", excluded_parsed_noun.matches.qualified],
              "Parser"
            );

            if (excluded_parsed_noun) {
              for (
                let exp = 0;
                exp < excluded_parsed_noun.matches.qualified.length;
                exp++
              ) {
                let excluded_id = excluded_parsed_noun.matches.qualified[exp];
                if (context_contents.includes(excluded_id)) {
                  context_contents.splice(
                    context_contents.indexOf(excluded_id),
                    1
                  );
                }
              }
            }
          }
        }

        if (phrase1.parsedNoun.is_all) {
          // if player asked for all
          // set matches.qualified to contents of phrase2 asset
          phrase1.parsedNoun.matches.qualified = context_contents;

          // reset is_qualified in case it was set by the first loop
          phrase1.parsedNoun.is_qualified = "";

          // requalify the list
          phrase1.parsedNoun = this.qualifyParsedNoun({
            parsedNoun: phrase1.parsedNoun,
            parsedVerb: dictionary_verb.name,
            nounIndex: 1,
            context: context,
          });

          // if no parsedNoun, qualifyParsedNoun
          // will have already printed an error, so...
          if (!phrase1.parsedNoun) return false;
        } else {
          // player asked for something like "all keys"
          // and phrase1.parsedNoun.matches.qualified
          // has already been parsed for keys but now
          // needs to remove anything not in context

          for (
            let q = phrase1.parsedNoun.matches.qualified.length - 1;
            q > -1;
            q--
          ) {
            if (
              !context_contents.includes(
                phrase1.parsedNoun.matches.qualified[q]
              )
            ) {
              phrase1.parsedNoun.matches.qualified.splice(q, 1);
              if (phrase1.parsedNoun.qindex === q)
                phrase1.parsedNoun.qindex = "";
              if (phrase1.parsedNoun.qindex && phrase1.parsedNoun.qindex > q) {
                phrase1.parsedNoun.qindex--;
              }
            }
          }
          if (phrase1.parsedNoun.matches.qualified.length === 0) {
            this.game.debug(
              `D1515`,
              `${fx} `,
              ` parser.handleSentence() eliminated all possibilities`
            );
            let msg = `{We} don't see any ${phrase1.parsedNoun.input} ${context.default_aspect} ${context.article_name}. `;
            this.game.print(msg, this_turn.output_class);
            return false;
          }
        }
      }
    }

    // --------------------------------------------------
    // each phrase redux
    // --------------------------------------------------

    // reiterating to account for changes made during the first pass
    for (let n = 1; n <= count.phrase; n++) {
      var phrase = this_turn.verified_sentence[`phrase${n}`];
      var next_phrase = this_turn.verified_sentence[`phrase${n + 1}`];
      var parsedNoun = phrase.parsedNoun;
      if (!parsedNoun) continue;

      this.game.log(
        "L1205",
        "log",
        "high",
        `[${fx}] final check on phrase ${n}`,
        "Parser"
      );

      if (parsedNoun.is_substance) {
        phrase["specified_substance"] = parsedNoun.is_substance;
      }

      // --------------------------------------------------
      // multiple assets found?
      // --------------------------------------------------

      // special test for substances
      // If input referred to a substance, we wrote the substance id to
      // parsedNoun.is_unambiguous but parsedNoun.matches.qualified
      // may contain multiple substance containers.
      // This is something like "take water" returning three glasses.
      // We have no handling for multiple instances of substances
      // because subatances are singular.

      if (
        (parsedNoun.matches.qualified.length > 1 &&
          !parsedNoun.is_unambiguous) ||
        (parsedNoun.matches.qualified.length > 1 &&
          parsedNoun.is_unambiguous &&
          parsedNoun.is_substance)
      ) {
        var is_plural = false;

        // check input word against world_lookup which may return an object
        // with multiple asset IDs, for example "take silverware" may return:
        // {
        //   IDs: (3) ['knife', 'fork', 'spoon']
        //   singular: "key"
        //   type: "plural"
        // }
        var lookup = this.game.world_lookup[parsedNoun.normalized_input];

        // if lookup returned a list, we're interpreting player's input
        // as referring to plural assets as in our "take silverware" example
        if (lookup) {
          if ("plural" === lookup.type || "group" === lookup.type) {
            is_plural = true;
          }
        }

        // alternately if there's an "&" in the original input,
        // it means player explicitly referenced multiple objects,
        // for example "take knife and fork"
        if (1 < parsedNoun.original_input.split("&").length) {
          is_plural = true;
        }

        // --------------------------------------------------
        // if noun is plural...
        // --------------------------------------------------

        if (is_plural) {
          // --------------------------------------------------
          // ...can the current verb handle a plural noun?
          // --------------------------------------------------
          if (!dictionary_verb[`phrase${n}`].accepts_plural_noun) {
            // verb doesn't accept a plural noun

            // can we exclude any of our multiple assets for not being dov?
            let q = parsedNoun.matches.qualified.slice();
            for (let index = q.length - 1; index > -1; index--) {
              let qid = q[index];
              let qasset = this.game.getAsset(qid);
              if (!qasset.isDOV(dictionary_verb.name)) {
                q.splice(index, 1);
              }
            }
            if (q.length === 1) {
              parsedNoun.matches.qualified = q.slice();
            }

            // is there a collection among the assets?
            // for example, there are three drawers
            // and also a drawers collection
            // if so, prefer that
            if (parsedNoun.matches.qualified.length > 1) {
              let collections = [];
              for (let cid in parsedNoun.matches.qualified) {
                let casset = this.game.getAsset(
                  parsedNoun.matches.qualified[cid]
                );
                if (!casset) continue;
                if (casset.hasClass("Collection")) {
                  collections.push(casset);
                }
              }
              if (collections.length === 1) {
                parsedNoun.matches.qualified = [collections[0].id];
              }
            }

            // --------------------------------------------------
            // can we exclude assets marked as exclude_from_disambiguation?
            // don't exclude them if they were the only assets found,
            // but only if there is a mix of excluded and non-excluded assets
            // chiefly used for global placeholders like global_wall and global_exit
            // note that parser.selectPresent should have already narrowed global
            // assets according to area_scenery / area_scenery / global_scenery
            // --------------------------------------------------
            let { excludable_assets } = this.categorizeAssets(
              parsedNoun.matches.qualified
            );

            if (
              parsedNoun.matches.qualified.length > 1 &&
              excludable_assets.length
            ) {
              // reset the excludable_assets list to only include things from
              // parsedNoun.matches.qualified, which may have been modified by a prior block
              let { excludable_assets } = this.categorizeAssets(
                parsedNoun.matches.qualified
              );

              // only exclude excludable assets if there are non excludable assets
              // ex: if there is one tangible exit and three global exits,
              // default to the one tangible exit
              // but if there are only global exits, offer those
              if (
                excludable_assets.length < parsedNoun.matches.qualified.length
              ) {
                parsedNoun = this.excludeFromParsedNoun(
                  parsedNoun,
                  excludable_assets
                );
              }
            } // exclude_from_disambiguation

            // did we manage to narrow down the list?
            if (parsedNoun.matches.qualified.length > 1) {
              this.game.debug(
                `D1202`,
                `${fx} `,
                ` ${dictionary_verb.name} doesn't handle multiple objects `
              );
              this.game.log(
                "L1206",
                "warn",
                "high",
                `[${fx}] ${dictionary_verb.name} doesn't handle multiple objecs: ${parsedNoun.original_input}`,
                "Parser"
              );
              this.printNounDisambiguation({
                parsedNoun: parsedNoun,
                nounIndex: n,
              });
              return false;
            }
          }
        } // is_plural

        // the input was not found to be plural,
        // but the parser did find multiple - aka ambiguous - assets
        // for example "unlock door with key" returned [ "silver key", "gold key", "bronze key" ]
        // and the current verb can't handle this - so we need to disambiguate - aka pick one
        // this is one of the famously chief issues with text games
        // we'll do our best to narrow down the list of options
        // and if we can't we'll have to ask the player to pick one

        if (!is_plural && !dictionary_verb.let_verb_handle_disambiguation) {
          // --------------------------------------------------
          // is parsedNoun a substance?
          // we handle substance disambiguation differently from others
          // --------------------------------------------------
          if (parsedNoun.is_substance) {
            this.game.debug(
              `D1447`,
              `${fx} `,
              ` phrase${n}.parsedNoun found multiple substance containers `
            );

            if (this.game.settings.disambiguation_considers_last_turn) {
              // did last turn refer to a container of this substance?
              // if so infer that container
              for (
                let phrase = 1;
                phrase < last_turn.getPhraseCount();
                phrase++
              ) {
                let last_turn_asset = last_turn.getAsset(phrase);
                if (last_turn_asset.contains === parsedNoun.is_substance) {
                  // asset contains substance but is it one of the qualified matches?
                  for (
                    let container = 0;
                    container < parsedNoun.matches.qualified.length;
                    container++
                  ) {
                    let match = parsedNoun.matches.qualified[container];
                    let asset = this.game.getAsset(match);
                    if (asset.id === last_turn_asset.id) {
                      parsedNoun.matches.qualified = [match];
                      parsedNoun.is_qualified = match;
                      parsedNoun.matches.setby = "MC01";
                    }
                  }
                }
              }
            }

            // --------------------------------------------------
            // infer_containers_prefers_reservoir?
            // For example, if player input "throw sand"
            // is there a body of sand at hand?
            // Does not prompt for disambiguation,
            // just uses the first one found.
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              this.game.settings.infer_containers_prefers_reservoir
            ) {
              let { reservoir_assets, emitter_assets } = this.categorizeAssets(
                parsedNoun.matches.qualified
              );

              // this takes substance from the first available reservoir
              // aka pond, lake, ocean
              if (reservoir_assets.length) {
                parsedNoun.matches.qualified = [reservoir_assets[0]];
                parsedNoun.is_qualified = reservoir_assets[0];
                parsedNoun.matches.setby = "MC02";
              }

              // this takes substance from the first available emitter
              // aka hose, faucet, waterfall
              else if (emitter_assets.length) {
                parsedNoun.matches.qualified = [emitter_assets[0]];
                parsedNoun.is_qualified = emitter_assets[0];
                parsedNoun.matches.setby = "MC03";
              }
            }

            // --------------------------------------------------
            // if we still haven't found a singular vessel, do
            // game settings allow us to pick one automatically?
            // --------------------------------------------------
            if (parsedNoun.matches.qualified.length > 1) {
              if (this.game.settings.auto_pick_inferred_container) {
                let match = parsedNoun.matches.qualified[0];
                parsedNoun.matches.qualified = [match];
                parsedNoun.is_qualified = match;
                parsedNoun.matches.setby = "MC04";
              }
            }
          } // parsedNoun.is_substance

          // --------------------------------------------------
          // handling for everything else besides substances
          // --------------------------------------------------
          else if (!parsedNoun.is_substance) {
            this.game.debug(
              `D1201`,
              `${fx} `,
              ` phrase${n}.parsedNoun needs disambiguation `
            );
            this.game.log(
              "L1207",
              "log",
              "high",
              `[${fx}] parsedNoun ${n} needs disambiguation: ${parsedNoun.matches.qualified.join(", ")}`,
              "Parser"
            );

            var phrase = this_turn.verified_sentence[`phrase${n}`];
            var parsedNoun = phrase.parsedNoun;

            // --------------------------------------------------
            // Categorize the found assets
            // to help us narrow the list
            // based on verb phrase options.
            // --------------------------------------------------
            let {
              present_assets,
              absent_assets,
              carried_assets,
              uncarried_assets,
              local_assets,
              global_assets,
              excludable_assets,
              dispenser_assets,
              fungible_assets,
              room_assets,
            } = this.categorizeAssets(parsedNoun.matches.qualified);

            // --------------------------------------------------
            // disambiguation considers last turn
            // --------------------------------------------------
            // @TODO Broaden this to consider whether this turn's
            // items are related to last turn, for example, here,
            // obviously the player meant sink's hot water.
            // > x sink
            // > turn on hot water
            //   did you mean sink's hot water or tub's hot water?
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              this.game.settings.disambiguation_considers_last_turn
            ) {
              this.game.log(
                "L1687",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} disambiguation_considers_last_turn`,
                "Parser"
              );

              // look at each phrase of last turn to see if this asset was mentioned
              for (
                let phrasenum = 1;
                phrasenum <= last_turn.getPhraseCount();
                phrasenum++
              ) {
                let phrase = last_turn.verified_sentence[`phrase${phrasenum}`];

                // @TODO should this get parsedNoun.asset_id? See ParsedNoun.js
                // 5/15/26 there is some nuance to be worked out here
                // parsedNoun.is_unambiguous is set unambiguous asset is found
                // parsedNoun.asset_id is set when any asset is resolved
                // using is_unambiguous leads to an issue with fungibles
                // where you can't take two different fungibles in a row because
                // the parser defaults to the fungible found last turn.

                if (phrase?.parsedNoun?.is_unambiguous) {
                  let prior_id = phrase.parsedNoun.is_unambiguous;
                  // if (phrase?.parsedNoun?.asset_id) {
                  // let prior_id = phrase.parsedNoun.asset_id;

                  if (prior_id) {
                    if (parsedNoun.matches.qualified.includes(prior_id)) {
                      this.game.debug(
                        `D1373`,
                        `${fx} `,
                        ` disambiguation defaulting to last turn's ${prior_id}`
                      );
                      parsedNoun.matches.qualified = [prior_id];
                      parsedNoun.is_qualified = prior_id;
                      parsedNoun.matches.setby = "MC05";
                    }
                  }
                }
              }
            } // disambiguation_considers_last_turn

            // --------------------------------------------------
            // present?
            // for verbs like "go to asset" where we can handle a non-present
            // asset but prefer a present asset if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.present
            ) {
              this.game.log(
                "L1688",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers present assets`,
                "Parser"
              );

              if (present_assets.length && absent_assets.length) {
                absent_assets.forEach((item) => {
                  console.warn(`remove absent asset`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC06";
                }
              }
            } // present

            // --------------------------------------------------
            // absent?
            // for verbs like "go to asset" where we can handle present
            // or absent but prefer an absent asset if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.absent
            ) {
              this.game.log(
                "L1689",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers absent assets`,
                "Parser"
              );

              if (present_assets.length && absent_assets.length) {
                present_assets.forEach((item) => {
                  console.warn(`remove absent asset`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC07";
                }
              }
            } // absent

            // --------------------------------------------------
            // carried?
            // for verbs like "throw asset" where we can handle an uncarried
            // asset but prefer a carried asset if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.carried
            ) {
              this.game.log(
                "L1690",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers carried assets`,
                "Parser"
              );

              if (carried_assets.length && uncarried_assets.length) {
                uncarried_assets.forEach((item) => {
                  console.warn(`remove uncarried asset`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC08";
                }
              }
            } // carried

            // --------------------------------------------------
            // uncarried?
            // for verbs like "hit asset" where we can handle a carried
            // asset but prefer an uncarried asset if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.uncarried
            ) {
              this.game.log(
                "L1691",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers uncarried assets`,
                "Parser"
              );

              if (carried_assets.length && uncarried_assets.length) {
                carried_assets.forEach((item) => {
                  console.warn(`remove carried asset`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC09";
                }
              }
            } // prefer_uncarried_over_carried

            // --------------------------------------------------
            // discard duplicate fungibles
            // we'll just take the first one
            // --------------------------------------------------
            if (fungible_assets.length > 1) {
              for (let i = fungible_assets.length - 1; i > 0; i--) {
                let item = fungible_assets[i];
                let index = parsedNoun.matches.qualified.indexOf(item);
                if (index > -1) {
                  console.warn(`remove fungible asset`, item);
                  parsedNoun.matches.qualified.splice(index, 1);
                }
                fungible_assets.pop();
              }
            }

            // --------------------------------------------------
            // discard duplicate dispensers?
            // @TODO do we need to verify that dispensers dispense the same thing?
            // or hasn't this already been handled?
            // Do we want to take the first available dispenser
            // or prompt for disambiguation?
            // --------------------------------------------------
            if (dispenser_assets.length > 1) {
              for (let i = dispenser_assets.length - 1; i > 0; i--) {
                let item = dispenser_assets[i];
                let index = parsedNoun.matches.qualified.indexOf(item);
                if (index > -1) {
                  console.warn(`remove dispenser asset`, item);
                  parsedNoun.matches.qualified.splice(index, 1);
                }
                dispenser_assets.pop();
              }
            }

            // --------------------------------------------------
            // dispenser?
            // for verbs like "take asset" where we might have a fungible
            // asset and a dispenser of same and prefer to take from the
            // dispenser if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.dispenser
            ) {
              this.game.log(
                "L1692",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers dispenser assets`,
                "Parser"
              );

              if (dispenser_assets.length && fungible_assets.length) {
                fungible_assets.forEach((item) => {
                  console.warn(`remove fungible`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC10";
                }
              }
            } // dispenser

            // --------------------------------------------------
            // fungible?
            // for verbs like "drop asset" where we might have a fungible
            // asset and a dispenser of same and prefer to drop the
            // fungible if there's ambiguity
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              dictionary_verb[`phrase${n}`].noun_prefers.fungible
            ) {
              this.game.log(
                "L1693",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefers fungible assets`,
                "Parser"
              );

              if (dispenser_assets.length && fungible_assets.length) {
                dispenser_assets.forEach((item) => {
                  console.warn(`remove dispenser`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC11";
                }
              }
            } // fungible

            // --------------------------------------------------
            // local vs global?
            // if we will have multiple assets, does omitting
            // globals get us down to one?
            // --------------------------------------------------
            if (parsedNoun.matches.qualified.length > 1) {
              this.game.log(
                "L1694",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} prefer local over global`,
                "Parser"
              );

              if (local_assets.length && global_assets.length) {
                global_assets.forEach((item) => {
                  console.warn(`remove global`, item);
                  let index = parsedNoun.matches.qualified.indexOf(item);
                  if (index > -1) {
                    parsedNoun.matches.qualified.splice(index, 1);
                  }
                });
                if (parsedNoun.matches.qualified.length === 1) {
                  parsedNoun.is_qualified = parsedNoun.matches.qualified[0];
                  parsedNoun.matches.setby = "MC12";
                }
              }
            }

            // --------------------------------------------------
            // can we consider assets mentioned in the last turn?
            // this doesn't apply if last turn was plural
            // ex: if "take all keys", then "x key" should not
            // default to the last key handled
            //
            //  @TODO is this redundant with L1687?
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              this.game.settings.disambiguation_considers_last_turn &&
              !last_turn.plural
            ) {
              this.game.log(
                "L1695",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} disambiguation_considers_last_turn`,
                "Parser"
              );
              // did last turn's input refer to an asset that could resolve
              // this ambiguity? if so infer that asset
              for (
                let phrase = 1;
                phrase <= last_turn.getPhraseCount();
                phrase++
              ) {
                let last_turn_asset = last_turn.getAsset(phrase);
                if (
                  last_turn_asset &&
                  parsedNoun.is_qualified === last_turn_asset.id
                ) {
                  this.game.debug(
                    `D1589`,
                    `${fx} `,
                    ` disambiguation defaulting to last turn's ${last_turn_asset.name}`
                  );
                  parsedNoun.matches.qualified = [last_turn_asset.id];
                  parsedNoun.is_qualified = last_turn_asset.id;
                  parsedNoun.matches.setby = "MC13";
                }
              }
            }

            // --------------------------------------------------
            // can we exclude assets marked as exclude_from_disambiguation?
            // don't exclude them if they were the only assets found,
            // but only if there is a mix of excluded and non-excluded assets
            // chiefly used for global placeholders like global_wall and global_exit
            // note that parser.selectPresent should have already narrowed global
            // assets according to area_scenery / area_scenery / global_scenery
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              excludable_assets.length
            ) {
              this.game.log(
                "L1696",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} excludable_assets`,
                "Parser"
              );
              // reset the excludable_assets list to only include things from
              // parsedNoun.matches.qualified, which may have been modified by a prior block
              let { excludable_assets } = this.categorizeAssets(
                parsedNoun.matches.qualified
              );

              // only exclude excludable assets if there are non excludable assets
              // ex: if there is one tangible exit and three global exits,
              // default to the one tangible exit
              // but if there are only global exits, offer those
              if (
                excludable_assets.length < parsedNoun.matches.qualified.length
              ) {
                parsedNoun = this.excludeFromParsedNoun(
                  parsedNoun,
                  excludable_assets
                );
              }
            } // exclude_from_disambiguation

            // --------------------------------------------------
            // can we exclude the current room from disambiguation?
            // --------------------------------------------------
            if (
              parsedNoun.matches.qualified.length > 1 &&
              room_assets.length &&
              this.game.settings.disambiguation_can_exclude_room
            ) {
              this.game.log(
                "L1697",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} disambiguation_can_exclude_room`,
                "Parser"
              );
              // reset the room_assets list to only include things from
              // parsedNoun.matches.qualified, which may have been modified by a prior block
              let { room_assets } = this.categorizeAssets(
                parsedNoun.matches.qualified
              );

              // only exclude rooms if there are other assets
              if (room_assets.length < parsedNoun.matches.qualified.length) {
                parsedNoun = this.excludeFromParsedNoun(
                  parsedNoun,
                  room_assets
                );
              }
            } // disambiguation_can_exclude_room

            // --------------------------------------------------
            // did player input something like "go east through exit"?
            // parser will have returned all available exits
            // but we should be able to infer the correct one
            // --------------------------------------------------
            if (parsedNoun.matches.qualified.length > 1 && n === 2) {
              this.game.log(
                "L1698",
                "log",
                "high",
                `[${fx}] parsedNoun ${n} infer second noun following direction`,
                "Parser"
              );
              let phrase1 = this_turn.verified_sentence.phrase1;
              if (
                phrase1.parsedNoun?.is_direction &&
                phrase1.parsedNoun.is_qualified &&
                parsedNoun.matches.qualified.includes(
                  phrase1.parsedNoun.is_qualified
                )
              ) {
                parsedNoun.matches.qualified = [
                  phrase1.parsedNoun.is_qualified,
                ];
                parsedNoun.is_qualified = phrase1.parsedNoun.is_qualified;
                parsedNoun.matches.setby = "MC14";
              }
            }
          } // if not substance

          // --------------------------------------------------
          // regardless if substance or other,
          // if we made it here without an unambiguous asset,
          // we just have to ask the player
          // --------------------------------------------------
          if (parsedNoun.matches.qualified.length > 1) {
            this.game.log(
              "L1699",
              "log",
              "high",
              `[${fx}] parsedNoun ${n} unable to resolve disambiguation`,
              "Parser"
            );
            this.printNounDisambiguation({
              parsedNoun: parsedNoun,
              nounIndex: n,
            });
            return false;
          }
        } // ! let verb handle disambiguation
      } // more than one qualified

      // --------------------------------------------------
      // did player ask for a fungible class?
      // --------------------------------------------------
      let [type, Class, aspect, dispenser] =
        parsedNoun.matches.qualified[0].split(":");

      if (dispenser && type === "fungible") {
        this.game.log(
          "L1700",
          "log",
          "high",
          `[${fx}] parsedNoun ${n} found fungible dispenser`,
          "Parser"
        );
        // is the class listed in fungible_classes?
        const fungible_class = this.game.fungible_classes[Class];
        if ("undefined" !== typeof fungible_class) {
          // @TODO check max_extant and max_count
          // let msg = `{We} can't take any more pistachios. `;
          // this.game.print(msg, output_class);
          // return false;

          // create a new instance of the fungible asset
          dispenser = this.game.getAsset(dispenser);
          const dispensed = this.game.dispenseAsset(Class, aspect, dispenser);
          if (!this.game.world._fungibles.includes(dispensed.id)) {
            this.game.world._fungibles.push(dispensed.id);
          }
          parsedNoun.matches.qualified[0] = dispensed.id;
          parsedNoun.is_qualified = dispensed.id;
          parsedNoun.matches.setby = "MC15";
        }
      }
      // end fungible
    } // end each phrase redux

    // --------------------------------------------------
    // ensure that the verb handles the sentence structure
    // --------------------------------------------------
    if (false === this.verifySentenceStructure()) return false;

    // --------------------------------------------------
    // it looks like we're ready to do the thing!
    // --------------------------------------------------

    this.game.log(
      "L1208",
      "log",
      "high",
      "[${fx}] handleSentence() end",
      "Parser"
    );
    let direct_object = this_turn.getAsset(1);
    if (direct_object?.is?.collection && dictionary_verb.enqueue_collections) {
      this.game.debug(
        `D1091`,
        `${fx} `,
        ` ${this_turn.getAsset(1).name}.is.collection, enqueueing collection`
      );
      this.game.print(msg);
      dictionary_verb.enqueueCollection(direct_object);
    } else {
      this.game.log(
        "L1209",
        "log",
        "high",
        `[${fx}] doVerb ${dictionary_verb.name}`,
        "Parser"
      );
      return dictionary_verb.do();
    }

    return true;
  }; // handleSentence
})();