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

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

  /**
   * @augments {adventurejs.Verb}
   * @class look
   * @ajsnode game.dictionary.verbs.look
   * @ajsconstruct MyGame.createVerb({ "name": "look", [...] });
   * @ajsconstructedby adventurejs.Dictionary#createVerb
   * @hideconstructor
   * @ajsinstanceof Verb
   * @ajsnavheading SensationVerbs
   * @summary Verb meaning look at an asset.
   * @tutorial Scripting_VerbSubscriptions
   * @tutorial Verbs_VerbAnatomy
   * @tutorial Verbs_VerbProcess
   * @tutorial Verbs_ModifyVerbs
   * @tutorial Verbs_WriteVerbs
   * @classdesc
   * <pre class="display border outline">
   * <span class="input">&gt; look behind moose head</span>
   * You look behind the stuffed and mounted moose head. You find
   * a brittle, age-stained business card for ACE TAXIDERMY.
   * Written on the back of the card in a shaky cursive script
   * is a name: ABRAHAM MANAHAN.
   * </pre>
   * <pre class="display border outline">
   * <span class="input">&gt; look in genie's bottle</span>
   * You look in the genie's bottle. The bottle glass is
   * dark green and smoky, almost opaque, but by turning
   * the glass this way and that in the light, you can
   * almost make out a dark powerful shape roiling inside it.
   * </pre>
   * <pre class="display border outline">
   * <span class="input">&gt; look on car roof</span>
   * You look on the car roof. And a good thing you did,
   * because you almost left the baby in its basket there.
   * </pre>
   * <pre class="display border outline">
   * <span class="input">&gt; look under left foot</span>
   * You look under your left foot. In the immemorial time
   * since you took up your position, an underground ecology
   * has established there. Blind creatures lay revealed
   * beneath your giant raised foot, and writhe away from
   * the sunlight. An underground spring fills in the depression
   * where your heel pressed into the earth.
   * </pre>
   * <strong>look</strong> can be used in a number of ways.
   * <ul>
   * <li><strong>look</strong> by itself returns a description of the room from
   * room.descriptions.look, along with a list of things in the room. If those
   * things have aspects, ex: asset.aspects.on.list_in_room set to true, their
   * contents will also be listed</li>
   * <li><strong>look at asset</strong> returns a description of the asset from
   * asset.descriptions.look</li>
   * <li><strong>look at asset</strong> can be configured so that it returns
   * different values depending on the subject's position. For example if subject
   * is in a tree, "look at asset" will try to find a description at
   * asset.descriptions.look["from tree"]</li>
   * <li><strong>look in asset</strong>, used with "in" or any other preposition,
   * will try to find a description at asset.descriptions[preposition]</li>
   * <li><strong>look at asset through another asset</strong>, used with through or
   * any other preposition, will try to find a description at
   * asset.descriptions.look["through other asset"]</li>
   * </ul>
   * @ajsverbphases doBeforeTry, doAfterTry, doBeforeSuccess, doAfterSuccess
   * @TODO "look at" doesn't make attached assets known as "examine" does
   */
  A.Preverbs.look = {
    name: "look",
    prettyname: "look",
    past_tense: "looked",
    synonyms: ["look", "l"],
    gerund: "looking",

    /**
     * @ajsverbstructures
     * @memberof look
     */
    accepts_structures: [
      "verb", // look

      "verb noun",
      // look east
      // look asset - politely accept this to mean look at asset

      "verb preposition", // look up/down/in/out

      "verb preposition noun",
      // look in/under/at/through/etc asset

      // @TODO "look out at playground"

      "verb noun preposition noun",
      // look east through telescope

      "verb preposition noun preposition noun",
      // look at asset with binoculars
      // look through binoculars at asset
      // look at water in bucket

      "verb preposition noun preposition noun preposition noun",
      // look at water on slide with microscope
    ],

    /**
     * @ajsadverbs
     * @memberof look
     */
    accepts_adverbs: [
      "carefully",
      // "up",
      // "down",
      "left",
      "right",
      "back",
      "backward",
      "forward",
      "towards",
      "upstairs",
      "downstairs",
      "sideways",
      "over",
    ], //, "in", "out"

    /**
     * @memberof look
     * @ajsverbphrase
     * phrase1:
     * {
     *   accepts_noun:true,
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *     singular: false,
     *   },
     *   accepts_preposition: true,
     *   accepts_preposition_without_noun: true,
     * },
     */
    phrase1: {
      accepts_noun: true,
      noun_must_be: {
        known: true,
        matter: true,
        present: true,
        visible: true,
      },
      accepts_preposition: true,
      accepts_preposition_without_noun: true,
    },

    /**
     * @memberof look
     * @ajsverbphrase
     * phrase2:
     * {
     *   accepts_noun:true,
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *   },
     *   accepts_preposition: true,
     *   requires_preposition: true,
     * },
     */
    phrase2: {
      accepts_noun: true,
      noun_must_be: {
        known: true,
        tangible: true,
        present: true,
        visible: true,
      },
      accepts_preposition: true,
      requires_preposition: true,
    },

    /**
     * @memberof look
     * @ajsverbphrase
     * phrase3:
     * {
     *   accepts_noun:true,
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *   },
     *   accepts_preposition: true,
     *   requires_preposition: true,
     * },
     */
    phrase3: {
      accepts_noun: true,
      noun_must_be: {
        known: true,
        tangible: true,
        present: true,
        visible: true,
      },
      accepts_preposition: true,
      requires_preposition: true,
    },

    /**
     * @memberof look
     * @ajsverbparams
     * with_params: {
     *   worn: false,
     *   held: false,
     * },
     */
    with_params: {
      worn: false,
      held: false,
      must_be_worn: false,
      must_be_held: false,
    },

    doTry: function () {
      var input = this.game.getInput();
      var subject = input.getSubject();
      var verb_phrase = input.verb_phrase;
      var adverb = input.getAdverb();
      var room = this.game.getCurrentRoom();

      var direct_object = input.getAsset(1);
      var direct_preposition = input.getPreposition(1);
      var direct_substance = input.getSubstance(1);
      var direct_container;

      var indirect_object = input.getAsset(2);
      var indirect_preposition = input.getPreposition(2);
      var indirect_substance = input.getSubstance(2);
      var indirect_container;

      var indirect_object2 = input.getAsset(3);
      var indirect_preposition2 = input.getPreposition(3);
      var indirect_substance2 = input.getSubstance(3);
      var indirect_container2;

      var msg = "";
      var results;

      var atcount = 0;

      var wornoptics, lightemitters;

      if (!subject.can.see) {
        this.game.debug(
          `D2135 | ${this.name}.js | ${subject.id}.can.see is unset`
        );
        msg += `$(We) can't see anything. `;
        this.handleFailure(msg);
        return null;
      }

      // we're interpreting any version of "look asset" as "look at asset"
      if (direct_object && !direct_preposition) {
        // direct_preposition = "at";
        // input.setPreposition(1, "at");
        // input.updateStructure();
      }

      /**
       * AUTOMATICALLY FIND VIEW MODIFIERS
       * Is player wearing any viewports (like eyeglasses)?
       * Are there any non-ambient light sources (flashlight, candle, etc)?
       * Is player on a viewpoint (a nest from which the view is different)?
       */

      // include adverb
      if (adverb) {
        input.pushViewModifier(adverb, null, "input");
      }

      // include any worn lenses
      wornoptics = subject.getWornOptics();
      for (let i = 0; i < wornoptics.length; i++) {
        this.pushViewModifierAndDeletePhrase("through", wornoptics[i], "auto");
      }

      // include anything emitting light
      lightemitters = room.findNestedAssetsWithProperty("is.emitting_light");
      for (let i = 0; i < lightemitters.length; i++) {
        let light = lightemitters[i];
        if (
          light.must.hold_to_see_with &&
          !this.game.parser.selectInHands(light.id).length
        ) {
          continue;
        }
        if (
          light.must.wear_to_see_with &&
          !this.game.parser.selectInHands(light.id).length
        ) {
          continue;
        }
        this.pushViewModifierAndDeletePhrase("with", light, "auto");
      }

      // include subject nest
      if (subject.isNested()) {
        this.pushViewModifierAndDeletePhrase(
          "from",
          subject.getNestAsset(),
          "auto"
        );
      }

      // include current room
      input.pushViewModifier("in", this.game.getCurrentRoom(), "auto");

      /**
       * PRE-PROCESS EACH INPUT PHRASE IN ISOLATION
       * Usually we process verbs by sentence structure, but sentences
       * using "look" may be constructed in a variety of ways, and
       * the asset that we'd consider the direct object may not be
       * in the first phrase.
       * Verify that there's a target "at" object, and restructure
       * if needed to ensure that view target is direct object.
       * Then look for modifiers: viewports (like eyeglasses),
       * light sources (like flashlights),
       * viewpoints (nests from which the view is different),
       * and containers (in the case of substances).
       * If player has explicitly named modifiers that we've
       * automatically accounted for, disregard that input.
       */
      for (let phrase = input.getPhraseCount(); phrase > 0; phrase--) {
        let prep = input.getPreposition(phrase);
        let asset = input.getAsset(phrase);
        let substance = input.getSubstance(phrase);
        let nest;

        // if player input "look at substance in container"
        // we want to note it because we'll be treating
        // "in container" as a view modifier and deleting that phrase
        // but we may tailor the response if player explicitly
        // named the container vs finding it automatically
        if (
          substance &&
          input.getPhrase(phrase + 1) &&
          input.getPreposition(phrase + 1) === "in"
        ) {
          // console.warn("SUBSTANCE IN CONTAINER!!!");
          input.verified_sentence[
            `phrase${phrase}`
          ].player_specified_container = true;
        }

        switch (prep) {
          case "at":
            atcount++;
            // are multiple prepositions "at" ?
            // we don't handle any version of that
            // alt unused method: if (A.hasDuplicates(prepositions, "at")) {
            if (atcount > 1) {
              this.game.debug(
                `D1595 | ${this.name}.js | input includes multiple "at" targets `
              );
              msg += `That doesn't even make sense. `;
              this.handleFailure(msg);
              return null;
            }
            if (asset.hasClass("Tangible")) {
              this.pushViewModifierAndDeletePhrase(
                asset.getNestOrPlacePreposition(),
                asset.getNestOrPlaceAsset(),
                "auto"
              );
            }
            break;
          case "with":
            // is asset a light source or a viewport?
            if (asset.is.viewport) {
              results = this.tryThroughViewport(asset);
              if ("undefined" !== typeof results) return results;
              this.pushViewModifierAndDeletePhrase("through", asset, "input");
            } else if (asset.is.light_source && asset.is.on) {
              // add asset to modifiers if it isn't already there
              // if it's a viewport use "through"
              results = this.tryWith(asset);
              if ("undefined" !== typeof results) return results;
              this.pushViewModifierAndDeletePhrase(
                asset.is.viewport ? "through" : "with",
                asset,
                "input"
              );
            } else if (asset.is.light_source && !asset.is.on) {
              // do nothing?
              // you can still look at the thing
              // though the (ex: unlit candle) will reveal nothing new
              // delete phrase?
              // I think leave it because if doSuccess sees it without
              // it being a modifier, that's how it knows nothing new is revealed
            } else {
              // can "look with" asset?
              results = this.tryWith(asset);
              if ("undefined" !== typeof results) return results;
            }
            break;
          case "through":
            // is asset a viewport or has a through description?
            if (asset.is.viewport) {
              results = this.tryThroughViewport(asset);
              if ("undefined" !== typeof results) return results;
              // add asset to modifiers if it isn't already there
              this.pushViewModifierAndDeletePhrase("through", asset, "input");
            } else {
              // can "look through" asset?
              // is it right to ask this here when we're out of word sequence?
              results = this.tryThroughDescription(asset);
              if ("undefined" !== typeof results) return results;
            }
            break;
          case "from":
            // is subject nested within asset?
            results = this.tryFrom(asset);
            if ("undefined" !== typeof results) return results;
            this.pushViewModifierAndDeletePhrase("from", asset, "input");
            break;
          case "in":
          case "on":
          case "under":
          case "behind":
            // we'll handle these next,
            // in the normal parse-by-sentence-structure blocks
            break;
          default:
            // ditto
            break;
        }
      }

      /**
       * PARSE BY SENTENCE STRUCTURE
       * The remainder of doTry follows standard verb handling.
       */

      // sentence structure: verb
      if (input.hasStructure("verb")) {
        input.setPreposition(1, "at");
        input.setAsset(1, room);
        input.updateStructure();
        return true;
      } // verb

      // sentence structure: verb preposition
      if (input.hasStructure("verb preposition")) {
        if (this.game.dictionary.getDirection(direct_preposition)) {
          let exit = this.game.getExitFromDirection(direct_preposition);
          // let exit = this.game.getCurrentRoom().exits[direct_preposition];
          if (exit) exit = this.game.getAsset(exit);
          if (exit) {
            input.setAsset(1, exit);
            input.setPreposition(1, "at");
            input.setStructure("verb preposition noun");
          }
          return true;
        }
        this.game.debug(
          `D1100 | ${this.name}.js | received preposition/${direct_preposition} with no noun, soft prompt noun1`
        );
        input.setSoftPrompt({
          index: 1,
          type: "noun",
          noun1: true,
        });
        msg += `Look ${direct_preposition} what? `;
        this.handleFailure(msg);
        return null;
      } // verb preposition

      /**
       * RESET LOCAL VARS
       * though we initialized all our objects/prepositions
       * at the beginning of doTry, it's likely that some
       * or all of them were mutated during the modifier search
       * and pre-processing by phrase.
       */

      direct_object = input.getAsset(1);
      direct_preposition = input.getPreposition(1);
      direct_substance = input.getSubstance(1);

      indirect_object = input.getAsset(2);
      indirect_preposition = input.getPreposition(2);
      indirect_substance = input.getSubstance(2);

      indirect_object2 = input.getAsset(2);
      indirect_preposition2 = input.getPreposition(2);
      indirect_substance2 = input.getSubstance(2);

      /**
       * CHECK ORDER OF ASSETS IN SENTENCE STRUCTURE
       * This check may be redundant after the modifier
       * search and pre-processing, but we still need to
       * verify that the view target is our direct object.
       * If it's not, we need to reorder our phrases.
       */
      let reordered = false;

      if (
        input.hasStructure("verb preposition noun preposition noun") &&
        indirect_preposition === "at"
      ) {
        // assuming player input something like
        // look "through telescope at harbor" which is reasonable
        // but we prefer "look at harbor through telescope"
        input.swapPhrases(1, 2);
        reordered = true;
      }

      if (
        input.hasStructure(
          "verb preposition noun preposition noun preposition noun"
        ) &&
        indirect_preposition === "at"
      ) {
        // assuming player input something like
        // "look through telescope at boat in harbor" which is reasonable
        // but we prefer "look at boat in harbor through telescope"
        reordered = true;
        // towers of hanoi
        input.swapPhrases(1, 4);
        input.swapPhrases(2, 1);
        input.swapPhrases(3, 2);
        input.swapPhrases(4, 3);
        input.deletePhrase(4);
      }

      if (
        input.hasStructure(
          "verb preposition noun preposition noun preposition noun"
        ) &&
        indirect_preposition2 === "at"
      ) {
        // it seems unlikely that anyone would use this sentence structure
        // but we'll still move "at asset" to direct object
        reordered = true;
        // towers of hanoi, again
        input.swapPhrases(3, 4);
        input.swapPhrases(2, 3);
        input.swapPhrases(1, 2);
        input.swapPhrases(4, 1);
        input.deletePhrase(4);
      }

      if (reordered) {
        // reset our local vars
        this.game.debug(
          `D1852 | ${this.name}.js | reordering phrases in sentence`
        );
        direct_object = input.getAsset(1);
        direct_preposition = input.getPreposition(1);
        direct_substance = input.getSubstance(1);

        indirect_object = input.getAsset(2);
        indirect_preposition = input.getPreposition(2);
        indirect_substance = input.getSubstance(2);

        indirect_object2 = input.getAsset(2);
        indirect_preposition2 = input.getPreposition(2);
        indirect_substance2 = input.getSubstance(2);
      }

      /**
       * CHECK FOR SUBSTANCES
       * Are any of our objects substances? If player input
       * a substance, such as "look at sand", parser will have
       * found a container and passed it along in the form of
       * "asset:aspect:substance" such as "bucket:in:sand"
       */

      if (direct_substance) {
        // finding a direct_substance means player input something like
        // "look at water" and the parser inferred a container as direct object
        // we want to treat the substance as our direct object while still
        // keeping a reference to the container
        direct_container = direct_object;
        direct_object = direct_substance;
      }

      if (indirect_substance) {
        // finding an indirect_substance means player input something like
        // "look at ducky in water" and the parser inferred a container as indirect object
        // we want to treat the substance as our indirect object while still
        // keeping a reference to the container
        indirect_container = indirect_object;
        indirect_object = indirect_substance;
      }

      if (indirect_substance2) {
        // can't think of a case for this, but covering bases
        indirect_container2 = indirect_object2;
        indirect_object2 = indirect_substance2;
      }

      // verb enabled for direct object?
      // seems unlikely that it wouldn't be, but anything's possible
      if (!direct_object.isDOV(this.name)) {
        this.game.debug(
          `D2127 | ${this.name}.js | ${direct_object.id}.dov.${this.name}.enabled is false `
        );
        msg += `$(We) can't ${this.name} ${
          direct_preposition ? direct_preposition : ""
        } ${direct_object.articlename}. `;
        this.handleFailure(msg);
        return null;
      }

      // single use direct object?
      // it seems unlikely you'd only be able to look at
      // something once, but that's our universal pattern,
      // which means we've committed to supporting it everywhere
      // and maybe someone wants to make "you can only look once"
      // the central conceit of their game
      if (
        direct_object.allowVerbOnce(this.name, "dov") &&
        direct_object.didVerb(this.name, "dov")
      ) {
        this.game.debug(
          `D1659 | ${this.name}.js | ${direct_object.id}.dov.${this.name}.once and ${direct_object.id}.did.${this.name}.directly `
        );
        msg += `$(We've) ${this.past_tense} ${
          direct_preposition ? direct_preposition : "at"
        } ${direct_object.articlename} enough. `;
        this.handleFailure(msg);
        return false;
      }

      // is direct object a substance?
      if (direct_substance) {
        this.game.debug(`D2142 | ${this.name}.js | found substance `);
        // is indirect_object also a substance?
        if (indirect_substance) {
          // currently we don't handle "look at substance in substance"
          // @TODO there might be situations where this makes sense
          // such as a body of water with sand at its bottom "examine sand in water"
          // or a colloidal suspension - or would that be a new substance?
          this.game.debug(
            `D1589 | ${this.name}.js | ${direct_object.id} and ${indirect_object.id} are both substances `
          );
          msg += `$(We) can't see any ${direct_substance.name} in ${indirect_substance.name}. `;
          this.handleFailure(msg);
          return null;
        } // indirect_substance

        if (indirect_object) {
          // if player input look at "substance in container"
          // parser will have supplied a container that is not necessarily
          // the same one specified by player
          // so confirm that the specified container contains substance
          // and if parser has supplied a different container, update that

          if (indirect_preposition === "in") {
            // @TODO reminder that, though we pay lip service to storing
            // substances in aspects other than "in", currently "in" is
            // still the only aspect that holds substances
            // if (["in","on","under","behind"].includes(indirect_preposition)) {
            let contained = indirect_object.containsSubstanceAt(
              direct_substance.id,
              indirect_preposition
            );

            if (!contained) {
              // if player input "look at substance in container"
              // and the container doesn't actually contain substance
              // if neither contained nor described then we've got nothing to work with
              this.game.debug(
                `D1651 | ${this.name}.js | ${indirect_object.id}${
                  indirect_preposition ? ".aspects." + indirect_preposition : ""
                } doesn't contain ${direct_substance.id} `
              );
              msg += `$(We) can't see any ${direct_substance.name} ${
                indirect_preposition ? indirect_preposition : "in"
              } ${indirect_object.articlename}. `;
              this.handleFailure(msg);
              return null;
            }

            // did parser identify a different substance container
            // from the one specified by player?
            if (direct_container.id !== indirect_object.id) {
              input.setContainer(1, indirect_object);
              direct_container = indirect_object;
            }
            input.deletePhrase(2);
          } // in
        }

        // if we get here, we have a substance and a container
        input.pushViewModifier("in", direct_container, "auto"); // it's auto even if player specified container

        return true;
      } // substance

      if (
        input.hasStructure("verb preposition noun") &&
        input.parsedNoun1.matches.direction
      ) {
        // @TODO "look east" returns standing_room_east description
        // "look east exit" returns blank

        if ("at" === direct_preposition) {
          return true;
        }
        if (!direct_object.hasDescription(direct_preposition)) {
          this.game.debug(
            `D1587 | ${this.name}.js | ${direct_object.id}.descriptions.${direct_preposition} is unset`
          );
          msg += `$(We) $(don't) know how to ${this.name} ${direct_preposition} ${direct_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }
        return true;
      }

      // sentence structure: verb preposition noun
      if (input.hasStructure("verb preposition noun")) {
        // no blockers, pass to success
        if ("at" === direct_preposition) {
          return true;
        }

        if (
          ["in", "on", "under", "behind"].includes(direct_preposition) &&
          !direct_object.hasAspectAt(direct_preposition)
        ) {
          this.game.debug(
            `D2141 | ${this.name}.js | ${direct_object.id}.aspects.${direct_preposition} is unset`
          );
          msg += `$(We) $(don't) see anything ${direct_preposition} ${direct_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }

        // no aspect for this preposition
        if (
          !direct_object.hasDescription(direct_preposition) &&
          !direct_object.hasAspectAt(direct_preposition)
        ) {
          this.game.debug(
            `D1017 | ${this.name}.js | ${direct_object.id}.aspects.${direct_preposition} is not defined`
          );
          msg += `$(We) can't see anything ${direct_preposition} ${direct_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }

        return true;
      } // verb noun / verb preposition noun

      // sentence structure: verb noun preposition noun / verb preposition noun preposition noun
      else if (
        input.hasStructure("verb noun preposition noun") ||
        input.hasStructure("verb preposition noun preposition noun")
      ) {
        // "look at x with y" and "look at x through y" are the only
        // versions of this we accept

        // what about "look through window with telescope" ?

        // this should get caught by sentence structure check
        // in parseInput.js but leaving it here for now
        if (!indirect_preposition) {
          this.game.debug(`D1228 | ${this.name}.js | no indirect_preposition`);
          msg += `$(We) can't look ${direct_preposition} ${direct_object.articlename} ${indirect_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }

        if (
          indirect_preposition === "with" &&
          indirect_object.hasQuirk("look_with_means_look_through")
        ) {
          this.game.debug(
            `D1848 | ${this.name}.js | direct_preposition 'with' means 'through'`
          );
          indirect_preposition = "through";
          input.setPreposition(2, "through");
        }

        if (indirect_preposition === "with") {
          results = this.tryWith(indirect_object);
          if ("undefined" !== typeof results) return results;
        } else if (indirect_preposition === "through") {
          this.game.debug(
            `D1849 | ${this.name}.js | direct_preposition 'through'`
          );
          results = this.tryThrough(indirect_object);
          if ("undefined" !== typeof results) return results;
        }

        // from is a special case where subject
        // must be nested in the object
        else if (indirect_preposition === "from") {
          this.game.debug(
            `D1850 | ${this.name}.js | direct_preposition 'from'`
          );
          results = this.tryFrom(indirect_object);
          if ("undefined" !== typeof results) return results;
        } // from

        // TRY > TWONOUNS > AT > NO INDIRECT ASPECT
        // assume indirect preposition is aspect
        // and check for it
        else if (!indirect_object.hasAspectAt(indirect_preposition)) {
          this.game.debug(
            `D1226 | ${this.name}.js | ${indirect_object.id} has no ${indirect_preposition} aspect`
          );
          msg += `${direct_object.Articlename_is} not ${indirect_preposition} ${indirect_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }

        // TRY > TWONOUNS > AT > NO PLACE
        // if indirect_object has aspect, check if direct_object is in it
        else if (
          indirect_object.hasAspectAt(indirect_preposition) &&
          !direct_object.isPlacedAtAspectAndAsset(
            indirect_preposition,
            indirect_object.id
          )
        ) {
          this.game.debug(
            `D1225 | ${this.name}.js | ${direct_object.id} not ${indirect_preposition} ${indirect_object.id}`
          );
          msg += `${direct_object.Articlename_is} not ${indirect_preposition} ${indirect_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        } else if (
          direct_preposition !== "at" &&
          !direct_object.hasAspectAt(direct_preposition)
        ) {
          this.game.debug(
            `D1227 | ${this.name}.js | ${direct_preposition} is not at`
          );
          msg += `$(We) can't look ${indirect_preposition} ${indirect_object.articlename} ${direct_preposition} ${direct_object.articlename}. `;
          this.handleFailure(msg);
          return null;
        }
      } // verb noun preposition noun / verb preposition noun preposition noun

      return true;
    },

    doSuccess: function () {
      var input = this.game.getInput();
      var subject = input.getSubject();
      var verb_phrase = input.verb_phrase;
      var adverb = input.getAdverb();
      var nest_asset = subject.getNestAsset();
      var currentRoom = this.game.getCurrentRoom();
      var msg = "";

      var direct_object = input.getAsset(1);
      var direct_preposition = input.getPreposition(1);
      var direct_substance = input.getSubstance(1);
      var direct_container;
      var direct_container_aspect;
      var player_specified_container =
        input.verified_sentence.phrase1.player_specified_container;

      var indirect_object = input.getAsset(2);
      var indirect_preposition = input.getPreposition(2);
      var indirect_substance = input.getSubstance(2);
      var indirect_container;

      var indirect_object2 = input.getAsset(3);
      var indirect_preposition2 = input.getPreposition(3);
      var indirect_substance2 = input.getSubstance(3);
      var indirect_container2;

      if (subject.id !== this.game.getPlayer().id) {
        msg += `${subject.articlename} glances 
          ${
            direct_object && direct_object.hasClass("Room")
              ? "around"
              : direct_object
                ? "towards " + direct_object.articlename
                : "around"
          }. `;
        return this.handleSuccess(msg, direct_object);
      }

      if (direct_substance) {
        direct_container = direct_object;
        direct_object = direct_substance;
        direct_container_aspect = direct_container.getVessel().id;
      }

      if (indirect_substance) {
        indirect_container = indirect_object;
        indirect_object = indirect_substance;
      }

      if (indirect_substance2) {
        indirect_container2 = indirect_object2;
        indirect_object2 = indirect_substance2;
      }

      // room?
      if (direct_object.id === currentRoom.id) {
        // if(msg) this.game.print(msg);
        this.game.printCurrentRoom({ verbose: true });
        return this.handleSuccess(msg, direct_object);
      }

      // global?
      else if (direct_object.is.global) {
        msg += this.game.getGlobalAssetDescription(direct_object);
      } // is.global

      // direction?
      else if (direct_object.direction || direct_object.hasClass("Exit")) {
        msg +=
          this.game.getModifiedDescription({
            asset: direct_object,
            identifier: direct_preposition,
            fallback_base: true,
          }) ||
          `$(We) see a passage leading ${direct_object.direction}${
            direct_object.destination && direct_object.is.used
              ? " to the " + direct_object.destination
              : ""
          }. `;
      } // direction

      // substance
      else if (direct_substance) {
        // did player specify the container?
        // if not, identify the container we chose
        if (!player_specified_container) {
          if (direct_container.is.reservoir) {
            // if it's a reservoir print it as an inference, because duh
            // this.game.print(
            //   `(in ${direct_container.articlename})`,
            //   "inference"
            // );
            msg += `$(We) consider the ${direct_substance.name} of ${direct_container.articlename}. `;
          } else {
            // otherwise prepend output to indicate what we found
            msg += `$(We) see some ${direct_substance.name} ${direct_container_aspect} ${direct_container.articlename}. `;
          }
        }

        // if player input "look in water" convert to "look at water"
        if ("in" === direct_preposition) {
          direct_preposition = "at";
        }

        if ("at" === direct_preposition) {
          msg += this.game.getModifiedDescription({
            asset: direct_object,
            identifier: direct_preposition,
            fallback_base: true,
          });

          // also print the contents of its container
          direct_container.setAspectContentsKnown(
            direct_container_aspect,
            true
          );
          if (
            !direct_container.hasClass("Room") &&
            direct_container.aspects[direct_container_aspect].list_in_examine
          ) {
            msg += direct_container.getPrintableListOfContentsAt(
              direct_container_aspect,
              {
                exclude_substance: true,
              }
            );
          }
        } else {
          this.game.debug(
            `D1048 | ${this.name}.js | not at, ${direct_preposition} ${direct_substance.id}`
          );

          msg += this.game.getModifiedDescription({
            asset: direct_substance,
            identifier: direct_preposition,
            fallback_base: true,
          });

          direct_container.setAspectContentsKnown(
            direct_container_aspect,
            true
          );

          if (!direct_container.hasClass("Room")) {
            msg += direct_container.getPrintableListOfContentsAt(
              direct_container_aspect,
              {
                caller: "examine",
                exclude_substance: true,
              }
            );
          }
          if (!msg) {
            msg += `Looking ${adverb ? adverb : ""} ${
              direct_preposition ? direct_preposition : "at"
            } ${direct_object.articlename} doesn't reveal anything notable. `;
          }
        }
      }

      // tangible / general conditions
      else {
        if (
          "in" === direct_preposition &&
          direct_object.isDOV("close") &&
          direct_object.is.closed &&
          1 >= direct_object.appearance.opacity
        ) {
          // closed things need to be open or transparent to see inside
          msg +=
            this.game.getModifiedDescription({
              asset: direct_object,
              identifier: direct_preposition,
            }) ||
            `$(We) can't see into ${direct_object.articlename} while it's closed. `;
        }

        if ("at" === direct_preposition) {
          msg += this.game.getModifiedDescription({
            asset: direct_object,
            identifier: direct_preposition,
            fallback_base: true,
          });

          // if it's tangible, also print a complete aspect list
          this.game.log(
            "L1330",
            "log",
            "high",
            `${this.name}.js > at: print ${direct_object.id}.aspects`,
            "Verbs"
          );

          for (var i = 0; i < this.game.dictionary.prepositions.length; i++) {
            var preposition = this.game.dictionary.prepositions[i];
            if (direct_object.hasAspectAt(preposition)) {
              if (
                preposition === "in" &&
                direct_object.isDOV("open") &&
                direct_object.is.closed &&
                direct_object.appearance.opacity === 1
              ) {
                // it's closed and opaque
                continue;
              }
              direct_object.setAspectContentsKnown(preposition, true);
              if (!direct_object.aspects[preposition].list_in_examine) {
                continue;
              }
              msg += direct_object.getPrintableListOfContentsAt(
                preposition,
                {}
              );
            }
          }
        } else if (
          direct_object.hasAspectAt(direct_preposition) ||
          direct_object.hasDescription(direct_preposition)
        ) {
          this.game.debug(
            `D1051 | ${this.name}.js | not at, hasAspectAt || hasDescription`
          );

          msg += this.game.getModifiedDescription({
            asset: direct_object,
            identifier: direct_preposition,
            fallback_base: false,
          });

          if (direct_object.hasAspectAt(direct_preposition)) {
            direct_object.setAspectContentsKnown(direct_preposition, true);
            msg +=
              (direct_object.getPrintableListOfContentsAt &&
                direct_object.getPrintableListOfContentsAt(direct_preposition, {
                  caller: "examine",
                  exclude_substance: true,
                })) ||
              ``;
          }
        } else {
          msg +=
            (direct_object.getPrintableListOfContents &&
              direct_object.getPrintableListOfContents({
                caller: "examine",
              })) ||
            `Looking ${adverb ? adverb : ""} ${
              direct_preposition ? direct_preposition : "at"
            } ${direct_object.articlename} doesn't reveal anything notable. `;
        }

        // anything written on it?
        if (
          direct_object.isDOV("write") &&
          direct_object.written_strings.length &&
          direct_object.append_written_strings_to_description
        ) {
          msg += `Written on ${direct_object.articlename}`;

          if (1 === direct_object.written_strings.length) {
            msg += ` is a phrase: ${direct_object.written_strings[0]}`;
          } else {
            msg += ` are several phrases: `;
            for (var i = 0; i < direct_object.written_strings.length; i++) {
              msg += direct_object.written_strings[i];
              if (direct_object.written_strings.length > i + 1) msg += `, `;
            }
          }
          msg += `. `;
        }

        if (
          direct_object.is.typing_target &&
          direct_object.written_strings.length &&
          direct_object.append_written_strings_to_description
        ) {
          if (1 === direct_object.written_strings.length) {
            msg += `A line of input has been entered on the screen: `;
            msg += `<br> ${direct_object.written_strings[0]}`;
          } else {
            msg += `Several lines of input have been entered on the screen: `;
            for (var i = 0; i < direct_object.written_strings.length; i++) {
              msg += `<br> ${direct_object.written_strings[i]}`;
            }
          }
        }

        // connections
        if (direct_object.isConnectedToAnything("tie", "to_iov")) {
          // tie this to that
          let objects = {
            objects: direct_object.getVerbConnections("tie", "to_iov"),
          };
          msg += `${direct_object.Articlename_is} tied to `;
          msg += this.game.getPrintableObjectList(objects);
          msg += direct_object.isWithin(subject)
            ? `, while $(we) hold an end of it`
            : ``;
          msg += `. `;
          // @TODO draw distinction between carrying vs holding
        } else if (direct_object.isConnectedToAnything("tie", "to_dov")) {
          // tie that to this
          let connections = direct_object.getVerbConnections("tie", "to_dov");
          for (let r = 0; r < connections.length; r++) {
            let rope = this.game.getAsset(connections[r]);
            msg += `Tied to ${direct_object.articlename} is a ${rope.name}`;
            if (1 < rope.getVerbConnectionCount("tie", "to_iov")) {
              let objects = {
                objects: rope.getVerbConnections("tie", "to_iov"),
                exclusions: direct_object.id,
              };
              msg += `, which is also tied to ${this.game.getPrintableObjectList(
                objects
              )}`;
            }
            msg += rope.isWithin(subject)
              ? `, while $(we) hold an end of it`
              : ``;
            msg += `. `;
          }
        }
      } // general tangible

      if (input.view_modifiers.length) {
        let intro = "";
        let outro = "";
        let middle = "";
        let count = 0;
        let adverb_modifier = "";
        for (let i = 0; i < input.view_modifiers.length; i++) {
          let modifier = input.view_modifiers[i];
          if (modifier.type === "input" && !modifier.used) {
            if (modifier.adverb) {
              adverb_modifier = modifier.identifier;
              continue;
            }
            let comma = count === 0 ? "" : ",";
            count++;
            middle += comma;
            middle += ` ${modifier.identifier} ${
              modifier.asset ? modifier.asset.articlename : ""
            }`;
            middle += comma;
          }
        }
        intro += `Looking ${adverb_modifier ? adverb_modifier : ""} `;
        intro += `${direct_preposition ? direct_preposition : "at"} `;
        intro += `${direct_object.articlename}`;
        outro += " reveals nothing new. ";
        if (count > 1) middle = ", " + middle;
        if (middle) msg = intro + middle + outro + msg;
      }

      if (!msg) {
        // fallback in case there is absolutely no description
        this.game.debug(`D1031 | ${this.name}.js | no description found`);
        msg += `Looking ${adverb ? adverb : ""} ${
          direct_preposition ? direct_preposition : "at"
        } ${direct_object.articlename} doesn't reveal anything notable. `;
      }
      return this.handleSuccess(msg, direct_object);
    }, // doSuccess

    tryThroughViewport: function (asset) {
      var msg = "";

      // @todo If item is in inventory, not worn, see if it can be
      // worn, allow it, then take it out on doSuccess.
      if (asset.must.wear_to_look_through && !asset.is.worn) {
        this.game.debug(
          `D1015 | ${this.name}.js | ${asset.id}.must.wear_to_look_through and ${asset.id}.is.worn is false`
        );
        msg += `$(We're) not wearing ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }

      // @todo If item is in inventory, not in hands, see if it can be
      // taken, allow it, then take it out on doSuccess.
      if (
        asset.must.hold_to_see_through &&
        !this.game.parser.selectInHands(asset.id).length
      ) {
        this.game.debug(
          `D1014 | ${this.name}.js | ${asset.id}.must.hold_to_see_through and ${asset.id} is not held`
        );
        msg += `$(We're) not holding ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }
    },

    tryThroughDescription: function (asset) {
      var msg = "";

      if (!asset.hasDescription("through")) {
        this.game.debug(
          `D1851 | ${this.name}.js | ${asset.id}.descriptions.through not found`
        );
        if (1 < asset.appearance.opacity) {
          msg += `Though ${asset.articlename} is 
              ${
                asset.appearance.opacity > 0.5 ? "translucent" : "transparent"
              }, 
              $(we) can't ${this.name} through it. `;
        } else {
          msg += `$(We) can't ${this.name} through ${asset.articlename}. `;
        }
        this.handleFailure(msg);
        return null;
      }
    },

    tryWith: function (asset) {
      var msg = "";
      // can subject look with this object?
      if (!asset.isIOV("look")) {
        this.game.debug(
          `D1224 | ${this.name}.js | ${asset.id}.iov.look.enabled is false`
        );
        msg += `$(We) can't look with ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }

      // @todo If item is in inventory, not worn, see if it can be
      // worn, allow it, then take it out on doSuccess.
      if (asset.must.wear_to_look_with && !asset.is.worn) {
        this.game.debug(
          `D1845 | ${this.name}.js | ${asset.id}.must.wear_to_look_with and ${asset.id}.is.worn is false`
        );
        msg += `$(We're) not wearing ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }

      // @todo If item is in inventory, not in hands, see if it can be
      // taken, allow it, then take it out on doSuccess.
      if (
        asset.must.hold_to_see_with &&
        !this.game.parser.selectInHands(asset.id).length
      ) {
        this.game.debug(
          `D2140 | ${this.name}.js | ${asset.id}.must.hold_to_see_with and ${asset.id} is not held`
        );
        msg += `$(We're) not holding ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }
    },

    tryFrom: function (asset) {
      var msg = "";
      // is subject nested in the object? that's the only way to look from it
      // if so just let it go so we can print that
      if (
        asset.id === this.game.getInput().getSubject().getNestOrPlaceAsset().id
      ) {
        return;
      } else {
        this.game.debug(
          `D1016 | ${this.name}.js | subject not nested within ${asset.id}`
        );
        msg += `$(We're) not ${asset.default_aspect} ${asset.articlename}. `;
        this.handleFailure(msg);
        return null;
      }
    },

    // view modifiers such as lenses and light sources are saved
    // to the input object for use with getModifiedDescription()
    pushViewModifierAndDeletePhrase: function (identifier, asset, type) {
      let input = this.game.getInput();
      for (let m = 0; m < input.view_modifiers.length; m++) {
        // it's possible that this asset was already found
        // in which case we only want to update the type
        // exclude adverbs
        if (
          input.view_modifiers[m].asset &&
          asset.id === input.view_modifiers[m].asset.id
        ) {
          input.view_modifiers[m].type = type;
          return;
        }
      }

      for (let phrase = input.getPhraseCount(); phrase > 0; phrase--) {
        let inputprep = input.getPreposition(phrase);
        let inputasset = input.getAsset(phrase);
        if (inputprep === identifier && inputasset?.id === asset.id) {
          input.deletePhrase(phrase);

          // though we may have already found it automatically,
          // note that player input it
          type = "input";
        }
      }

      if (identifier) {
        input.pushViewModifier(identifier, asset, type);
      }
    },
  };
})();