const Vector = require('engine/Vector');
const { clamp } = require('engine/utils/math');
const { UP, DOWN, LEFT, RIGHT, OVERLAP, BOX, CIRC } = require('./const');
/**
* AABB collision solver. This collision solver only supports
* Box vs Box, Box vs Circle collisions.
*
* @class AABBSolver
*/
class AABBSolver {
/**
* @constructor
*/
constructor() {
this.response = [
new Vector(),
new Vector(),
];
}
/**
* Hit test a versus b.
* @memberof AABBSolver#
* @method hitTest
* @param {Collider} a First collider
* @param {Collider} b Second collider
* @return {boolean} return true if bodies hit.
*/
hitTest(a, b) {
// Skip when shape is not available
if (!a.shape || !b.shape) {return false;}
if (a.shape.type === BOX && b.shape.type === BOX) {
return !(
a.bottom <= b.top ||
a.top >= b.bottom ||
a.left >= b.right ||
a.right <= b.left
);
}
if (a.shape.type === CIRC && b.shape.type === CIRC) {
// AABB overlap
if (!(
a.bottom <= b.top ||
a.top >= b.bottom ||
a.left >= b.right ||
a.right <= b.left)
) {
return a.position.squaredDistance(b.position) < (a.shape.radius + b.shape.radius) * (a.shape.radius + b.shape.radius);
}
return false;
}
if ((a.shape.type === BOX && b.shape.type === CIRC) || (a.shape.type === CIRC && b.shape.type === BOX)) {
let box = (a.shape.type === BOX) ? a : b;
let circle = (a.shape.type === CIRC) ? a : b;
// AABB overlap
if (!(
a.bottom <= b.top ||
a.top >= b.bottom ||
a.left >= b.right ||
a.right <= b.left)
) {
let distX = circle.position.x - clamp(circle.position.x, box.left, box.right);
let distY = circle.position.y - clamp(circle.position.y, box.top, box.bottom);
return (distX * distX + distY * distY) < (circle.shape.radius * circle.shape.radius);
}
}
return false;
}
/**
* Hit response a versus b.
* @memberof AABBSolver#
* @method hitResponse
* @param {Collider} a First collider
* @param {Collider} b Second collider
* @param {boolean} a2b Whether first collider receives hit response
* @param {boolean} b2a Whether second collider receives hit response
*/
hitResponse(a, b, a2b, b2a) {
let pushA = false, pushB = false;
let resA = this.response[0].set(0), resB = this.response[1].set(0);
let angle, dist;
if (a.shape.type === BOX && b.shape.type === BOX) {
// a.bottom <-> b.top
if (a.lastBottom <= b.lastTop) {
pushA = (a2b && a.collide(b, DOWN));
pushB = (b2a && b.collide(a, UP));
if (pushA && pushB) {
resA.y = (b.top - a.bottom) * 0.5;
resB.y = (a.bottom - b.top) * 0.5;
}
else if (pushA) {
resA.y = (b.top - a.bottom);
}
else if (pushB) {
resB.y = (a.bottom - b.top);
}
}
// a.top <-> b.bottom
else if (a.lastTop >= b.lastBottom) {
pushA = (a2b && a.collide(b, UP));
pushB = (b2a && b.collide(a, DOWN));
if (pushA && pushB) {
resA.y = (b.bottom - a.top) * 0.5;
resB.y = (a.top - b.bottom) * 0.5;
}
else if (pushA) {
resA.y = (b.bottom - a.top);
}
else if (pushB) {
resB.y = (a.top - b.bottom);
}
}
else if (a.lastRight <= b.lastLeft) {
pushA = (a2b && a.collide(b, RIGHT));
pushB = (b2a && b.collide(a, LEFT));
if (pushA && pushB) {
resA.x = (b.left - a.right) * 0.5;
resB.x = (a.right - b.left) * 0.5;
}
else if (pushA) {
resA.x = (b.left - a.right);
}
else if (pushB) {
resB.x = (a.right - b.left);
}
}
else if (a.lastLeft >= b.lastRight) {
pushA = (a2b && a.collide(b, LEFT));
pushB = (b2a && b.collide(a, RIGHT));
if (pushA && pushB) {
resA.x = (b.right - a.left) * 0.5;
resB.x = (a.left - b.right) * 0.5;
}
else if (pushA) {
resA.x = (b.right - a.left);
}
else if (pushB) {
resB.x = (a.left - b.right);
}
}
else {
// Overlap does not push at all
a2b && a.collide(b, OVERLAP);
b2a && b.collide(a, OVERLAP);
}
}
else if (a.shape.type === CIRC && b.shape.type === CIRC) {
angle = b.position.angle(a.position);
dist = a.shape.radius + b.shape.radius;
pushA = (a2b && a.collide(b, angle));
pushB = (b2a && b.collide(a, angle));
if (pushA && pushB) {
resA.x = Math.cos(angle) * dist * 0.5;
resA.y = Math.sin(angle) * dist * 0.5;
resB.x = -resA.x;
resB.y = -resA.y;
}
else if (pushA) {
resA.x = Math.cos(angle) * dist;
resA.y = Math.sin(angle) * dist;
}
else if (pushB) {
resB.x = -Math.cos(angle) * dist;
resB.y = -Math.sin(angle) * dist;
}
}
else {
let box, circle;
if (a.shape.type === BOX && b.shape.type === CIRC) {
box = a;
circle = b;
}
else {
box = b;
circle = a;
}
let closeX = clamp(circle.position.x, box.left, box.right);
let closeY = clamp(circle.position.y, box.top, box.bottom);
let radiusSq = circle.shape.radius * circle.shape.radius;
let overlapX = Math.sqrt(radiusSq - (closeY - circle.position.y) * (closeY - circle.position.y)) - Math.abs(closeX - circle.position.x);
let overlapY = Math.sqrt(radiusSq - (closeX - circle.position.x) * (closeX - circle.position.x)) - Math.abs(closeY - circle.position.y);
overlapX = Math.max(0, overlapX);
overlapY = Math.max(0, overlapY);
angle = Math.atan2(b.velocity.y - a.velocity.y, b.velocity.x - a.velocity.x);
pushA = (a2b && a.collide(b, -angle));
pushB = (b2a && b.collide(a, angle));
if (pushA && pushB) {
resA.x = overlapX * Math.cos(angle) * 0.5;
resA.y = overlapY * Math.sin(angle) * 0.5;
resB.x = -resA.x;
resB.y = -resA.y;
}
else if (pushA) {
resA.x = overlapX * Math.cos(angle);
resA.y = overlapY * Math.sin(angle);
}
else if (pushB) {
resB.x = -overlapX * Math.cos(angle);
resB.y = -overlapY * Math.sin(angle);
}
}
// Apply response to colliders
a.position.x = a.position.x + resA.x;
a.position.y = a.position.y + resA.y;
b.position.x = b.position.x + resB.x;
b.position.y = b.position.y + resB.y;
}
}
/**
* AABBSolver factory
* @return {AABBSolver} solver instance.
*/
module.exports = function() {
return new AABBSolver();
};