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

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

  var p = AdventureJS.Parser.prototype;

  /**
   * Handle noun input by creating a new ParsedNoun object
   * and then populating it. In other words parseNoun results
   * in a ParsedNoun.
   * @memberOf AdventureJS.Parser
   * @method AdventureJS.Parser#parseNoun
   * @param {String} word
   * @returns {adventurejs.parsedNoun}
   */
  p.parseNoun = function Parser_parseNoun(word, context = null) {
    this.game.log(
      "L1098",
      "log",
      "high",
      `[parseNoun.js] parseNoun() receive: ${word}`,
      "Parser"
    );
    var input = this.game.getInput();
    var subject = input.getSubject();
    var room = subject.getRoom();
    var this_turn = this.input_history[0];
    var last_turn = this.input_history[1];

    // We're using the input word to create a parsedNoun
    // with properties needed by logic down the road.
    var parsedNoun = new AdventureJS.Parser.ParsedNoun();
    parsedNoun.input = word;
    parsedNoun.normalized_input = A.FX.normalize(word);
    parsedNoun.original_input =
      this_turn.tokens[word]?.source ||
      this_turn.tokens[parsedNoun.normalized_input]?.source ||
      A.FX.denormalize(word);

    // player can enter "it" to refer to last turn's direct object
    if (
      "it" === word &&
      last_turn &&
      last_turn.parsedNoun1 &&
      last_turn.parsedNoun1.is_qualified
    ) {
      word = last_turn.parsedNoun1.is_qualified;
    }

    // Does the input word match a direction?
    // getDirection converts shortcuts to full name,
    // eg ne to northeast
    // and returns the full name as a string.
    var direction = this.dictionary.getDirection(word);
    if (direction) {
      // save the direction string to matches object
      parsedNoun.is_direction = direction;
    }

    // get the asset
    var asset = this.game.getAsset(word);

    // is the asset a substance?
    if (asset && asset instanceof AdventureJS.Assets.Substance) {
      // save the substance id to matches object
      parsedNoun.is_substance = asset.id;
    }

    // is the asset a dispenser?
    if (asset && asset.dispenser?.class) {
      // save the dispenser id to matches object
      parsedNoun.is_dispenser = asset.id;
    }

    // is the asset fungible?
    if (asset && asset.fungible?.dispensers?.length) {
      // save the fungible id to matches object
      parsedNoun.is_fungible = asset.id;
    }

    // Noun is direction and room has an exit in that direction.
    if (direction && "undefined" !== typeof room.exits[direction]) {
      // save the room's direction object id to matches object
      var exitID = room.exits[direction];
      parsedNoun.matches.all.push(exitID);
      parsedNoun.matches.qualified.push(exitID);
      parsedNoun.is_qualified = exitID;
    } else if (
      // Noun is direction and room does not have an exit in that direction.
      // Get global direction object.
      direction &&
      "undefined" === typeof room.exits[direction]
    ) {
      // global direction
      parsedNoun.matches.all.push("global_" + direction);
      parsedNoun.matches.qualified.push("global_" + direction);
    } // direction

    // all
    else if ("all" === word) {
      // was there a disambiguation prompt?
      // if so, all means all of the things in the prompt
      if (last_turn.disambiguate.enabled) {
        parsedNoun.matches.all =
          last_turn.verified_sentence[
            `phrase${last_turn.disambiguate.index}`
          ].parsedNoun.matches.qualified;
      } else {
        // otherwise get the current room's contents

        // @TODO 5/17/26 considering an option to set context
        // we should only get room contents if no other scope was specified
        // such as "take all from table" which should only get table's contents
        // currently handled at bottom of handleSentence.js
        let container = context || room;
        parsedNoun.matches.all = container.getAllNestedContents();
      }
    } else if (1 === word.split("&").length && 1 === word.split("=").length) {
      // User did not ask for "this AND that"
      // and world_lookup search returned single ID string
      parsedNoun.matches.all = this.game.parser.selectAll(word, context);
    } else if (1 < word.split("&").length || 1 < word.split("=").length) {
      // User asked for "this AND that"
      // or world_lookup search returned multiple IDs

      // Multiple inputs come from player input such as:
      // "get brass key and silver key"

      var multipleInput = word.split("&");
      for (var mI = 0; mI < multipleInput.length; mI++) {
        // Ambiguous inputs come from combineCompoundPhrases,
        // which searches world_lookup trying to match
        // player input with world objects.
        // It doesn't do disambiguation but just passes
        // along multiple values.
        //
        // Example:
        // brass_key=old_brass_key,new_brass_key,small_brass_key
        //
        // The item before the = is the original input.
        //
        var ambiguousInput = multipleInput[mI].split("=");
        if (1 === ambiguousInput.length) {
          parsedNoun.matches.all = parsedNoun.matches.all.concat(
            this.game.parser.selectAll(ambiguousInput[0])
          );
          this.game.log(
            "L1099",
            "log",
            "low",
            `[parseNoun.js] Unambiguous match: ${ambiguousInput[0]}`,
            "Parser"
          );
        } else {
          this.game.log(
            "L1100",
            "log",
            "low",
            "[parseNoun.js] Ambiguous matches:",
            "Parser"
          );
          var ambiguousMatches = ambiguousInput[1].split(",");
          parsedNoun.input = ambiguousInput[0];
          parsedNoun.normalized_input = ambiguousInput[0];
          parsedNoun.original_input = A.FX.denormalize(ambiguousInput[0]);

          for (var aM = 0; aM < ambiguousMatches.length; aM++) {
            var tempParsedNoun = this.game.parser.selectAll(
              ambiguousMatches[aM]
            );
            this.game.log(
              "L1101",
              "log",
              "low",
              `[parseNoun.js] tempParsedNoun: ${tempParsedNoun}`,
              "Parser"
            );
            for (var tPN = 0; tPN < tempParsedNoun.length; tPN++) {
              parsedNoun.matches.all.push(tempParsedNoun[tPN]);
            }
            this.game.log(
              "L1102",
              "log",
              "low",
              `[parseNoun.js] ambiguousMatches: ${ambiguousMatches[aM]}`,
              "Parser"
            );
          } // for loop
        } // if( 1 === ambiguousInput.length )
      } // for loop
    } // else if

    // get an array of objects
    // we clone all -> qualified so that we can delete
    // unqualified objects but still keep a record of all found
    parsedNoun.matches.qualified = Object.assign([], parsedNoun.matches.all);

    if (0 === parsedNoun.matches.all.length) {
      // no objects this word, but it might be a direction
      return parsedNoun;
    }

    // @NOTE looking at this block some time after it was commented,
    // I believe it was accepting the given raw word as gospel in the
    // case of an exact match, which meant that it might exclude other
    // assets before considering knowledge/reachability/etc.
    // Leaving it here as a reminder not to do it again.

    // for( var i = 0; i < parsedNoun.matches.all.length; i++)
    // {
    //   if( word === parsedNoun.matches.all[i] )
    //   {
    //     parsedNoun.matches.all = [word];
    //     parsedNoun.matches.qualified = [word];
    //   }
    // }

    if (1 === parsedNoun.matches.all.length) {
      // Exact match! Whoo hoo!
      // Though is_unambiguous seems like it would be the end result,
      // we still need to know if it's present/visible/reachable,
      // and also we do some further processing down the road for
      // substance containers and platonic dispensers, such that
      // is_unambiguous may not be the same as is_qualified.
      // In the case of substances and platonics, we can always refer
      // back to is_unambiguous for the actual asset, even if
      // is_qualified returns something else.
      parsedNoun.is_unambiguous = parsedNoun.matches.all[0];
    }

    //this.game.log( "log", "high", [ `[parseNoun.js] matches.all > ${parsedNoun.matches.all}` ] , 'Parser' );

    //
    // Is the input word plural?
    // Bearing in mind that we might have received
    // for example, the word "keys" which would simply mean "all keys"
    // or we might've received a compound string like
    // "grass_keys=red_grass_key,green_grass_key,blue_grass_key"
    //
    var lookup = this.game.world_lookup[parsedNoun.original_input];
    if ("undefined" !== typeof lookup) {
      parsedNoun.type = lookup.type;
      if ("singular" === parsedNoun.type) {
        parsedNoun.plural = lookup.plural;
      }
      if ("plural" === lookup.type) {
        parsedNoun.is_plural = true;
        parsedNoun.singular = lookup.singular;
      }
      if ("all" === lookup.singular) {
        parsedNoun.is_all = true;
      }
      if ("group" === lookup.type) {
        parsedNoun.is_group = true;
        parsedNoun.singular = lookup.singular;
      }
    }
    //if( -1 < word.indexOf( "=" ) ) {
    //  parsedNoun.plural = true;
    //}

    this.game.log(
      "L1103",
      "log",
      "high",
      `[parseNoun.js] parseNoun() return:\n${parsedNoun.matches.qualified}`,
      "Parser"
    );
    return parsedNoun;
  };
})();