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);
};