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

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

  /**
   * @augments {adventurejs.Verb}
   * @class tie
   * @ajsnode game.dictionary.verbs.tie
   * @ajsconstruct MyGame.createVerb({ "name": "tie", [...] });
   * @ajsconstructedby adventurejs.Dictionary#createVerb
   * @hideconstructor
   * @ajsinstanceof Verb
   * @ajsnavheading ManipulationVerbs
   * @summary Summary.
   * @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; tie boat to bollard with rope</span>
   * You tie the rope to the boat and the bollard.
   * </pre>
   * <p>
   * <strong>Tie</strong> one
   * {@link adventurejs.Tangible|Tangible}
   * {@link adventurejs.Asset|Asset} to one or more others.
   * In order to be tied, the asset must have its
   * <code class="property">asset.dov.tie.enabled</code> property
   * set to true.
   * </p>
   * <p>
   * To apply tie to a non-rope asset such as a shoe
   * or a bowtie, set its
   * <code class="property">asset.dov.tie.with_params.with_nothing</code>
   * to true.
   * </p>
   * <h3 class="example">Example</h3>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Clothing",
   *   name: "shoe"
   *   dov: {
   *     tie: { with_nothing: true },
   *   },
   *   is: { tied: false },
   * });
   * </code></pre>
   * <p>
   * If a rope is being tied to another asset, the other asset must have its
   * <code class="property">asset.iov.tie.enabled</code> set to true.
   * This is true even if a player inputs the other asset as a direct object.
   * For example, "tie rope to zebra" and "tie zebra with rope" will both be
   * parsed with the rope as the direct object and the zebra as the indirect object.
   * (Instances of the {@link adventurejs.Rope|Rope} class will inherit
   * a direct object subscription, though you may redefine it in order
   * to set custom parameters.)
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Rope",
   *   name: "sisal rope"
   *   dov: {
   *     tie: {
   *       with_assets: [ "balustrade" ],
   *     },
   *   },
   * });
   * MyGame.createAsset({
   *   class: "Thing",
   *   name: "balustrade",
   *   dov: { climb: true },
   *   iov: {
   *     tie: {
   *       with_assets: [ "sisal rope" ],
   *     },
   *   },
   * });
   * </code></pre>
   * @ajsverbreactions doRemoveThisFromThat, doRemoveThatFromThis, doMoveThisToThat, doMoveThatToThis
   * @ajsverbphases doBeforeTry, doAfterTry, doBeforeSuccess, doAfterSuccess
   * @TODO tie knot in rope
   */
  A.Preverbs.tie = {
    name: "tie",
    prettyname: "tie",
    synonyms: ["tie"],
    past_tense: "tied",
    state: "tied",
    state_string: "tied",
    unstate_string: "untied",
    gerund: "tying",
    verb_prep_noun: ["tie up"],
    verb_noun_prep: ["tie up"],
    makes_connections: true,

    /**
     * @ajsverbstructures
     * @memberof tie
     */
    accepts_structures: [
      "verb noun", // tie bowtie, tie shoelace
      "verb noun preposition noun", // tie thief to chair (with rope), tie thief with rope
      // tie knot in rope, tie rope in knots
      "verb noun preposition noun preposition noun", // tie thief to chair with rope
      // tie rope to thief and chair
    ],

    /**
     * @memberof tie
     * @ajsverbphrase
     * phrase1:
     * {
     *   accepts_noun: true,
     *   accepts_plural_noun: true, // tie A and B with rope
     *   requires_noun: true,
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *     reachable: true,
     *   },
     * },
     */
    phrase1: {
      accepts_noun: true,
      accepts_plural_noun: true,
      requires_noun: true,
      noun_must_be: {
        known: true,
        tangible: true,
        present: true,
        visible: true,
        reachable: true,
      },
    },

    /**
     * @memberof tie
     * @ajsverbphrase
     * phrase2:
     * {
     *   accepts_noun: true,
     *   accepts_plural_noun: true, // tie rope to A and B
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *     reachable: true,
     *   },
     *   accepts_preposition: true,
     *   requires_preposition: true,
     *   accepts_these_prepositions: [ "to", "with", "in" ], // "in" only for "tie knot in rope"
     * },
     */
    phrase2: {
      accepts_noun: true,
      accepts_plural_noun: true,
      noun_must_be: {
        known: true,
        tangible: true,
        present: true,
        visible: true,
        reachable: true,
      },
      accepts_preposition: true,
      requires_preposition: true,
      accepts_these_prepositions: ["to", "with", "in"],
    },

    /**
     * @memberof tie
     * @ajsverbphrase
     * phrase3:
     * {
     *   accepts_noun: true,
     *   requires_noun: true,
     *   noun_must_be:
     *   {
     *     known: true,
     *     tangible: true,
     *     present: true,
     *     visible: true,
     *     reachable: true,
     *   },
     *   accepts_preposition: true,
     *   requires_preposition: true,
     *   accepts_these_prepositions: [ 'to', 'with' ],
     * },
     */
    phrase3: {
      accepts_noun: true,
      requires_noun: true,
      noun_must_be: {
        known: true,
        tangible: true,
        present: true,
        visible: true,
        reachable: true,
      },
      accepts_preposition: true,
      requires_preposition: true,
      accepts_these_prepositions: ["to", "with"],
    },

    /**
     * @memberof tie
     * @ajsverbparams
     * with_params: {
     *
     *   // If true, player can tie knots in this asset.
     *   // (@TODO)
     *   can_knot: true,
     *
     *   // The maximum number of connections that this
     *   // asset can make with this verb.
     *   max_connections: 2,
     *
     *   // If true, player must be holding this asset
     *   // to apply this verb.
     *   must_hold_to_use: false,
     *
     *   // If true, player will drop rope on tying it to the
     *   // maximum number of assets.
     *   on_max_connections_drop_rope: true,
     *
     *   // If true, taking this asset while tied will
     *   // untie it from any other asset(s) it is tied to.
     *   on_take_break_connections: false,
     *
     *   // If true, taking this asset while tied will
     *   // also take any other asset(s) it is tied to.
     *   on_take_take_connections: false,
     * },
     */
    with_params: {
      can_knot: true,
      max_connections: 2,
      must_hold_to_use: false,
      on_max_connections_drop_rope: true,
      on_take_break_connections: false,
      on_take_take_connections: false,
    },

    doTry: function () {
      var input = this.game.getInput();
      var verb_phrase = input.verb_phrase;
      var direct_object = input.getAsset(1);
      var direct_preposition = input.getPreposition(1);
      var indirect_object1 = input.getAsset(2);
      var indirect_preposition1 = input.getPreposition(2);
      var indirect_object2 = input.getAsset(3);
      var indirect_preposition2 = input.getPreposition(3);
      var player = this.game.getPlayer();
      var results;
      var inferred_rope;
      var msg = "";

      var rope;
      var targets = [];

      if (verb_phrase === "tie up") {
        //
      }

      // did player input something like
      // "tie x with y with z" or "tie x to y to z" ?
      if (
        indirect_preposition1 &&
        indirect_preposition2 &&
        indirect_preposition1 === indirect_preposition2
      ) {
        this.game.debug(
          `D1594 | ${this.name}.js | can't handle "${indirect_preposition1}" and "${indirect_preposition2}"`
        );
        msg += `That doesn't even make sense. `;
        this.handleFailure(msg);
        return false;
      }

      // sentence structure: verb noun
      // ie tie shoe
      if (input.hasStructure("verb noun")) {
        // no indirect object required?
        if (
          direct_object.isDOV(this.name) &&
          direct_object.allowVerbWithNothing(this.name, "dov")
        ) {
          if (this.hasState() && direct_object.isVerbState(this.name)) {
            this.game.debug(
              `D2165 | ${this.name}.js | ${
                direct_object.id
              }.is.${this.getState()} is ${direct_object.isVerbState(
                this.name
              )}`
            );
            msg += `${
              direct_object.Articlename_is
            } already ${this.getState()}. `;
            this.handleFailure(msg);
            return false;
          }
          return true;
        }

        // this.game.debug(
        //   `D2161 | ${this.name}.js | ${direct_object.id}.dov.${this.name} or ${direct_object.id}.dov.${this.name}.with_nothing is unset`,
        // );

        if (direct_object.isDOV(this.name)) {
          msg += `To what would $(we) like to ${this.name} ${direct_object.articlename}? `;
          input.setPreposition(2, "to");
          input.setSoftPrompt({
            index: 2,
            type: "noun",
            noun2: true,
            preposition2: true,
            structure: "verb noun preposition noun",
          });
          this.handleFailure(msg);
          return false;
        } else if (direct_object.isIOV(this.name)) {
          this.game.debug(`Fxxxx | ${this.name}.js | direct_object.isIOV `);
          // can we infer a rope?
          results = this.tryToInferRope([direct_object]);
          if (results.prompt) {
            let index = 2;
            input.setPreposition(index, "with");
            input.setSoftPrompt({
              index: index,
              type: "noun",
              ["noun" + index]: true,
              ["preposition" + index]: true,
              structure: "verb noun preposition noun",
            });
            this.game.debug(
              `D2166 | ${this.name}.js | soft prompt for noun${index} `
            );
            msg += `With what would $(we) like to ${this.name} ${direct_object.articlename}? `;
            this.handleFailure(msg);
            return false;
          } else if (results.ropes.length) {
            this.game.debug(`Fxxxx | ${this.name}.js | found a rope `);
            // found a rope
            inferred_rope = true;
            rope = results.ropes[0];
            input.setPreposition(2, "with");
            input.setAsset(2, rope);
            input.setStructure("verb noun preposition noun");
          }
        } else {
          msg += `$(We) don't know how to ${this.name} ${direct_object.articlename}. `;
          this.handleFailure(msg);
          return false;
        }
      }

      // we accept "tie rope to thing" or "tie thing with rope"
      // which means direct object could be rope or target
      // so look for uses of "to" and "with" to help identify
      // make lists of ropes and targets
      for (let i = 1; i <= 3; i++) {
        let asset = input.getAsset(i);
        if (!asset) continue;
        let preposition = input.getPreposition(i);

        // did player input a substance?
        // (which would give us a tangible substance container)
        if (input.getSubstance(i)) {
          this.game.debug(
            `D2164 | ${this.name}.js | substances can't be tied `
          );
          msg += `${input.getSubstance(i).Name} can't be tied. `;
          this.handleFailure(msg);
          return false;
        }

        // can asset be tied at all?
        if (!asset.isOV(this.name)) {
          this.game.debug(
            `D1590 | ${this.name}.js | neither ${asset.id}.dov.${this.name} nor ${asset.id}.iov.${this.name} is set `
          );
          msg += `${asset.Articlename} can't be tied. `;
          this.handleFailure(msg);
          return false;
        }

        // direct object is the only asset without a preposition
        // is that what we're looking at?
        // @TODO this doesn't handle knots
        if (!preposition) {
          if (asset.isDOV(this.name)) {
            rope = asset;
            continue;
          } else if (asset.isIOV(this.name)) {
            targets.push(asset);
            continue;
          }
        }

        // "tie with" assumes it's a rope
        if (preposition && preposition === "with") {
          if (!asset.isDOV(this.name)) {
            this.game.debug(
              `D2162 | ${this.name}.js | ${asset.id}.iov.${this.name} is unset`
            );
            msg += `$(We) can't ${this.name} anything with ${asset.articlename}. `;
            this.handleFailure(msg);
            return false;
          }
          rope = asset;
          continue;
        }

        // "tie to" assumes it's a target
        if (preposition && preposition === "to") {
          if (asset.isIOV(this.name)) {
            targets.push(asset);
            continue;
          } else if (asset.isDOV(this.name)) {
            if (!rope) {
              rope = asset;
              continue;
            }
            this.game.debug(
              `D2163 | ${this.name}.js | ${asset.id}.dov.${this.name} is set but already found ${rope.id}`
            );
            msg += `$(We) can't ${this.name} anything to ${asset.articlename}. `;
            this.handleFailure(msg);
            return false;
          }
        }
      }

      // did player input something like
      // "tie boat to dock" without specifying a rope?
      // try to infer a rope
      if (targets.length && !rope) {
        // infer indirect object?
        results = this.tryToInferRope();
        let index = targets.length + 1;
        if (results.prompt) {
          this.game.debug(
            `D1593 | ${this.name}.js | soft prompt for noun${index} `
          );
          input.setPreposition(index, "with");
          input.setSoftPrompt({
            index: index,
            type: "noun",
            ["noun" + index]: true,
            structure:
              "verb noun preposition noun" +
              (index === "3" ? " preposition noun" : ""),
          });
          msg += `With what would $(we) like to ${this.name} ${
            targets.length > 0 ? targets[0].articlename : ""
          } ${targets.length > 1 ? "to " + targets[1].articlename : ""}? `;
          this.handleFailure(msg);
          return false;
        }
        // found a rope
        inferred_rope = true;
        rope = results.ropes[0];
        this.game.printInferred(`with ${rope.articlename}`);
      }

      // no rope found
      if (!rope) {
        this.game.debug(`D2158 | ${this.name}.js | no ropes `);
        // ignore rope/targets and fall back to the original vars
        msg += `$(We) don't know how to ${this.name} ${
          direct_preposition ? direct_preposition : ""
        } ${direct_object.articlename}${
          indirect_preposition1 ? " " + indirect_preposition1 : ""
        }${indirect_object1 ? " " + indirect_object1.articlename : ""}${
          indirect_preposition2 ? " " + indirect_preposition2 : ""
        }${indirect_object2 ? " " + indirect_object2.articlename : ""}. `;
        this.handleFailure(msg);
        return false;
      }

      // can't tie if not holding rope
      if (
        rope.dov[this.name].with_params.must_hold_to_use &&
        !player.$has(rope)
      ) {
        this.game.debug(
          `D1591 | ${this.name}.js | ${rope.id}.dov.${this.name}.with_params.must_hold_to_use is true`
        );
        msg += `$(We)'re not holding ${rope.articlename}. `;
        this.handleFailure(msg);
        return false;
      }

      // rope already tied to both targets
      if (
        targets.length > 1 &&
        rope.isConnectedToAsset("tie", targets[0], "to_iov") &&
        rope.isConnectedToAsset("tie", targets[1], "to_iov")
      ) {
        msg += `${rope.Articlename} is already tied to both ${targets[0].articlename} and ${targets[1].articlename}. `;
        this.handleFailure(msg);
        return false;
      }

      for (let i = 0; i < targets.length; i++) {
        // can't tie to target? - redundant?
        if (targets[i] && !targets[i].isIOV(this.name)) {
          this.game.debug(
            `D1649 | ${this.name}.js | ${targets[i].id}.iov.${this.name} is unset`
          );
          msg += `$(We) can't tie anything to ${targets[i].articlename}. `;
          this.handleFailure(msg);
          return false;
        }

        // can't tie this specific rope to this specific target?
        if (
          !rope.allowVerbWithAnything(this.name, "dov") &&
          !rope.allowVerbWithAsset("tie", targets[i].id, "dov")
        ) {
          this.game.debug(
            `D2167 | ${this.name}.js | ${rope.id}.dov.${this.name}.with_anything is false and neither .with_assets / .with_classes includes ${targets[i].id} / ${targets[i].class} `
          );
          msg += `${rope.Articlename} can't be ${this.past_tense} to ${targets[i].articlename}. `;
          this.handleFailure(msg);
          return false;
        }

        // can't tie if not holding target?
        if (
          targets[i] &&
          targets[i].iov[this.name].with_params.must_hold_to_use &&
          !player.$has(targets[i])
        ) {
          this.game.debug(
            `D2159 | ${this.name}.js | ${targets[i].id}.iov.${this.name}.with_params.must_hold_to_use is true`
          );
          msg += `$(We)'re not holding ${targets[i].articlename}. `;
          this.handleFailure(msg);
          return false;
        }

        // rope already tied to target?
        if (rope.isConnectedToAsset("tie", targets[i], "to_iov")) {
          this.game.debug(
            `D1483 | ${this.name}.js | ${rope.id}.is.connected_by.${this.name}.to_iov contains ${targets[i].id}`
          );
          msg += `${rope.Articlename} is already tied to ${targets[i].articlename}. `;
          this.handleFailure(msg);
          return false;
        }

        // target is maxed out?
        if (
          targets[i].getVerbConnectionCount("tie", "to_dov") >=
          targets[i].getVerbMaxConnections("tie", "iov")
        ) {
          this.game.debug(
            `D2169 | ${this.name}.js | ${targets[i].id}.is.connected_by.${this.name}.to_dov has the maximum number of connections specified at ${targets[i].id}.iov.${this.name}.with_params.max_connections`
          );
          msg += `${targets[i].Articlename} can't have any more things tied to it. `;
          this.handleFailure(msg);
          return false;
        }
      }

      // rope is maxed out?
      if (
        rope.getVerbConnectionCount("tie", "to_iov") >=
        rope.getVerbMaxConnections("tie", "dov")
      ) {
        this.game.debug(
          `D2170 | ${this.name}.js | ${rope.id}.is.connected_by.${this.name}.to_iov has the maximum number of connections specified at ${rope.id}.dov.${this.name}.with_params.max_connections`
        );
        msg += `${rope.Articlename} can't be tied to any more things. `;
        this.handleFailure(msg);
        return false;
      }

      input.verb_params.rope = rope;
      input.verb_params.targets = targets;

      return true;
    },

    doSuccess: function () {
      var input = this.game.getInput();
      var verb_phrase = input.verb_phrase;
      var direct_object = input.getAsset(1);
      var indirect_object1 = input.getAsset(2);
      var indirect_object2 = input.getAsset(3);
      var player = this.game.getPlayer();
      var results;
      var msg = "";
      var rope = input.verb_params.rope;
      var targets = input.verb_params.targets;

      if (input.hasStructure("verb noun")) {
        // if we got here without an indirect object
        // it means none is required
        msg = `$(We) ${this.name} ${direct_object.articlename}. `;
        // apply state changes
        this.setState(direct_object, true);
        // print output
        this.handleSuccess(msg, direct_object);
        return true;
      }

      let diff =
        rope.getVerbMaxConnections("tie", "dov") -
        rope.getVerbConnectionCount("tie", "to_iov");
      // compose output
      msg +=
        `$(We) ${this.name}` +
        `${diff <= 1 ? " the free" : " one"}` +
        ` end of ${rope.articlename}` +
        `${targets[0] ? " to " + targets[0].articlename : ""}` +
        `${
          targets[1] ? ", and the other end to " + targets[1].articlename : ""
        }`;
      // no period yet because we don't end here...
      // the following logic determines additional output

      if (targets.length) {
        for (let i = 0; i < targets.length; i++) {
          // set verb connection
          this.setVerbConnection(rope, targets[i]);

          // If rope.dov.take.with_params.on_take_take_connections
          // is true move takeable targets into player's inventory.
          if (
            rope.isWithin(player) &&
            rope.dov[this.name].with_params.on_take_take_connections &&
            !targets[i].isWithin(player) &&
            targets[i].isDOV("take")
          ) {
            results = player.onMoveThatToThis(targets[i], "in");
            if ("undefined" !== typeof results) return results;
            msg += ", and take " + targets[i].articlename;
          }
        }
      }

      // If tying rope to its max number of items, and
      // the items are not in player's inventory, remove
      // rope from inventory.
      var tiedObjectsNotInInventory = 0;
      var ropeIsTiedToMaxObjects =
        rope.getVerbConnectionCount("tie", "to_iov") ===
        rope.getVerbMaxConnections("tie", "dov");
      if (ropeIsTiedToMaxObjects) {
        let count = rope.getVerbConnectionCount("tie", "to_iov");
        for (var i = 0; i < count; i++) {
          var tiedObject = this.game.getAsset(
            rope.getVerbConnections("tie", "to_iov")[i]
          );
          if (!tiedObject.isWithin(player)) {
            tiedObjectsNotInInventory++;
          }
        }
      }
      var noTiedObjectsInInventory =
        tiedObjectsNotInInventory === rope.getVerbMaxConnections("tie", "dov");
      if (
        rope.dov.tie.with_params.on_max_connections_drop_rope &&
        rope.isWithin(player) &&
        ropeIsTiedToMaxObjects &&
        noTiedObjectsInInventory
      ) {
        var currentRoom = this.game.getCurrentRoom();

        results = player.onRemoveThatFromThis(rope);
        if ("undefined" !== typeof results) return results;

        // set thing's new location to player's location
        results = currentRoom.onMoveThatToThis(rope, "in");
        if ("undefined" !== typeof results) return results;

        msg += ", and let go of " + rope.articlename;
      }

      msg += ". ";

      if (
        this.game.parser.isParsingMultiple() &&
        -1 === input.output_class.indexOf("concatenate_output")
      ) {
        input.output_class += " concatenate_output ";
      }

      this.handleSuccess(msg, rope);

      return true;
    },

    tryToInferRope: function (targets) {
      var result = { prompt: false, ropes: [] };
      var player = this.game.getPlayer();
      var ropes = player.findNestedAssetsWithClass("Rope");

      // allowed to infer?
      if (!this.game.settings.infer_objects) {
        result.prompt = true;
      }

      if (!ropes || !ropes.length) {
        // look for other carried objects that are dov tie
        ropes = player.findNestedAssetsWithProperty("dov.tie");
      }

      // can this rope be tied to that object?
      for (let i = ropes.length - 1; i > 0; i--) {
        let rope = ropes[i];
        if (rope.allowVerbWithNothing()) {
          // it's a tieable non-rope, like a bowtie
          ropes.splice(i, 1);
          continue;
        } else if (!rope.allowVerbWithAnything(this.name, "dov")) {
          for (let t = 0; t < targets.length; t++) {
            let target = targets[t];
            if (!rope.allowVerbWithAsset("tie", target.id, "dov")) {
              ropes.splice(i, 1);
              continue;
            }
          }
        }
      }

      // done everything we can
      result.ropes = ropes;

      // still no ropes?
      if (!ropes || !ropes.length) {
        result.prompt = true;
      }

      // more than one rope to choose from?
      if (
        ropes.length > 1 &&
        !this.game.settings.infer_objects_automatically_picks
      ) {
        result.prompt = true;
      }

      return result;
    },
  };
})(); // tie