Pre-release
AdventureJS Docs Downloads Bundler Devlog Score: 0 Moves: 0
// tryTravel.js

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

  var p = AdventureJS.Game.prototype;

  /**
   * Tries to move the subject in the specified direction.
   * @memberOf AdventureJS.Game
   * @method AdventureJS.Game#tryTravel
   * @param {String} direction
   * @param {Object} params
   */
  p.tryTravel = function Game_tryTravel(direction, params = {}) {
    const fx = `tryTravel.js`;
    var exit, aperture;
    var input = this.game.getInput();
    var subject = input.getSubject();
    var room = this.getRoom();
    var nest_asset = subject.getNestAsset();
    var nest_preposition = subject.getNestPreposition();
    var nest_aspect = subject.getNestAspect();

    // did we get an exit asset?
    if (direction.direction) {
      exit = direction;
      direction = exit.direction;
    }

    this.game.log(
      "L1078",
      "log",
      "high",
      `[${fx}] tryTravel ${direction}`,
      "Travel"
    );

    console.warn(`tryTravel: direction`, direction);
    console.warn(`tryTravel: room.exits`, room.exits);

    var exit_id = exit?.id || room.exits[direction];
    if (!exit_id) {
      // does exit list have an exit with a destination matching this direction?
      for (let key in room.exits) {
        if (direction === room.exits[key]) exit_id = key;
      }
    }

    var msg = "";
    var results;

    // does this room have an exit in the specified direction?
    if (!exit_id) {
      this.game.debug(`D1115`, `${fx}`, `exit_id is undefined`);

      var global_object = this.game.world["global_" + direction];

      if (global_object) {
        var global_description;
        var currentZone = this.game.world[room.zone];

        // try to get current room's scenery settings
        // description can be left undefined
        if (
          room.area_scenery[global_object.name]?.enabled &&
          room.area_scenery[global_object.name]?.description
        ) {
          // console.warn( "USE ROOM SCENERY OBJECT DESCRIPTION" );
          this.game.log(
            "L1491",
            "log",
            "high",
            `[${fx}] use ${room.id}.area_scenery.${global_object.name} for description`,
            "Travel"
          );
          global_description =
            room.area_scenery[global_object.name].description;
        }

        // otherwise try to get current room's zone scenery settings
        else if (
          currentZone?.area_scenery[global_object.name]?.enabled &&
          currentZone?.area_scenery[global_object.name]?.description
        ) {
          this.game.log(
            "L1592",
            "log",
            "high",
            `[${fx}] use ${currentZone.id}.area_scenery.${global_object.name} for description`,
            "Travel"
          );
          // console.warn( "USE ZONE SCENERY OBJECT DESCRIPTION" );
          global_description =
            currentZone.area_scenery[global_object.name].description;
        }

        // otherwise if room enables object,
        // try to get object's native (aka global) description
        else if (
          room.area_scenery[global_object.name]?.enabled &&
          global_object.description
        ) {
          this.game.log(
            "L1493",
            "log",
            "high",
            `[${fx}] use ${global_object.name}.description for description`,
            "Travel"
          );
          // console.warn( "USE GLOBAL OBJECT DESCRIPTION" );
          global_description = global_object.description;
        }

        if (global_description) {
          msg += A.FX.getSAF.call(this.game, global_description);
        }
      }

      if (
        this.settings.print_exits_when_travel_fails ||
        this.world[this.world._room].print_exits_when_travel_fails
      ) {
        this.game.debug(
          `D1120`,
          `${fx}`,
          ` 
          ${
            this.world[this.world._room].print_exits_when_travel_fails
              ? this.world._room + ".print_exits_when_travel_fails"
              : this.game_name + ".settings.print_exits_when_travel_fails"
          }`
        );
        var exits = this.getRoomExits();
        if (exits) msg += " " + exits;
        else msg += " " + this.game.settings.responses.no_exits;
      }
      this.print(msg, input.output_class);
      return false; // return null;
    }

    // ok we think there's an exit
    if (!exit) exit = this.game.getAsset(exit_id);

    // is there actually an exit object?
    if (!exit) {
      this.game.debug(`D1104`, `${fx}`, ` no exit found matching ${exit_id}`);
      msg += `That doesn't appear to be an exit. `;
      this.print(msg, input.output_class);
      return false;
    }

    // is subject already there?
    if (subject.getRoom().id === exit.destination) {
      this.game.debug(
        `D1772`,
        `${fx}`,
        `${subject.id} is already in ${exit.destination}`
      );
      msg += `{We're} already there. `;
      this.print(msg, input.output_class);
      return false;
    }

    aperture = this.game.getAsset(exit.aperture);
    this.game.log(
      "L1079",
      "log",
      "high",
      `[${fx}] found exit: ${exit.id} ${aperture ? "with aperture: " + aperture.id : ""}`,
      "Travel"
    );

    // call doVerbAction
    results = subject.doVerbAction({
      action: "tryTravel",
      target: exit,
      type: "Travel",
    });
    if ("undefined" !== typeof results) return results;

    results = exit.doVerbAction({
      action: "tryTravel",
      target: subject,
      type: "Travel",
    });
    if ("undefined" !== typeof results) return results;

    // is subject constrained?
    if (subject.is.constrained) {
      this.game.debug(`D1103`, `${fx}`, `subject.is.constrained`);
      msg +=
        A.FX.getSAF.call(this.game, subject.constrained_msg) ||
        "{We} can't go anywhere. ";
      this.game.print(msg, input.output_class);
      return false; // return null;
    }

    // is subject holding something like a rope or a railing?
    let held = subject.getVerbConnections("hold", "to_dov");
    if (held.length > 0) {
      //
      // @TODO automatically let go
      // that will go to doTravel
      input.travel.held = held;

      this.game.debug(
        `D1133`,
        `${fx}`,
        ` ${subject.id}.is.connected_by.hold.to_dov contains ${subject.is.connected_by.hold.to_dov}`
      );
      msg += `{We'll} have to let go of ${this.game.getPrintableObjectList({
        objects: held,
      })}. `;
      this.game.print(msg, input.output_class);
      return false; // return null;
    }

    if (subject.isOnFloor() && this.game.settings.on_floor_blocks_travel) {
      this.game.debug(
        `D1215`,
        `${fx}`,
        ` ${subject.id}.isOnFloor and game.settings.on_floor_blocks_travel is true `
      );
      msg += `{We} can't do that from {our} position ${subject.getPostureGerund()} on the floor. `;
      this.game.print(msg, input.output_class);
      return false; // return null;
    }

    // @TODO doTravel - move this there
    if (subject.isOnFloor() && this.game.settings.auto_stand_on_travel) {
      this.game.printInferred(
        `rising from {our} position ${subject.getPostureGerund()} on the floor`
      );
      if (nest_asset && nest_asset instanceof AdventureJS.Assets.Floor) {
        results = subject.onUnnestThisFromThat(nest_asset);
        if ("undefined" !== typeof results) return results;
      }
      subject.posture = "stand";
    }

    // is subject in/on a vehicle?
    if (
      nest_asset &&
      (nest_asset.hasClass("Vehicle") || nest_asset.isDOV("ride"))
    ) {
      var allowed = true;

      // does the exit allow vehicles?
      // does it allow THIS vehicle?
      if (
        (!exit.allow_vehicles &&
          !exit.allow_these_vehicles.includes(nest_asset.id)) ||
        exit.deny_these_vehicles.includes(nest_asset.id)
      ) {
        allowed = false;
      }

      if (!allowed) {
        this.game.debug(
          `D1323`,
          `${fx}`,
          ` ${exit.id}.allow_vehicles is false or ${exit.id}.deny_these_vehicles contains ${nest_asset.id} `
        );
        msg += `{We} can't ${input.input_verb} ${direction} ${exit.aperture ? this.game.getAsset(exit.aperture).article_name : ""} while ${nest_aspect.preposition} ${nest_asset.article_name}. `;
        this.game.print(msg, input.output_class);
        return false; // return null;
      }
      // nest_asset is vehicle
    }

    // is subject nested in something?
    // exclude the room from this as we've just established
    // that player can be nested in a room such as being in an ocean
    else if (nest_asset && nest_asset !== room) {
      var reachable = this.game.settings.auto_unnest_on_travel;

      // check y distance
      if (reachable && this.game.settings.y_determines_reachability) {
        let distance = exit.getY() - subject.getY();
        if (distance > subject.jump_height) {
          reachable = false;
        }
        if (!reachable) console.warn(`y unreachable`);
      }

      // check xz distance
      if (reachable && this.game.settings.xz_determines_reachability) {
        let distance = A.FX.getHorizontalDistance(
          subject.position,
          exit.position
        );
        if (distance > subject.jump_length) {
          reachable = false;
        }
        if (!reachable) console.warn(`xz unreachable`);
      }

      // did author explicitly intend for this exit
      // to be reachable from this nest?
      // @TODO reach_from_top, reach_from_bottom
      if (
        nest_aspect.nest.can.reach.includes(exit.name) ||
        nest_aspect.nest.can.reach.includes(exit.id)
      ) {
        reachable = true;
      }

      if (!reachable) {
        this.game.debug(
          `D1134`,
          `${fx}`,
          ` ${direction} is unreachable from ${nest_preposition} ${nest_asset.id}`
        );

        // two questions here:
        // 1) is the verb a direction verb?
        // 2) is the direction a compass direction or a named exit?
        let verb = this.game.dictionary.getDirection(input.input_verb)
          ? "go"
          : input.input_verb;
        let dest = this.game.dictionary.getDirection(direction)
          ? direction
          : exit.article_name;
        let posture = subject.getPostureGerund();
        let prep = subject.getNestPreposition();
        let nestname = subject.getNestAsset().article_name;
        msg += `{We} can't ${verb} ${dest} from 
        {our} position ${posture} ${prep} ${nestname}. `;

        this.game.print(msg, input.output_class);
        return false; // return null;
      }
    } // nest_asset

    if (aperture) {
      var key_assets = [];
      let inferred;
      var locked_msg = `${aperture.Article_name_is} closed and locked. `;
      var sealed_msg = `${aperture.Article_name_is} sealed shut. `;
      var closed_msg = `${aperture.Article_name_is} closed. `;

      this.game.log(
        "L1080",
        "log",
        "high",
        `[${fx}] found aperture ${aperture.id}`,
        "Travel"
      );

      //
      // LOCKED
      //
      if (aperture.isDOV("unlock") && aperture.is.locked) {
        inferred = this.game.dictionary.verbs.unlock.tryToInferIndirectObject({
          direct_object: aperture,
        });
        if (inferred.indirect_object) {
          key_assets = [inferred.indirect_object];
        }

        // if door is locked and subject hasn't got key, pass
        if (
          !aperture.allowVerbWithNothing("unlock", "dov") &&
          !key_assets.length
        ) {
          this.game.debug(
            `D1105`,
            `${fx}`,
            ` ${aperture.id}.is.locked and subject hasn't got a key`
          );
          this.print(msg + locked_msg);
          return false;
        }

        if (!aperture.canDoVerbAutomatically("unlock")) {
          this.game.debug(
            `D1106`,
            `${fx}`,
            ` ${aperture.id}.is.locked and either ${aperture.id}.dov.unlock.automatically is false or ${aperture.id}.dov.unlock.automatically_after_use is true and subject has not previously unlocked ${aperture.id}`
          );
          this.print(msg + locked_msg);
          return false;
        }
      }

      //
      // SEALED
      //
      if (aperture.isDOV("unseal") && aperture.is.sealed) {
        inferred = this.game.dictionary.verbs.unseal.tryToInferIndirectObject({
          direct_object: aperture,
        });
        if (inferred.indirect_object) {
          key_assets = [inferred.indirect_object];
        }
        input.travel.key_assets = key_assets;

        // has subject got a key?
        if (
          !aperture.allowVerbWithNothing("unseal", "dov") &&
          !key_assets.length
        ) {
          this.game.debug(
            `D1108`,
            `${fx}`,
            ` ${aperture.id}.is.sealed and subject hasn't got a key`
          );
          this.print(msg + sealed_msg);
          return false;
        }

        if (!aperture.canDoVerbAutomatically("unseal")) {
          this.game.debug(
            `D1111`,
            `${fx}`,
            ` ${aperture.id}.is.sealed and either ${aperture.id}.dov.unseal.automatically is false or ${aperture.id}.dov.unseal.automatically_after_use is true and subject has not previously unsealed ${aperture.id}`
          );
          this.print(msg + sealed_msg);
          return false;
        }
      }

      //
      // CLOSED
      //
      if (aperture.isDOV("open") && aperture.is.closed) {
        inferred = this.game.dictionary.verbs.open.tryToInferIndirectObject({
          direct_object: aperture,
        });
        if (inferred.indirect_object) {
          key_assets = [inferred.indirect_object];
        }
        input.travel.key_assets = key_assets;

        // has subject got a key?
        if (
          !aperture.allowVerbWithNothing("open", "dov") &&
          !key_assets.length
        ) {
          this.game.debug(
            `D1108`,
            `${fx}`,
            ` ${aperture.id}.is.closed and subject hasn't got a key`
          );
          this.print(msg + closed_msg);
          return false;
        }

        if (!aperture.canDoVerbAutomatically("open")) {
          this.game.debug(
            `D1719`,
            `${fx}`,
            ` ${aperture.id}.is.closed and either ${aperture.id}.dov.open.automatically is false or ${aperture.id}.dov.open.automatically_after_use is true and subject has not previously opened ${aperture.id}`
          );
          this.print(msg + closed_msg);
          return false;
        }
      }

      // check if aperture is hidden
      if (aperture.is.hidden) {
        this.game.debug(`D1049`, `${fx}`, ` ${aperture.id}.is.hidden`);
        msg += `Hmm. You don't see any exit ${exit.direction}. `;
        this.print(msg, input.output_class);
        return false;
      }
    } // if aperture

    // direction is valid but has no destination, try to print description
    if (
      "undefined" === typeof exit.destinationID &&
      (exit.descriptions.travel || exit.descriptions.look)
    ) {
      this.game.log(
        "L1081",
        "log",
        "high",
        `[${fx}] ${exit.direction} has no destination. Printing description.`,
        "Travel"
      );
      this.game.debug(`D1112`, `${fx}`, ` ${exit.id}.destinationID is unset`);

      msg += A.FX.getSAF.call(
        this,
        exit.descriptions.travel
          ? exit.descriptions.travel
          : exit.descriptions.look
      );

      if (
        this.settings.print_exits_when_travel_fails ||
        this.world[this.world._room].print_exits_when_travel_fails
      ) {
        this.game.debug(
          `D1124`,
          `${fx}`,
          `${
            this.world[this.world._room].print_exits_when_travel_fails
              ? this.world._room + ".print_exits_when_travel_fails"
              : this.game_name + ".settings.print_exits_when_travel_fails"
          }`
        );
        var exits = this.getRoomExits();
        if (exits) msg += " " + exits;
      }

      this.print(msg);
      return false; // return null;
    }

    // is subject holding a rope that is tied to something in the room?
    // @TODO expand this to include other attachment types
    // such as things that are plugged into each other
    if (subject.hasRopesThatBlockTravel()) {
      var ropes = subject.getRopesThatBlockTravel();
      var rope = this.game.getAsset(ropes[0]);
      var object_rope_is_tied_to;
      for (var i = 0; i < rope.is.connected_by.tie.to_iov.length; i++) {
        if (subject.id !== rope.is.connected_by.tie.to_iov[i]) {
          object_rope_is_tied_to = this.game.getAsset(
            rope.is.connected_by.tie.to_iov[i]
          );
        }
      }
      this.game.debug(`D1113`, `${fx}`, `subject.hasRopesThatBlockTravel`);
      msg = `{We're} preventing from leaving by ${rope.article_name}`;
      if (object_rope_is_tied_to) {
        msg += ` tied to ${object_rope_is_tied_to.article_name}`;
      }
      msg = msg.trim();
      msg += `. `;

      this.game.print(msg, input.output_class);
      return false; // return null;
    }

    // direction is valid and destination exists
    if (exit.destinationID) {
      this.game.log(
        "L1082",
        "log",
        "high",
        `[${fx}] doSuccess, travel to ${exit.destinationID}.`,
        "Travel"
      );

      input.travel = {
        exit: exit,
        aperture: aperture,
        direction: direction,
        params: params,
      };
      input.did.tryTravel = true;
      return true;
    }

    // final fallback
    this.game.debug(`D1114`, `${fx}`, ` no ${direction} exit found`);
    msg += `There doesn't appear to be an exit ${direction}. `;
    if (
      this.settings.print_exits_when_travel_fails ||
      this.world[this.world._room].print_exits_when_travel_fails
    ) {
      this.game.debug(
        `D1119`,
        `${fx}`,
        `${this.game_name}.settings.print_exits_when_travel_fails is true`
      );
      var exits = this.getRoomExits();
      if (exits) msg += ` ${exits}`;
    }

    this.print(msg, input.output_class);
    return false;
  };
})();