import {arrayCache, greenSplice, union} from '../utils/array.js';
import AABB from '../AABB.js';
import CollisionData from '../CollisionData.js';
import CollisionDataContainer from '../CollisionDataContainer.js';
import Data from '../Data.js';
import DataMap from '../DataMap.js';
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';
const
BIT_16 = 0xffff,
combine = function (x, y) {
return (x << 16) | (y & BIT_16);
},
getBucketId = function (x, y, bits) {
return combine(x >> bits, y >> bits);
},
triggerMessage = {
entity: null,
target: null,
type: null,
x: 0,
y: 0,
hitType: null,
myType: null
},
groupSortBySize = function (a, b) {
return a.collisionGroup.getAllEntities() - b.collisionGroup.getAllEntities();
},
epsilon = 0.000001;
export default createComponentClass(/** @lends platypus.components.HandlerCollision.prototype */{
id: 'HandlerCollision',
properties: {
/**
*
*/
gridBits: 8
},
/**
* This component checks for collisions between entities which typically have either a [CollisionTiles](platypus.components.CollisionTiles.html) component for tile maps or a [CollisionBasic](platypus.components.CollisionBasic.html) component for other entities. It uses `EntityContainer` component messages if triggered to add to its collision list and also listens for explicit add/remove messages (useful in the absence of an `EntityContainer` component).
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#add-collision-entity
* @listens platypus.Entity#check-collision-group
* @listens platypus.Entity#child-entity-added
* @listens platypus.Entity#child-entity-removed
* @listens platypus.Entity#child-entity-updated
* @listens platypus.Entity#remove-collision-entity
* @fires platypus.Entity#hit-by-*
* @fires platypus.Entity#relocate-entity
*/
initialize: function () {
this.againstGrid = Data.setUp();
this.solidEntitiesLive = arrayCache.setUp();
this.softEntitiesLive = arrayCache.setUp();
this.allEntitiesLive = arrayCache.setUp();
this.groupsLive = arrayCache.setUp();
this.nonColliders = arrayCache.setUp();
this.terrains = [];
this.owner.previousX = this.owner.previousX || this.owner.x;
this.owner.previousY = this.owner.previousY || this.owner.y;
this.relocationMessage = Data.setUp(
"position", Vector.setUp(),
"relative", false
);
},
events: {
"child-entity-added": function (entity) {
if (!entity.collideOff) {
this.addCollisionEntity(entity);
}
},
"add-collision-entity": function (entity) {
this.addCollisionEntity(entity);
},
"child-entity-removed": function (entity) {
this.removeCollisionEntity(entity);
},
"remove-collision-entity": function (entity) {
this.removeCollisionEntity(entity);
},
"child-entity-updated": function (entity) {
this.removeCollisionEntity(entity);
this.addCollisionEntity(entity);
},
"check-collision-group": function (resp) {
this.checkCamera(resp.camera, resp.entities);
this.checkGroupCollisions();
this.checkSolidCollisions();
this.resolveNonCollisions();
this.checkSoftCollisions(resp);
}
},
methods: {
mapDown (aabb2) {
const
aabb1 = AABB.setUp(),
gb = this.gridBits;
return aabb1.setBounds(aabb2.left >> gb, aabb2.top >> gb, aabb2.right >> gb, aabb2.bottom >> gb);
},
getAgainstGrid (entity, sweep, types) {
const
aabb = this.mapDown(sweep),
data = Data.setUp(),
thisAgainstGrid = this.againstGrid;
if (entity && sweep.equals(entity.againstAABB)) {
return this.getEntityAgainstGrid(entity, types);
}
for (let x = aabb.left; x <= aabb.right; x++) {
for (let y = aabb.top; y <= aabb.bottom; y++) {
const
list = thisAgainstGrid[combine(x, y)];
if (list) {
this.mergeAGCell(list, data, types);
}
}
}
aabb.recycle();
return data;
},
getEntityAgainstGrid: function (entity, types) {
const
ag = entity.againstGrid,
data = Data.setUp();
let i = ag.length;
while (i--) {
this.mergeAGCell(ag[i], data, types);
}
return data;
},
mergeAGCell: function (list, data, types) {
let i = types.length;
while (i--) {
const
type = types[i],
arr = list.get(type);
if (arr?.length) {
const tList = data[type];
if (!tList) {
data[type] = union(arrayCache.setUp(), arr);
} else {
union(tList, arr);
}
}
}
},
removeAgainst (entity) {
const
{collisionTypes, againstGrid} = entity,
len = collisionTypes.length;
let i = againstGrid.length;
while (i--) {
const
list = againstGrid[i];
let j = len;
while (j--) {
const
arr = list.get(collisionTypes[j]);
if (arr) {
const
id = arr.indexOf(entity);
if (id >= 0) {
greenSplice(arr, id);
}
}
}
}
againstGrid.length = 0;
},
updateAgainst (entity) {
const
{againstAABB, againstGrid: entityAgainstGrid, collisionTypes} = entity,
aabb = this.mapDown(entity.getAABB()),
{againstGrid} = this;
if (!aabb.equals(againstAABB)) {
againstAABB.set(aabb);
this.removeAgainst(entity);
for (let x = aabb.left; x <= aabb.right; x++) {
for (let y = aabb.top; y <= aabb.bottom; y++) {
const
id = combine(x, y);
let list = againstGrid[id],
i = collisionTypes.length;
if (!list) {
list = againstGrid[id] = DataMap.setUp();
}
while (i--) {
const
type = collisionTypes[i],
arr = list.get(type) ?? list.set(type, arrayCache.setUp());
arr.push(entity);
}
entityAgainstGrid.push(list);
}
}
}
aabb.recycle();
},
addCollisionEntity: function (entity) {
if (entity.getTileShapes) { // Has a CollisionTiles component
this.terrains.push(entity);
} else if (entity.collisionTypes && !entity.againstGrid) {
entity.againstGrid = arrayCache.setUp();
entity.againstAABB = AABB.setUp();
this.updateAgainst(entity);
}
},
removeCollisionEntity: function (entity) {
if (entity.getTileShapes) { // Has a CollisionTiles component
const index = this.terrains.indexOf(entity);
if (index !== -1) {
this.terrains.splice(index, 1);
}
} if (entity.againstGrid) {
this.removeAgainst(entity);
arrayCache.recycle(entity.againstGrid);
entity.againstGrid = null;
entity.againstAABB.recycle();
entity.againstAABB = null;
}
},
checkCamera: function (camera, all) {
const
{allEntitiesLive, solidEntitiesLive, softEntitiesLive, nonColliders, groupsLive} = this;
let i = all.length;
allEntitiesLive.length = 0;
solidEntitiesLive.length = 0;
softEntitiesLive.length = 0;
nonColliders.length = 0;
groupsLive.length = 0;
while (i--) {
const
entity = all[i],
{collisionTypes, immobile} = entity;
if (!immobile && collisionTypes?.length) {
let collides = false,
j = collisionTypes.length;
allEntitiesLive.push(entity);
if (entity !== this.owner) {
let k = collisionTypes.length;
while (k--) {
if (entity.solidCollisionMap.get(collisionTypes[k]).length) {
solidEntitiesLive.push(entity);
collides = true;
break;
}
}
}
while (j--) {
if (entity.softCollisionMap.get(collisionTypes[j]).length) {
softEntitiesLive.push(entity);
break;
}
}
if (!collides) {
nonColliders.push(entity);
}
if (entity.collisionGroup) {
groupsLive.push(entity);
}
}
}
groupsLive.sort(groupSortBySize);
},
resolveNonCollisions: function () {
const
msg = this.relocationMessage,
nons = this.nonColliders;
let i = nons.length;
msg.relative = false;
while (i--) {
const
entity = nons[i];
if ((entity.position.x !== entity.previousPosition.x) || (entity.position.y !== entity.previousPosition.y)) {
msg.position.setVector(entity.position);
entity.triggerEvent('relocate-entity', msg);
this.updateAgainst(entity);
}
}
},
checkGroupCollisions: (function () {
/**
* When an entity collides with an entity of a listed collision-type, this message is triggered on the entity. * is the other entity's collision-type.
*
* @event platypus.Entity#hit-by-*
* @param collision {Object}
* @param collision.entity {Entity} The entity with which the collision occurred.
* @param collision.target {Entity} The entity that's receiving the collision event.
* @param collision.type {String} The collision type of the other entity.
* @param collision.shape {CollisionShape} This is the shape of the other entity that caused the collision.
* @param collision.x {number} Returns -1, 0, or 1 indicating on which side of this entity the collision occurred: left, neither, or right respectively.
* @param collision.y {number} Returns -1, 0, or 1 indicating on which side of this entity the collision occurred: top, neither, or bottom respectively.
*/
const
triggerCollisionMessages = function (entity, otherEntity, thisType, thatType, x, y, hitType, vector) {
const
msg = triggerMessage;
msg.entity = otherEntity;
msg.target = entity;
msg.myType = thisType;
msg.type = thatType;
msg.x = x;
msg.y = y;
msg.direction = vector;
msg.hitType = hitType;
entity.triggerEvent('hit-by-' + thatType, msg);
if (otherEntity) {
msg.entity = entity;
msg.target = otherEntity;
msg.type = thisType;
msg.myType = thatType;
msg.x = -x;
msg.y = -y;
msg.direction = vector.getInverse();
msg.hitType = hitType;
otherEntity.triggerEvent('hit-by-' + thisType, msg);
msg.direction.recycle();
}
};
return function () {
const
entities = this.groupsLive;
let i = entities.length;
while (i--) {
const
entity = entities[i];
if (entity.collisionGroup.getSize() > 1) {
const
entityCDC = this.checkSolidEntityCollision(entity, entity.collisionGroup),
{xData, yData} = entityCDC;
let x = xData.length,
y = yData.length
while (x--) {
const
messageData = xData[x];
triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, messageData.direction, 0, 'solid', messageData.vector);
}
while (y--) {
const
messageData = yData[y];
triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, 0, messageData.direction, 'solid', messageData.vector);
}
entityCDC.recycle();
}
}
};
}()),
checkSolidCollisions: (function () {
const
triggerCollisionMessages = function (entity, otherEntity, thisType, thatType, x, y, hitType, vector) {
const
msg = triggerMessage;
msg.entity = otherEntity;
msg.target = entity;
msg.myType = thisType;
msg.type = thatType;
msg.x = x;
msg.y = y;
msg.direction = vector;
msg.hitType = hitType;
entity.triggerEvent('hit-by-' + thatType, msg);
if (otherEntity) {
msg.entity = entity;
msg.target = otherEntity;
msg.type = thisType;
msg.myType = thatType;
msg.x = -x;
msg.y = -y;
msg.direction = vector.getInverse();
msg.hitType = hitType;
otherEntity.triggerEvent('hit-by-' + thisType, msg);
msg.direction.recycle();
}
};
return function () {
const
entities = this.solidEntitiesLive;
let i = entities.length;
while (i--) {
const
entity = entities[i],
entityCDC = this.checkSolidEntityCollision(entity, entity),
{xData, yData} = entityCDC;
let x = xData.length,
y = yData.length
while (x--) {
const
messageData = xData[x];
triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, messageData.direction, 0, 'solid', messageData.vector);
}
while (y--) {
const
messageData = yData[y];
triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, 0, messageData.direction, 'solid', messageData.vector);
}
entityCDC.recycle();
}
};
}()),
checkSolidEntityCollision: function (ent, entityOrGroup) {
const
{bullet, collisionDirty, previousX, previousY, x, y} = ent,
collisionDataCollection = CollisionDataContainer.setUp(),
dX = x - previousX,
dY = y - previousY,
collisionTypes = entityOrGroup.getCollisionTypes(),
ignoredEntities = entityOrGroup.getSolidEntities?.() ?? false;
let finalMovementInfo = Vector.setUp(ent.position);
if (dX || dY || collisionDirty) {
if (bullet) {
let sW = Infinity,
sH = Infinity,
i = collisionTypes.length;
while (i--) {
const
aabb = entityOrGroup.getAABB(collisionTypes[i]);
sW = Math.min(sW, aabb.width);
sH = Math.min(sH, aabb.height);
}
{
const
//Stepping to catch really fast entities - this is not perfect, but should prevent the majority of fallthrough cases.
steps = Math.min(Math.ceil(Math.max(Math.abs(dX) / sW, Math.abs(dY) / sH)), 100), //Prevent memory overflow if things move exponentially far.
stepDX = dX / steps,
stepDY = dY / steps;
let step = steps;
while (step--) {
entityOrGroup.prepareCollision(ent.previousX + stepDX, ent.previousY + stepDY);
finalMovementInfo = this.processCollisionStep(ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo.setVector(ent.position), stepDX, stepDY, collisionTypes);
if ((finalMovementInfo.x === ent.previousX) && (finalMovementInfo.y === ent.previousY)) {
entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
//No more movement so we bail!
break;
} else {
entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
}
}
}
} else {
entityOrGroup.prepareCollision(previousX + dX, previousY + dY);
finalMovementInfo = this.processCollisionStep(ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo, dX, dY, collisionTypes);
entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
}
if ((finalMovementInfo.x !== previousX) || (finalMovementInfo.y !== previousY)) {
this.updateAgainst(ent);
// Update children affected by this movement.
if (ent !== entityOrGroup) {
const
children = entityOrGroup.getSolidEntities();
for (let i = 0; i < children.length; i++) {
this.updateAgainst(children[i]);
}
}
}
}
finalMovementInfo.recycle();
return collisionDataCollection;
},
processCollisionStep: (function () {
const
sweeper = AABB.setUp(),
getJumpThroughNormal = function (shape) {
const
jt = shape.jumpThrough;
if (!jt) {
return null;
} else {
const
owner = shape.owner,
v = Vector.setUp(jt.x * (owner.scaleX || 1), jt.y * (owner.scaleY || 1));
if (owner.rotation) {
v.rotate(owner.rotation * Math.PI / 180);
}
return v;
}
},
shouldSkipJumpThroughCollision = function (platformEntity, movingAABB, platformAABB) {
//TODO: currently does not handle individual shape jumpThrough. That will need to happen a layer deeper.
const collisionTypes = platformEntity.collisionTypes;
if (!collisionTypes) {
return true;
}
let i = collisionTypes.length;
while (i--) {
const shapes = platformEntity.getShapes(collisionTypes[i]);
if (!shapes) {
continue;
}
let j = shapes.length;
while (j--) {
const
shape = shapes[j],
normal = getJumpThroughNormal(shape);
if (!normal) {
continue;
} else {
const
absx = Math.abs(normal.x),
absy = Math.abs(normal.y),
ignore = absy >= absx
? ( // Primarily vertical normal
(normal.y < -epsilon && movingAABB.bottom > platformAABB.top) ||
(normal.y > epsilon && movingAABB.top < platformAABB.bottom)
)
: ( // Primarily horizontal normal
(normal.x < -epsilon && movingAABB.right > platformAABB.left) ||
(normal.x > epsilon && movingAABB.left < platformAABB.right)
)
normal.recycle();
if (ignore) {
return ignore;
}
}
}
}
return false;
},
includeEntity = function (thisEntity, aabb, otherEntity, otherAABB, ignoredEntities, sweepAABB) {
//Chop out all the special case entities we don't want to check against.
if (otherEntity === thisEntity) {
return false;
} else if (shouldSkipJumpThroughCollision(otherEntity, aabb, otherAABB)) {
return false;
} else if (shouldSkipJumpThroughCollision(thisEntity, otherAABB, aabb)) { // This will allow platforms to hit something solid sideways if it runs into them from the side even though originally they were above the top. - DDD
return false;
} else if (ignoredEntities) {
let i = ignoredEntities.length;
while (i--) {
if (otherEntity === ignoredEntities[i]) {
return false;
}
}
}
return sweepAABB.collides(otherAABB);
};
return function (ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo, entityDeltaX, entityDeltaY, collisionTypes) {
const
potentialCollidingShapes = arrayCache.setUp(),
terrains = this.terrains,
solidCollisionMap = entityOrGroup.getSolidCollisions(),
sweepAABB = sweeper;
let i = collisionTypes.length,
potentialCollision = false;
while (i--) {
//Sweep the full movement of each collision type
const
pcsGroup = potentialCollidingShapes[i] = arrayCache.setUp(),
collisionType = collisionTypes[i],
previousAABB = entityOrGroup.getPreviousAABB(collisionType),
currentAABB = entityOrGroup.getAABB(collisionType),
collisionSubTypes = solidCollisionMap.get(collisionType);
let againstGrid = null,
j = collisionSubTypes.length;;
sweepAABB.set(currentAABB);
sweepAABB.include(previousAABB);
againstGrid = this.getAgainstGrid(ent, sweepAABB, collisionSubTypes);
while (j--) {
const
otherCollisionType = collisionSubTypes[j],
otherEntities = againstGrid[otherCollisionType];
if (otherEntities) {
let k = otherEntities.length;
while (k--) {
const
otherEntity = otherEntities[k],
otherAABB = otherEntity.getAABB(otherCollisionType);
//Do our sweep check against the AABB of the other object and add potentially colliding shapes to our list.
if (includeEntity(ent, previousAABB, otherEntity, otherAABB, ignoredEntities, sweepAABB)) {
const
otherShapes = otherEntity.getShapes(otherCollisionType);
let l = otherShapes.length;
while (l--) {
//Push the shapes on the end!
pcsGroup.push(otherShapes[l]);
}
potentialCollision = true;
}
}
arrayCache.recycle(otherEntities);
} else if (terrains.length) {
//Do our sweep check against the tiles and add potentially colliding shapes to our list.
let l = 0;
for (l = 0; l < terrains.length; l++) {
const otherShapes = terrains[l].getTileShapes(sweepAABB, previousAABB, otherCollisionType);
let k = otherShapes.length;
while (k--) {
//Push the shapes on the end!
pcsGroup.push(otherShapes[k]);
potentialCollision = true;
}
}
}
}
againstGrid.recycle();
}
if (potentialCollision) {
finalMovementInfo = this.resolveCollisionPosition(ent, entityOrGroup, finalMovementInfo, potentialCollidingShapes, collisionDataCollection, collisionTypes, entityDeltaX, entityDeltaY);
}
// Array recycling
arrayCache.recycle(potentialCollidingShapes, 2);
return finalMovementInfo;
};
}()),
resolveCollisionPosition: function (ent, entityOrGroup, finalMovementInfo, potentialCollidingShapes, collisionDataCollection, collisionTypes, entityDeltaX, entityDeltaY) {
if (entityDeltaX !== 0) {
let j = collisionTypes.length;
while (j--) {
//Move each collision type in X to find the min X movement
const
cd = this.findMinAxisMovement(ent, entityOrGroup, collisionTypes[j], 'x', potentialCollidingShapes[j]);
if (!cd.occurred || !collisionDataCollection.tryToAddX(cd)) {
cd.recycle();
}
}
}
{
const
cd = collisionDataCollection.xData[0];
if (cd) {
finalMovementInfo.x = ent.previousX + cd.deltaMovement * cd.direction;
} else {
finalMovementInfo.x = ent.x;
}
}
// This moves the previous position of everything so that the check in Y can begin.
entityOrGroup.movePreviousX(finalMovementInfo.x);
if (entityDeltaY !== 0) {
let j = collisionTypes.length;
while (j--) {
//Move each collision type in Y to find the min Y movement
const
cd = this.findMinAxisMovement(ent, entityOrGroup, collisionTypes[j], 'y', potentialCollidingShapes[j]);
if (!cd.occurred || !collisionDataCollection.tryToAddY(cd)) {
cd.recycle();
}
}
}
{
const
cd = collisionDataCollection.yData[0];
if (cd) {
finalMovementInfo.y = ent.previousY + cd.deltaMovement * cd.direction;
} else {
finalMovementInfo.y = ent.y;
}
}
return finalMovementInfo;
},
findMinAxisMovement: function (ent, entityOrGroup, collisionType, axis, potentialCollidingShapes) {
//Loop through my shapes of this type vs the colliding shapes and do precise collision returning the shortest movement in axis direction
const
shapes = entityOrGroup.getShapes(collisionType),
prevShapes = entityOrGroup.getPrevShapes(collisionType);
let bestCD = CollisionData.setUp(),
i = shapes.length;
while (i--) {
const
cd = this.findMinShapeMovementCollision(prevShapes[i], shapes[i], axis, potentialCollidingShapes);
if (cd.occurred && (!bestCD.occurred //if a collision occurred and we haven't already had a collision.
|| (cd.deltaMovement < bestCD.deltaMovement))) { //if a collision occurred and the diff is smaller than our best diff.
bestCD.recycle();
bestCD = cd;
} else {
cd.recycle();
}
}
return bestCD;
},
/**
* Find the earliest point at which this shape collides with one of the potential colliding shapes along this axis.
* For example, cycles through shapes a, b, and c to find the earliest position:
*
* O----> [b] [a] [c]
*
* Returns collision location for:
*
* O[b]
*
*/
findMinShapeMovementCollision: (function () {
const
returnInfo = {
position: 0,
contactVector: Vector.setUp()
},
getMovementDistance = (currentDistance, minimumDistance) => Math.sqrt((minimumDistance ** 2) - (currentDistance ** 2)),
getCorner = function (circlePos, rectanglePos, half) {
const
diff = circlePos - rectanglePos;
return diff - (diff / Math.abs(diff)) * half;
},
getOffsetForCircleVsAABBX = function (circle, rect, moving, direction, v) {
const
aabb = rect.aABB,
hw = aabb.halfWidth,
{x, y} = circle;
if (y >= aabb.top && y <= aabb.bottom) {
return hw + circle.radius;
} else {
const
cornerY = getCorner(y, rect.y, aabb.halfHeight),
newAxisPosition = hw + getMovementDistance(cornerY, circle.radius);
if (moving === circle) {
v.x = -getCorner(x - direction * newAxisPosition, rect.x, hw) / 2;
v.y = -cornerY;
} else {
v.x = getCorner(x, rect.x - direction * newAxisPosition, hw) / 2;
v.y = cornerY;
}
v.normalize();
return newAxisPosition;
}
},
getOffsetForCircleVsAABBY = function (circle, rect, moving, direction, v) {
const
aabb = rect.aABB,
hh = aabb.halfHeight,
{x, y} = circle;
if (x >= aabb.left && x <= aabb.right) {
return hh + circle.radius;
} else {
const
cornerX = getCorner(x, rect.x, aabb.halfWidth),
newAxisPosition = hh + getMovementDistance(cornerX, circle.radius);
if (moving === circle) {
v.x = -cornerX;
v.y = -getCorner(y - direction * newAxisPosition, rect.y, hh) / 2;
} else {
v.x = cornerX;
v.y = getCorner(y, rect.y - direction * newAxisPosition, hh) / 2;
}
v.normalize();
return newAxisPosition;
}
},
findAxisCollisionPosition = { // Decision tree for quicker access, optimized for mobile devices.
x: {
rectangle: {
rectangle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.x - direction * (thatShape.aABB.halfWidth + thisShape.aABB.halfWidth);
ri.contactVector.setXYZ(direction, 0);
return ri;
},
circle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.x - direction * getOffsetForCircleVsAABBX(thatShape, thisShape, thisShape, direction, ri.contactVector.setXYZ(direction, 0));
return ri;
}
},
circle: {
rectangle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.x - direction * getOffsetForCircleVsAABBX(thisShape, thatShape, thisShape, direction, ri.contactVector.setXYZ(direction, 0));
return ri;
},
circle: function (direction, thisShape, thatShape) {
const
y = thatShape.y - thisShape.y,
position = thatShape.x - direction * getMovementDistance(y, thisShape.radius + thatShape.radius),
ri = returnInfo;
ri.contactVector.setXYZ(thatShape.x - position, y).normalize();
ri.position = position;
return ri;
}
}
},
y: {
rectangle: {
rectangle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.y - direction * (thatShape.aABB.halfHeight + thisShape.aABB.halfHeight);
ri.contactVector.setXYZ(0, direction);
return ri;
},
circle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.y - direction * getOffsetForCircleVsAABBY(thatShape, thisShape, thisShape, direction, ri.contactVector.setXYZ(0, direction));
return ri;
}
},
circle: {
rectangle: function (direction, thisShape, thatShape) {
const
ri = returnInfo;
ri.position = thatShape.y - direction * getOffsetForCircleVsAABBY(thisShape, thatShape, thisShape, direction, ri.contactVector.setXYZ(0, direction));
return ri;
},
circle: function (direction, thisShape, thatShape) {
const
x = thatShape.x - thisShape.x,
position = thatShape.y - direction * getMovementDistance(x, thisShape.radius + thatShape.radius),
ri = returnInfo;
ri.contactVector.setXYZ(x, thatShape.y - position).normalize();
ri.position = position;
return ri;
}
}
}
};
return function (translatedShape, currentShape, axis, potentialCollidingShapes) {
const
initialPoint = translatedShape[axis],
goalPoint = currentShape[axis],
direction = ((initialPoint < goalPoint) ? 1 : -1),
cd = CollisionData.setUp();
let finalPosition = goalPoint;
if (initialPoint !== goalPoint) {
const
findACP = findAxisCollisionPosition[axis][translatedShape.type];
let i = potentialCollidingShapes.length
if (axis === 'x') {
translatedShape.moveX(goalPoint);
} else if (axis === 'y') {
translatedShape.moveY(goalPoint);
}
while (i--) {
const
pcShape = potentialCollidingShapes[i];
if (translatedShape.collides(pcShape)) {
const
collisionInfo = findACP[pcShape.type](direction, translatedShape, pcShape),
position = collisionInfo.position;
if (direction > 0) {
if (position < finalPosition) {
finalPosition = position < initialPoint ? initialPoint : position; // Reality check: I think this is necessary due to floating point inaccuracies. - DDD
cd.set(true, direction, finalPosition, Math.abs(finalPosition - initialPoint), pcShape.aABB, currentShape, pcShape, collisionInfo.contactVector, 0);
}
} else if (position > finalPosition) {
finalPosition = position > initialPoint ? initialPoint : position; // Reality check: I think this is necessary due to floating point inaccuracies. - DDD
cd.set(true, direction, finalPosition, Math.abs(finalPosition - initialPoint), pcShape.aABB, currentShape, pcShape, collisionInfo.contactVector, 0);
}
}
}
}
return cd;
};
}()),
checkSoftCollisions () {
const
softs = this.softEntitiesLive;
let i = softs.length;
while (i--) {
const
entity = softs[i];
this.checkEntityForSoftCollisions(entity, (collision) => {
entity.triggerEvent('hit-by-' + collision.type, collision);
});
}
},
checkEntityForSoftCollisions: function (ent, callback) {
const
message = triggerMessage,
terrains = this.terrains;
let i = ent.collisionTypes.length;
message.x = 0;
message.y = 0;
while (i--) {
const
collisionType = ent.collisionTypes[i],
softCollisionMap = ent.softCollisionMap.get(collisionType),
againstGrid = this.getEntityAgainstGrid(ent, softCollisionMap);
let j = softCollisionMap.length;
while (j--) {
const
otherCollisionType = softCollisionMap[j],
otherEntities = againstGrid[otherCollisionType];
if (otherEntities) {
let k = otherEntities.length;
while (k--) {
const
otherEntity = otherEntities[k];
if ((otherEntity !== ent) && (ent.getAABB(collisionType).collides(otherEntity.getAABB(otherCollisionType)))) {
const
shapes = ent.getShapes(collisionType),
otherShapes = otherEntity.getShapes(otherCollisionType);
let collisionFound = false,
l = shapes.length;
while (l--) {
let m = otherShapes.length;
while (m--) {
if (shapes[l].collides(otherShapes[m])) {
//TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
message.entity = otherEntity;
message.target = ent;
message.type = otherCollisionType;
message.myType = collisionType;
message.shape = otherShapes[m];
message.hitType = 'soft';
callback(message);
collisionFound = true;
break;
}
}
if (collisionFound) {
break;
}
}
}
}
arrayCache.recycle(otherEntities);
}
if (terrains.length) {
//Do our sweep check against the tiles and add potentially colliding shapes to our list.
let l = 0;
for (l = 0; l < terrains.length; l++) {
//Sending the same AABB twice because we don't do sweep or jumpThrough checks with soft collision.
const entAABB = ent.getAABB(collisionType),
otherShapes = terrains[l].getTileShapes(entAABB, entAABB, otherCollisionType);
let k = otherShapes.length;
while (k--) {
message.entity = terrains[l];
message.target = ent;
message.type = otherCollisionType;
message.myType = collisionType;
message.shape = otherShapes[k];
message.hitType = 'soft';
callback(message);
}
}
}
}
againstGrid.recycle();
}
},
checkShapeForCollisions: function (shape, softCollisionMap, callback) {
const
againstGrid = this.getAgainstGrid(null, shape.getAABB(), softCollisionMap),
message = triggerMessage;
let j = softCollisionMap.length;
message.x = 0;
message.y = 0;
while (j--) {
const
otherCollisionType = softCollisionMap[j],
otherEntities = againstGrid[otherCollisionType];
if (otherEntities) {
let k = otherEntities.length;
while (k--) {
const
otherEntity = otherEntities[k];
if ((shape.getAABB().collides(otherEntity.getAABB(otherCollisionType)))) {
const
otherShapes = otherEntity.getShapes(otherCollisionType);
let m = otherShapes.length;
while (m--) {
if (shape.collides(otherShapes[m])) {
//TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
message.entity = otherEntity;
message.target = null;
message.type = otherCollisionType;
message.myType = '';
message.shape = otherShapes[m];
message.hitType = 'soft';
callback(message);
break;
}
}
}
}
arrayCache.recycle(otherEntities);
}
}
againstGrid.recycle();
},
checkPointForCollisions: function (x, y, collisions, callback) {
const
gb = this.gridBits,
againstGrid = this.againstGrid[getBucketId(x, y, gb)],
message = triggerMessage;
let j = collisions.length;
message.x = 0;
message.y = 0;
if (!againstGrid) {
return;
}
while (j--) {
const
otherCollisionType = collisions[j],
otherEntities = againstGrid.get(otherCollisionType);
if (otherEntities) {
let k = otherEntities.length;
while (k--) {
const
otherEntity = otherEntities[k];
if (otherEntity.getAABB(otherCollisionType).containsPoint(x, y)) {
const
otherShapes = otherEntity.getShapes(otherCollisionType);
let m = otherShapes.length;
while (m--) {
if (otherShapes[m].containsPoint(x, y)) {
//TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
message.entity = otherEntity;
message.target = null;
message.type = otherCollisionType;
message.myType = '';
message.shape = otherShapes[m];
message.hitType = 'soft';
callback(message);
break;
}
}
}
}
}
}
},
destroy: function () {
const
ag = this.againstGrid,
keys = Object.keys(ag),
{length} = keys;
arrayCache.recycle(this.groupsLive);
arrayCache.recycle(this.nonColliders);
arrayCache.recycle(this.allEntitiesLive);
arrayCache.recycle(this.softEntitiesLive);
arrayCache.recycle(this.solidEntitiesLive);
this.relocationMessage.position.recycle();
this.relocationMessage.recycle();
for (let i = 0; i < length; i++) {
const
data = ag[keys[i]],
dataKeys = data.keys;
let j = dataKeys.length;
while (j--) {
arrayCache.recycle(data.get(dataKeys[j]));
}
data.recycle();
}
ag.recycle();
this.againstGrid = null;
}
},
publicMethods: {
/**
* This method returns an object containing world entities.
*
* @method platypus.components.HandlerCollision#getWorldEntities
* @return {Array} A list of all world collision entities.
*/
getWorldEntities: function () {
return this.allEntitiesLive;
},
/**
* This method returns an array of entities representing the collision maps of the world.
*
* @method platypus.components.HandlerCollision#getWorldTerrain
* @return {Array.<platypus.Entity>} An array of entities describing the collision maps of the world. This entity typically includes a `CollisionTiles` component.
*/
getWorldTerrain: function () {
return this.terrains;
},
/**
* This method returns a list of collision objects describing soft collisions between an entity and a list of other entities.
*
* @method platypus.components.HandlerCollision#getEntityCollisions
* @param entity {Entity} The entity to test against the world.
* @return collisions {Array} This is a list of collision objects describing the soft collisions.
*/
getEntityCollisions: function (entity) {
const
collisions = arrayCache.setUp();
this.checkEntityForSoftCollisions(entity, (collision) => collisions.push(Data.setUp(collision)));
return collisions;
},
/**
* This method returns a list of collision objects describing collisions between a shape and a list of other entities.
*
* @method platypus.components.HandlerCollision#getShapeCollisions
* @param shape {CollisionShape} The shape to check for collisions.
* @param collisionTypes {String[]} The collision types to check against.
* @return collisions {Array} This is a list of collision objects describing the soft collisions.
*/
getShapeCollisions: function (shape, collisionTypes) {
const
collisions = arrayCache.setUp();
this.checkShapeForCollisions(shape, collisionTypes, (collision) => collisions.push(Data.setUp(collision)));
return collisions;
},
/**
* This method returns a list of collision objects describing collisions between a point and a list of other entities.
*
* @method platypus.components.HandlerCollision#getPointCollisions
* @param x {number} The x-axis value.
* @param y {number} The y-axis value.
* @param collisionTypes {String[]} The collision types to check against.
* @return collisions {Array} This is a list of collision objects describing the soft collisions.
*/
getPointCollisions: function (x, y, collisionTypes) {
const
collisions = arrayCache.setUp();
this.checkPointForCollisions(x, y, collisionTypes, (collision) => collisions.push(Data.setUp(collision)));
return collisions;
}
}
});