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

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

  var p = adventurejs.Parser.prototype;

  /**
   * Parse an input string.
   * @memberOf adventurejs.Parser
   * @method adventurejs.Parser#parseInput
   * @param {String} input Player input.
   */
  p.parseInput = function Parser_parseInput(input) {
    this.game.log(
      "L1091",
      "log",
      "high",
      `[parseInput.js] parseInput() ${input}`,
      "Parser"
    );
    let results;

    // we already dispatched inputEnter when player entered input
    // but parser.parseInput() can be called by other means so we distinguish
    // between dispatching inputEnter vs inputParseBegin
    this.game.reactor.emit("inputParseBegin");

    // The undo verb supercedes all parsing.
    if ("undo" === input) {
      this.game.display.printInput(input);
      this.game.dictionary.doVerb("undo");
      this.game.endTurn();
      return;
    }

    /**
     * Save a snapshot of the game state.
     * Technically this is saving LAST turn's snapshot
     * before engaging in this turn.
     * IMPORTANT: undo must come first.
     */
    A.addWorldToHistory.call(this.game, A.getBaselineDiff.call(this.game)); // delta from baseline

    /**
     * input_history_index is part of a convenience that lets
     * players use the arrow keys to re-enter their own
     * prior input, as in a shell. Player might have used it
     * to enter current input, so reset it now.
     */
    this.input_history_index = -1;

    /**
     * output_class is one of the params that's carried through
     * the entire parse and then used to apply css styles to
     * the output, if applicable.
     */
    var output_class = "";

    /**
     * Run any custom parsers created by author.
     * If custom parser returns a string, parse will continue.
     * If it returns null or false, parse will end.
     */
    if (0 < this.custom_parsers.length) {
      var keys = Object.keys(this.custom_parsers);
      for (var i = 0; i < this.keys.length; i++) {
        var parser_name = keys[i];
        if (true === this.custom_parsers_enabled[parser_name]) {
          input = this.custom_parsers[parser_name].parseInput();
          // false & null give same results but following the
          // pattern we're using almost everywhere else,
          // we're leaving open the option to handle distinctly
          if (false === input) {
            this.is_input_queued = false;
            this.game.endTurn();
            return false;
          } else if (null === input) {
            this.is_input_queued = false;
            this.game.endTurn();
            return null;
          }
        }
      }
    }

    // --------------------------------------------------
    // make a new input object
    // @TODO moved this block from below next block
    // and watching to ensure no bad side effects
    // --------------------------------------------------
    this.input_history.unshift(
      new adventurejs.Input({ game_name: this.game.game_name })
    );
    var this_turn = this.input_history[0];

    // --------------------------------------------------
    // document the input
    // At this point "input" is just the string entered by player.
    // If we're processing stacked input, then input will already
    // have been parsed giving us serialized object ids.
    // --------------------------------------------------
    var parsed_input = input;
    var normalized_input = input;

    // --------------------------------------------------
    // keep a reference to last turn
    // --------------------------------------------------
    var last_turn = this.input_history[1];

    // --------------------------------------------------
    // Are we parsing queued commands?
    // Queued commands are made when user inputs
    // something like "do this THEN do that".
    // if so, carry several items from original input
    // --------------------------------------------------
    var is_queued = false;

    var did_print_input = false;

    // --------------------------------------------------
    // process new item input...
    // --------------------------------------------------
    if (!this.input_queue.length) {
      this_turn.raw_input = input;

      // --------------------------------------------------
      // convert quoted substrings to tokens
      // --------------------------------------------------
      parsed_input = this.tokenizeQuotes(parsed_input);
      if (false === parsed_input) return false;

      // --------------------------------------------------
      // clean input of several conditions
      // parsed and normalized remain the same for now
      // --------------------------------------------------
      parsed_input = this.normalizeInput(parsed_input);

      // --------------------------------------------------
      // Convert any of these FROM
      //   "verb1 noun1, then verb2 noun2"
      //   "verb1 noun1 then verb2 noun2"
      //   "verb1 noun1 and then verb2 noun2"
      // TO
      //   "verb1 noun1. verb2 noun2."
      // Try to convert FROM
      //   "verb1 then verb2 noun1"
      // TO
      //   "verb1 noun1. verb2 noun2."
      // --------------------------------------------------
      // @TODO temp disabling this to try calling it later
      // parsed_input = this.convertThenToPeriod(parsed_input);
    }

    // --------------------------------------------------
    // ... or process next item in queue
    // --------------------------------------------------
    else if (this.input_queue.length > 0) {
      is_queued = true;

      let queued_input = this.input_queue[0];

      // enqueued inputs may pass output classes
      if ("undefined" !== typeof queued_input.output_class) {
        output_class = queued_input.output_class;
      }

      // enqueued inputs may include pre-set output
      // see goto for example
      if ("undefined" !== typeof queued_input.output) {
        this.game.print(queued_input.output, output_class);
      }

      // enqueued inputs may request linefeeds between lines of output
      if (
        "undefined" !== typeof queued_input.linefeed &&
        queued_input.linefeed
      ) {
        this.game.print("", "linefeed");
      }

      if (queued_input.normalized_input) {
        this_turn.normalized_input = queued_input.normalized_input;
      }

      if (queued_input.raw_input) {
        this_turn.raw_input = queued_input.raw_input;
      }

      if (queued_input.quote_tokens) {
        this_turn.quote_tokens = queued_input.quote_tokens;
      }

      this_turn.subject = queued_input.subject;

      this_turn.tokens = Object.assign(this_turn.tokens, last_turn.tokens);

      if (queued_input.printInput) {
        // @TODO this needs to print the actual original input
        // preserving capitalization but separating into clauses
        this.game.display.printInput(input);
        did_print_input = true;
      }

      // We're currently parsing the first item in the stack,
      // so remove that item.
      this.input_queue.shift();
    }
    // --------------------------------------------------
    // end queued input
    // --------------------------------------------------

    // --------------------------------------------------
    // remove some irrelevant social niceties
    // --------------------------------------------------
    parsed_input = this.roboticizeInput(parsed_input);
    // normalize again in case roboticization left extra spaces
    parsed_input = this.normalizeInput(parsed_input);

    // --------------------------------------------------
    // save our normalized input
    // from this point forward, parsed_input may diverge
    // --------------------------------------------------
    normalized_input = parsed_input;

    // --------------------------------------------------
    // @TODO is TEMP ???
    // expand tokenized quotes
    // haven't yet implemented handling for tokens
    // downstream of here, so for now rehydrate tokens
    // --------------------------------------------------
    parsed_input = this.untokenizeQuotes(parsed_input);
    normalized_input = this.untokenizeQuotes(normalized_input);

    // --------------------------------------------------
    // save input as its processed to this point
    // this is the only spot we're saving these currently
    // --------------------------------------------------
    this_turn.input = normalized_input;
    this_turn.normalized_input = normalized_input;

    this_turn.output_class += output_class;

    // @TODO multi-turn handling for instructions given to NPCs

    // --------------------------------------------------
    // Handle quote delimited substrings in input.
    // @TODO need to reflect new tokenization scheme
    // --------------------------------------------------
    parsed_input = this.parseStrings(parsed_input);

    // --------------------------------------------------
    // Handle numbers in input.
    // @TODO tokenize numbers?
    // --------------------------------------------------
    parsed_input = this.parseNumbers(parsed_input);

    // --------------------------------------------------
    // temporarily split the string by 'but'
    // we're going to handle 'but' in a later step
    // join compound names into asset IDs
    // this was breaking on 'item but other item'
    // --------------------------------------------------
    let but_arr = parsed_input.split(" but ");
    let but_str = "";
    for (let i = 0; i < but_arr.length; i++) {
      but_str += this.joinCompoundPhrases(but_arr[i]);
      if (i < but_arr.length - 1) but_str += " but ";
    }
    parsed_input = but_str;

    // --------------------------------------------------
    // Look for NPC directive
    // this can return false, if target is provided but not found
    // @TODO does this need to happen before split input?
    // and we need to carry target through queue
    // --------------------------------------------------
    parsed_input = this.findDirective(parsed_input);
    if (false === parsed_input) return false;

    // --------------------------------------------------
    // join some common compound verbs into single words
    // @WATCH this was originally sequenced a couple steps
    // later - testing it here to catch "go for a walk"
    // --------------------------------------------------
    parsed_input = this.joinPhrasalVerbs(parsed_input);

    // --------------------------------------------------
    // strip out articles
    // --------------------------------------------------
    parsed_input = this.stripArticles(parsed_input);

    // --------------------------------------------------
    // did player input "with thing" in response to
    // "which thing did you mean?" (aka soft prompt)?
    // if so remove "with"
    // --------------------------------------------------
    if (
      parsed_input.startsWith("with ") &&
      last_turn.soft_prompt.enabled &&
      last_turn.soft_prompt.noun
    ) {
      parsed_input = parsed_input.replace("with ", "");
    }

    // --------------------------------------------------
    // join some common compound prepositions into single words
    // --------------------------------------------------
    parsed_input = this.joinCompoundPrepositions(parsed_input);

    // --------------------------------------------------
    // join some common compound verbs into single words
    // @WATCH this is its original position in the parse
    // moved above stripArticles to catch "go for a walk"
    // --------------------------------------------------
    // parsed_input = this.joinPhrasalVerbs(parsed_input);

    // --------------------------------------------------
    // strip conjunctions and convert them into symbols we can handle
    // --------------------------------------------------
    parsed_input = this.stripConjunctions(parsed_input);

    // --------------------------------------------------
    // convert then to period for sentence boundaries
    // --------------------------------------------------
    parsed_input = this.convertThenToPeriod(parsed_input);

    // --------------------------------------------------
    // split input at periods
    // When input is split, processing only continues
    // for the first item and the remainder get queued.
    // --------------------------------------------------
    if (!this.input_queue.length) {
      parsed_input = this.splitInput(parsed_input);
      if (false === parsed_input) return false;
    }

    // --------------------------------------------------
    // Done parsing input, save parsed_input to the global input object.
    // --------------------------------------------------
    this_turn.parsed_input = parsed_input;

    // --------------------------------------------------
    // print the processed input
    // --------------------------------------------------
    if (!did_print_input) {
      this.game.display.printInput();
    }

    // --------------------------------------------------
    // callPreScripts
    // this must come after printInput
    // --------------------------------------------------
    results = this.game.callPreScripts(input);
    if ("undefined" !== typeof results) return results;

    // --------------------------------------------------
    // Split input into an array of individual words.
    // --------------------------------------------------
    var parsed_input_array = parsed_input.split(" ");

    // --------------------------------------------------
    // Save the input array to the global object.
    // --------------------------------------------------
    this_turn.parsed_input_array = parsed_input_array;

    // Each of the "compressVerb" functions can write to input_verb
    // but if verb was just one word, it won't have been saved yet,
    // and we may want to send to handleWord. But it's possible
    // that player is replying to a soft prompt and hasn't entered
    // a verb, so first let's verify whether the first word is in
    // fact a verb.

    if (!this_turn.input_verb) {
      // if there's an & in the first word, it likely means that
      // player input something like "take and wear glasses"
      // and the "and" was converted to "&" by stripConjunctions
      // we'll split by & and if each word is a verb, we'll
      // queue it up as separate inputs
      if (parsed_input_array[0].includes("&")) {
        let potential_verbs = parsed_input_array[0].split("&");
        let allverbs = true;
        for (let i in potential_verbs) {
          const word = potential_verbs[i];
          const potential_verb = this.parseVerb(word);
          if (!potential_verb) allverbs = false;
        }
        if (allverbs) {
          for (let i = 1; i < potential_verbs.length; i++) {
            this.input_queue.push({
              input: parsed_input.replace(
                parsed_input_array[0],
                potential_verbs[i]
              ),
              printInput: true,
            });
          }
          parsed_input_array[0] = potential_verbs[0];
        }
      }

      // resume normal singular verb handling here
      var parsedVerb = this.parseVerb(parsed_input_array[0]);
      var input_verb;

      if (parsedVerb) {
        input_verb = parsed_input_array[0];
      }

      // we only handle a couple of cases where first word is not verb
      else if (this.game.dictionary.getAdverb(parsed_input_array[0])) {
        // "adverb verb"
        parsedVerb = this.parseVerb(parsed_input_array[1]);
        if (parsedVerb) {
          input_verb = parsed_input_array[1];
        }
      }
      // we also handle "character verb" as in "Floyd go east"
      // but we check for that in verifySentence

      // recognize it as a verb, save it
      if (parsedVerb) {
        this_turn.input_verb = input_verb;
      } else if (last_turn.soft_prompt.noun1) {
        // last turn prompted for a word so assume we're recyling last turn's verb
        parsed_input_array.unshift(last_turn.getVerb());
        parsed_input = this.input_history[1].parsed_input + " " + parsed_input;
        this_turn.parsed_input = parsed_input;
        parsed_input_array = parsed_input.split(" ");
        this_turn.parsed_input_array = parsed_input_array;
        this.game.log(
          "L1093",
          "log",
          "high",
          `[parseInput.js] handle soft prompt for noun1`,
          "Parser"
        );
        this_turn.input_verb =
          last_turn.soft_prompt.input_verb || last_turn.input_verb;
        this_turn.verb_phrase =
          last_turn.soft_prompt.verb_phrase || last_turn.verb_phrase;
        this_turn.soft_prompt.satisfied = true;
      }
    }

    // for use in cases of number prompts
    var isWordNumber =
      null !== last_turn.disambiguate.index &&
      !isNaN(Number(parsed_input_array[0]));

    // IS INPUT USEABLE?
    // We can't do anything with the input.
    if (
      1 === parsed_input_array.length &&
      "" === parsed_input_array[0] &&
      false === isWordNumber
    ) {
      this.parseNoInput();
    }

    // valid one word inputs include intransitive verbs
    // such as look, inventory, and directions
    // we don't know that one of these has been input, but we can move forward
    else if (1 === parsed_input_array.length) {
      this_turn.found_word = parsed_input_array[0];
      this.handleWord();
    }

    // Parse the full sentence
    else {
      if (false === this.parseSentence()) return false;
      if (false === this.verifySentence()) return false;
      if (false === this.verifyAdverbs()) return false;
      // @TODO can I put a verifyPronouns() here?
      // @TODO can I put a verifyPossessives() here?
      if (false === this.verifyCharacterVerb()) return false;
      if (false === this.saveVerbPhrase()) return false;
      if (false === this.handleSentence()) return false;
    }

    // TODO special handling for say / ask / tell

    this.display.updateRoom();

    // If player used a period to input distinct actions,
    // perform the next one in the queue.
    if (this.input_queue.length > 0) {
      this.game.midTurn();
      this.game.reactor.emit("inputQueueNext");
      this.is_input_queued = true;
      this.parseInput(this.input_queue[0].input);
    } else {
      this.is_input_queued = false;
      this.game.endTurn();
      return;
    }
  }; // parseInput
})();