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

(function () {
  /*global adventurejs A*/
  "use strict";

  var p = adventurejs.Parser.prototype;

  /**
   * Handle multi-word input.
   * @memberOf adventurejs.Parser
   * @method adventurejs.Parser#handleSentence
   */
  p.handleSentence = function Parser_handleSentence() {
    this.game.log("log", "high", "handleSentence.js > BEGIN.", "Parser");
    var this_turn = this.input_history[0];
    var last_turn = this.input_history[1];
    var parsed_verb_name = "",
      dictionary_verb = null,
      dictionary_verb_name = "",
      nouns = [],
      exclusions = [],
      prepositions = [],
      parsedNouns = [],
      count = { phrase: 0 },
      msg = "";

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

    if (this_turn.parsed_word.enabled) {
      // we've arrived here via handleWord and
      // we're probably amending last turn's input
      this_turn.verified_sentence = A.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]) {
          this_turn.setNoun(i, this_turn.parsed_word["noun" + 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()) {
      //console.warn('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);
          exclusions[i] = this_turn.getExclusion(i);
        }
      }
    } else {
      // we shouldn't arrive here but may need an error message
      this.game.debug(`F1207 | handleSentence.js | didn't receive words `);
      msg += this.getUnparsedMessage(this_turn.input);
      this.game.print(msg, this_turn.output_class);
    }

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

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

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

    if (!parsed_verb_name || !this.dictionary.verbs[parsed_verb_name]) {
      let err = `handleSentence.js > 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("warn", "high", err, "Parser");

      this.game.debug(
        `F1721 | handleSentence.js | verb not found. Check the console for more info. `,
      );
      msg += this.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++;
      }
    }
    //console.warn("count.phrase", count.phrase);

    // ----------
    // for each phrase
    // ----------

    for (let n = 1; n <= count.phrase; n++) {
      //var n = parseInt(key.replace("phrase", ""), 10);
      var phrase = this_turn.verified_sentence[`phrase${n}`];
      var noun = phrase.noun;
      var preposition = phrase.preposition;
      var exclusion = phrase.exclusion;

      this.game.log(
        "log",
        "high",
        `handleSentence.js > handle phrase ${n}`,
        "Parser",
      );

      // this shouldn't happen
      if (!noun && !preposition) {
        this.game.debug(
          `F1034 | handleSentence.js | ${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(
          `F1036 | handleSentence.js | ${dictionary_verb.name} received a noun it can't handle`,
        );
        msg += this.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(
          `F1033 | handleSentence.js | ${dictionary_verb.name} received preposition without noun, soft prompt noun${n}`,
        );
        msg += `What would $(we) like to ${this_turn.input}? `;
        this_turn.setSoftPrompt({
          [`noun${n}`]: true,
          verb: this_turn.input_verb,
        });
        this.game.print(msg, this_turn.output_class);
        return false;
      }

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

      if (
        noun &&
        !preposition &&
        dictionary_verb[`phrase${n}`].requires_preposition
      ) {
        this.game.debug(
          `F1041 | handleSentence.js | ${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(parsedNouns[1].object_id).articlename
            } did $(we) want to ${dictionary_verb.prettyname}? `;
            break;
          case 2:
            msg += `Where in relation to ${
              this.game.getAsset(parsedNouns[2].object_id).articlename
            } did $(we) want to ${dictionary_verb.prettyname} ${
              this.game.getAsset(parsedNouns[1].object_id).articlename
            }? `;
            break;
          case 3:
            msg += `That seems to be missing a preposition. Can $(we) try phrasing that another way? `;
            break;
        }

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

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

      if (
        preposition &&
        dictionary_verb[`phrase${n}`].accepts_these_prepositions.length &&
        -1 ===
          dictionary_verb[`phrase${n}`].accepts_these_prepositions.indexOf(
            preposition,
          )
      ) {
        this.game.debug(
          `F1222 | handleSentence.js | ${
            dictionary_verb.name
          }.phrase${n}.accepts_these_prepositions ${dictionary_verb[
            `phrase${n}`
          ].accepts_these_prepositions.toString()}`,
        );
        msg += `<em class='unparsed'>${A.propercase(preposition)} ${
          noun ? noun : ""
        }</em> doesn't seem to work here. `;
        this.game.print(msg, this_turn.output_class);
        return false;
      }

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

      if (noun) {
        // 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

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

        if (
          noun_is_plural &&
          !dictionary_verb[`phrase${n}`].accepts_plural_noun
        ) {
          this.game.debug(
            `F1032 | handleSentence.js | ${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
        phrase.parsedNoun = this.parseNoun(noun);
      } // noun

      // parsedNoun empty? ----------

      if (phrase.parsedNoun && !phrase.parsedNoun.matches.all.length) {
        this.game.log(
          "warn",
          "high",
          [
            "handleSentence.js > input: " +
              this_turn.input +
              ", parsedNouns[1]: ",
            parsedNouns[1],
          ],
          "Parser",
        );

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

        this.game.debug(
          `F1035 | handleSentence.js | ${noun} isn't recognized `,
        );
        msg += this.getUnparsedMessage(phrase.parsedNoun.deserialized_input);
        this.game.print(msg, this_turn.output_class);
        return false;
      } // parsedNoun
      this.game.log(
        "log",
        "medium",
        [`handleSentence.js > phrase${n}.parsedNoun:`, phrase.parsedNoun],
        "Parser",
      );

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

      // Replace 'in' with 'on' where applicable such as 'sit in chair'
      if (phrase.parsedNoun && !dictionary_verb[`phrase${n}`].accepts_noun) {
        this.game.debug(
          `F1036 | handleSentence.js | ${dictionary_verb.name} received a noun it can't handle`,
        );
        msg += this.getUnparsedMessage(this_turn.input);
        this.game.print(msg, this_turn.output_class);
        return false;
      }
      if (
        phrase.parsedNoun &&
        "in" === preposition &&
        this.game.getAsset(phrase.parsedNoun.object_id).quirks.in_means_on &&
        dictionary_verb.in_can_mean_on
      ) {
        this_turn.setInPhrase(n, "preposition", "on");
      }

      // exclusion (aka but) ----------

      // examples of exclusions:
      //  "take all but sword"
      //  "take all gems but green gem"
      //  "take all gems but green gem and blue gem"

      if (exclusion && "string" === typeof exclusion) {
        var exclusion_split = exclusion.split("&");

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

          this.game.log(
            "log",
            "medium",
            ["handleSentence.js > Exclude:", excluded_parsed_noun],
            "Parser",
          );

          if (excluded_parsed_noun) {
            for (let i = 0; i < excluded_parsed_noun.matches.all.length; i++) {
              let excluded_id = excluded_parsed_noun.matches.all[i];
              // remove excluded_id from parsedNoun.matches.all
              phrase.parsedNoun.matches.all.splice(
                phrase.parsedNoun.matches.all.indexOf(excluded_id),
                1,
              );
              // remove excluded_id from parsedNoun.matches.qualified
              phrase.parsedNoun.matches.qualified.splice(
                phrase.parsedNoun.matches.qualified.indexOf(excluded_id),
                1,
              );
              // remove excluded_id from parsedNoun.matches.unambiguous
              if (phrase.parsedNoun.matches.unambiguous === excluded_id)
                phrase.parsedNoun.matches.unambiguous = "";
            } // for excluded_parsed_noun.matches.all
          } // excluded_parsed_noun
        } // for exclusion_split

        this.game.log(
          "log",
          "medium",
          [
            `handleSentence.js > phrase${n}.parsedNoun minus exclusions: `,
            phrase.parsedNoun,
          ],
          "Parser",
        );
      } // exclusion

      // exclude from prior

      if (n > 1) {
        let first_phrase = this_turn.getPhrase(1);
        let last_split_noun = [];
        last_split_noun = first_phrase.noun && first_phrase.noun.split("&");
        let last_noun_is_plural =
          first_phrase.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.matches.unambiguous) {
            // remove unambiguous from prior noun's matches
            let excluded_id = phrase.parsedNoun.matches.unambiguous;
            let allindex =
              first_phrase.parsedNoun.matches.all.indexOf(excluded_id);
            let qualindex =
              first_phrase.parsedNoun.matches.qualified.indexOf(excluded_id);
            // remove excluded_id from parsedNoun.matches.all
            first_phrase.parsedNoun.matches.all.splice(allindex, 1);
            // remove excluded_id from parsedNoun.matches.qualified
            first_phrase.parsedNoun.matches.qualified.splice(qualindex, 1);
            // remove excluded_id from parsedNoun.matches.unambiguous
            if (first_phrase.parsedNoun.matches.unambiguous === excluded_id)
              first_phrase.parsedNoun.matches.unambiguous = "";
            if (allindex > -1 || qualindex > -1) {
              // we might need to re-qualify the last noun
              if (first_phrase.parsedNoun) {
                first_phrase.parsedNoun = this.qualifyParsedNoun({
                  parsedNoun: first_phrase.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 ----------
        // @TODO reexamine - this seems like it should be caught in prior block
        if (
          phrase.parsedNoun &&
          phrase.parsedNoun.object_id &&
          first_phrase.parsedNoun.object_id &&
          first_phrase.parsedNoun.matches.qualified.length > 1
        ) {
          for (
            var i = first_phrase.parsedNoun.matches.qualified.length - 1;
            i > -1;
            i--
          ) {
            if (
              phrase.parsedNoun.qualified_object_id ===
              first_phrase.parsedNoun.matches.qualified[i]
            ) {
              first_phrase.parsedNoun.matches.qualified.splice(i, 1);
            }
          }
        } // remove iobj from dobj collection
      } // n>1 / exclude from prior

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

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

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

      // Has author used redirectVerb for this verb on direct_object?
      if (
        n === 1 &&
        phrase.parsedNoun &&
        phrase.parsedNoun.qualified_object_id
      ) {
        let direct_object = this.game.getAsset(
          phrase.parsedNoun.qualified_object_id,
        );
        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

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

    // I think this is 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 parsedNoun = phrase.parsedNoun;
      if (!parsedNoun) continue;

      this.game.log(
        "log",
        "high",
        `handleSentence.js > final check on phrase ${n}`,
        "Parser",
      );

      if (parsedNoun.matches.qualified.length > 1) {
        var lookup = this.game.world_lookup[parsedNoun.serialized_input];
        var is_plural = false;
        if (1 < parsedNoun.deserialized_input.split("&").length) {
          is_plural = true;
        }
        if (lookup) {
          if ("plural" === lookup.type || "group" === lookup.type) {
            is_plural = true;
          }
        }

        if (is_plural) {
          if (!dictionary_verb[`phrase${n}`].accepts_plural_noun) {
            // verb doesn't accept a plural noun
            this.game.debug(
              `F1202 | handleSentence.js | ${dictionary_verb.name} doesn't handle multiple objects `,
            );
            this.game.log(
              "warn",
              "high",
              "handleSentence.js > " +
                dictionary_verb.name +
                " doesn't handle multiple objecs: " +
                parsedNoun.deserialized_input,
              "Parser",
            );
            this.printNounDisambiguation({
              parsedNoun: parsedNoun,
              nounIndex: n,
            });
            return false;
          }
        }

        // meaning, parsedNoun.matches.qualified.length > 1
        // but verb doesn't handle plurals
        if (!is_plural && !dictionary_verb.let_verb_handle_disambiguation) {
          this.game.debug(
            `F1201 | handleSentence.js | phrase${n}.parsedNoun needs disambiguation `,
          );
          this.game.log(
            "log",
            "high",
            "handleSentence.js > parsedNoun" +
              n +
              " needs disambiguation: " +
              parsedNoun.matches.qualified,
            "Parser",
          );
          this.printNounDisambiguation({
            parsedNoun: parsedNoun,
            nounIndex: n,
          });
          return false;
        }
      } // more than one qualified
    } // each phrase redux

    // do the thing! ----------

    this.game.log("log", "high", "handleSentence.js > END.", "Parser");
    let direct_object = this_turn.getAsset(1);
    if (direct_object?.is.collection && dictionary_verb.enqueue_collections) {
      this.game.debug(
        `F1091 | handleSentence.js | ${
          this_turn.getAsset(1).name
        }.is.collection, enqueueing collection`,
      );
      this.game.print(msg);
      dictionary_verb.enqueueCollection(direct_object);
    } else {
      this.game.debug(
        `F1143 | handleSentence.js | doVerb ${dictionary_verb.name} `,
      );
      dictionary_verb.do();
    }

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