NativeCommandManager.js

const AbstractMap = require('./AbstractMap');
const { COMMAND } = require('./constant');

const { Vector3, Euler } = require('three');
const {
  ScriptBase,
  Object3D,
  ExternalScriptComponent,
} = require('@ud-viz/game_shared');

/**
 * @classdesc - Manage native command
 * @augments ScriptBase
 */
const NativeCommandManager = class extends ScriptBase {
  init() {
    /**
     * state of the objects moving
     * 
     @type {Object<string,Array>}*/
    this.objectsMoving = {};
    this.objectsMoving[COMMAND.MOVE_FORWARD_START] = [];
    this.objectsMoving[COMMAND.MOVE_BACKWARD_START] = [];
    this.objectsMoving[COMMAND.MOVE_LEFT_START] = [];
    this.objectsMoving[COMMAND.MOVE_RIGHT_START] = [];

    /** @type {AbstractMap|null} */
    this.map = this.context.findGameScriptWithID(AbstractMap.ID_SCRIPT);
  }

  /**
   * Removes an object from the list of objects moving.
   *
   * @param {string} type - The type of movement command.
   * @param {string} uuid - The UUID of the object to remove.
   * @private
   */
  _removeObjectMoving(type, uuid) {
    for (let index = 0; index < this.objectsMoving[type].length; index++) {
      const element = this.objectsMoving[type][index];
      if (element.object3D.uuid == uuid) {
        this.objectsMoving[type].splice(index, 1);
        break;
      }
    }
  }

  /**
   * Handles the tick event.
   */
  tick() {
    // apply commands callback
    this.applyCommandCallbackOf(COMMAND.MOVE_FORWARD, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (
        NativeCommandManager.moveForward(
          updatedObject3D,
          this.computeObjectSpeedTranslate(updatedObject3D) * this.context.dt,
          this.map,
          data.withMap
        )
      )
        this.dispatchEvent({
          type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
          object3D: updatedObject3D,
        });

      return true;
    });
    this.applyCommandCallbackOf(COMMAND.MOVE_BACKWARD, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (
        NativeCommandManager.moveBackward(
          updatedObject3D,
          this.computeObjectSpeedTranslate(updatedObject3D) * this.context.dt,
          this.map,
          data.withMap
        )
      )
        this.dispatchEvent({
          type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
          object3D: updatedObject3D,
        });

      return true;
    });
    this.applyCommandCallbackOf(COMMAND.ROTATE_LEFT, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      NativeCommandManager.rotate(
        updatedObject3D,
        new Vector3(
          0,
          0,
          this.computeObjectSpeedRotate(updatedObject3D) * this.context.dt
        )
      );

      return true;
    });
    this.applyCommandCallbackOf(COMMAND.ROTATE_RIGHT, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      NativeCommandManager.rotate(
        updatedObject3D,
        new Vector3(
          0,
          0,
          -this.computeObjectSpeedRotate(updatedObject3D) * this.context.dt
        )
      );
      return true;
    });

    this.applyCommandCallbackOf(COMMAND.MOVE_UP, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (this.checkMaxAltitude(updatedObject3D)) {
        NativeCommandManager.moveUp(
          updatedObject3D,
          this.computeObjectSpeedTranslate(updatedObject3D) * this.context.dt
        );
        return true;
      }
      return false;
    });

    this.applyCommandCallbackOf(COMMAND.MOVE_DOWN, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (this.checkMinAltitude(updatedObject3D)) {
        NativeCommandManager.moveDown(
          updatedObject3D,
          this.computeObjectSpeedTranslate(updatedObject3D) * this.context.dt
        );
        return true;
      }
      return false;
    });

    this.applyCommandCallbackOf(COMMAND.UPDATE_TRANSFORM, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (!updatedObject3D) return true;
      if (data.position) {
        if (!isNaN(data.position.x)) {
          updatedObject3D.position.x = data.position.x;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.position.y)) {
          updatedObject3D.position.y = data.position.y;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.position.z)) {
          updatedObject3D.position.z = data.position.z;
          updatedObject3D.setOutdated(true);
        }
      }
      if (data.rotation) {
        if (!isNaN(data.rotation.x)) {
          updatedObject3D.rotation.x = data.rotation.x;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.rotation.y)) {
          updatedObject3D.rotation.y = data.rotation.y;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.rotation.z)) {
          updatedObject3D.rotation.z = data.rotation.z;
          updatedObject3D.setOutdated(true);
        }
      }
      if (data.scale) {
        if (!isNaN(data.scale.x)) {
          updatedObject3D.scale.x = data.scale.x;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.scale.y)) {
          updatedObject3D.scale.y = data.scale.y;
          updatedObject3D.setOutdated(true);
        }
        if (!isNaN(data.scale.z)) {
          updatedObject3D.scale.z = data.scale.z;
          updatedObject3D.setOutdated(true);
        }
      }

      return true;
    });
    this.applyCommandCallbackOf(
      COMMAND.UPDATE_EXTERNALSCRIPT_VARIABLES,
      (data) => {
        const updatedObject3D = this.context.object3D.getObjectByProperty(
          'uuid',
          data.object3DUUID
        );
        const externalScriptComponent = updatedObject3D.getComponent(
          ExternalScriptComponent.TYPE
        );
        externalScriptComponent.getModel().variables[data.variableName] =
          data.variableValue;
        updatedObject3D.setOutdated(true);
        return true;
      }
    );
    this.applyCommandCallbackOf(COMMAND.ROTATE, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      if (!isNaN(data.vector.x)) {
        updatedObject3D.rotateX(
          data.vector.x *
            this.context.dt *
            this.computeObjectSpeedRotate(updatedObject3D)
        );
      }
      if (!isNaN(data.vector.y)) {
        updatedObject3D.rotateY(
          data.vector.y *
            this.context.dt *
            this.computeObjectSpeedRotate(updatedObject3D)
        );
      }
      if (!isNaN(data.vector.z)) {
        updatedObject3D.rotateZ(
          data.vector.z *
            this.context.dt *
            this.computeObjectSpeedRotate(updatedObject3D)
        );
      }
      this.clampRotation(updatedObject3D);
      updatedObject3D.setOutdated(true);
      return true;
    });
    this.applyCommandCallbackOf(COMMAND.ADD_OBJECT3D, (data) => {
      this.context.addObject3D(new Object3D(data.object3D), data.parentUUID);
      return true;
    });
    this.applyCommandCallbackOf(COMMAND.REMOVE_OBJECT3D, (data) => {
      const updatedObject3D = this.context.object3D.getObjectByProperty(
        'uuid',
        data.object3DUUID
      );
      this.context.removeObject3D(updatedObject3D.uuid);
      return true;
    });

    [
      { start: COMMAND.MOVE_FORWARD_START, end: COMMAND.MOVE_FORWARD_END },
      { start: COMMAND.MOVE_BACKWARD_START, end: COMMAND.MOVE_BACKWARD_END },
      { start: COMMAND.MOVE_LEFT_START, end: COMMAND.MOVE_LEFT_END },
      { start: COMMAND.MOVE_RIGHT_START, end: COMMAND.MOVE_RIGHT_END },
    ].forEach(({ start, end }) => {
      this.applyCommandCallbackOf(start, (data) => {
        const updatedObject3D = this.context.object3D.getObjectByProperty(
          'uuid',
          data.object3DUUID
        );
        const alreadyPushed =
          this.objectsMoving[start].filter(
            (el) => el.object3D == updatedObject3D
          ).length > 0;
        if (!alreadyPushed)
          this.objectsMoving[start].push({
            object3D: updatedObject3D,
            withMap: data.withMap,
          });

        return true;
      });
      this.applyCommandCallbackOf(end, (data) => {
        this._removeObjectMoving(start, data.object3DUUID);
        return true;
      });
    });

    // move objectsMoving
    this.objectsMoving[COMMAND.MOVE_FORWARD_START].forEach(
      ({ object3D, withMap }) => {
        if (
          NativeCommandManager.moveForward(
            object3D,
            this.computeObjectSpeedTranslate(object3D) * this.context.dt,
            this.map,
            withMap
          )
        ) {
          this.dispatchEvent({
            type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
            object3D: object3D,
          });
        }
      }
    );
    this.objectsMoving[COMMAND.MOVE_BACKWARD_START].forEach(
      ({ object3D, withMap }) => {
        if (
          NativeCommandManager.moveBackward(
            object3D,
            this.computeObjectSpeedTranslate(object3D) * this.context.dt,
            this.map,
            withMap
          )
        ) {
          this.dispatchEvent({
            type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
            object3D: object3D,
          });
        }
      }
    );
    this.objectsMoving[COMMAND.MOVE_LEFT_START].forEach(
      ({ object3D, withMap }) => {
        if (
          NativeCommandManager.moveLeft(
            object3D,
            this.computeObjectSpeedTranslate(object3D) * this.context.dt,
            this.map,
            withMap
          )
        ) {
          this.dispatchEvent({
            type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
            object3D: object3D,
          });
        }
      }
    );
    this.objectsMoving[COMMAND.MOVE_RIGHT_START].forEach(
      ({ object3D, withMap }) => {
        if (
          NativeCommandManager.moveRight(
            object3D,
            this.computeObjectSpeedTranslate(object3D) * this.context.dt,
            this.map,
            withMap
          )
        ) {
          this.dispatchEvent({
            type: NativeCommandManager.EVENT.OBJECT_3D_LEAVE_MAP,
            object3D: object3D,
          });
        }
      }
    );
  }

  /**
   * If you want to have different translation speed for gameobject you should override this method
   *
   * @returns {number} speed of the translation
   */
  computeObjectSpeedTranslate() {
    return this.variables.defaultSpeedTranslate;
  }

  /**
   * If you want to have different rotation speed for gameobject you should override this method
   *
   * @returns {number} speed of the rotation
   */
  computeObjectSpeedRotate() {
    return this.variables.defaultSpeedRotate;
  }

  /**
   * Checks if the given object's altitude is below the maximum allowed altitude.
   *
   * @param {Object3D} object3D - The object to check.
   * @returns {boolean} - True if the object's altitude is below the maximum allowed altitude, false otherwise.
   */
  checkMaxAltitude(object3D) {
    return (
      object3D.getWorldPosition(new Vector3()).z <=
      this.computeMaxAltitude(object3D)
    );
  }

  /**
   * Checks if the given object's altitude is above the minimum allowed altitude.
   *
   * @param {Object3D} object3D - The object to check.
   * @returns {boolean} - True if the object's altitude is above the minimum allowed altitude, false otherwise.
   */
  checkMinAltitude(object3D) {
    return (
      object3D.getWorldPosition(new Vector3()).z >=
      this.computeMinAltitude(object3D)
    );
  }

  /**
   * Computes the maximum allowed altitude.
   * If you want to have different max altitude for gameobject you should override this method
   *
   * @returns {number} - The maximum allowed altitude.
   */
  computeMaxAltitude() {
    return this.variables.maxAltitude;
  }

  /**
   * Computes the minimum allowed altitude.
   * If you want to have different min altitude for gameobject you should override this method
   *
   * @returns {number} - The minimum allowed altitude.
   */
  computeMinAltitude() {
    return this.variables.minAltitude;
  }

  /**
   * End Movement of an object3D
   *
   * @param {Object3D} object3D - object3D to stop
   */
  stop(object3D) {
    this._removeObjectMoving(COMMAND.MOVE_FORWARD_START, object3D.uuid);
    this._removeObjectMoving(COMMAND.MOVE_BACKWARD_START, object3D.uuid);
    this._removeObjectMoving(COMMAND.MOVE_LEFT_START, object3D.uuid);
    this._removeObjectMoving(COMMAND.MOVE_RIGHT_START, object3D.uuid);
  }

  /**
   * An object3D freezed cant be move by manager
   *
   * @param {Object3D} object3D - object3D to freeze or not
   * @param {boolean} value - freeze or not an object3D
   */
  freeze(object3D, value) {
    object3D.userData[NativeCommandManager.FREEZE_KEY] = value;
  }

  /**
   *
   * @param {Object3D} object3D - object3d to clamp rotation
   */
  clampRotation(object3D) {
    // clamp
    object3D.rotation.y = 0;
    object3D.rotation.x = Math.max(
      Math.min(this.variables.angleMax, object3D.rotation.x),
      this.variables.angleMin
    );
  }

  /**
   * Gets the script ID.
   *
   * @returns {string} The ID of the NativeCommandManager script.
   * @static
   */
  static get ID_SCRIPT() {
    return 'native_command_manager_id';
  }

  static get EVENT() {
    return { OBJECT_3D_LEAVE_MAP: 'object_3d_leave_map' };
  }

  static get FREEZE_KEY() {
    return 'freeze_key';
  }

  /**
   * @typedef NativeCommandManagerVariables
   * @property {number} angleMin - angle min in the clamp rotation in radian
   * @property {number} angleMax - angle max in the clamp rotation in radian
   * @property {number} defaultSpeedRotate - speed rotate
   * @property {number} defaultSpeedTranslate - speed translate
   */

  /**
   * @returns {NativeCommandManagerVariables} - default native command manager variables
   */
  static get DEFAULT_VARIABLES() {
    return {
      angleMin: Math.PI / 5,
      angleMax: 2 * Math.PI - Math.PI / 10,
      defaultSpeedTranslate: 0.04,
      defaultSpeedRotate: 0.00001,
      maxAltitude: 500,
      minAltitude: 0,
    };
  }
};

/**
 * Move forward object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move forward
 * @param {number} value - amount to move forward
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveForward = function (
  object3D,
  value,
  map,
  withMap = true
) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeForward(object3D).setLength(value),
    map,
    withMap
  );
};

/**
 * Move backward object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move backward
 * @param {number} value - amount to move backward
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveBackward = function (
  object3D,
  value,
  map,
  withMap = true
) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeForward(object3D).negate().setLength(value),
    map,
    withMap
  );
};

/**
 * Move letf object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move left
 * @param {number} value - amount to move left
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveLeft = function (
  object3D,
  value,
  map,
  withMap = true
) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeForward(object3D)
      .applyAxisAngle(new Vector3(0, 0, 1), Math.PI * 0.5)
      .setLength(value),
    map,
    withMap
  );
};

/**
 * Move right object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move right
 * @param {number} value - amount to move right
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveRight = function (
  object3D,
  value,
  map,
  withMap = true
) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeForward(object3D)
      .applyAxisAngle(new Vector3(0, 0, 1), -Math.PI * 0.5)
      .setLength(value),
    map,
    withMap
  );
};

/**
 * Move up object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move up
 * @param {number} value - amount to move up
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveUp = function (object3D, value, map, withMap = true) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeUp(object3D)
      .applyAxisAngle(new Vector3(0, 0, 1), -Math.PI * 0.5)
      .setLength(value),
    map,
    withMap
  );
};

/**
 * Move up object3D of a certain value
 *
 * @param {Object3D} object3D - object3D to move up
 * @param {number} value - amount to move up
 * @param {AbstractMap} map - map script
 * @param {boolean} [withMap=true] - map should be consider
 * @returns {boolean} - the movement make the object3D leaves the map
 */
NativeCommandManager.moveDown = function (object3D, value, map, withMap) {
  return NativeCommandManager.move(
    object3D,
    Object3D.computeDown(object3D)
      .applyAxisAngle(new Vector3(0, 0, 1), -Math.PI * 0.5)
      .setLength(value),
    map,
    withMap
  );
};

/**
 * Move object3D on a map
 *
 * @param {Object3D} object3D - object3D to move
 * @param {Vector3} vector - move vector
 * @param {AbstractMap} map - map script
 * @param {boolean} withMap - map should be consider
 * @returns {boolean} isOutOfMap - the movement make the object3D leaves the map
 */
NativeCommandManager.move = function (object3D, vector, map, withMap) {
  if (object3D.userData[NativeCommandManager.FREEZE_KEY]) return false; // object freezed cant move
  const oldPosition = object3D.position.clone();
  object3D.position.add(vector);
  let isOutOfMap = false;
  if (map && withMap) {
    isOutOfMap = !map.updateElevation(object3D);
    if (isOutOfMap) {
      object3D.position.copy(oldPosition); // cant leave the map
    }
  }
  object3D.setOutdated(true);
  return isOutOfMap;
};

/**
 * Rotate an object3D with an euler
 *
 * @param {Object3D} object3D - object3D to rotate
 * @param {Euler} euler - euler to rotate from
 */
NativeCommandManager.rotate = function (object3D, euler) {
  // should check euler order
  object3D.rotateZ(euler.z);
  object3D.rotateX(euler.x);
  object3D.rotateY(euler.y);
  object3D.setOutdated(true);
};

module.exports = NativeCommandManager;