Source: physics/Collider.js

const Vector = require('engine/Vector');
const { Box, Circle } = require('./shapes');

/**
 * Collider is the core element of physics module.
 *
 * @example <caption>Create a collider</caption>
 * const Collider = require('engine/physics/Collider');
 *
 * // Create collider instance
 * let collider = Collider({
 *   shape: 'Box',
 *   width: 20, height: 20,
 *   dumping: 0.6,
 * });
 *
 * @example <caption>Define collision groups</caption>
 * const { getGroupMask } = require('engine/physics');
 *
 * // It is recommend to define once in a module, and import it to use.
 * const GROUPS = {
 *   SOLID:   getGroupMask(0),
 *   PLAYER:  getGroupMask(1),
 *   TRIGGER: getGroupMask(2),
 * };
 *
 * @example <caption>Setup collision</caption>
 * let bodyA = Collider({
 *   // A is a SOLID collider
 *   collisionGroup: GROUPS.SOLID,
 * });
 *
 * let bodyB = Collider({
 *   // B is a player collider
 *   collisionGroup: GROUPS.PLAYER,
 *   // This collider will collide with SOLID bodies
 *   collideAgainst: GROUPS.SOLID,
 *   // Collision response handler
 *   collide: function(other) {
 *     // Response to the collision when collide with something SOLID,
 *     // which means this will be moved back and won't get through
 *     // the SOLID collider.
 *     if (other.collisionGroup & GROUPS.SOLID) {
 *       // When return false here, this collider will keep as is.
 *       // In this case, player will get through the SOLID collider.
 *       return true;
 *     }
 *   },
 * });
 *
 * For more complex samples, take a look at the [physics sample code](https://github.com/pixelpicosean/lesser-panda-samples/blob/master/src/game/samples/physics.js).
 *
 * @class Collider
 */
class Collider {
  /**
   * @constructor
   * @param {object} [properties] Settings to merge.
   */
  constructor(properties) {
    /**
     * ID of this collider.
     * @type {number}
     */
    this.id = Collider.nextId++;
    /**
     * Static collider will never update or response to collisions.
     * @type {Boolean}
     * @default false
     */
    this.isStatic = false;
    /**
     * Collider's parent world.
     * @type {SystemPhysics}
     */
    this.world = null;
    /**
     * Collider's shape.
     * @type {Box|Circle}
     */
    this.shape = null;
    /**
     * Position of collider.
     * @type {Vector}
     */
    this.position = Vector.create();
    /**
     * Last position of collider.
     * @type {Vector}
     */
    this.last = Vector.create();
    /**
     * Collider's velocity.
     * @type {Vector}
     */
    this.velocity = Vector.create();
    /**
     * Collider's maximum velocity.
     * @type {Vector}
     * @default 400, 400
     */
    this.velocityLimit = Vector.create(400, 400);
    /**
     * Collider's mass.
     * @type {number}
     * @default 0
     */
    this.mass = 0;
    /**
     * Collider's collision group.
     * @type {number}
     * @default null
     */
    this.collisionGroup = null;
    /**
     * Collision groups that this collider collides against.
     * Note: this will be a Number when broadPhase is "SpatialHash",
     * but will be an Array while using "Simple".
     * @type {array|number}
     */
    this.collideAgainst = 0;
    /**
     * Collider's force.
     * @type {Vector}
     * @default 0,0
     */
    this.force = Vector.create();
    /**
     * Collider's damping. Should be number between 0 and 1.
     * @type {number}
     * @default 0
     */
    this.damping = 0;

    // Bounding info
    this.left = 0;
    this.right = 0;
    this.top = 0;
    this.bottom = 0;

    this.lastLeft = 0;
    this.lastRight = 0;
    this.lastTop = 0;
    this.lastBottom = 0;

    this.setup(properties);
  }

  /**
   * Width of this collider(of its shape or 0).
   * @type {number}
   * @readonly
   */
  get width() {
    return this.shape ? this.shape.width : 0;
  }

  /**
   * Height of this collider(of its shape or 0).
   * @type {number}
   * @readonly
   */
  get height() {
    return this.shape ? this.shape.height : 0;
  }

  /**
   * Add this collider to the world.
   * @memberof Collider#
   * @method addTo
   * @param {SystemPhysics} world   Physics system instance to add to
   * @return {Collider}             Self for chaining
   */
  addTo(world) {
    world.addCollider(this);
    return this;
  }

  /**
   * Remove collider from it's world.
   * @memberof Collider#
   * @method remove
   */
  remove() {
    if (this.world) {
      this.world.removeCollider(this);
    }
  }

  /**
   * This will be called before collision checking.
   * You can clean up collision related flags here.
   * @memberof Collider#
   * @method beforeCollide
   */
  beforeCollide() {}

  /**
   * This is called while overlapping another collider.
   * @memberof Collider#
   * @method collide
   * @param {Collider} other  Collider that is currently overlapping.
   * @param {*} response      Response infomration(direction for box, and angle for circle).
   * @return {boolean}        Return true to apply hit response.
   */
  collide(other, response) { /* eslint no-unused-vars:0 */
    return true;
  }

  /**
   * This is called after hit response.
   * @memberof Collider#
   * @method afterCollide
   */
  afterCollide() {}

  /**
   * Handle collision map tracing result.
   * @param {Object} res  Tracing result to handle.
   */
  handleMovementTrace(res) {} /* eslint no-unused-vars:0 */

  /**
   * Setup this collider with settings.
   * @memberof Collider#
   * @method setup
   * @param {Object} settings Setting object.
   * @return {Collider}       Self for chaining
   */
  setup(settings) {
    for (let k in settings) {
      switch (k) {
        // Set value
        case 'mass':
        case 'damping':
        case 'collisionGroup':
        case 'collideAgainst':
        case 'isStatic':
        case 'beforeCollide':
        case 'collide':
        case 'afterCollide':
        case 'handleMovementTrace':
          this[k] = settings[k];
          break;

        // Set vector
        case 'position':
        case 'velocity':
        case 'force':
        case 'velocityLimit':
          this[k].x = settings[k].x || 0;
          this[k].y = settings[k].y || 0;
          break;

        // Set shape
        case 'shape':
          if (typeof(settings.shape) === 'string') {
            if (settings.shape === 'Box') {
              this.shape = new Box(settings.width || 8, settings.height || 8);
            }
            else if (settings.shape === 'Circle') {
              this.shape = new Circle(settings.radius || 4);
            }
          }
          else {
            this.shape = settings.shape;
          }
      }
    }

    return this;
  }
}

Collider.nextId = 0;

module.exports = function(settings) {
  return new Collider(settings);
};